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

Time zone support in schedule_from_ical #526

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 27 additions & 3 deletions lib/ice_cube/parsers/ical_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(";")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This _tzid contained the ignored time zone information.

(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"
Expand Down Expand Up @@ -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
12 changes: 6 additions & 6 deletions lib/ice_cube/time_util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 63 additions & 0 deletions spec/examples/from_ical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +381 to +395
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may seem like a superfluous test compared to the one below, but before the last commit on this branch, this test would have failed when run during the winter (standard time vs. daylight saving time period).

I tested this using Timecop locally, but I didn't want to add a dependency to your repository just for this PR, so I didn't commit that part.


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
Expand Down