From 5e630555e0cd79a2c7a8845766673bef9a2b9951 Mon Sep 17 00:00:00 2001 From: Marcus Deans Date: Sat, 22 Jun 2024 13:24:42 -0500 Subject: [PATCH] feat: add timezone parsing support with tests from @epologee --- CHANGELOG.md | 1 + lib/ice_cube/parsers/ical_parser.rb | 30 ++++++++++++-- spec/examples/from_ical_spec.rb | 63 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c869887..bf4d3b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Indonesian translations. ([#505](https://github.com/seejohnrun/ice_cube/pull/505)) by [@achmiral](https://github.com/achmiral) +- The `IceCube::IcalParser` finds the time zones matching an iCal schedule's date/time strings, accomodating for times with daylight savings ([#555](https://github.com/ice-cube-ruby/ice_cube/pull/555)) by [@jankeesvw](https://github.com/jankeesvw) and [@epologee](https://github.com/epologee) ### Changed - Removed use of `delegate` method added in [66f1d797](https://github.com/ice-cube-ruby/ice_cube/commit/66f1d797092734563bfabd2132c024c7d087f683) , reverting to previous implementation. ([#522](https://github.com/ice-cube-ruby/ice_cube/pull/522)) by [@pacso](https://github.com/pacso) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 429bd84c..e3e15061 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -4,18 +4,27 @@ def self.schedule_from_ical(ical_string, options = {}) data = {} ical_string.each_line do |line| (property, value) = line.split(":") - (property, _tzid) = property.split(";") + (property, tzid) = property.split(";") + zone = find_zone(tzid, value) if tzid case property when "DTSTART" + value = { time: value, zone: zone } if zone data[:start_time] = TimeUtil.deserialize_time(value) when "DTEND" + value = { time: value, zone: zone } if zone data[:end_time] = TimeUtil.deserialize_time(value) when "RDATE" data[:rtimes] ||= [] - data[:rtimes] += value.split(",").map { |v| TimeUtil.deserialize_time(v) } + data[:rtimes] += value.split(",").map do |v| + v = {time: v, zone: zone} if zone + TimeUtil.deserialize_time(v) + end when "EXDATE" data[:extimes] ||= [] - data[:extimes] += value.split(",").map { |v| TimeUtil.deserialize_time(v) } + data[:extimes] += value.split(",").map do |v| + v = {time: v, zone: zone} if zone + TimeUtil.deserialize_time(v) + end when "DURATION" data[:duration] # FIXME when "RRULE" @@ -83,5 +92,20 @@ def self.rule_from_ical(ical) Rule.from_hash(params) end + + private_class_method def self.find_zone(tzid, time_string) + (_, zone) = tzid&.split("=") + begin + Time.find_zone!(zone) if zone + rescue ArgumentError + (rails_zone, _tzinfo_id) = ActiveSupport::TimeZone::MAPPING.find do |(k, _)| + time = Time.parse(time_string) + + Time.find_zone!(k).local(time.year, time.month, time.day, time.hour, time.min).strftime("%Z") == zone + end + + Time.find_zone(rails_zone) + end + end end end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 2ab66c3c..80f5fa4c 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -348,6 +348,69 @@ def sorted_ical(ical) end end + describe "time zone support" do + it "parses start time with the correct time zone" do + schedule = IceCube::Schedule.from_ical ical_string_with_multiple_rules + + expect(schedule.start_time).to eq Time.find_zone!("America/Chicago").local(2015, 10, 5, 19, 55, 41) + end + + it "parses time zones correctly" do + schedule = IceCube::Schedule.from_ical ical_string_with_multiple_exdates_and_rdates + + utc_times = [ + schedule.recurrence_rules.map(&:until_time) + ].flatten + + denver_times = [ + schedule.start_time, + schedule.end_time, + schedule.exception_times, + schedule.rtimes + ].flatten + + utc_times.each do |t| + expect(t.zone).to eq "UTC" + end + + denver_times.each do |t| + expect(t.zone).to eq "MDT" + end + end + + it "round trips from and to ical with time zones in the summer (MDT)" do + original = <<-ICAL.gsub(/^\s*/, "").strip + DTSTART;TZID=MDT:20130731T143000 + RRULE:FREQ=WEEKLY;UNTIL=20140730T203000Z;BYDAY=MO,WE,FR + RDATE;TZID=MDT:20150812T143000 + RDATE;TZID=MDT:20150807T143000 + EXDATE;TZID=MDT:20130823T143000 + EXDATE;TZID=MDT:20130812T143000 + EXDATE;TZID=MDT:20130807T143000 + DTEND;TZID=MDT:20130731T153000 + ICAL + + schedule_from_ical = IceCube::Schedule.from_ical original + expect(schedule_from_ical.to_ical).to eq original + end + + it "round trips from and to ical with time zones in the winter (MST)" do + original = <<-ICAL.gsub(/^\s*/, "").strip + DTSTART;TZID=MST:20130131T143000 + RRULE:FREQ=WEEKLY;UNTIL=20140130T203000Z;BYDAY=MO,WE,FR + RDATE;TZID=MST:20150212T143000 + RDATE;TZID=MST:20150207T143000 + EXDATE;TZID=MST:20130223T143000 + EXDATE;TZID=MST:20130212T143000 + EXDATE;TZID=MST:20130207T143000 + DTEND;TZID=MST:20130131T153000 + ICAL + + schedule_from_ical = IceCube::Schedule.from_ical original + expect(schedule_from_ical.to_ical).to eq original + end + end + describe "exceptions" do it "handles single EXDATE lines, single RDATE lines" do start_time = Time.now