rubyice-cube

Monthly schedule that catches ends of the month


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?


Solution

  • 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