Skip to content

Commit

Permalink
Skip double occurrences over DST
Browse files Browse the repository at this point in the history
Fixed #189
  • Loading branch information
avit committed Mar 25, 2014
1 parent 2d88c80 commit c93fa7e
Show file tree
Hide file tree
Showing 18 changed files with 127 additions and 17 deletions.
31 changes: 20 additions & 11 deletions lib/ice_cube/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -397,25 +397,27 @@ 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)
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
reset
opening_time = start_time if opening_time < start_time
# walk up to the opening time - and off we go
# If we have rules with counts, we need to walk from the beginning of time,
# otherwise opening_time
time = full_required? ? start_time : opening_time
t1 = full_required? ? start_time : opening_time
e = Enumerator.new do |yielder|
loop do
res = next_time(time, closing_time)
break unless res
break if closing_time && res > closing_time
if res >= opening_time
yielder.yield (block_given? ? block.call(res) : res)
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
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
time = res + 1
yielder << (block_given? ? block.call(t1) : t1) if t1 >= opening_time
t1 += 1
end
end
end
Expand All @@ -437,7 +439,8 @@ def next_time(time, closing_time)
end
end

# Return a boolean indicating if any rule needs to be run from the start of time
# Indicate if any rule needs to be run from the start of time
# If we have rules with counts, we need to walk from the beginning of time
def full_required?
@all_recurrence_rules.any?(&:full_required?) ||
@all_exception_rules.any?(&:full_required?)
Expand Down Expand Up @@ -481,6 +484,12 @@ def recurrence_rules_with_implicit_start_occurrence
end
end

def wind_back_dst
recurrence_rules.each do |rule|
rule.skipped_for_dst
end
end

end

end
6 changes: 6 additions & 0 deletions lib/ice_cube/time_util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ module TimeUtil
:november => 11, :december => 12
}

CLOCK_VALUES = [:year, :month, :day, :hour, :min, :sec]

# Provides a Time.now without the usec, in the reference zone or utc offset
def self.now(reference=Time.now)
match_zone(Time.at(Time.now.to_i), reference)
Expand Down Expand Up @@ -210,6 +212,10 @@ def self.dst_change(time)
end
end

def self.same_clock?(t1, t2)
CLOCK_VALUES.all? { |i| t1.send(i) == t2.send(i) }
end

# A utility class for safely moving time around
class TimeWrapper

Expand Down
19 changes: 13 additions & 6 deletions lib/ice_cube/validated_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def next_time(time, schedule, closing_time)
@time
end

def skipped_for_dst
@uses -= 1 if @uses > 0
end

def dst_adjust?
@validations[:interval].any? &:dst_adjust?
end

def to_s
builder = StringBuilder.new
@validations.each do |name, validations|
Expand Down Expand Up @@ -132,12 +140,11 @@ def validated_results(validations_for_type)
end

def shift_time_by_validation(res, vals)
return unless res.min
type = vals.first.type # get the jump type
dst_adjust = !vals.first.respond_to?(:dst_adjust?) || vals.first.dst_adjust?
wrapper = TimeUtil::TimeWrapper.new(@time, dst_adjust)
wrapper.add(type, res.min)
wrapper.clear_below(type)
return unless (interval = res.min)
validation = vals.first
wrapper = TimeUtil::TimeWrapper.new(@time, validation.dst_adjust?)
wrapper.add(validation.type, interval)
wrapper.clear_below(validation.type)

# Move over DST if blocked, no adjustments
if wrapper.to_time <= @time
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/count.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def type
:limit
end

def dst_adjust?
false
end

def validate(time, schedule)
raise CountExceeded if rule.uses && rule.uses >= count
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/daily_interval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def type
:day
end

def dst_adjust?
true
end

def validate(step_time, schedule)
t0, t1 = schedule.start_time, step_time
days = Date.new(t1.year, t1.month, t1.day) -
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/day.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def type
:wday
end

def dst_adjust?
true
end

def build_s(builder)
builder.piece(:day) << day
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/day_of_month.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def type
:day
end

def dst_adjust?
true
end

def build_s(builder)
builder.piece(:day_of_month) << StringBuilder.nice_number(day)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/day_of_week.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def type
:day
end

