Skip to content

Commit 87a03e8

Browse files
committed
Option to include prior occurrences with overlapping duration
Adding an option on schedule methods to include occurrences whose duration intersects a time window. Issue #154 #154
1 parent 6868b2c commit 87a03e8

File tree

3 files changed

+80
-19
lines changed

3 files changed

+80
-19
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ schedule.last(2) # [now + 1.day, now + 2.days]
7777
schedule.last # now + 2.days
7878

7979
# or the next occurrence
80-
schedule.next_occurrence(from_time) # defaults to Time.now
81-
schedule.next_occurrences(3, from_time) # defaults to Time.now
82-
schedule.remaining_occurrences # for terminating schedules
80+
schedule.next_occurrence(from_time) # defaults to Time.now
81+
schedule.next_occurrences(3, from_time) # defaults to Time.now
82+
schedule.next_occurrences(3, from_time, true) # include prior occurrences with duration overlapping from_time
83+
schedule.remaining_occurrences # for terminating schedules
8384

8485
# or the previous occurrence
8586
schedule.previous_occurrence(from_time)

lib/ice_cube/schedule.rb

Lines changed: 20 additions & 16 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, spans = false)
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, spans).take(num)
172172
end
173173

174174
# The next occurrence after now (overridable)
175-
def next_occurrence(from = nil)
175+
def next_occurrence(from = nil, spans = false)
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, spans).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, spans = false)
199199
require_terminating_rules
200200
from ||= TimeUtil.now(@start_time)
201-
enumerate_occurrences(from).to_a
201+
enumerate_occurrences(from, nil, spans).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, spans = false)
206206
from ||= TimeUtil.now(@start_time)
207-
enumerate_occurrences(from)
207+
enumerate_occurrences(from, nil, spans)
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, spans = false)
212+
enumerate_occurrences(begin_time, closing_time, spans).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, spans = false)
217+
enumerate_occurrences(begin_time, closing_time, spans).next
218218
true
219219
rescue StopIteration
220220
false
@@ -404,25 +404,29 @@ def reset
404404
# Find all of the occurrences for the schedule between opening_time
405405
# and closing_time
406406
# Iteration is unrolled in pairs to skip duplicate times in end of DST
407-
def enumerate_occurrences(opening_time, closing_time = nil, &block)
407+
def enumerate_occurrences(opening_time, closing_time = nil, spans = false, &block)
408408
opening_time = TimeUtil.match_zone(opening_time, start_time)
409409
closing_time = TimeUtil.match_zone(closing_time, start_time)
410410
opening_time += start_time.subsec - opening_time.subsec rescue 0
411411
opening_time = start_time if opening_time < start_time
412412
Enumerator.new do |yielder|
413413
reset
414-
t1 = full_required? ? start_time : realign(opening_time)
414+
t1 = full_required? || spans ? start_time : realign(opening_time)
415415
loop do
416416
break unless (t0 = next_time(t1, closing_time))
417417
break if closing_time && t0 > closing_time
418-
yielder << (block_given? ? block.call(t0) : t0) if t0 >= opening_time
418+
if (spans ? t0.end_time : t0) >= opening_time
419+
yielder << (block_given? ? block.call(t0) : t0)
420+
end
419421
break unless (t1 = next_time(t0 + 1, closing_time))
420422
break if closing_time && t1 > closing_time
421423
if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?)
422424
wind_back_dst
423425
next (t1 += 1)
424426
end
425-
yielder << (block_given? ? block.call(t1) : t1) if t1 >= opening_time
427+
if (spans ? t1.end_time : t1) >= opening_time
428+
yielder << (block_given? ? block.call(t1) : t1)
429+
end
426430
next (t1 += 1)
427431
end
428432
end

spec/examples/schedule_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,62 @@
451451

452452
end
453453

454+
describe :spans do
455+
456+
it 'should find occurrence in past with duration beyond the start time' do
457+
t0 = Time.utc(2015, 10, 1, 15, 31)
458+
schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR)
459+
schedule.add_recurrence_rule IceCube::Rule.daily
460+
next_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, true)
461+
next_occ.should == t0
462+
end
463+
464+
it 'should include occurrence in past with duration beyond the start time' do
465+
t0 = Time.utc(2015, 10, 1, 15, 31)
466+
schedule = IceCube::Schedule.new(t0, :duration => 2 * IceCube::ONE_HOUR)
467+
schedule.add_recurrence_rule IceCube::Rule.daily.count(2)
468+
occs = schedule.next_occurrences(10, t0 + IceCube::ONE_HOUR, true)
469+
occs.should == [t0, t0 + IceCube::ONE_DAY]
470+
end
471+
472+
it 'should allow duration span on remaining_occurrences' do
473+
t0 = Time.utc(2015, 10, 1, 00, 00)
474+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY)
475+
schedule.add_recurrence_rule IceCube::Rule.daily.count(3)
476+
occs = schedule.remaining_occurrences(t0 + IceCube::ONE_DAY + IceCube::ONE_HOUR, true)
477+
occs.should == [t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY]
478+
end
479+
480+
it 'should include occurrences with duration spanning the requested start time' do
481+
t0 = Time.utc(2015, 10, 1, 15, 31)
482+
schedule = IceCube::Schedule.new(t0, :duration => 30 * IceCube::ONE_DAY)
483+
long_event = schedule.remaining_occurrences_enumerator(t0 + IceCube::ONE_DAY, true).take(1)
484+
long_event.should == [t0]
485+
end
486+
487+
it 'should find occurrences between including previous one with duration spanning start' do
488+
t0 = Time.utc(2015, 10, 1, 10, 00)
489+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR)
490+
schedule.add_recurrence_rule IceCube::Rule.hourly.count(10)
491+
occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR + 1, t0 + 3 * IceCube::ONE_HOUR + 1, true)
492+
occs.length.should == 3
493+
end
494+
495+
it 'should include long occurrences starting before and ending after' do
496+
t0 = Time.utc(2015, 10, 1, 00, 00)
497+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_DAY)
498+
occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR, t0 + IceCube::ONE_DAY - IceCube::ONE_HOUR, true)
499+
occs.should == [t0]
500+
end
501+
502+
it 'should include long occurrences starting before and ending after' do
503+
t0 = Time.utc(2015, 10, 1, 12, 00)
504+
schedule = IceCube::Schedule.new(t0, :duration => IceCube::ONE_HOUR)
505+
schedule.occurs_between?(t0 + IceCube::ONE_HOUR, t0 + 2 * IceCube::ONE_HOUR, true).should be_true
506+
end
507+
508+
end
509+
454510
describe :previous_occurrence do
455511

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

0 commit comments

Comments
 (0)