From 8db090e9709b85601044b0efbabed403c4b2d8ba Mon Sep 17 00:00:00 2001 From: Sean Devine Date: Mon, 29 Jun 2015 10:59:20 -0400 Subject: [PATCH 1/7] Parse time zones when creating schedules from ICAL; fix DST test bug #295 --- lib/ice_cube/parsers/ical_parser.rb | 16 +++++++++++++--- spec/examples/from_ical_spec.rb | 28 ++++++++++++++++++++++++---- spec/examples/hourly_rule_spec.rb | 13 +++++++------ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 6729fca8..09a44e2d 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -7,12 +7,14 @@ def self.schedule_from_ical(ical_string, options = {}) (property, tzid) = property.split(';') case property when 'DTSTART' - data[:start_time] = Time.parse(value) + data[:start_time] = _parse_in_tzid(value, tzid) when 'DTEND' - data[:end_time] = Time.parse(value) + data[:end_time] = _parse_in_tzid(value, tzid) when 'EXDATE' data[:extimes] ||= [] - data[:extimes] += value.split(',').map{|v| Time.parse(v)} + data[:extimes] += value.split(',').map do |v| + _parse_in_tzid(v, tzid) + end when 'DURATION' data[:duration] # FIXME when 'RRULE' @@ -22,6 +24,14 @@ def self.schedule_from_ical(ical_string, options = {}) Schedule.from_hash data end + def self._parse_in_tzid(value, tzid) + t = Time.parse(value) + if tzid + t = t.in_time_zone(ActiveSupport::TimeZone[tzid.split("=")[1]]) + end + t + end + def self.rule_from_ical(ical) params = { validations: { } } diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 68d218ea..31064166 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -94,7 +94,7 @@ module IceCube end - describe Schedule, 'from_ical' do + describe Schedule, 'from_ical', system_time_zone: "America/Chicago" do ical_string = <<-ICAL.gsub(/^\s*/, '') DTSTART:20130314T201500Z @@ -102,7 +102,14 @@ module IceCube RRULE:FREQ=WEEKLY;BYDAY=TH;UNTIL=20130531T100000Z ICAL - ical_string_woth_multiple_exdates = <<-ICAL.gsub(/^\s*/, '') + ical_string_with_time_zones = <<-ICAL.gsub(/^\s*/,'') + DTSTART;TZID=America/Denver:20130731T143000 + DTEND:20130731T153000 + RRULE:FREQ=WEEKLY + EXDATE;TZID=America/Chicago:20130823T143000 + ICAL + + ical_string_with_multiple_exdates = <<-ICAL.gsub(/^\s*/, '') DTSTART;TZID=America/Denver:20130731T143000 DTEND;TZID=America/Denver:20130731T153000 RRULE:FREQ=WEEKLY;UNTIL=20140730T203000Z;BYDAY=MO,WE,FR @@ -125,6 +132,20 @@ def sorted_ical(ical) it "loads an ICAL string" do expect(IceCube::Schedule.from_ical(ical_string)).to be_a(IceCube::Schedule) end + describe "parsing time zones" do + it "sets the time zone of the start time" do + schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) + expect(schedule.start_time.time_zone).to eq ActiveSupport::TimeZone.new("America/Denver") + end + it "uses the system time if a time zone is not explicity provided" do + schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) + expect(schedule.end_time).not_to respond_to :time_zone + end + it "sets the time zone of the exception times" do + schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) + expect(schedule.exception_times[0].time_zone).to eq ActiveSupport::TimeZone.new("America/Chicago") + end + end end describe "daily frequency" do @@ -235,7 +256,6 @@ def sorted_ical(ical) describe 'monthly frequency' do it 'matches simple monthly' do start_time = Time.now - schedule = IceCube::Schedule.new(start_time) schedule.add_recurrence_rule(IceCube::Rule.monthly) @@ -359,7 +379,7 @@ def sorted_ical(ical) end it 'handles multiple EXDATE lines' do - schedule = IceCube::Schedule.from_ical ical_string_woth_multiple_exdates + schedule = IceCube::Schedule.from_ical ical_string_with_multiple_exdates schedule.exception_times.count.should == 3 end end diff --git a/spec/examples/hourly_rule_spec.rb b/spec/examples/hourly_rule_spec.rb index d0685585..77cd10be 100644 --- a/spec/examples/hourly_rule_spec.rb +++ b/spec/examples/hourly_rule_spec.rb @@ -39,13 +39,14 @@ module IceCube end it 'should not skip times in DST end hour' do - schedule = Schedule.new(t0 = Time.local(2013, 11, 3, 0, 0, 0)) + tz = ActiveSupport::TimeZone["America/Vancouver"] + schedule = Schedule.new(t0 = tz.local(2013, 11, 3, 0, 0, 0)) schedule.add_recurrence_rule Rule.hourly - schedule.first(4).should == [ - Time.local(2013, 11, 3, 0, 0, 0), # -0700 - Time.local(2013, 11, 3, 1, 0, 0) - ONE_HOUR, # -0700 - Time.local(2013, 11, 3, 1, 0, 0), # -0800 - Time.local(2013, 11, 3, 2, 0, 0), # -0800 + expect(schedule.first(4)).to eq [ + tz.local(2013, 11, 3, 0, 0, 0), # -0700 + tz.local(2013, 11, 3, 1, 0, 0), # -0700 + tz.local(2013, 11, 3, 2, 0, 0) - ONE_HOUR, # -0800 + tz.local(2013, 11, 3, 2, 0, 0), # -0800 ] end From f754169289194399d75d4189e841bb736dee81b0 Mon Sep 17 00:00:00 2001 From: John Hamelink Date: Sat, 3 Oct 2015 16:17:51 +0100 Subject: [PATCH 2/7] Add correct offset without changing time --- lib/ice_cube/parsers/ical_parser.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 09a44e2d..6f795131 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -25,11 +25,11 @@ def self.schedule_from_ical(ical_string, options = {}) end def self._parse_in_tzid(value, tzid) - t = Time.parse(value) + time_parser = Time if tzid - t = t.in_time_zone(ActiveSupport::TimeZone[tzid.split("=")[1]]) + time_parser = ActiveSupport::TimeZone.new(tzid.split('=')[1]) || Time end - t + time_parser.parse(value) end def self.rule_from_ical(ical) From f03e439b279650035941a596d661f864f1332673 Mon Sep 17 00:00:00 2001 From: John Hamelink Date: Sat, 3 Oct 2015 16:29:16 +0100 Subject: [PATCH 3/7] Add spec which proves hour doesn't change when timezone is set --- spec/examples/from_ical_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 31064166..f1fec335 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -145,6 +145,10 @@ def sorted_ical(ical) schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) expect(schedule.exception_times[0].time_zone).to eq ActiveSupport::TimeZone.new("America/Chicago") end + it "adding the offset doesnt also change the time" do + schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) + expect(schedule.exception_times[0].hour).to eq 14 + end end end From 237e7a96908100851b11b7e00c9afeae16bf7f47 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Mon, 9 May 2016 19:03:25 -0700 Subject: [PATCH 4/7] Use real Timezones in ical serialization, instead of US timezone offsets like PDT, PST, etc. --- lib/ice_cube/builders/ical_builder.rb | 6 ++-- spec/examples/from_ical_spec.rb | 10 ++++-- spec/examples/to_ical_spec.rb | 49 ++++++++++++++++----------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/ice_cube/builders/ical_builder.rb b/lib/ice_cube/builders/ical_builder.rb index 2ba4e646..3067e1f3 100644 --- a/lib/ice_cube/builders/ical_builder.rb +++ b/lib/ice_cube/builders/ical_builder.rb @@ -36,11 +36,11 @@ def self.ical_utc_format(time) end def self.ical_format(time, force_utc) - time = time.dup.utc if force_utc + time = time.dup.utc if force_utc || !time.respond_to?('time_zone') if time.utc? - ":#{IceCube::I18n.l(time, format: '%Y%m%dT%H%M%SZ')}" # utc time + ":#{IceCube::I18n.l(time.utc, 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 + ";TZID=#{time.time_zone.name}:#{IceCube::I18n.l(time, format: '%Y%m%dT%H%M%S')}" # local time specified end end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 3694bdc2..9b6e8d55 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -118,8 +118,8 @@ module IceCube EXDATE;TZID=America/Denver:20130807T143000 ICAL - ical_string_with_multiple_rules = <<-ICAL.gsub(/^\s*/, '' ) - DTSTART;TZID=CDT:20151005T195541 + ical_string_with_multiple_rules = <<-ICAL.gsub(/^\s*/, '' ) + DTSTART;TZID=America/Denver:20151005T195541 RRULE:FREQ=WEEKLY;BYDAY=MO,TU RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;BYDAY=FR ICAL @@ -158,6 +158,12 @@ def sorted_ical(ical) schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) expect(schedule.exception_times[0].hour).to eq 14 end + + it "loads the ical DTSTART as output by IceCube to_ical method" do + now = Time.new(2016,5,9,12).in_time_zone("America/Los_Angeles") + schedule = IceCube::Schedule.from_ical(IceCube::Schedule.new(now).to_ical) + expect(schedule.start_time).to eq(now) + end end end diff --git a/spec/examples/to_ical_spec.rb b/spec/examples/to_ical_spec.rb index fdd42b45..72ec6ec0 100644 --- a/spec/examples/to_ical_spec.rb +++ b/spec/examples/to_ical_spec.rb @@ -94,10 +94,16 @@ ].include?(rule.to_ical).should be_true end - it 'should be able to serialize a base schedule to ical in local time' do + it 'should be able to serialize a base schedule to ical in local time, using a US timezone' do Time.zone = "Eastern Time (US & Canada)" schedule = IceCube::Schedule.new(Time.zone.local(2010, 5, 10, 9, 0, 0)) - schedule.to_ical.should == "DTSTART;TZID=EDT:20100510T090000" + schedule.to_ical.should == "DTSTART;TZID=Eastern Time (US & Canada):20100510T090000" + end + + it 'should be able to serialize a base schedule to ical in local time, using an Olson timezone' do + Time.zone = "America/New_York" + schedule = IceCube::Schedule.new(Time.zone.local(2010, 5, 10, 9, 0, 0)) + schedule.to_ical.should == "DTSTART;TZID=America/New_York:20100510T090000" end it 'should be able to serialize a base schedule to ical in UTC time' do @@ -110,7 +116,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' schedule.to_ical.should == expectation end @@ -120,7 +126,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" schedule.to_ical.should == expectation @@ -131,17 +137,17 @@ 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' schedule.to_ical.should == expectation end it 'should be able to serialize a schedule with multiple exrules' do - Time.zone ='Eastern Time (US & Canada)' + Time.zone ='America/New_York' 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=America/New_York:20101020T043000\n" expectation<< "EXRULE:FREQ=WEEKLY;BYDAY=2MO,-1MO\n" expectation<< "EXRULE:FREQ=HOURLY" schedule.to_ical.should == expectation @@ -192,12 +198,6 @@ schedule.duration.should == 3600 end - it 'should default to to_ical using local time' do - time = Time.now - schedule = IceCube::Schedule.new(Time.now) - schedule.to_ical.should == "DTSTART;TZID=#{time.zone}:#{time.strftime('%Y%m%dT%H%M%S')}" # default false - end - it 'should not have an rtime that duplicates start time' do start = Time.utc(2012, 12, 12, 12, 0, 0) schedule = IceCube::Schedule.new(start) @@ -205,12 +205,23 @@ schedule.to_ical.should == "DTSTART:20121212T120000Z" end - it 'should be able to receive a to_ical in utc time' do - time = Time.now - schedule = IceCube::Schedule.new(Time.now) - schedule.to_ical.should == "DTSTART;TZID=#{time.zone}:#{time.strftime('%Y%m%dT%H%M%S')}" # default false - schedule.to_ical(false).should == "DTSTART;TZID=#{time.zone}:#{time.strftime('%Y%m%dT%H%M%S')}" - schedule.to_ical(true).should == "DTSTART:#{time.utc.strftime('%Y%m%dT%H%M%S')}Z" + it 'displays an ActiveSupport::TimeWithZone at utc time as Z' do + time = Time.now.utc + schedule = IceCube::Schedule.new(time) + schedule.to_ical(false).should == "DTSTART:#{time.strftime('%Y%m%dT%H%M%S')}Z" + end + + it 'displays an ActiveSupport::TimeWithZone to utc when using force_utc' do + # this is 8am in NY, 12pm UTC (UTC -4 in summer) + time = Time.new(2016, 5, 9, 12, 0, 0, 0).in_time_zone('America/New_York') + schedule = IceCube::Schedule.new(time) + schedule.to_ical(true).should == "DTSTART:20160509T120000Z" + end + + it 'displays a Time utc time as Z' do + time = Time.now.utc + schedule = IceCube::Schedule.new(time) + schedule.to_ical(true).should == "DTSTART:#{time.strftime('%Y%m%dT%H%M%S')}Z" end it 'should be able to serialize to ical with an until date' do From a8f222b0c1cbe8401cb8a706fcdc535353baa06f Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Mon, 9 May 2016 21:57:31 -0700 Subject: [PATCH 5/7] add a test for daylight savings --- spec/examples/recur_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/examples/recur_spec.rb b/spec/examples/recur_spec.rb index 6f31422c..ba377c6f 100644 --- a/spec/examples/recur_spec.rb +++ b/spec/examples/recur_spec.rb @@ -115,6 +115,15 @@ schedule.next_occurrence(schedule.start_time).should == schedule.start_time + 30 * ONE_MINUTE end + it 'should get the next occurrence across the daylight savings time boundary' do + # 2016 daylight savings time cutoff is Sunday March 13 + Time.zone = 'America/New_York' + start_time = Time.zone.local(2016, 3, 13, 12, 0, 0) + next_time = Time.zone.local(2016, 3, 14, 12, 0, 0) + schedule = Schedule.new(start_time, :end_time => start_time + 14 * 24 * ONE_HOUR) + schedule.add_recurrence_rule(Rule.daily) + schedule.next_occurrence(schedule.start_time).should == next_time + end end describe :next_occurrences do From 9db21046d0adbfa1833ce93eecd223ae43562be7 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Tue, 10 May 2016 13:43:05 -0700 Subject: [PATCH 6/7] add test that the start_time object when parsing UTC is a simple Time not TimeWithZone, while when parsing a timezone it is a TimeWithZone --- spec/examples/from_ical_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 9b6e8d55..65e6ec6d 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -142,6 +142,15 @@ def sorted_ical(ical) it "sets the time zone of the start time" do schedule = IceCube::Schedule.from_ical(ical_string_with_time_zones) expect(schedule.start_time.time_zone).to eq ActiveSupport::TimeZone.new("America/Denver") + expect(schedule.start_time.is_a?(Time)).to be true + expect(schedule.start_time.is_a?(ActiveSupport::TimeWithZone)).to be true + end + + it "treats UTC as a Time rather than TimeWithZone" do + schedule = IceCube::Schedule.from_ical(ical_string) + expect(schedule.start_time.utc_offset).to eq 0 + expect(schedule.start_time.is_a?(Time)).to be true + expect(schedule.start_time.is_a?(ActiveSupport::TimeWithZone)).to be false end it "uses the system time if a time zone is not explicity provided" do From f5b00253428fa3cb43803cf9aa9411e3aa920a07 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Thu, 12 May 2016 13:46:29 -0700 Subject: [PATCH 7/7] update test of DST cutover --- ice_cube.gemspec | 1 + spec/examples/recur_spec.rb | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ice_cube.gemspec b/ice_cube.gemspec index 195a6de6..f1ca1ac4 100644 --- a/ice_cube.gemspec +++ b/ice_cube.gemspec @@ -21,5 +21,6 @@ Gem::Specification.new do |s| s.add_development_dependency('rspec', '~> 2.12.0') s.add_development_dependency('activesupport', '>= 3.0.0') s.add_development_dependency('tzinfo') + s.add_development_dependency('timecop') s.add_development_dependency('i18n') end diff --git a/spec/examples/recur_spec.rb b/spec/examples/recur_spec.rb index ba377c6f..ac15e5f2 100644 --- a/spec/examples/recur_spec.rb +++ b/spec/examples/recur_spec.rb @@ -1,4 +1,5 @@ require File.dirname(__FILE__) + '/../spec_helper' +require 'timecop' include IceCube @@ -117,12 +118,15 @@ it 'should get the next occurrence across the daylight savings time boundary' do # 2016 daylight savings time cutoff is Sunday March 13 - Time.zone = 'America/New_York' - start_time = Time.zone.local(2016, 3, 13, 12, 0, 0) - next_time = Time.zone.local(2016, 3, 14, 12, 0, 0) - schedule = Schedule.new(start_time, :end_time => start_time + 14 * 24 * ONE_HOUR) - schedule.add_recurrence_rule(Rule.daily) - schedule.next_occurrence(schedule.start_time).should == next_time + # Time.zone = 'America/New_York' + start_time = Time.zone.local(2016, 3, 13, 0, 0, 0) + expected_next_time = Time.zone.local(2016, 3, 13, 5, 0, 0) + schedule = Schedule.new(start_time) + schedule.add_recurrence_rule(Rule.hourly(interval=4)) + + Timecop.freeze(start_time) do + schedule.next_occurrence(schedule.start_time).should == expected_next_time + end end end