def dst_adjust?
true
end

def validate(step_time, schedule)
wday = step_time.wday
offset = (day < wday) ? (7 - wday + day) : (day - wday)
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/day_of_year.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def type
:day
end

def dst_adjust?
true
end

def validate(step_time, schedule)
days_in_year = TimeUtil.days_in_year(step_time)
yday = day < 0 ? day + days_in_year : day
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/hour_of_day.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def type
:hour
end

def dst_adjust?
true
end

def build_s(builder)
builder.piece(:hour_of_day) << StringBuilder.nice_number(hour)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/minute_of_hour.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def type
:min
end

def dst_adjust?
false
end

def build_s(builder)
builder.piece(:minute_of_hour) << StringBuilder.nice_number(minute)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/month_of_year.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def type
:month
end

def dst_adjust?
true
end

def build_s(builder)
builder.piece(:month_of_year) << Date::MONTHNAMES[month]
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/monthly_interval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def type
:month
end

def dst_adjust?
true
end

def validate(step_time, schedule)
t0, t1 = schedule.start_time, step_time
months = (t1.month - t0.month) +
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/second_of_minute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def type
:sec
end

def dst_adjust?
false
end

def build_s(builder)
builder.piece(:second_of_minute) << StringBuilder.nice_number(second)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/until.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def type
:limit
end

def dst_adjust?
false
end

def validate(step_time, schedule)
raise UntilExceeded if step_time > time
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/weekly_interval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def type
:day
end

def dst_adjust?
true
end

def validate(step_time, schedule)
t0, t1 = schedule.start_time, step_time
d0 = Date.new(t0.year, t0.month, t0.day)
Expand Down
4 changes: 4 additions & 0 deletions lib/ice_cube/validations/yearly_interval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def type
:year
end

def dst_adjust?
true
end

def validate(step_time, schedule)
years = step_time.year - schedule.start_time.year
offset = (years % interval).nonzero?
Expand Down
32 changes: 32 additions & 0 deletions spec/examples/dst_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,36 @@
schedule.first(3).should == [Time.local(2010, 4, 10, 12, 0, 0), Time.local(2011, 4, 10, 12, 0, 0), Time.local(2012, 4, 10, 12, 0, 0)]
end

it "skips double occurrences from end of DST" do
Time.zone = "America/Denver"
t0 = Time.zone.parse("Sun, 03 Nov 2013 01:30:00 MDT -06:00")
schedule = IceCube::Schedule.new(t0) { |s| s.rrule IceCube::Rule.daily.count(3) }
schedule.all_occurrences.should == [t0, t0 + 25*ONE_HOUR, t0 + 49*ONE_HOUR]
end

it "does not skip hourly rules over DST" do
Time.zone = "America/Denver"
t0 = Time.zone.parse("Sun, 03 Nov 2013 01:30:00 MDT -06:00")
schedule = IceCube::Schedule.new(t0) { |s| s.rrule IceCube::Rule.hourly.count(3) }
schedule.all_occurrences.should == [t0, t0 + ONE_HOUR, t0 + 2*ONE_HOUR]
end

it "does not skip minutely rules with minute of hour over DST" do
Time.zone = "America/Denver"
t0 = Time.zone.parse("Sun, 03 Nov 2013 01:30:00 MDT -06:00")
schedule = IceCube::Schedule.new(t0) { |s| s.rrule IceCube::Rule.hourly.count(3) }
schedule.rrule IceCube::Rule.minutely.minute_of_hour([0, 15, 30, 45])
schedule.first(5).should == [t0, t0 + 15*60, t0 + 30*60, t0 + 45*60, t0 + 60*60]
end

it "does not skip minutely rules with second of minute over DST" do
Time.zone = "America/Denver"
t0 = Time.zone.parse("Sun, 03 Nov 2013 01:30:00 MDT -06:00")
schedule = IceCube::Schedule.new(t0) { |s| s.rrule IceCube::Rule.hourly.count(3) }
schedule.rrule IceCube::Rule.minutely(15).second_of_minute(0)
schedule.first(5).should == [t0, t0 + 15*60, t0 + 30*60, t0 + 45*60, t0 + 60*60]
end



end

0 comments on commit c93fa7e

Please sign in to comment.