ice_cube_conrad 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ice_cube.rb +80 -0
- data/lib/ice_cube/builders/hash_builder.rb +27 -0
- data/lib/ice_cube/builders/ical_builder.rb +59 -0
- data/lib/ice_cube/builders/string_builder.rb +74 -0
- data/lib/ice_cube/deprecated.rb +28 -0
- data/lib/ice_cube/errors/count_exceeded.rb +7 -0
- data/lib/ice_cube/errors/until_exceeded.rb +7 -0
- data/lib/ice_cube/errors/zero_interval.rb +7 -0
- data/lib/ice_cube/rule.rb +182 -0
- data/lib/ice_cube/rules/daily_rule.rb +14 -0
- data/lib/ice_cube/rules/hourly_rule.rb +14 -0
- data/lib/ice_cube/rules/minutely_rule.rb +14 -0
- data/lib/ice_cube/rules/monthly_rule.rb +14 -0
- data/lib/ice_cube/rules/secondly_rule.rb +13 -0
- data/lib/ice_cube/rules/weekly_rule.rb +14 -0
- data/lib/ice_cube/rules/yearly_rule.rb +14 -0
- data/lib/ice_cube/schedule.rb +414 -0
- data/lib/ice_cube/single_occurrence_rule.rb +28 -0
- data/lib/ice_cube/time_util.rb +250 -0
- data/lib/ice_cube/validated_rule.rb +108 -0
- data/lib/ice_cube/validations/count.rb +56 -0
- data/lib/ice_cube/validations/daily_interval.rb +55 -0
- data/lib/ice_cube/validations/day.rb +65 -0
- data/lib/ice_cube/validations/day_of_month.rb +52 -0
- data/lib/ice_cube/validations/day_of_week.rb +70 -0
- data/lib/ice_cube/validations/day_of_year.rb +55 -0
- data/lib/ice_cube/validations/hour_of_day.rb +52 -0
- data/lib/ice_cube/validations/hourly_interval.rb +57 -0
- data/lib/ice_cube/validations/lock.rb +47 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +51 -0
- data/lib/ice_cube/validations/minutely_interval.rb +57 -0
- data/lib/ice_cube/validations/month_of_year.rb +49 -0
- data/lib/ice_cube/validations/monthly_interval.rb +51 -0
- data/lib/ice_cube/validations/schedule_lock.rb +41 -0
- data/lib/ice_cube/validations/second_of_minute.rb +49 -0
- data/lib/ice_cube/validations/secondly_interval.rb +54 -0
- data/lib/ice_cube/validations/until.rb +51 -0
- data/lib/ice_cube/validations/weekly_interval.rb +60 -0
- data/lib/ice_cube/validations/yearly_interval.rb +49 -0
- data/lib/ice_cube/version.rb +5 -0
- data/spec/spec_helper.rb +11 -0
- metadata +120 -0
data/lib/ice_cube.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'ice_cube/deprecated'
|
3
|
+
|
4
|
+
# Use psych if we can
|
5
|
+
begin
|
6
|
+
require 'psych'
|
7
|
+
rescue LoadError
|
8
|
+
require 'yaml'
|
9
|
+
end
|
10
|
+
|
11
|
+
module IceCube
|
12
|
+
|
13
|
+
autoload :VERSION, 'ice_cube/version'
|
14
|
+
|
15
|
+
autoload :TimeUtil, 'ice_cube/time_util'
|
16
|
+
|
17
|
+
autoload :Rule, 'ice_cube/rule'
|
18
|
+
autoload :Schedule, 'ice_cube/schedule'
|
19
|
+
|
20
|
+
autoload :IcalBuilder, 'ice_cube/builders/ical_builder'
|
21
|
+
autoload :HashBuilder, 'ice_cube/builders/hash_builder'
|
22
|
+
autoload :StringBuilder, 'ice_cube/builders/string_builder'
|
23
|
+
|
24
|
+
autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
|
25
|
+
autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'
|
26
|
+
autoload :ZeroInterval, 'ice_cube/errors/zero_interval'
|
27
|
+
|
28
|
+
autoload :ValidatedRule, 'ice_cube/validated_rule'
|
29
|
+
autoload :SingleOccurrenceRule, 'ice_cube/single_occurrence_rule'
|
30
|
+
|
31
|
+
autoload :SecondlyRule, 'ice_cube/rules/secondly_rule'
|
32
|
+
autoload :MinutelyRule, 'ice_cube/rules/minutely_rule'
|
33
|
+
autoload :HourlyRule, 'ice_cube/rules/hourly_rule'
|
34
|
+
autoload :DailyRule, 'ice_cube/rules/daily_rule'
|
35
|
+
autoload :WeeklyRule, 'ice_cube/rules/weekly_rule'
|
36
|
+
autoload :MonthlyRule, 'ice_cube/rules/monthly_rule'
|
37
|
+
autoload :YearlyRule, 'ice_cube/rules/yearly_rule'
|
38
|
+
|
39
|
+
module Validations
|
40
|
+
|
41
|
+
autoload :Lock, 'ice_cube/validations/lock'
|
42
|
+
autoload :ScheduleLock, 'ice_cube/validations/schedule_lock'
|
43
|
+
|
44
|
+
autoload :Count, 'ice_cube/validations/count'
|
45
|
+
autoload :Until, 'ice_cube/validations/until'
|
46
|
+
|
47
|
+
autoload :SecondlyInterval, 'ice_cube/validations/secondly_interval'
|
48
|
+
autoload :MinutelyInterval, 'ice_cube/validations/minutely_interval'
|
49
|
+
autoload :DailyInterval, 'ice_cube/validations/daily_interval'
|
50
|
+
autoload :WeeklyInterval, 'ice_cube/validations/weekly_interval'
|
51
|
+
autoload :MonthlyInterval, 'ice_cube/validations/monthly_interval'
|
52
|
+
autoload :YearlyInterval, 'ice_cube/validations/yearly_interval'
|
53
|
+
autoload :HourlyInterval, 'ice_cube/validations/hourly_interval'
|
54
|
+
|
55
|
+
autoload :HourOfDay, 'ice_cube/validations/hour_of_day'
|
56
|
+
autoload :MonthOfYear, 'ice_cube/validations/month_of_year'
|
57
|
+
autoload :MinuteOfHour, 'ice_cube/validations/minute_of_hour'
|
58
|
+
autoload :SecondOfMinute, 'ice_cube/validations/second_of_minute'
|
59
|
+
autoload :DayOfMonth, 'ice_cube/validations/day_of_month'
|
60
|
+
autoload :DayOfWeek, 'ice_cube/validations/day_of_week'
|
61
|
+
autoload :Day, 'ice_cube/validations/day'
|
62
|
+
autoload :DayOfYear, 'ice_cube/validations/day_of_year'
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
# Define some useful constants
|
67
|
+
ONE_SECOND = 1
|
68
|
+
ONE_MINUTE = ONE_SECOND * 60
|
69
|
+
ONE_HOUR = ONE_MINUTE * 60
|
70
|
+
ONE_DAY = ONE_HOUR * 24
|
71
|
+
ONE_WEEK = ONE_DAY * 7
|
72
|
+
|
73
|
+
# Formatting
|
74
|
+
TO_S_TIME_FORMAT = '%B %e, %Y'
|
75
|
+
|
76
|
+
def self.use_psych?
|
77
|
+
@use_psych ||= defined?(Psych) && defined?(Psych::VERSION)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module IceCube
|
2
|
+
|
3
|
+
class HashBuilder
|
4
|
+
|
5
|
+
def initialize(rule = nil)
|
6
|
+
@hash = { :validations => {}, :rule_type => rule.class.name }
|
7
|
+
end
|
8
|
+
|
9
|
+
def validations
|
10
|
+
@hash[:validations]
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, value)
|
14
|
+
@hash[key] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def validations_array(type)
|
18
|
+
validations[type] ||= []
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
@hash
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module IceCube
|
2
|
+
|
3
|
+
class IcalBuilder
|
4
|
+
|
5
|
+
ICAL_DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@hash = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.fixnum_to_ical_day(num)
|
12
|
+
ICAL_DAYS[num]
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@hash[key] ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Build for a single rule entry
|
20
|
+
def to_s
|
21
|
+
arr = []
|
22
|
+
if freq = @hash.delete('FREQ')
|
23
|
+
arr << "FREQ=#{freq.join(',')}"
|
24
|
+
end
|
25
|
+
arr.concat(@hash.map do |key, value|
|
26
|
+
if value.is_a?(Array)
|
27
|
+
"#{key}=#{value.join(',')}"
|
28
|
+
end
|
29
|
+
end.compact)
|
30
|
+
arr.join(';')
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.ical_utc_format(time)
|
34
|
+
time = time.dup.utc
|
35
|
+
"#{time.strftime('%Y%m%dT%H%M%SZ')}" # utc time
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.ical_format(time, force_utc)
|
39
|
+
time = time.dup.utc if force_utc
|
40
|
+
if time.utc?
|
41
|
+
":#{time.strftime('%Y%m%dT%H%M%SZ')}" # utc time
|
42
|
+
else
|
43
|
+
";TZID=#{time.strftime('%Z:%Y%m%dT%H%M%S')}" # local time specified
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.ical_duration(duration)
|
48
|
+
hours = duration / 3600; duration %= 3600
|
49
|
+
minutes = duration / 60; duration %= 60
|
50
|
+
repr = ''
|
51
|
+
repr << "#{hours}H" if hours > 0
|
52
|
+
repr << "#{minutes}M" if minutes > 0
|
53
|
+
repr << "#{duration}S" if duration > 0
|
54
|
+
"PT#{repr}"
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module IceCube
|
2
|
+
|
3
|
+
class StringBuilder
|
4
|
+
|
5
|
+
attr_writer :base
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@types = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def piece(type, prefix = nil, suffix = nil)
|
12
|
+
@types[type] ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
str = @base || ''
|
17
|
+
res = @types.map do |type, segments|
|
18
|
+
if f = self.class.formatter(type)
|
19
|
+
str << ' ' + f.call(segments)
|
20
|
+
else
|
21
|
+
next if segments.empty?
|
22
|
+
str << ' ' + self.class.sentence(segments)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
str
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
|
30
|
+
def formatter(type)
|
31
|
+
@formatters[type]
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_formatter(type, &formatter)
|
35
|
+
@formatters ||= {}
|
36
|
+
@formatters[type] = formatter
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
|
43
|
+
NUMBER_SUFFIX = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th']
|
44
|
+
SPECIAL_SUFFIX = { 11 => 'th', 12 => 'th', 13 => 'th', 14 => 'th' }
|
45
|
+
|
46
|
+
# influenced by ActiveSupport's to_sentence
|
47
|
+
def sentence(array)
|
48
|
+
case array.length
|
49
|
+
when 0 ; ''
|
50
|
+
when 1 ; array[0].to_s
|
51
|
+
when 2 ; "#{array[0]} and #{array[1]}"
|
52
|
+
else ; "#{array[0...-1].join(', ')}, and #{array[-1]}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def nice_number(number)
|
57
|
+
if number == -1
|
58
|
+
'last'
|
59
|
+
elsif number < -1
|
60
|
+
suffix = SPECIAL_SUFFIX.include?(number) ?
|
61
|
+
SPECIAL_SUFFIX[number] : NUMBER_SUFFIX[number.abs % 10]
|
62
|
+
number.abs.to_s << suffix << ' to last'
|
63
|
+
else
|
64
|
+
suffix = SPECIAL_SUFFIX.include?(number) ?
|
65
|
+
SPECIAL_SUFFIX[number] : NUMBER_SUFFIX[number.abs % 10]
|
66
|
+
number.to_s << suffix
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Deprecated
|
2
|
+
|
3
|
+
# Define a deprecated alias for a method
|
4
|
+
# @param [Symbol] name - name of method to define
|
5
|
+
# @param [Symbol] replacement - name of method to replace (alias)
|
6
|
+
def deprecated_alias(name, replacement)
|
7
|
+
# Create a wrapped version
|
8
|
+
define_method(name) do |*args, &block|
|
9
|
+
warn "IceCube: ##{name} deprecated (please use ##{replacement})"
|
10
|
+
send replacement, *args, &block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Deprecate a defined method
|
15
|
+
# @param [Symbol] name - name of deprecated method
|
16
|
+
# @param [Symbol] replacement - name of the desired replacement
|
17
|
+
def deprecated(name, replacement)
|
18
|
+
# Replace old method
|
19
|
+
old_name = :"#{name}_without_deprecation"
|
20
|
+
alias_method old_name, name
|
21
|
+
# And replace it with a wrapped version
|
22
|
+
define_method(name) do |*args, &block|
|
23
|
+
warn "IceCube: ##{name} deprecated (please use ##{replacement})"
|
24
|
+
send old_name, *args, &block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module IceCube
|
4
|
+
|
5
|
+
class Rule
|
6
|
+
|
7
|
+
attr_reader :uses
|
8
|
+
|
9
|
+
# Is this a terminating schedule?
|
10
|
+
def terminating?
|
11
|
+
until_time || occurrence_count
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(rule)
|
15
|
+
if rule.is_a? Rule
|
16
|
+
hash = to_hash
|
17
|
+
hash && hash == rule.to_hash
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def hash
|
22
|
+
h = to_hash
|
23
|
+
h.nil? ? super : h.hash
|
24
|
+
end
|
25
|
+
|
26
|
+
# Expected to be overridden by subclasses
|
27
|
+
def to_ical
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.from_ical ical
|
32
|
+
params = {:validations => {}}
|
33
|
+
|
34
|
+
ical.split(';').each do |rule|
|
35
|
+
(name, value) = rule.split('=')
|
36
|
+
case name
|
37
|
+
when 'FREQ'
|
38
|
+
params[:freq] = value.downcase
|
39
|
+
when 'INTERVAL'
|
40
|
+
params[:interval] = value.to_i
|
41
|
+
when 'COUNT'
|
42
|
+
params[:count] = value.to_i
|
43
|
+
when 'UNTIL'
|
44
|
+
params[:until] = DateTime.parse(value).to_time.utc
|
45
|
+
when 'WKST'
|
46
|
+
params[:wkst] = TimeUtil.ical_day_to_symbol(value)
|
47
|
+
|
48
|
+
when 'BYSECOND'
|
49
|
+
params[:validations][:second_of_minute] = value.split(',').collect{ |v| v.to_i }
|
50
|
+
when "BYMINUTE"
|
51
|
+
params[:validations][:minute_of_hour] = value.split(',').collect{ |v| v.to_i }
|
52
|
+
when "BYHOUR"
|
53
|
+
params[:validations][:hour_of_day] = value.split(',').collect{ |v| v.to_i }
|
54
|
+
when "BYDAY"
|
55
|
+
dows = {}
|
56
|
+
days = []
|
57
|
+
value.split(',').each do |expr|
|
58
|
+
day = TimeUtil.ical_day_to_symbol(expr.strip[-2..-1])
|
59
|
+
if expr.strip.length > 2 # day with occurence
|
60
|
+
occ = expr[0..-3].to_i
|
61
|
+
dows[day].nil? ? dows[day] = [occ] : dows[day].push(occ)
|
62
|
+
days.delete(TimeUtil.symbol_to_day(day))
|
63
|
+
else
|
64
|
+
days.push TimeUtil.symbol_to_day(day) if dows[day].nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
params[:validations][:day_of_week] = dows unless dows.empty?
|
68
|
+
params[:validations][:day] = days unless days.empty?
|
69
|
+
when "BYMONTHDAY"
|
70
|
+
params[:validations][:day_of_month] = value.split(',').collect{ |v| v.to_i }
|
71
|
+
when "BYMONTH"
|
72
|
+
params[:validations][:month_of_year] = value.split(',').collect{ |v| v.to_i }
|
73
|
+
when "BYYEARDAY"
|
74
|
+
params[:validations][:day_of_year] = value.split(',').collect{ |v| v.to_i }
|
75
|
+
|
76
|
+
else
|
77
|
+
raise "Invalid or unsupported rrule command : #{name}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
params[:interval] ||= 1
|
82
|
+
# WKST only valid for weekly rules
|
83
|
+
params.delete(:wkst) unless params[:freq] == 'weekly'
|
84
|
+
|
85
|
+
rule = IceCube::Rule.send(*params.values_at(:freq, :interval, :wkst).compact)
|
86
|
+
rule.count(params[:count]) if params[:count]
|
87
|
+
rule.until(params[:until]) if params[:until]
|
88
|
+
params[:validations].each do |key, value|
|
89
|
+
value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value)
|
90
|
+
end
|
91
|
+
|
92
|
+
rule
|
93
|
+
end
|
94
|
+
|
95
|
+
# Yaml implementation
|
96
|
+
def to_yaml(*args)
|
97
|
+
IceCube::use_psych? ? Psych::dump(to_hash) : YAML::dump(to_hash, *args)
|
98
|
+
end
|
99
|
+
|
100
|
+
# From yaml
|
101
|
+
def self.from_yaml(yaml)
|
102
|
+
from_hash IceCube::use_psych? ? Psych::load(yaml) : YAML::load(yaml)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Expected to be overridden by subclasses
|
106
|
+
def to_hash
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convert from a hash and create a rule
|
111
|
+
def self.from_hash(hash)
|
112
|
+
return nil unless match = hash[:rule_type].match(/\:\:(.+?)Rule/)
|
113
|
+
rule = IceCube::Rule.send(match[1].downcase.to_sym, hash[:interval] || 1)
|
114
|
+
rule.until(TimeUtil.deserialize_time(hash[:until])) if hash[:until]
|
115
|
+
rule.count(hash[:count]) if hash[:count]
|
116
|
+
hash[:validations] && hash[:validations].each do |key, value|
|
117
|
+
key = key.to_sym unless key.is_a?(Symbol)
|
118
|
+
value.is_a?(::Array) ? rule.send(key, *value) : rule.send(key, value)
|
119
|
+
end
|
120
|
+
rule
|
121
|
+
end
|
122
|
+
|
123
|
+
# Reset the uses on the rule to 0
|
124
|
+
def reset
|
125
|
+
@uses = 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def next_time(time, schedule, closing_time)
|
129
|
+
end
|
130
|
+
|
131
|
+
def on?(time, schedule)
|
132
|
+
next_time(time, schedule, time) == time
|
133
|
+
end
|
134
|
+
|
135
|
+
# Whether this rule requires a full run
|
136
|
+
def full_required?
|
137
|
+
!@count.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
# Convenience methods for creating Rules
|
141
|
+
class << self
|
142
|
+
|
143
|
+
# Secondly Rule
|
144
|
+
def secondly(interval = 1)
|
145
|
+
SecondlyRule.new(interval)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Minutely Rule
|
149
|
+
def minutely(interval = 1)
|
150
|
+
MinutelyRule.new(interval)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Hourly Rule
|
154
|
+
def hourly(interval = 1)
|
155
|
+
HourlyRule.new(interval)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Daily Rule
|
159
|
+
def daily(interval = 1)
|
160
|
+
DailyRule.new(interval)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Weekly Rule
|
164
|
+
def weekly(interval = 1, week_start = :sunday)
|
165
|
+
WeeklyRule.new(interval, week_start)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Monthly Rule
|
169
|
+
def monthly(interval = 1)
|
170
|
+
MonthlyRule.new(interval)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Yearly Rule
|
174
|
+
def yearly(interval = 1)
|
175
|
+
YearlyRule.new(interval)
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|