Skip to content
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