Question

I am using the ice_cube gem to create schedules. Making a monthly schedule starting on the 31st misses all months with fewer than 31 days. I'd like to schedule the last day of the month on those months. If my schedule starts on the 30th I want the 30th of every month and the last day of February. Leap years complicate the matter further.

What's a good way to create schedules that handles starting on the 29th, 30th or 31st?

Was it helpful?

Solution 4

This passes all my specs, but is fugly and probably breaks for schedules longer than a year (which I don't care about yet).

class LeasePaymentSchedule

  def self.monthly(a bunch of args)
    case start_day

    when 31
      schedule = IceCube::Schedule.new(start, scheduler_options) do |s|
        s.add_recurrence_rule IceCube::Rule.monthly.day_of_month(-1).until(end_time)
      end

    when 30,29
      schedule = IceCube::Schedule.new(start, scheduler_options) do |s|
        s.add_recurrence_rule IceCube::Rule.monthly.day_of_month(start_day).until(end_time)
      end

      schedule.all_occurrences.each do |o|
        next unless [1,3,6,8,10].include? o.month
        missed = (o + 1.month).yday
        # Probably breaks for durations longer than 1 year
        schedule.add_recurrence_rule IceCube::Rule.yearly.day_of_year(missed).count(1)
      end

    else
      schedule = IceCube::Schedule.new(start, scheduler_options) do |s|
        s.add_recurrence_rule IceCube::Rule.monthly.day_of_month(start_day).until(end_time)
      end
    end
    schedule
   end
end

So many specs:

Finished in 4.17 seconds
390 examples, 0 failures

-

shared_examples_for :a_schedule do
  it 'returns an IceCube Schedule' do
    schedule.should be_a IceCube::Schedule
  end
  it 'should start on the correct day' do
    schedule.start_time.should eq expected_start
  end
  it 'has the right number of occurrences' do
    schedule.all_occurrences.size.should eq expected_occurrences
  end
end

describe :monthly do
  let(:expected_occurrences) { 12 }
  let(:expected_start) { date.next_month.beginning_of_day }
  let(:schedule) { LeasePaymentSchedule.monthly }

  before do
    Date.stub(:today).and_return(date)
  end

  shared_examples_for :on_the_28th do
    let(:date) { Time.parse "#{year}-#{month}-28" }
    it_behaves_like :a_schedule
  end

  shared_examples_for :on_the_29th do
    let(:date) { Time.parse "#{year}-#{month}-29" }
    it_behaves_like :on_the_28th
    it_behaves_like :a_schedule
  end

  shared_examples_for :on_the_30th do
    let(:date) { Time.parse "#{year}-#{month}-30" }
    it_behaves_like :on_the_29th
    it_behaves_like :a_schedule
  end

  shared_examples_for :on_the_31st do
    let(:date) { Time.parse "#{year}-#{month}-31" }
    it_behaves_like :on_the_30th
    it_behaves_like :a_schedule
  end

  shared_examples_for :the_whole_year do
    context :february do
      let(:month) { 2 }
      it_behaves_like :on_the_28th
    end
    [ 4, 7, 9, 11 ].each do |month_num|
      let(:month) { month_num }
      it_behaves_like :on_the_30th
    end
    [ 1, 3, 5, 6, 8, 10, 12].each do |month_num|
      let(:month) { month_num }
      it_behaves_like :on_the_31st
    end
  end

  context :a_leap_year do
    let(:year) { 2012 }
    context :february_29th do
      let(:month) { 2 }
      it_behaves_like :on_the_29th
    end
    it_behaves_like :the_whole_year
  end

  context :before_a_leap_year do
    let(:year) { 2011 }
    it_behaves_like :the_whole_year
  end

  context :nowhere_near_a_leap_year do
    let(:year) { 2010 }
    it_behaves_like :the_whole_year
  end

end

OTHER TIPS

This has been fixed in the latest IceCube:

IceCube::Schedule.new(Time.parse('Oct 31, 2013 9:00am PDT')) do |s|
  s.add_recurrence_rule(IceCube::Rule.monthly(1))
end.first(5)

[2013-10-31 09:00:00 -0700,
 2013-11-30 09:00:00 -0800,
 2013-12-31 09:00:00 -0800,
 2014-01-31 09:00:00 -0800,
 2014-02-28 09:00:00 -0800]

You can use day_of_month(-1) for the last day of the month.

You could set the schedules using day_of_month with 29, 30, 31 as you normally would. Then use a function to determine which months were skipped and set them using a single occurrence event. Admittedly a bit of a hack, but works around ice_cube's limitation.

Here is a simple script to determine which months would be skipped in any given year.

require 'date'
def months_less_than_days(days, year = Date.today.year)
    months = []
    (1..11).each do |m| 
        eom = (Date.new(year, m+1, 1) - 1)
        months << eom if eom.day < days
    end
    eom = (Date.new(year+1, 1, 1) - 1)
    months << eom if eom.day < days
    months
end

puts months_less_than_days(31) # => [2013-02-28, 2013-04-30, 2013-06-30, 2013-09-30, 2013-11-30]
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top