70b0265140eaff6e312eb3efcff3c195

I am reasonably happy with this code, but wish it was a bit simpler and more declarative. Anyone know of a simple way to get the last day of the month, or the last Sunday, or the next Saturday without resulting to while loops?

class Monthly
  def initialize(date = Date.today)
    @date = date
  end

  def first
    Date.civil(@date.year, @date.month, 1)
  end

  def last
    date = first
    date += 1 until date.succ.day == 1
    date
  end

  def first_on_calendar
    date = first
    date -= 1 until date.wday == 0
    date
  end

  def last_on_calendar
    date = last
    date += 1 until date.wday == 6
    date
  end

  def calendar_dates
    dates = []
    first_on_calendar.upto(last_on_calendar) {|d| dates << d}
    dates
  end
end
require 'timecop'
require 'monthly'

describe Monthly do
  describe 'April 2009' do
    before do
      Timecop.freeze(Date.civil(2009, 4, 16)) do
        @monthly = Monthly.new
      end
    end

    it 'should give april 1 for the first day' do
      @monthly.first.should == Date.civil(2009, 4, 1)
    end

    it 'should give april 30 for the last day' do
      @monthly.last.should == Date.civil(2009, 4, 30)
    end

    it 'should give march 29 for the first day on calendar' do
      @monthly.first_on_calendar.should == Date.civil(2009, 3, 29)
    end

    it 'should give may 2 for the last day on calendar' do
      @monthly.last_on_calendar.should == Date.civil(2009, 5, 2)
    end

    it 'should give the correct set of dates' do
      dates = %w(
        29 30 31  1  2  3  4
         5  6  7  8  9 10 11
        12 13 14 15 16 17 18
        19 20 21 22 23 24 25
        26 27 28 29 30  1  2
      ).map {|s| s.to_i}
      @monthly.calendar_dates.map {|d| d.day}.should == dates
    end
  end

  describe 'October 2008' do
    before do
      @monthly = Monthly.new(Date.civil(2008, 10, 20))
    end

    it 'should give october 1 for the first day' do
      @monthly.first.should == Date.civil(2008, 10, 1)
    end

    it 'should give october 31 for the last day' do
      @monthly.last.should == Date.civil(2008, 10, 31)
    end

    it 'should give september 28 for the first day on calendar' do
      @monthly.first_on_calendar.should == Date.civil(2008, 9, 28)
    end

    it 'should give november 1 for the last day on calendar' do
      @monthly.last_on_calendar.should == Date.civil(2008, 11, 1)
    end

    it 'should give the correct set of dates' do
      dates = %w(
        28 29 30  1  2  3  4
         5  6  7  8  9 10 11
        12 13 14 15 16 17 18
        19 20 21 22 23 24 25
        26 27 28 29 30 31  1
      ).map {|s| s.to_i}
      @monthly.calendar_dates.map {|d| d.day}.should == dates
    end
  end
end

Refactorings

No refactoring yet !

8d8978eba1922a74b91c4b361c7706cc

roman

April 16, 2009, April 16, 2009 18:56, permalink

1 rating. Login to rate!

Here you have a version that does not have loops for getting the last day of the month, first and last day of the week... it uses metadata for the month length, I thought there was some of this info on the Date class, but I didn't find any in the documentation... at least this is a good start I think :-).

class Monthly
  
  DAYS_OF_MONTH = [
    31,
    [28, 29],
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31
  ]
  
  def initialize(date = Date.today)
    @date = date
  end

  def first
    Date.civil(@date.year, @date.month, 1)
  end

  def last
    date = first
    date += (days_of_month - 1)
    date
  end

  def first_on_calendar
    date = first
    date -= date.wday # this will result in 0
    date
  end

  def last_on_calendar
    date = last
    date += (6 - date.wday)
    date
  end

  def calendar_dates
    dates = []
    first_on_calendar.upto(last_on_calendar) {|d| dates << d}
    dates
  end
  
private
  
  def days_of_month
    month_days = DAYS_OF_MONTH[@date.month - 1]  # 0 based index
    if month_days.kind_of?(Array)
      month_days[(@date.leap? ? 1 : 0)]
    else
      month_days
    end
  end
  
end
70b0265140eaff6e312eb3efcff3c195

Ben Atkin

April 17, 2009, April 17, 2009 05:59, permalink

No rating. Login to rate!

