From c93fa7e72ed633e70180e186381761ae7131a33f Mon Sep 17 00:00:00 2001 From: Andrew Vit Date: Mon, 24 Mar 2014 22:42:13 -0700 Subject: [PATCH] Skip double occurrences over DST Fixed #189 --- lib/ice_cube/schedule.rb | 31 ++++++++++++------- lib/ice_cube/time_util.rb | 6 ++++ lib/ice_cube/validated_rule.rb | 19 ++++++++---- lib/ice_cube/validations/count.rb | 4 +++ lib/ice_cube/validations/daily_interval.rb | 4 +++ lib/ice_cube/validations/day.rb | 4 +++ lib/ice_cube/validations/day_of_month.rb | 4 +++ lib/ice_cube/validations/day_of_week.rb | 4 +++ lib/ice_cube/validations/day_of_year.rb | 4 +++ lib/ice_cube/validations/hour_of_day.rb | 4 +++ lib/ice_cube/validations/minute_of_hour.rb | 4 +++ lib/ice_cube/validations/month_of_year.rb | 4 +++ lib/ice_cube/validations/monthly_interval.rb | 4 +++ lib/ice_cube/validations/second_of_minute.rb | 4 +++ lib/ice_cube/validations/until.rb | 4 +++ lib/ice_cube/validations/weekly_interval.rb | 4 +++ lib/ice_cube/validations/yearly_interval.rb | 4 +++ spec/examples/dst_spec.rb | 32 ++++++++++++++++++++ 18 files changed, 127 insertions(+), 17 deletions(-) diff --git a/lib/ice_cube/schedule.rb b/lib/ice_cube/schedule.rb index 17c2c490..9ae6d2ab 100644 --- a/lib/ice_cube/schedule.rb +++ b/lib/ice_cube/schedule.rb @@ -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 @@ -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?) @@ -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 diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index 4d696d79..f64bc3af 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -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) @@ -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 diff --git a/lib/ice_cube/validated_rule.rb b/lib/ice_cube/validated_rule.rb index a705f6d2..dc4ad91d 100644 --- a/lib/ice_cube/validated_rule.rb +++ b/lib/ice_cube/validated_rule.rb @@ -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| @@ -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 diff --git a/lib/ice_cube/validations/count.rb b/lib/ice_cube/validations/count.rb index 8b6a00b0..61099433 100644 --- a/lib/ice_cube/validations/count.rb +++ b/lib/ice_cube/validations/count.rb @@ -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 diff --git a/lib/ice_cube/validations/daily_interval.rb b/lib/ice_cube/validations/daily_interval.rb index 82f4c064..1b9fdc95 100644 --- a/lib/ice_cube/validations/daily_interval.rb +++ b/lib/ice_cube/validations/daily_interval.rb @@ -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) - diff --git a/lib/ice_cube/validations/day.rb b/lib/ice_cube/validations/day.rb index 14d4d0c2..1a64cd92 100644 --- a/lib/ice_cube/validations/day.rb +++ b/lib/ice_cube/validations/day.rb @@ -31,6 +31,10 @@ def type :wday end + def dst_adjust? + true + end + def build_s(builder) builder.piece(:day) << day end diff --git a/lib/ice_cube/validations/day_of_month.rb b/lib/ice_cube/validations/day_of_month.rb index 3d36b558..58383b2b 100644 --- a/lib/ice_cube/validations/day_of_month.rb +++ b/lib/ice_cube/validations/day_of_month.rb @@ -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 diff --git a/lib/ice_cube/validations/day_of_week.rb b/lib/ice_cube/validations/day_of_week.rb index aec5fa49..de40040e 100644 --- a/lib/ice_cube/validations/day_of_week.rb +++ b/lib/ice_cube/validations/day_of_week.rb @@ -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) diff --git a/lib/ice_cube/validations/day_of_year.rb b/lib/ice_cube/validations/day_of_year.rb index 48741498..b68c980b 100644 --- a/lib/ice_cube/validations/day_of_year.rb +++ b/lib/ice_cube/validations/day_of_year.rb @@ -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 diff --git a/lib/ice_cube/validations/hour_of_day.rb b/lib/ice_cube/validations/hour_of_day.rb index 9311ed38..e35d44e7 100644 --- a/lib/ice_cube/validations/hour_of_day.rb +++ b/lib/ice_cube/validations/hour_of_day.rb @@ -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 diff --git a/lib/ice_cube/validations/minute_of_hour.rb b/lib/ice_cube/validations/minute_of_hour.rb index 6956d1ea..20faa2de 100644 --- a/lib/ice_cube/validations/minute_of_hour.rb +++ b/lib/ice_cube/validations/minute_of_hour.rb @@ -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 diff --git a/lib/ice_cube/validations/month_of_year.rb b/lib/ice_cube/validations/month_of_year.rb index c2abb635..f1794580 100644 --- a/lib/ice_cube/validations/month_of_year.rb +++ b/lib/ice_cube/validations/month_of_year.rb @@ -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 diff --git a/lib/ice_cube/validations/monthly_interval.rb b/lib/ice_cube/validations/monthly_interval.rb index e620b2a7..5214da0e 100644 --- a/lib/ice_cube/validations/monthly_interval.rb +++ b/lib/ice_cube/validations/monthly_interval.rb @@ -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) + diff --git a/lib/ice_cube/validations/second_of_minute.rb b/lib/ice_cube/validations/second_of_minute.rb index addaa264..354e54a3 100644 --- a/lib/ice_cube/validations/second_of_minute.rb +++ b/lib/ice_cube/validations/second_of_minute.rb @@ -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 diff --git a/lib/ice_cube/validations/until.rb b/lib/ice_cube/validations/until.rb index 9daa83ad..d834e431 100644 --- a/lib/ice_cube/validations/until.rb +++ b/lib/ice_cube/validations/until.rb @@ -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 diff --git a/lib/ice_cube/validations/weekly_interval.rb b/lib/ice_cube/validations/weekly_interval.rb index 46cd8ae8..0d78de17 100644 --- a/lib/ice_cube/validations/weekly_interval.rb +++ b/lib/ice_cube/validations/weekly_interval.rb @@ -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) diff --git a/lib/ice_cube/validations/yearly_interval.rb b/lib/ice_cube/validations/yearly_interval.rb index d29b467b..79b8ca22 100644 --- a/lib/ice_cube/validations/yearly_interval.rb +++ b/lib/ice_cube/validations/yearly_interval.rb @@ -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? diff --git a/spec/examples/dst_spec.rb b/spec/examples/dst_spec.rb index a308ea36..42e72d1e 100644 --- a/spec/examples/dst_spec.rb +++ b/spec/examples/dst_spec.rb @@ -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