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

Issues/50 from ical, cleaned up #258

Merged
merged 4 commits into from
May 27, 2015
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ice_cube.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module IceCube

autoload :HashParser, 'ice_cube/parsers/hash_parser'
autoload :YamlParser, 'ice_cube/parsers/yaml_parser'
autoload :IcalParser, 'ice_cube/parsers/ical_parser'

autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'
Expand Down
8 changes: 6 additions & 2 deletions lib/ice_cube/parsers/hash_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,19 @@ def apply_end_time(schedule, data)
def apply_rrules(schedule, data)
return unless data[:rrules]
data[:rrules].each do |h|
schedule.rrule(IceCube::Rule.from_hash(h))
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)

schedule.rrule(rrule)
end
end

def apply_exrules(schedule, data)
return unless data[:exrules]
warn "IceCube: :exrules is deprecated, and will be removed in a future release. at: #{ caller[0] }"
data[:exrules].each do |h|
schedule.exrule(IceCube::Rule.from_hash(h))
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)

schedule.exrule(rrule)
end
end

Expand Down
89 changes: 89 additions & 0 deletions lib/ice_cube/parsers/ical_parser.rb
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
Copy link

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.

Copy link
Contributor Author

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 a DateTime; ActiveSupport doesn't seem to change that method, right? Also:

$ rails c
> dt=DateTime.parse('2015-10-10T00:00:00+00:00')
Sat, 10 Oct 2015 00:00:00 +0000
> dt.to_time.utc
2015-10-10 00:00:00 UTC
> dt.utc.to_time
2015-10-10 02:00:00 +0200
> dt=DateTime.parse('2015-10-10T00:00:00+02:00')
Sat, 10 Oct 2015 00:00:00 +0200
> dt.to_time.utc
2015-10-09 22:00:00 UTC
> dt.utc.to_time
2015-10-10 00:00:00 +0200

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 think to_time.utc is right. What do you think?

Thanks for taking a look at this.

Copy link

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:

[1] pry(main)> DateTime.now.to_s
=> "2014-11-19T10:03:57-08:00"
[2] pry(main)> DateTime.now.class
=> DateTime
[3] pry(main)> DateTime.now.to_time.class
=> DateTime
[4] pry(main)> DateTime.now.utc.to_time.class
=> Time
[5] pry(main)> show-method DateTime.now.to_time

From: /home/thann/.rvm/gems/ruby-2.1.2/gems/activesupport-3.2.19/lib/active_support/core_ext/date_time/conversions.rb @ line 68:
Owner: DateTime
Visibility: public
Number of lines: 3

def to_time
  self.offset == 0 ? ::Time.utc_time(year, month, day, hour, min, sec, sec_fraction * (RUBY_VERSION < '1.9' ? 86400000000 : 1000000)) : self
end

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.

Copy link
Contributor Author

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:

[1] pry(main)> DateTime.now
=> Wed, 19 Nov 2014 22:05:32 +0100
[2] pry(main)> DateTime.now.class
=> DateTime
[3] pry(main)> DateTime.now.to_time.class
=> Time
[4] pry(main)> DateTime.now.utc.to_time.class
=> Time
[5] pry(main)> show-method DateTime.now.to_time
Error: Cannot locate this method: to_time.

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 :)

Copy link

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.

Copy link
Contributor Author

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.

Copy link

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 =)

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
5 changes: 5 additions & 0 deletions lib/ice_cube/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def to_ical
raise MethodNotImplemented, "Expected to be overrridden by subclasses"
end

# Convert from ical string and create a rule
def self.from_ical(ical)
IceCube::IcalParser.rule_from_ical(ical)
end

# Yaml implementation
def to_yaml(*args)
YAML::dump(to_hash, *args)
Expand Down
5 changes: 5 additions & 0 deletions lib/ice_cube/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ def to_ical(force_utc = false)
pieces.join("\n")
end

# Load the schedule from ical
def self.from_ical(ical, options = {})
IcalParser.schedule_from_ical(ical, options)
end

# Convert the schedule to yaml
def to_yaml(*args)
YAML::dump(to_hash, *args)
Expand Down
18 changes: 18 additions & 0 deletions lib/ice_cube/time_util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ module TimeUtil
:thursday => 4, :friday => 5, :saturday => 6
}

ICAL_DAYS = {
'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday,
'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday
}

MONTHS = {
:january => 1, :february => 2, :march => 3, :april => 4, :may => 5,
:june => 6, :july => 7, :august => 8, :september => 9, :october => 10,
Expand Down Expand Up @@ -147,12 +152,25 @@ def self.wday_to_sym(wday)
end
end

# Convert a symbol to an ical day (SU, MO)
def self.week_start(sym)
raise ArgumentError, "Invalid day: #{str}" unless DAYS.keys.include?(sym)
day = sym.to_s.upcase[0..1]
day
end

# Convert weekday from base sunday to the schedule's week start.
def self.normalize_wday(wday, week_start)
(wday - sym_to_wday(week_start)) % 7
end
deprecated_alias :normalize_weekday, :normalize_wday

def self.ical_day_to_symbol(str)
day = ICAL_DAYS[str]
raise ArgumentError, "Invalid day: #{str}" if day.nil?
day
end

# Return the count of the number of times wday appears in the month,
# and which of those time falls on
def self.which_occurrence_in_month(time, wday)
Expand Down
95 changes: 95 additions & 0 deletions lib/ice_cube/validations/lock.rb
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
Loading