Skip to content

Commit 656a3b4

Browse files
committed
Merge pull request #302 from bookwhen/master
Option to include prior occurrences with overlapping duration
2 parents 1d18483 + 9742d42 commit 656a3b4

File tree

3 files changed

+103
-19
lines changed

3 files changed

+103
-19
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ schedule.remaining_occurrences # for terminating schedules
8585
schedule.previous_occurrence(from_time)
8686
schedule.previous_occurrences(3, from_time)
8787

88+
# or include prior occurrences with a duration overlapping from_time
89+
schedule.next_occurrences(3, from_time, :spans => true)
90+
schedule.occurrences_between(from_time, to_time, :spans => true)
8891

8992
# or give the schedule a duration and ask if occurring_at?
9093
schedule = IceCube::Schedule.new(now, :duration => 3600)

lib/ice_cube/schedule.rb

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@ def each_occurrence(&block)
166166
end
167167

168168
# The next n occurrences after now
169-
def next_occurrences(num, from = nil)
169+
def next_occurrences(num, from = nil, options = {})
170170
from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
171-
enumerate_occurrences(from + 1, nil).take(num)
171+
enumerate_occurrences(from + 1, nil, options).take(num)
172172
end
173173

174174
# The next occurrence after now (overridable)
175-
def next_occurrence(from = nil)
175+
def next_occurrence(from = nil, options = {})
176176
from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
177-
enumerate_occurrences(from + 1, nil).next
177+
enumerate_occurrences(from + 1, nil, options).next
178178
rescue StopIteration
179179
nil
180180
end
@@ -195,26 +195,26 @@ def previous_occurrences(num, from)
195195
end
196196

197197
# The remaining occurrences (same requirements as all_occurrences)
198-
def remaining_occurrences(from = nil)
198+
def remaining_occurrences(from = nil, options = {})
199199
require_terminating_rules
200200
from ||= TimeUtil.now(@start_time)
201-
enumerate_occurrences(from).to_a
201+
enumerate_occurrences(from, nil, options).to_a
202202
end
203203

204204
# Returns an enumerator for all remaining occurrences
205-
def remaining_occurrences_enumerator(from = nil)
205+
def remaining_occurrences_enumerator(from = nil, options = {})
206206
from ||= TimeUtil.now(@start_time)
207-
enumerate_occurrences(from)
207+
enumerate_occurrences(from, nil, options)
208208
end
209209

210210
# Occurrences between two times
211-
def occurrences_between(begin_time, closing_time)
212-
enumerate_occurrences(begin_time, closing_time).to_a
211+
def occurrences_between(begin_time, closing_time, options = {})
212+
enumerate_occurrences(begin_time, closing_time, options).to_a
213213
end
214214

215215
# Return a boolean indicating if an occurrence falls between two times
216-
def occurs_between?(begin_time, closing_time)
217-
enumerate_occurrences(begin_time, closing_time).next
216+
def occurs_between?(begin_time, closing_time, options = {})
217+
enumerate_occurrences(begin_time, closing_time, options).next
218218
true
219219
rescue StopIteration
220220
false
@@ -226,9 +226,7 @@ def occurs_between?(begin_time, closing_time)
226226
# occurrences at the end of the range since none of their duration
227227
# intersects the range.
228228
def occurring_between?(opening_time, closing_time)
229-
opening_time = opening_time - duration
230-
closing_time = closing_time - 1 if duration > 0
231-
occurs_between?(opening_time, closing_time)
229+
occurs_between?(opening_time, closing_time, :spans => true)
232230
end
233231

234232
# Return a boolean indicating if an occurrence falls on a certain date
@@ -407,25 +405,30 @@ def reset
407405
# Find all of the occurrences for the schedule between opening_time
408406
# and closing_time
409407
# Iteration is unrolled in pairs to skip duplicate times in end of DST
410-
def enumerate_occurrences(opening_time, closing_time = nil, &block)
408+
def enumerate_occurrences(opening_time, closing_time = nil, options = {}, &block)
411409
opening_time = TimeUtil.match_zone(opening_time, start_time)
412410
closing_time = TimeUtil.match_zone(closing_time, start_time)
413411
opening_time += start_time.subsec - opening_time.subsec rescue 0
414412
opening_time = start_time if opening_time < start_time
413+
spans = options[:spans] == true && duration != 0
415414
Enumerator.new do |yielder|
416415
reset
417-
t1 = full_required? ? start_time : realign(opening_time)
416+
t1 = full_required? ? start_time : realign((spans ? opening_time - duration : opening_time))
418417
loop do
419418
break unless (t0 = next_time(t1, closing_time))
420419
break if closing_time && t0 > closing_time
421-
yielder << (block_given? ? block.call(t0) : t0) if t0 >= opening_time
420+
if (spans ? (t0.end_time > opening_time) : (t0 >= opening_time))
421+
yielder << (block_given? ? block.call(t0) : t0)
422+
end
422423
break unless (t1 = next_time(t0 + 1, closing_time))
423424
break if closing_time && t1 > closing_time
424425
if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?)
425426
wind_back_dst
426427
next (t1 += 1)
427428
end
428-
yielder << (block_given? ? block.call(t1) : t1) if t1 >= opening_time
429+
if (spans ? (t1.end_time > opening_time) : (t1 >= opening_time))
430+
yielder << (block_given? ? block.call(t1) : t1)
431+
end
429432
next (t1 += 1)
430433
end
431434
end

