Skip to content

Commit 623d7c0

Browse files
committed
Merge pull request #258 from wvengen/issues/50-from_ical-rebased
Issues/50 from ical, cleaned up
2 parents 7d5e975 + addacaa commit 623d7c0

File tree

10 files changed

+632
-2
lines changed

10 files changed

+632
-2
lines changed

lib/ice_cube.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module IceCube
1818

1919
autoload :HashParser, 'ice_cube/parsers/hash_parser'
2020
autoload :YamlParser, 'ice_cube/parsers/yaml_parser'
21+
autoload :IcalParser, 'ice_cube/parsers/ical_parser'
2122

2223
autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
2324
autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'

lib/ice_cube/parsers/hash_parser.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,19 @@ def apply_end_time(schedule, data)
5353
def apply_rrules(schedule, data)
5454
return unless data[:rrules]
5555
data[:rrules].each do |h|
56-
schedule.rrule(IceCube::Rule.from_hash(h))
56+
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)
57+
58+
schedule.rrule(rrule)
5759
end
5860
end
5961

6062
def apply_exrules(schedule, data)
6163
return unless data[:exrules]
6264
warn "IceCube: :exrules is deprecated, and will be removed in a future release. at: #{ caller[0] }"
6365
data[:exrules].each do |h|
64-
schedule.exrule(IceCube::Rule.from_hash(h))
66+
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)
67+
68+
schedule.exrule(rrule)
6569
end
6670
end
6771

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
module IceCube
2+
class IcalParser
3+
def self.schedule_from_ical(ical_string, options = {})
4+
data = {}
5+
ical_string.each_line do |line|
6+
(property, value) = line.split(':')
7+
(property, tzid) = property.split(';')
8+
case property
9+
when 'DTSTART'
10+
data[:start_time] = Time.parse(value)
11+
when 'DTEND'
12+
data[:end_time] = Time.parse(value)
13+
when 'EXDATE'
14+
data[:extimes] ||= []
15+
data[:extimes] += value.split(',').map{|v| Time.parse(v)}
16+
when 'DURATION'
17+
data[:duration] # FIXME
18+
when 'RRULE'
19+
data[:rrules] = [rule_from_ical(value)]
20+
end
21+
end
22+
Schedule.from_hash data
23+
end
24+
25+
def self.rule_from_ical(ical)
26+
params = { validations: { } }
27+
28+
ical.split(';').each do |rule|
29+
(name, value) = rule.split('=')
30+
value.strip!
31+
case name
32+
when 'FREQ'
33+
params[:freq] = value.downcase
34+
when 'INTERVAL'
35+
params[:interval] = value.to_i
36+
when 'COUNT'
37+
params[:count] = value.to_i
38+
when 'UNTIL'
39+
params[:until] = Time.parse(value).utc
40+
when 'WKST'
41+
params[:wkst] = TimeUtil.ical_day_to_symbol(value)
42+
when 'BYSECOND'
43+
params[:validations][:second_of_minute] = value.split(',').collect{ |v| v.to_i }
44+
when "BYMINUTE"
45+
params[:validations][:minute_of_hour] = value.split(',').collect{ |v| v.to_i }
46+
when "BYHOUR"
47+
params[:validations][:hour_of_day] = value.split(',').collect{ |v| v.to_i }
48+
when "BYDAY"
49+
dows = {}
50+
days = []
51+
value.split(',').each do |expr|
52+
day = TimeUtil.ical_day_to_symbol(expr.strip[-2..-1])
53+
if expr.strip.length > 2 # day with occurence
54+
occ = expr[0..-3].to_i
55+
dows[day].nil? ? dows[day] = [occ] : dows[day].push(occ)
56+
days.delete(TimeUtil.sym_to_wday(day))
57+
else
58+
days.push TimeUtil.sym_to_wday(day) if dows[day].nil?
59+
end
60+
end
61+
params[:validations][:day_of_week] = dows unless dows.empty?
62+
params[:validations][:day] = days unless days.empty?
63+
when "BYMONTHDAY"
64+
params[:validations][:day_of_month] = value.split(',').collect{ |v| v.to_i }
65+
when "BYMONTH"
66+
params[:validations][:month_of_year] = value.split(',').collect{ |v| v.to_i }
67+
when "BYYEARDAY"
68+
params[:validations][:day_of_year] = value.split(',').collect{ |v| v.to_i }
69+
when "BYSETPOS"
70+
else
71+
raise "Invalid or unsupported rrule command : #{name}"
72+
end
73+
end
74+
75+
params[:interval] ||= 1
76+
# WKST only valid for weekly rules
77+
params.delete(:wkst) unless params[:freq] == 'weekly'
78+
79+
rule = Rule.send(*params.values_at(:freq, :interval, :wkst).compact)
80+
rule.count(params[:count]) if params[:count]
81+
rule.until(params[:until]) if params[:until]
82+
params[:validations].each do |key, value|
83+
value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value)
84+
end
85+
86+
rule
87+
end
88+
end
89+
end

lib/ice_cube/rule.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def to_ical
2727
raise MethodNotImplemented, "Expected to be overrridden by subclasses"
2828
end
2929

30+
# Convert from ical string and create a rule
31+
def self.from_ical(ical)
32+
IceCube::IcalParser.rule_from_ical(ical)
33+
end
34+
3035
# Yaml implementation
3136
def to_yaml(*args)
3237
YAML::dump(to_hash, *args)

