Skip to content

Commit

Permalink
Merge pull request #302 from bookwhen/master
Browse files Browse the repository at this point in the history
Option to include prior occurrences with overlapping duration
  • Loading branch information
seejohnrun committed Feb 23, 2016
2 parents 1d18483 + 9742d42 commit 656a3b4
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 19 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 22 additions & 19 deletions lib/ice_cube/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -407,25 +405,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
Expand Down
78 changes: 78 additions & 0 deletions spec/examples/schedule_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require File.dirname(__FILE__) + '/../spec_helper'
require 'benchmark'

describe IceCube::Schedule do

Expand Down Expand Up @@ -451,6 +452,83 @@

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
Expand Down

0 comments on commit 656a3b4

Please sign in to comment.