spec/examples/schedule_spec.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require File.dirname(__FILE__) + '/../spec_helper'
2+
require 'benchmark'
23

34
describe IceCube::Schedule do
45

@@ -451,6 +452,83 @@
451452

452453
end
453454

455+
describe :spans do
456+
457+
it 'should find occurrence in past with duration beyond the start time' do
458+
t0 = Time.utc(2015, 10, 1, 15, 31)
459+
schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR)
460+
schedule.add_recurrence_rule IceCube::Rule.daily
461+
next_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, :spans => true)
462+
next_occ.should == t0
463+
end
464+
465+
it 'should include occurrence in past with duration beyond the start time' do
466+
t0 = Time.utc(2015, 10, 1, 15, 31)
467+
schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR)
468+
schedule.add_recurrence_rule IceCube::Rule.daily.count(2)
469+
occs = schedule.next_occurrences(10, t0 + IceCube::ONE_HOUR, :spans => true)
470+
occs.should == [t0, t0 + IceCube::ONE_DAY]
471+
end
472+
473+
it 'should allow duration span on remaining_occurrences' do
474+
t0 = Time.utc(2015, 10, 1, 00, 00)
475+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY)
476+
schedule.add_recurrence_rule IceCube::Rule.daily.count(3)
477+
occs = schedule.remaining_occurrences(t0 + IceCube::ONE_DAY + IceCube::ONE_HOUR, :spans => true)
478+
occs.should == [t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY]
479+
end
480+
481+
it 'should include occurrences with duration spanning the requested start time' do
482+
t0 = Time.utc(2015, 10, 1, 15, 31)
483+
schedule = IceCube::Schedule.new(t0, :duration => 30 * IceCube::ONE_DAY)
484+
long_event = schedule.remaining_occurrences_enumerator(t0 + IceCube::ONE_DAY, :spans => true).take(1)
485+
long_event.should == [t0]
486+
end
487+
488+
it 'should find occurrences between including previous one with duration spanning start' do
489+
t0 = Time.utc(2015, 10, 1, 10, 00)
490+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR)
491+
schedule.add_recurrence_rule IceCube::Rule.hourly.count(10)
492+
occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR + 1, t0 + 3 * IceCube::ONE_HOUR + 1, :spans => true)
493+
occs.length.should == 3
494+
end
495+
496+
it 'should include long occurrences starting before and ending after' do
497+
t0 = Time.utc(2015, 10, 1, 00, 00)
498+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY)
499+
occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR, t0 + IceCube::ONE_DAY - IceCube::ONE_HOUR, :spans => true)
500+
occs.should == [t0]
501+
end
502+
503+
it 'should not find occurrence with duration ending on start time' do
504+
t0 = Time.utc(2015, 10, 1, 12, 00)
505+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR)
506+
schedule.occurs_between?(t0 + IceCube::ONE_HOUR, t0 + 2 * IceCube::ONE_HOUR, :spans => true).should be_false
507+
end
508+
509+
it 'should quickly fetch a future time from a recurring schedule' do
510+
t0 = Time.utc(2000, 10, 1, 00, 00)
511+
t1 = Time.utc(2015, 10, 1, 12, 00)
512+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR - 1)
513+
schedule.add_recurrence_rule IceCube::Rule.hourly
514+
occ = nil
515+
timing = Benchmark.realtime do
516+
occ = schedule.remaining_occurrences_enumerator(t1, :spans => true).take(1)
517+
end
518+
timing.should < 0.1
519+
occ.should == [t1]
520+
end
521+
522+
it 'should not include occurrence ending on start time' do
523+
t0 = Time.utc(2015, 10, 1, 10, 00)
524+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR / 2)
525+
schedule.add_recurrence_rule IceCube::Rule.minutely(30).count(6)
526+
third_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, :spans => true)
527+
third_occ.should == t0 + IceCube::ONE_HOUR
528+
end
529+
530+
end
531+
454532
describe :previous_occurrence do
455533

456534
it 'returns the previous occurrence for a time in the schedule' do

0 commit comments

Comments
 (0)