Skip to content

Full Timezone Support #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions ice_cube.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions lib/ice_cube/builders/ical_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 13 additions & 3 deletions lib/ice_cube/parsers/ical_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -23,6 +25,14 @@ def self.schedule_from_ical(ical_string, options = {})
Schedule.from_hash data
end

def self._parse_in_tzid(value, tzid)
time_parser = Time
if tzid
time_parser = ActiveSupport::TimeZone.new(tzid.split('=')[1]) || Time
end
time_parser.parse(value)
end

def self.rule_from_ical(ical)
params = { validations: { } }

Expand Down
53 changes: 48 additions & 5 deletions spec/examples/from_ical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,22 @@ 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
DTEND:20130314T201545Z
RRULE:FREQ=WEEKLY;BYDAY=TH;UNTIL=20130531T100000Z
ICAL

ical_string_with_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
Expand All @@ -111,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
Expand All @@ -130,6 +137,43 @@ 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")
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
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

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

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

describe "daily frequency" do
Expand Down Expand Up @@ -240,7 +284,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)

Expand Down
13 changes: 7 additions & 6 deletions spec/examples/hourly_rule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions spec/examples/recur_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require File.dirname(__FILE__) + '/../spec_helper'
require 'timecop'

include IceCube

Expand Down Expand Up @@ -115,6 +116,18 @@
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, 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

describe :next_occurrences do
Expand Down
49 changes: 30 additions & 19 deletions spec/examples/to_ical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -192,25 +198,30 @@
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)
schedule.add_recurrence_time start
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
Expand Down