diff --git a/lib/ice_cube/rules/weekly_rule.rb b/lib/ice_cube/rules/weekly_rule.rb index 6c382d10..4b78733c 100644 --- a/lib/ice_cube/rules/weekly_rule.rb +++ b/lib/ice_cube/rules/weekly_rule.rb @@ -36,12 +36,15 @@ def realign(step_time, start_time) offset = wday_offset(step_time, start_time) time.add(:day, offset) realigned_time = time.to_time - # when the realigned time is in a different hour, we need to adjust the - # time to the correct hour with a fixed timezone offset, otherwise - # the time will be off by an hour - # WARNING: if the next DST change is within the interval, the occurrences - # after the next DST change will be off by an hour because the timezone is fixed - realigned_time = time.to_timezoneless_time if realigned_time.hour != start_time.hour + # when the realigned time is in a different hour, it means that + # time falls to the DST switch timespan. In this case, we need to + # move the time back by one day to ensure that the hour stays the same + # WARNING: this could not work if the DST change is on a monday + # as the realigned time would be moved to the previous week. + if realigned_time.hour != start_time.hour + time.add(:day, -1) + realigned_time = time.to_time + end super step_time, realigned_time end diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index 5db12ab3..001d927d 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -282,22 +282,6 @@ def to_time TimeUtil.build_in_zone(parts, @base) end - # This is used keep the correct hour within the interval during DST - # changes. It will use the time from the schedule start time to lock the - # hour. - def to_timezoneless_time - unwrapped_time = to_time - Time.new( - unwrapped_time.year, - unwrapped_time.month, - unwrapped_time.day, - @time.hour, - @time.min, - @time.sec, - unwrapped_time.utc_offset - ) - end - # DST-safely add an interval of time to the wrapped time def add(type, val) type = :day if type == :wday diff --git a/spec/examples/schedule_spec.rb b/spec/examples/schedule_spec.rb index ccc20d56..5b00e994 100644 --- a/spec/examples/schedule_spec.rb +++ b/spec/examples/schedule_spec.rb @@ -391,18 +391,17 @@ Time.utc(2014, 1, 2, 0o1, 34, 56)] end - context "Cairo timezone", system_time_zone: 'Africa/Cairo' do + context "Cairo timezone" do require "active_support/time" let(:schedule) do - IceCube::Schedule.from_yaml("---\n:start_time:\n :time: 2022-05-05 22:20:00.000000000 Z\n :zone: Africa/Cairo\n:end_time:\n :time: 2022-05-06 21:40:00.000000000 Z\n :zone: Africa/Cairo\n:rrules:\n- :validations:\n :day:\n - 5\n :rule_type: IceCube::WeeklyRule\n :interval: 1\n :week_start: 1\n:rtimes: []\n:extimes: []\n") - # IceCube::Schedule.new(ActiveSupport::TimeZone['Africa/Cairo'].parse("2022-05-05 00:20:00")).tap do |schedule| - # schedule.add_recurrence_rule IceCube::Rule.weekly.day(:friday) - # end + IceCube::Schedule.new(ActiveSupport::TimeZone['Africa/Cairo'].parse("2022-05-05 00:20:00")).tap do |schedule| + schedule.add_recurrence_rule IceCube::Rule.weekly.day(:friday) + end end it "has the correct start time" do - expect(schedule.start_time.iso8601).to eq("2022-05-06T00:20:00+02:00") + expect(schedule.start_time.iso8601).to eq("2022-05-05T00:20:00+02:00") end it "has the correct start time timezone" do @@ -412,7 +411,7 @@ it "calculates the correct occurrences from 2024-04-24" do occurrences = schedule.next_occurrences(3, Time.utc(2024, 4, 24, 12, 0, 0)) expect(occurrences.map(&:iso8601)).to eq([ - "2024-04-26T00:20:00+03:00", + "2024-04-26T01:20:00+03:00", "2024-05-03T00:20:00+03:00", "2024-05-10T00:20:00+03:00", ]) @@ -426,6 +425,15 @@ "2024-05-03T00:20:00+03:00", ]) end + + it "preserves the timezone for the next DST switch" do + occurrences = schedule.next_occurrences(28, Time.utc(2024, 4, 24, 12, 0, 0)) + expect(occurrences.map(&:iso8601).last(3)).to eq([ + "2024-10-18T00:20:00+03:00", + "2024-10-25T00:20:00+03:00", + "2024-11-01T00:20:00+02:00", + ]) + end end end diff --git a/spec/examples/weekly_rule_spec.rb b/spec/examples/weekly_rule_spec.rb index dc61bab3..259d07b8 100644 --- a/spec/examples/weekly_rule_spec.rb +++ b/spec/examples/weekly_rule_spec.rb @@ -402,7 +402,7 @@ module IceCube end it "calculates the correct time from 2024-04-24 12:00:00 UTC" do - expect(rule.next_time(utc_tz.parse("2024-04-24 12:00:00"), start_time, nil).iso8601).to eq("2024-04-26T00:20:00+03:00") + expect(rule.next_time(utc_tz.parse("2024-04-24 12:00:00"), start_time, nil).iso8601).to eq("2024-04-26T01:20:00+03:00") end it "calculates the correct time from 2024-04-26 00:20:01 Africa/Cairo" do @@ -412,7 +412,7 @@ module IceCube describe :realign do require "active_support/time" - + let(:timezone_name) { "Africa/Cairo" } let(:timezone) { ActiveSupport::TimeZone[timezone_name] } let(:utc_tz) { ActiveSupport::TimeZone["UTC"] } @@ -424,10 +424,10 @@ module IceCube subject { rule.realign(time, start_time) } it "realigns the start time to the correct time" do - expect(subject.iso8601).to eq("2024-04-26T00:20:00+03:00") + expect(subject.iso8601).to eq("2024-04-25T00:20:00+02:00") end - context "Berlin timezone" do + context "Berlin timezone CET -> CEST " do let(:recurrence_day) { :sunday } let(:timezone_name) { "Europe/Berlin" } let(:start_time) { timezone.parse("2024-03-24 02:30:00") } @@ -439,7 +439,18 @@ module IceCube # would result in faulty start times for the following # occurrences (03:30 instead of 02:30) it "realigns the start time to the correct time" do - expect(subject.iso8601).to eq("2024-03-31T02:30:00+02:00") + expect(subject.iso8601).to eq("2024-03-30T02:30:00+01:00") + end + end + + context "Berlin timezone CEST -> CET " do + let(:recurrence_day) { :sunday } + let(:timezone_name) { "Europe/Berlin" } + let(:start_time) { timezone.parse("2023-10-22 02:30:00") } + let(:time) { timezone.parse("2023-10-24 02:30:00") } + + it "realigns the start time to the correct time" do + expect(subject.iso8601).to eq("2023-10-29T02:30:00+02:00") end end end