-
Notifications
You must be signed in to change notification settings - Fork 358
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
Issues/50 from ical, cleaned up #258
Merged
seejohnrun
merged 4 commits into
ice-cube-ruby:master
from
wvengen:issues/50-from_ical-rebased
May 27, 2015
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
module IceCube | ||
class IcalParser | ||
def self.schedule_from_ical(ical_string, options = {}) | ||
data = {} | ||
ical_string.each_line do |line| | ||
(property, value) = line.split(':') | ||
(property, tzid) = property.split(';') | ||
case property | ||
when 'DTSTART' | ||
data[:start_time] = Time.parse(value) | ||
when 'DTEND' | ||
data[:end_time] = Time.parse(value) | ||
when 'EXDATE' | ||
data[:extimes] ||= [] | ||
data[:extimes] += value.split(',').map{|v| Time.parse(v)} | ||
when 'DURATION' | ||
data[:duration] # FIXME | ||
when 'RRULE' | ||
data[:rrules] = [rule_from_ical(value)] | ||
end | ||
end | ||
Schedule.from_hash data | ||
end | ||
|
||
def self.rule_from_ical(ical) | ||
params = { validations: { } } | ||
|
||
ical.split(';').each do |rule| | ||
(name, value) = rule.split('=') | ||
value.strip! | ||
case name | ||
when 'FREQ' | ||
params[:freq] = value.downcase | ||
when 'INTERVAL' | ||
params[:interval] = value.to_i | ||
when 'COUNT' | ||
params[:count] = value.to_i | ||
when 'UNTIL' | ||
params[:until] = DateTime.parse(value).to_time.utc | ||
when 'WKST' | ||
params[:wkst] = TimeUtil.ical_day_to_symbol(value) | ||
when 'BYSECOND' | ||
params[:validations][:second_of_minute] = value.split(',').collect{ |v| v.to_i } | ||
when "BYMINUTE" | ||
params[:validations][:minute_of_hour] = value.split(',').collect{ |v| v.to_i } | ||
when "BYHOUR" | ||
params[:validations][:hour_of_day] = value.split(',').collect{ |v| v.to_i } | ||
when "BYDAY" | ||
dows = {} | ||
days = [] | ||
value.split(',').each do |expr| | ||
day = TimeUtil.ical_day_to_symbol(expr.strip[-2..-1]) | ||
if expr.strip.length > 2 # day with occurence | ||
occ = expr[0..-3].to_i | ||
dows[day].nil? ? dows[day] = [occ] : dows[day].push(occ) | ||
days.delete(TimeUtil.sym_to_wday(day)) | ||
else | ||
days.push TimeUtil.sym_to_wday(day) if dows[day].nil? | ||
end | ||
end | ||
params[:validations][:day_of_week] = dows unless dows.empty? | ||
params[:validations][:day] = days unless days.empty? | ||
when "BYMONTHDAY" | ||
params[:validations][:day_of_month] = value.split(',').collect{ |v| v.to_i } | ||
when "BYMONTH" | ||
params[:validations][:month_of_year] = value.split(',').collect{ |v| v.to_i } | ||
when "BYYEARDAY" | ||
params[:validations][:day_of_year] = value.split(',').collect{ |v| v.to_i } | ||
when "BYSETPOS" | ||
else | ||
raise "Invalid or unsupported rrule command : #{name}" | ||
end | ||
end | ||
|
||
params[:interval] ||= 1 | ||
# WKST only valid for weekly rules | ||
params.delete(:wkst) unless params[:freq] == 'weekly' | ||
|
||
rule = Rule.send(*params.values_at(:freq, :interval, :wkst).compact) | ||
rule.count(params[:count]) if params[:count] | ||
rule.until(params[:until]) if params[:until] | ||
params[:validations].each do |key, value| | ||
value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value) | ||
end | ||
|
||
rule | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
module IceCube | ||
|
||
# This validation mixin is used by the various "fixed-time" (e.g. day, | ||
# day_of_month, hour_of_day) Validation and ScheduleLock::Validation modules. | ||
# It is not a standalone rule validation like the others. | ||
# | ||
# Given the including Validation's defined +type+ field, it will lock | ||
# to the specified +value+ or else the corresponding time unit from the | ||
# schedule's start_time | ||
# | ||
module Validations::Lock | ||
|
||
INTERVALS = {:min => 60, :sec => 60, :hour => 24, :month => 12, :wday => 7} | ||
|
||
def validate(time, schedule) | ||
case type | ||
when :day then validate_day_lock(time, schedule) | ||
when :hour then validate_hour_lock(time, schedule) | ||
else validate_interval_lock(time, schedule) | ||
end | ||
end | ||
|
||
private | ||
|
||
# Validate if the current time unit matches the same unit from the schedule | ||
# start time, returning the difference to the interval | ||
# | ||
def validate_interval_lock(time, schedule) | ||
t0 = starting_unit(schedule.start_time) | ||
t1 = time.send(type) | ||
t0 >= t1 ? t0 - t1 : INTERVALS[type] - t1 + t0 | ||
end | ||
|
||
# Lock the hour if explicitly set by hour_of_day, but allow for the nearest | ||
# hour during DST start to keep the correct interval. | ||
# | ||
def validate_hour_lock(time, schedule) | ||
h0 = starting_unit(schedule.start_time) | ||
h1 = time.hour | ||
if h0 >= h1 | ||
h0 - h1 | ||
else | ||
if dst_offset = TimeUtil.dst_change(time) | ||
h0 - h1 + dst_offset | ||
else | ||
24 - h1 + h0 | ||
end | ||
end | ||
end | ||
|
||
# For monthly rules that have no specified day value, the validation relies | ||
# on the schedule start time and jumps to include every month even if it | ||
# has fewer days than the schedule's start day. | ||
# | ||
# Negative day values (from month end) also include all months. | ||
# | ||
# Positive day values are taken literally so months with fewer days will | ||
# be skipped. | ||
# | ||
def validate_day_lock(time, schedule) | ||
days_in_month = TimeUtil.days_in_month(time) | ||
date = Date.new(time.year, time.month, time.day) | ||
|
||
if value && value < 0 | ||
start = TimeUtil.day_of_month(value, date) | ||
month_overflow = days_in_month - TimeUtil.days_in_next_month(time) | ||
elsif value && value > 0 | ||
start = value | ||
month_overflow = 0 | ||
else | ||
start = TimeUtil.day_of_month(schedule.start_time.day, date) | ||
month_overflow = 0 | ||
end | ||
|
||
sleeps = start - date.day | ||
|
||
if value && value > 0 | ||
until_next_month = days_in_month + sleeps | ||
else | ||
until_next_month = start < 28 ? days_in_month : TimeUtil.days_to_next_month(date) | ||
until_next_month += sleeps - month_overflow | ||
end | ||
|
||
sleeps >= 0 ? sleeps : until_next_month | ||
end | ||
|
||
def starting_unit(start_time) | ||
start = value || start_time.send(type) | ||
start += INTERVALS[type] while start < 0 | ||
start | ||
end | ||
|
||
end | ||
|
||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ActiveSupport::DateTime.to_time returns a DateTime unless the offset == 0 (UTC)
Instead, saying
DateTime.parse(value).utc.to_time
(".utc" has moved) will convert to UTC then successfully convert to Time.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your feedback! I'm trying to see where it happens, but all I can find is that
to_time
returns aDateTime
; ActiveSupport doesn't seem to change that method, right? Also:So if we want utc,
to_time.utc
is right. I'd need to look at what happens here and what the ical standard expects, but I thinkto_time.utc
is right. What do you think?Thanks for taking a look at this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey! I should preface this by saying I'm just a third party whose interested in this project, and I'm not entirely knowledgeable of everything.
Anyway, Here's the behaviour I have observed:
Presumably different versions of ActiveSupport behave differently, so IDK if everyone will have this issue, I just noticed that IceCube throws "DateTime support is deprecated" messages whenever I call the from_ical function because of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah thanks. I'm using Rails 4.1.6, which behaves differently:
I'm a bit at loss what to do now. It would be nice to know if the code functions incorrectly because of this - in that case I'd like to add a rails version conditional. If it's just a warning, I'd be inclined to leave it as it is.
And, thanks for your input - I'm not knowledgeable of everything, but perhaps together we cover the base :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This didn't seem obvious to me before, but I'm pretty sure a simple
Time.parse(value).utc
will get the job done.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I can't find it in Ruby doc nor Rails, but apidock shows it being present in 1.9 as well. But they're there, and with this change the ical specs still work for both Ruby 1.9.3 and 2.1.3 as well as ActiveSupport 4.1.8, 4.0.12, 3.0.20 and 3.1.12.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! It's unfortunate the documentation is so spotty.
Thanks dude =)