diff --git a/lib/ice_cube/builders/ical_builder.rb b/lib/ice_cube/builders/ical_builder.rb index 4af655ce..dce90694 100644 --- a/lib/ice_cube/builders/ical_builder.rb +++ b/lib/ice_cube/builders/ical_builder.rb @@ -35,10 +35,22 @@ def self.ical_utc_format(time) def self.ical_format(time, force_utc) time = time.dup.utc if force_utc + + # Keep timezone. strftime will serializer short versions of time zone (eg. EEST), + # which are not reversivible, as there are many repeated abbreviated zones. This will result in + # issues in parsing + if time.respond_to?(:time_zone) + tz_id = time.time_zone.name + return ";TZID=#{tz_id}:#{IceCube::I18n.l(time, format: "%Y%m%dT%H%M%S")}" # local time specified" + end + if time.utc? ":#{IceCube::I18n.l(time, format: "%Y%m%dT%H%M%SZ")}" # utc time else - ";TZID=#{IceCube::I18n.l(time, format: "%Z:%Y%m%dT%H%M%S")}" # local time specified + # Convert to UTC as TZID=+xxxx format is not recognized by JS libraries + warn "IceCube: Time object does not have timezone info. Assuming UTC: #{caller(1..1).first}" + utc_time = time.dup.utc + ":#{IceCube::I18n.l(utc_time, format: "%Y%m%dT%H%M%SZ")}" # converted to utc time end end diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 429bd84c..6df66cc5 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -4,18 +4,25 @@ 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_param) = property.split(";") + + # Extract TZID if present + tzid = nil + if tzid_param && tzid_param.start_with?("TZID=") + tzid = tzid_param[5..-1] # Remove "TZID=" prefix + end + case property when "DTSTART" - data[:start_time] = TimeUtil.deserialize_time(value) + data[:start_time] = deserialize_time_with_tzid(value, tzid) when "DTEND" - data[:end_time] = TimeUtil.deserialize_time(value) + data[:end_time] = deserialize_time_with_tzid(value, tzid) when "RDATE" data[:rtimes] ||= [] - data[:rtimes] += value.split(",").map { |v| TimeUtil.deserialize_time(v) } + data[:rtimes] += value.split(",").map { |v| deserialize_time_with_tzid(v, tzid) } when "EXDATE" data[:extimes] ||= [] - data[:extimes] += value.split(",").map { |v| TimeUtil.deserialize_time(v) } + data[:extimes] += value.split(",").map { |v| deserialize_time_with_tzid(v, tzid) } when "DURATION" data[:duration] # FIXME when "RRULE" @@ -26,6 +33,22 @@ def self.schedule_from_ical(ical_string, options = {}) Schedule.from_hash data end + def self.deserialize_time_with_tzid(time_value, tzid) + if tzid.nil? || tzid.empty? + # No TZID, use standard deserialization + TimeUtil.deserialize_time(time_value) + else + # TZID is a timezone name - Assume it's a valid timezone in a try-catch block + begin + TimeUtil.deserialize_time({time: time_value, zone: tzid}) + rescue ArgumentError + # If the timezone is invalid, fall back to standard deserialization + # Perhaps we want to log this? + TimeUtil.deserialize_time(time_value) + end + end + end + def self.rule_from_ical(ical) raise ArgumentError, "empty ical rule" if ical.nil? diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index a18f7758..cbf2dead 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -87,7 +87,8 @@ def self.serialize_time(time) case time when Time, Date if time.respond_to?(:time_zone) - {time: time.utc, zone: time.time_zone.name} + # avoid .utc as it changes the object timezone + {time: time.getutc, zone: time.time_zone.name} else time end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 2ab66c3c..4ef584cd 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -110,7 +110,7 @@ module IceCube ICAL ical_string_with_multiple_rules = <<-ICAL.gsub(/^\s*/, "") - DTSTART;TZID=CDT:20151005T195541 + DTSTART;TZID=America/Chicago:20151005T195541 RRULE:FREQ=WEEKLY;BYDAY=MO,TU RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;BYDAY=FR ICAL diff --git a/spec/examples/to_ical_spec.rb b/spec/examples/to_ical_spec.rb index 39dd5209..063a855e 100644 --- a/spec/examples/to_ical_spec.rb +++ b/spec/examples/to_ical_spec.rb @@ -97,7 +97,7 @@ it "should be able to serialize a base schedule to ical in local time" do Time.zone = "Eastern Time (US & Canada)" schedule = IceCube::Schedule.new(Time.zone.local(2010, 5, 10, 9, 0, 0)) - expect(schedule.to_ical).to eq("DTSTART;TZID=EDT:20100510T090000") + expect(schedule.to_ical).to eq("DTSTART;TZID=Eastern Time (US & Canada):20100510T090000") end it "should be able to serialize a base schedule to ical in UTC time" do @@ -110,7 +110,7 @@ schedule = IceCube::Schedule.new(Time.zone.local(2010, 5, 10, 9, 0, 0)) schedule.add_recurrence_rule IceCube::Rule.weekly # test equality - expectation = "DTSTART;TZID=PDT:20100510T090000\n" + expectation = "DTSTART;TZID=Pacific Time (US & Canada):20100510T090000\n" expectation << "RRULE:FREQ=WEEKLY" expect(schedule.to_ical).to eq(expectation) end @@ -120,7 +120,7 @@ schedule = IceCube::Schedule.new(Time.zone.local(2010, 10, 20, 4, 30, 0)) schedule.add_recurrence_rule IceCube::Rule.weekly.day_of_week(monday: [2, -1]) schedule.add_recurrence_rule IceCube::Rule.hourly - expectation = "DTSTART;TZID=EDT:20101020T043000\n" + expectation = "DTSTART;TZID=Eastern Time (US & Canada):20101020T043000\n" expectation << "RRULE:FREQ=WEEKLY;BYDAY=2MO,-1MO\n" expectation << "RRULE:FREQ=HOURLY" expect(schedule.to_ical).to eq(expectation) @@ -131,7 +131,7 @@ schedule = IceCube::Schedule.new(Time.zone.local(2010, 5, 10, 9, 0, 0)) schedule.add_exception_rule IceCube::Rule.weekly # test equality - expectation = "DTSTART;TZID=PDT:20100510T090000\n" + expectation = "DTSTART;TZID=Pacific Time (US & Canada):20100510T090000\n" expectation << "EXRULE:FREQ=WEEKLY" expect(schedule.to_ical).to eq(expectation) end @@ -141,7 +141,7 @@ schedule = IceCube::Schedule.new(Time.zone.local(2010, 10, 20, 4, 30, 0)) schedule.add_exception_rule IceCube::Rule.weekly.day_of_week(monday: [2, -1]) schedule.add_exception_rule IceCube::Rule.hourly - expectation = "DTSTART;TZID=EDT:20101020T043000\n" + expectation = "DTSTART;TZID=Eastern Time (US & Canada):20101020T043000\n" expectation << "EXRULE:FREQ=WEEKLY;BYDAY=2MO,-1MO\n" expectation << "EXRULE:FREQ=HOURLY" expect(schedule.to_ical).to eq(expectation) @@ -192,10 +192,10 @@ expect(schedule.duration).to eq(3600) end - it "should default to to_ical using local time" do + it "should default to to_ical using UTC when there is no timezone info" do time = Time.now - schedule = IceCube::Schedule.new(Time.now) - expect(schedule.to_ical).to eq("DTSTART;TZID=#{time.zone}:#{time.strftime("%Y%m%dT%H%M%S")}") # default false + schedule = IceCube::Schedule.new(time) + expect(schedule.to_ical).to eq("DTSTART:#{time.utc.strftime("%Y%m%dT%H%M%S")}Z") # converts local to UTC end it "should not have an rtime that duplicates start time" do @@ -207,10 +207,10 @@ it "should be able to receive a to_ical in utc time" do time = Time.now - schedule = IceCube::Schedule.new(Time.now) - expect(schedule.to_ical).to eq("DTSTART;TZID=#{time.zone}:#{time.strftime("%Y%m%dT%H%M%S")}") # default false - expect(schedule.to_ical(false)).to eq("DTSTART;TZID=#{time.zone}:#{time.strftime("%Y%m%dT%H%M%S")}") - expect(schedule.to_ical(true)).to eq("DTSTART:#{time.utc.strftime("%Y%m%dT%H%M%S")}Z") + schedule = IceCube::Schedule.new(time) + expect(schedule.to_ical).to eq("DTSTART:#{time.utc.strftime("%Y%m%dT%H%M%S")}Z") # converts local to UTC + expect(schedule.to_ical(false)).to eq("DTSTART:#{time.utc.strftime("%Y%m%dT%H%M%S")}Z") # still converts local to UTC + expect(schedule.to_ical(true)).to eq("DTSTART:#{time.utc.strftime("%Y%m%dT%H%M%S")}Z") # force UTC end it "should be able to serialize to ical with an until date" do