From ee6ce49ded3b0a3acbca1249a71dd18c8a5f4560 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 28 Apr 2022 11:17:52 +0200 Subject: [PATCH 1/8] Expose time zone issue when parsing from iCal format Co-authored-by: Jankees van Woezik --- spec/examples/from_ical_spec.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 2ab66c3c..4d9471c4 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -348,6 +348,37 @@ def sorted_ical(ical) end end + describe "time zones" 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 + end + describe "exceptions" do it "handles single EXDATE lines, single RDATE lines" do start_time = Time.now From 740125c10fa3120d75eacab4a22cc522cb929e17 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 28 Apr 2022 12:33:03 +0200 Subject: [PATCH 2/8] Spec that from_ical and to_ical can perform round trip conversion Co-authored-by: Jankees van Woezik --- spec/examples/from_ical_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 4d9471c4..f85c4ba8 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -377,6 +377,22 @@ def sorted_ical(ical) expect(t.zone).to eq "MDT" end end + + it "round trips from and to ical with time zones" 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 end describe "exceptions" do From f1d7f6e1db4b531cb7fda53f724cf12631d09c7b Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 28 Apr 2022 11:59:20 +0200 Subject: [PATCH 3/8] Lookup the Rails time zone using the tzid (fails for many zone ids) Co-authored-by: Jankees van Woezik --- lib/ice_cube/parsers/ical_parser.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 429bd84c..bc2b380f 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 = Time.find_zone(tzid) if tzid.present? case property when "DTSTART" + value = {time: value, zone: zone} if zone.present? data[:start_time] = TimeUtil.deserialize_time(value) when "DTEND" + value = {time: value, zone: zone} if zone.present? 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.present? + 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.present? + TimeUtil.deserialize_time(v) + end when "DURATION" data[:duration] # FIXME when "RRULE" From 3a4ab888b2c963a45a03f70f33e0da640b655e52 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 28 Apr 2022 12:00:20 +0200 Subject: [PATCH 4/8] Alternative time zone lookup using strftime, to complement Rails mapping Co-authored-by: Jankees van Woezik --- lib/ice_cube/parsers/ical_parser.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index bc2b380f..af6fd378 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -5,7 +5,7 @@ def self.schedule_from_ical(ical_string, options = {}) ical_string.each_line do |line| (property, value) = line.split(":") (property, tzid) = property.split(";") - zone = Time.find_zone(tzid) if tzid.present? + zone = find_zone(tzid) if tzid.present? case property when "DTSTART" value = {time: value, zone: zone} if zone.present? @@ -92,5 +92,18 @@ def self.rule_from_ical(ical) Rule.from_hash(params) end + + private_class_method def self.find_zone(tzid) + (_, zone) = tzid&.split("=") + begin + Time.find_zone!(zone) if zone.present? + rescue ArgumentError + (rails_zone, _tzinfo_id) = ActiveSupport::TimeZone::MAPPING.find do |(k, _)| + Time.find_zone!(k).now.strftime("%Z") == zone + end + + Time.find_zone(rails_zone) + end + end end end From afa978ab34d9109139870e0e74c63b884f21ecfd Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Fri, 29 Apr 2022 11:25:27 +0200 Subject: [PATCH 5/8] Find the right time zone for standard times or daylight saving times Using `.now` causes a search for "MST" in the winter, even if the time of the given schedule is in summer and would only match "MDT". By parsing the time value and using the components as values in the candidate time zone, this issue is fixed. --- lib/ice_cube/parsers/ical_parser.rb | 8 +++++--- spec/examples/from_ical_spec.rb | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index af6fd378..356cb068 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -5,7 +5,7 @@ def self.schedule_from_ical(ical_string, options = {}) ical_string.each_line do |line| (property, value) = line.split(":") (property, tzid) = property.split(";") - zone = find_zone(tzid) if tzid.present? + zone = find_zone(tzid, value) if tzid.present? case property when "DTSTART" value = {time: value, zone: zone} if zone.present? @@ -93,13 +93,15 @@ def self.rule_from_ical(ical) Rule.from_hash(params) end - private_class_method def self.find_zone(tzid) + private_class_method def self.find_zone(tzid, time_string) (_, zone) = tzid&.split("=") begin Time.find_zone!(zone) if zone.present? rescue ArgumentError (rails_zone, _tzinfo_id) = ActiveSupport::TimeZone::MAPPING.find do |(k, _)| - Time.find_zone!(k).now.strftime("%Z") == zone + 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) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index f85c4ba8..6a4e36cf 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -378,7 +378,7 @@ def sorted_ical(ical) end end - it "round trips from and to ical with time zones" do + 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 @@ -393,6 +393,22 @@ def sorted_ical(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 From c3a7ede24dca6c666a986c89da3a5d5828f853f9 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Tue, 3 May 2022 10:14:26 +0200 Subject: [PATCH 6/8] Changelog entry for PR #526 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0022b92..a17c68e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) ### Fixed +- The `IceCube::IcalParser` finds the time zones matching an iCal schedule's date/time strings, accomodating for times with daylight savings ([#526](https://github.com/ice-cube-ruby/ice_cube/pull/526)) by [@jankeesvw](https://github.com/jankeesvw) and [@epologee](https://github.com/epologee) - Fix for weekly interval results when requesting `occurrences_between` on a narrow range ([#487](https://github.com/seejohnrun/ice_cube/pull/487)) by [@jakebrady5](https://github.com/jakebrady5) - When using a rule with hour_of_day validations, and asking for occurrences on the day that DST skips forward, valid occurrences would be missed. ([#464](https://github.com/seejohnrun/ice_cube/pull/464)) by [@jakebrady5](https://github.com/jakebrady5) - Include `exrules` when exporting a schedule to YAML, JSON or a Hash. ([#519](https://github.com/ice-cube-ruby/ice_cube/pull/519)) by [@pacso](https://github.com/pacso) From c607f17102a5124323f217bc52107d00b1a8ba25 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 5 May 2022 10:10:27 +0200 Subject: [PATCH 7/8] Indentation fix by standardrb --- lib/ice_cube/time_util.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index 001d927d..d9ebbe99 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -286,12 +286,12 @@ def to_time def add(type, val) type = :day if type == :wday @time += case type - when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY - when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY - when :day then val * ONE_DAY - when :hour then val * ONE_HOUR - when :min then val * ONE_MINUTE - when :sec then val + when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY + when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY + when :day then val * ONE_DAY + when :hour then val * ONE_HOUR + when :min then val * ONE_MINUTE + when :sec then val end end From e91465abf7051f8b8f6f007012224867f33190a5 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Tue, 18 Jul 2023 12:48:53 +0200 Subject: [PATCH 8/8] Edit description to trigger GH actions --- spec/examples/from_ical_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 6a4e36cf..80f5fa4c 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -348,7 +348,7 @@ def sorted_ical(ical) end end - describe "time zones" do + 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