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) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 429bd84c..356cb068 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.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" @@ -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.present? + 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/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 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