Thanks to roman's solution, it just popped into my head how to get rid of the loops. Since I had to make a special case involving the end of the year, I added an additional example to my spec.

class Monthly
  attr_reader :date

  def initialize(d = Date.today)
    @date = d
  end

  def first
    Date.civil(date.year, date.month, 1)
  end

  def last
    date.month == 12 ? Date.civil(date.year, 12, 31) : Date.civil(date.year, date.month + 1, 1) - 1
  end

  def first_on_calendar
    first - first.wday
  end

  def last_on_calendar
    last + 6 - last.wday
  end

  def calendar_dates
    first_on_calendar..last_on_calendar
  end
end
require 'timecop'
require 'monthly'

describe Monthly do
  describe 'April 2009' do
    before do
      Timecop.freeze(Date.civil(2009, 4, 16)) do
        @monthly = Monthly.new
      end
    end

    it 'should give april 1 for the first day' do
      @monthly.first.should == Date.civil(2009, 4, 1)
    end

    it 'should give april 30 for the last day' do
      @monthly.last.should == Date.civil(2009, 4, 30)
    end

    it 'should give march 29 for the first day on calendar' do
      @monthly.first_on_calendar.should == Date.civil(2009, 3, 29)
    end

    it 'should give may 2 for the last day on calendar' do
      @monthly.last_on_calendar.should == Date.civil(2009, 5, 2)
    end

    it 'should give the correct set of dates' do
      dates = %w(
        29 30 31  1  2  3  4
         5  6  7  8  9 10 11
        12 13 14 15 16 17 18
        19 20 21 22 23 24 25
        26 27 28 29 30  1  2
      ).map {|s| s.to_i}
      @monthly.calendar_dates.map {|d| d.day}.should == dates
    end
  end

  describe 'October 2008' do
    before do
      @monthly = Monthly.new(Date.civil(2008, 10, 20))
    end

    it 'should give october 1 for the first day' do
      @monthly.first.should == Date.civil(2008, 10, 1)
    end

    it 'should give october 31 for the last day' do
      @monthly.last.should == Date.civil(2008, 10, 31)
    end

    it 'should give september 28 for the first day on calendar' do
      @monthly.first_on_calendar.should == Date.civil(2008, 9, 28)
    end

    it 'should give november 1 for the last day on calendar' do
      @monthly.last_on_calendar.should == Date.civil(2008, 11, 1)
    end

    it 'should give the correct set of dates' do
      dates = %w(
        28 29 30  1  2  3  4
         5  6  7  8  9 10 11
        12 13 14 15 16 17 18
        19 20 21 22 23 24 25
        26 27 28 29 30 31  1
      ).map {|s| s.to_i}
      @monthly.calendar_dates.map {|d| d.day}.should == dates
    end
  end

  describe 'December 2009' do
    before do
      Timecop.freeze(Date.civil(2009, 12, 31)) do
        @monthly = Monthly.new
      end
    end

    it 'should give december 1 for the first day' do
      @monthly.first.should == Date.civil(2009, 12, 1)
    end

    it 'should give december 31 for the last day' do
      @monthly.last.should == Date.civil(2009, 12, 31)
    end

    it 'should give november 29 for the first day on calendar' do
      @monthly.first_on_calendar.should == Date.civil(2009, 11, 29)
    end

    it 'should give january 2 for the last day on calendar' do
      @monthly.last_on_calendar.should == Date.civil(2010, 1, 2)
    end

    it 'should give the correct set of dates' do
      dates = %w(
        29 30  1  2  3  4  5
         6  7  8  9 10 11 12
        13 14 15 16 17 18 19
        20 21 22 23 24 25 26
        27 28 29 30 31  1  2
      ).map {|s| s.to_i}
      @monthly.calendar_dates.map {|d| d.day}.should == dates
    end
  end

end
D41d8cd98f00b204e9800998ecf8427e

steenslag

June 25, 2009, June 25, 2009 21:42, permalink

No rating. Login to rate!
require 'date'

def days_to_show(year, month, num_week_rows = 5)
  first_of_month = Date.new(year, month, 1)
  first = first_of_month - first_of_month.cwday #+ 1
  # +1 for calendar starting on monday, remove for starting on sunday
  last = first + num_week_rows * 7
  (first...last).to_a
end

Your refactoring





Format Copy from initial code

or Cancel