Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
Expand Down
92 changes: 85 additions & 7 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,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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down