lib/ice_cube/schedule.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ def to_ical(force_utc = false)
332332
pieces.join("\n")
333333
end
334334

335+
# Load the schedule from ical
336+
def self.from_ical(ical, options = {})
337+
IcalParser.schedule_from_ical(ical, options)
338+
end
339+
335340
# Convert the schedule to yaml
336341
def to_yaml(*args)
337342
YAML::dump(to_hash, *args)

lib/ice_cube/time_util.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ module TimeUtil
1111
:thursday => 4, :friday => 5, :saturday => 6
1212
}
1313

14+
ICAL_DAYS = {
15+
'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday,
16+
'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday
17+
}
18+
1419
MONTHS = {
1520
:january => 1, :february => 2, :march => 3, :april => 4, :may => 5,
1621
:june => 6, :july => 7, :august => 8, :september => 9, :october => 10,
@@ -147,12 +152,25 @@ def self.wday_to_sym(wday)
147152
end
148153
end
149154

155+
# Convert a symbol to an ical day (SU, MO)
156+
def self.week_start(sym)
157+
raise ArgumentError, "Invalid day: #{str}" unless DAYS.keys.include?(sym)
158+
day = sym.to_s.upcase[0..1]
159+
day
160+
end
161+
150162
# Convert weekday from base sunday to the schedule's week start.
151163
def self.normalize_wday(wday, week_start)
152164
(wday - sym_to_wday(week_start)) % 7
153165
end
154166
deprecated_alias :normalize_weekday, :normalize_wday
155167

168+
def self.ical_day_to_symbol(str)
169+
day = ICAL_DAYS[str]
170+
raise ArgumentError, "Invalid day: #{str}" if day.nil?
171+
day
172+
end
173+
156174
# Return the count of the number of times wday appears in the month,
157175
# and which of those time falls on
158176
def self.which_occurrence_in_month(time, wday)

lib/ice_cube/validations/lock.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
module IceCube
2+
3+
# This validation mixin is used by the various "fixed-time" (e.g. day,
4+
# day_of_month, hour_of_day) Validation and ScheduleLock::Validation modules.
5+
# It is not a standalone rule validation like the others.
6+
#
7+
# Given the including Validation's defined +type+ field, it will lock
8+
# to the specified +value+ or else the corresponding time unit from the
9+
# schedule's start_time
10+
#
11+
module Validations::Lock
12+
13+
INTERVALS = {:min => 60, :sec => 60, :hour => 24, :month => 12, :wday => 7}
14+
15+
def validate(time, schedule)
16+
case type
17+
when :day then validate_day_lock(time, schedule)
18+
when :hour then validate_hour_lock(time, schedule)
19+
else validate_interval_lock(time, schedule)
20+
end
21+
end
22+
23+
private
24+
25+
# Validate if the current time unit matches the same unit from the schedule
26+
# start time, returning the difference to the interval
27+
#
28+
def validate_interval_lock(time, schedule)
29+
t0 = starting_unit(schedule.start_time)
30+
t1 = time.send(type)
31+
t0 >= t1 ? t0 - t1 : INTERVALS[type] - t1 + t0
32+
end
33+
34+
# Lock the hour if explicitly set by hour_of_day, but allow for the nearest
35+
# hour during DST start to keep the correct interval.
36+
#
37+
def validate_hour_lock(time, schedule)
38+
h0 = starting_unit(schedule.start_time)
39+
h1 = time.hour
40+
if h0 >= h1
41+
h0 - h1
42+
else
43+
if dst_offset = TimeUtil.dst_change(time)
44+
h0 - h1 + dst_offset
45+
else
46+
24 - h1 + h0
47+
end
48+
end
49+
end
50+
51+
# For monthly rules that have no specified day value, the validation relies
52+
# on the schedule start time and jumps to include every month even if it
53+
# has fewer days than the schedule's start day.
54+
#
55+
# Negative day values (from month end) also include all months.
56+
#
57+
# Positive day values are taken literally so months with fewer days will
58+
# be skipped.
59+
#
60+
def validate_day_lock(time, schedule)
61+
days_in_month = TimeUtil.days_in_month(time)
62+
date = Date.new(time.year, time.month, time.day)
63+
64+
if value && value < 0
65+
start = TimeUtil.day_of_month(value, date)
66+
month_overflow = days_in_month - TimeUtil.days_in_next_month(time)
67+
elsif value && value > 0
68+
start = value
69+
month_overflow = 0
70+
else
71+
start = TimeUtil.day_of_month(schedule.start_time.day, date)
72+
month_overflow = 0
73+
end
74+
75+
sleeps = start - date.day
76+
77+
if value && value > 0
78+
until_next_month = days_in_month + sleeps
79+
else
80+
until_next_month = start < 28 ? days_in_month : TimeUtil.days_to_next_month(date)
81+
until_next_month += sleeps - month_overflow
82+
end
83+
84+
sleeps >= 0 ? sleeps : until_next_month
85+
end
86+
87+
def starting_unit(start_time)
88+
start = value || start_time.send(type)
89+
start += INTERVALS[type] while start < 0
90+
start
91+
end
92+
93+
end
94+
95+
end

0 commit comments

Comments
 (0)