ice_cube_conrad 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|