diff --git a/README.md b/README.md index ad599cc5..025f34ce 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ schedule.remaining_occurrences # for terminating schedules schedule.previous_occurrence(from_time) schedule.previous_occurrences(3, from_time) +# or include prior occurrences with a duration overlapping from_time +schedule.next_occurrences(3, from_time, :spans => true) +schedule.occurrences_between(from_time, to_time, :spans => true) # or give the schedule a duration and ask if occurring_at? schedule = IceCube::Schedule.new(now, :duration => 3600) diff --git a/lib/ice_cube/schedule.rb b/lib/ice_cube/schedule.rb index 73303ff3..0367879f 100644 --- a/lib/ice_cube/schedule.rb +++ b/lib/ice_cube/schedule.rb @@ -166,15 +166,15 @@ def each_occurrence(&block) end # The next n occurrences after now - def next_occurrences(num, from = nil) + def next_occurrences(num, from = nil, options = {}) from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time) - enumerate_occurrences(from + 1, nil).take(num) + enumerate_occurrences(from + 1, nil, options).take(num) end # The next occurrence after now (overridable) - def next_occurrence(from = nil) + def next_occurrence(from = nil, options = {}) from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time) - enumerate_occurrences(from + 1, nil).next + enumerate_occurrences(from + 1, nil, options).next rescue StopIteration nil end @@ -195,26 +195,26 @@ def previous_occurrences(num, from) end # The remaining occurrences (same requirements as all_occurrences) - def remaining_occurrences(from = nil) + def remaining_occurrences(from = nil, options = {}) require_terminating_rules from ||= TimeUtil.now(@start_time) - enumerate_occurrences(from).to_a + enumerate_occurrences(from, nil, options).to_a end # Returns an enumerator for all remaining occurrences - def remaining_occurrences_enumerator(from = nil) + def remaining_occurrences_enumerator(from = nil, options = {}) from ||= TimeUtil.now(@start_time) - enumerate_occurrences(from) + enumerate_occurrences(from, nil, options) end # Occurrences between two times - def occurrences_between(begin_time, closing_time) - enumerate_occurrences(begin_time, closing_time).to_a + def occurrences_between(begin_time, closing_time, options = {}) + enumerate_occurrences(begin_time, closing_time, options).to_a end # Return a boolean indicating if an occurrence falls between two times - def occurs_between?(begin_time, closing_time) - enumerate_occurrences(begin_time, closing_time).next + def occurs_between?(begin_time, closing_time, options = {}) + enumerate_occurrences(begin_time, closing_time, options).next true rescue StopIteration false @@ -226,9 +226,7 @@ def occurs_between?(begin_time, closing_time) # occurrences at the end of the range since none of their duration # intersects the range. def occurring_between?(opening_time, closing_time) - opening_time = opening_time - duration - closing_time = closing_time - 1 if duration > 0 - occurs_between?(opening_time, closing_time) + occurs_between?(opening_time, closing_time, :spans => true) end # Return a boolean indicating if an occurrence falls on a certain date @@ -404,25 +402,30 @@ def reset # Find all of the occurrences for the schedule between opening_time # and closing_time # Iteration is unrolled in pairs to skip duplicate times in end of DST - def enumerate_occurrences(opening_time, closing_time = nil, &block) + def enumerate_occurrences(opening_time, closing_time = nil, options = {}, &block) opening_time = TimeUtil.match_zone(opening_time, start_time) closing_time = TimeUtil.match_zone(closing_time, start_time) opening_time += start_time.subsec - opening_time.subsec rescue 0 opening_time = start_time if opening_time < start_time + spans = options[:spans] == true && duration != 0 Enumerator.new do |yielder| reset - t1 = full_required? ? start_time : realign(opening_time) + t1 = full_required? ? start_time : realign((spans ? opening_time - duration : opening_time)) loop do break unless (t0 = next_time(t1, closing_time)) break if closing_time && t0 > closing_time - yielder << (block_given? ? block.call(t0) : t0) if t0 >= opening_time + if (spans ? (t0.end_time > opening_time) : (t0 >= opening_time)) + yielder << (block_given? ? block.call(t0) : t0) + end break unless (t1 = next_time(t0 + 1, closing_time)) break if closing_time && t1 > closing_time if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?) wind_back_dst next (t1 += 1) end - yielder << (block_given? ? block.call(t1) : t1) if t1 >= opening_time + if (spans ? (t1.end_time > opening_time) : (t1 >= opening_time)) + yielder << (block_given? ? block.call(t1) : t1) + end next (t1 += 1) end end diff --git a/spec/examples/schedule_spec.rb b/spec/examples/schedule_spec.rb index ea49c951..0add833d 100644 --- a/spec/examples/schedule_spec.rb +++ b/spec/examples/schedule_spec.rb @@ -1,4 +1,5 @@ require File.dirname(__FILE__) + '/../spec_helper' +require 'benchmark' describe IceCube::Schedule do @@ -451,14 +452,91 @@ end + describe :spans do + + it 'should find occurrence in past with duration beyond the start time' do + t0 = Time.utc(2015, 10, 1, 15, 31) + schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR) + schedule.add_recurrence_rule IceCube::Rule.daily + next_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, :spans => true) + next_occ.should == t0 + end + + it 'should include occurrence in past with duration beyond the start time' do + t0 = Time.utc(2015, 10, 1, 15, 31) + schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR) + schedule.add_recurrence_rule IceCube::Rule.daily.count(2) + occs = schedule.next_occurrences(10, t0 + IceCube::ONE_HOUR, :spans => true) + occs.should == [t0, t0 + IceCube::ONE_DAY] + end + + it 'should allow duration span on remaining_occurrences' do + t0 = Time.utc(2015, 10, 1, 00, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY) + schedule.add_recurrence_rule IceCube::Rule.daily.count(3) + occs = schedule.remaining_occurrences(t0 + IceCube::ONE_DAY + IceCube::ONE_HOUR, :spans => true) + occs.should == [t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY] + end + + it 'should include occurrences with duration spanning the requested start time' do + t0 = Time.utc(2015, 10, 1, 15, 31) + schedule = IceCube::Schedule.new(t0, :duration => 30 * IceCube::ONE_DAY) + long_event = schedule.remaining_occurrences_enumerator(t0 + IceCube::ONE_DAY, :spans => true).take(1) + long_event.should == [t0] + end + + it 'should find occurrences between including previous one with duration spanning start' do + t0 = Time.utc(2015, 10, 1, 10, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR) + schedule.add_recurrence_rule IceCube::Rule.hourly.count(10) + occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR + 1, t0 + 3 * IceCube::ONE_HOUR + 1, :spans => true) + occs.length.should == 3 + end + + it 'should include long occurrences starting before and ending after' do + t0 = Time.utc(2015, 10, 1, 00, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY) + occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR, t0 + IceCube::ONE_DAY - IceCube::ONE_HOUR, :spans => true) + occs.should == [t0] + end + + it 'should not find occurrence with duration ending on start time' do + t0 = Time.utc(2015, 10, 1, 12, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR) + schedule.occurs_between?(t0 + IceCube::ONE_HOUR, t0 + 2 * IceCube::ONE_HOUR, :spans => true).should be_false + end + + it 'should quickly fetch a future time from a recurring schedule' do + t0 = Time.utc(2000, 10, 1, 00, 00) + t1 = Time.utc(2015, 10, 1, 12, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR - 1) + schedule.add_recurrence_rule IceCube::Rule.hourly + occ = nil + timing = Benchmark.realtime do + occ = schedule.remaining_occurrences_enumerator(t1, :spans => true).take(1) + end + timing.should < 0.1 + occ.should == [t1] + end + + it 'should not include occurrence ending on start time' do + t0 = Time.utc(2015, 10, 1, 10, 00) + schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR / 2) + schedule.add_recurrence_rule IceCube::Rule.minutely(30).count(6) + third_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, :spans => true) + third_occ.should == t0 + IceCube::ONE_HOUR + end + + end + describe :previous_occurrence do it 'returns the previous occurrence for a time in the schedule' do t0 = Time.utc(2013, 5, 18, 12, 34) schedule = IceCube::Schedule.new(t0) schedule.add_recurrence_rule IceCube::Rule.daily - previous = schedule.previous_occurrence(t0 + 2 * ONE_DAY) - previous.should == t0 + ONE_DAY + previous = schedule.previous_occurrence(t0 + 2 * IceCube::ONE_DAY) + previous.should == t0 + IceCube::ONE_DAY end it 'returns nil given the start time' do @@ -485,16 +563,16 @@ t0 = Time.utc(2013, 5, 18, 12, 34) schedule = IceCube::Schedule.new(t0) schedule.add_recurrence_rule IceCube::Rule.daily - previous = schedule.previous_occurrences(2, t0 + 3 * ONE_DAY) - previous.should == [t0 + ONE_DAY, t0 + 2 * ONE_DAY] + previous = schedule.previous_occurrences(2, t0 + 3 * IceCube::ONE_DAY) + previous.should == [t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY] end it 'limits the returned occurrences to a given count' do t0 = Time.utc(2013, 5, 18, 12, 34) schedule = IceCube::Schedule.new(t0) schedule.add_recurrence_rule IceCube::Rule.daily - previous = schedule.previous_occurrences(999, t0 + 2 * ONE_DAY) - previous.should == [t0, t0 + ONE_DAY] + previous = schedule.previous_occurrences(999, t0 + 2 * IceCube::ONE_DAY) + previous.should == [t0, t0 + IceCube::ONE_DAY] end it 'returns empty array given the start time' do @@ -531,7 +609,7 @@ t1 = Time.utc(2013, 5, 31, 12, 34) schedule = IceCube::Schedule.new(t0) schedule.add_recurrence_rule IceCube::Rule.daily.until(t1 + 1) - schedule.last(2).should == [t1 - ONE_DAY, t1] + schedule.last(2).should == [t1 - IceCube::ONE_DAY, t1] end it 'raises an error for a non-terminating schedule' do