ice_cube 0.11.3 → 0.12.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.
- checksums.yaml +4 -4
- data/lib/ice_cube.rb +13 -0
- data/lib/ice_cube/deprecated.rb +8 -0
- data/lib/ice_cube/enumerator.rb +63 -0
- data/lib/ice_cube/errors/count_exceeded.rb +1 -1
- data/lib/ice_cube/errors/until_exceeded.rb +1 -1
- data/lib/ice_cube/flexible_hash.rb +23 -7
- data/lib/ice_cube/formatters/string.rb +155 -0
- data/lib/ice_cube/hash_input.rb +71 -0
- data/lib/ice_cube/occurrence.rb +2 -1
- data/lib/ice_cube/parsers/hash_parser.rb +87 -0
- data/lib/ice_cube/parsers/yaml_parser.rb +21 -0
- data/lib/ice_cube/schedule.rb +36 -43
- data/lib/ice_cube/string_helpers.rb +10 -0
- data/lib/ice_cube/time_step.rb +30 -0
- data/lib/ice_cube/time_util.rb +10 -3
- data/lib/ice_cube/validated_rule.rb +13 -6
- data/lib/ice_cube/validations/count.rb +4 -0
- data/lib/ice_cube/validations/daily_interval.rb +4 -0
- data/lib/ice_cube/validations/day.rb +4 -0
- data/lib/ice_cube/validations/day_of_month.rb +4 -0
- data/lib/ice_cube/validations/day_of_week.rb +4 -0
- data/lib/ice_cube/validations/day_of_year.rb +4 -0
- data/lib/ice_cube/validations/hour_of_day.rb +4 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +4 -0
- data/lib/ice_cube/validations/month_of_year.rb +4 -0
- data/lib/ice_cube/validations/monthly_interval.rb +4 -0
- data/lib/ice_cube/validations/second_of_minute.rb +4 -0
- data/lib/ice_cube/validations/until.rb +4 -0
- data/lib/ice_cube/validations/weekly_interval.rb +4 -0
- data/lib/ice_cube/validations/yearly_interval.rb +4 -0
- data/lib/ice_cube/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8d575d65663825eb9be2a82f9e931537009d485
|
4
|
+
data.tar.gz: 2e292c2f229a8a7b7aaaf8f8dcaf009412741745
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98e4ffa4ddc22fb26da6eb632203e389361149216ad62193f64c773fe1a68da07dff80ab3f0470cbc1d0dabfc049780b86eb4f270469b2e47539e46016709c1e
|
7
|
+
data.tar.gz: 2882dc6f6af47db013a1ee079c2fc06ecf254274b0d39b00f24bc6a8e72021b4935e27463a65ab7c01740180bcab4236bfc15507825b3db910b4bf16f5ab6ce3
|
data/lib/ice_cube.rb
CHANGED
@@ -16,6 +16,9 @@ module IceCube
|
|
16
16
|
autoload :HashBuilder, 'ice_cube/builders/hash_builder'
|
17
17
|
autoload :StringBuilder, 'ice_cube/builders/string_builder'
|
18
18
|
|
19
|
+
autoload :HashParser, 'ice_cube/parsers/hash_parser'
|
20
|
+
autoload :YamlParser, 'ice_cube/parsers/yaml_parser'
|
21
|
+
|
19
22
|
autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
|
20
23
|
autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'
|
21
24
|
|
@@ -74,4 +77,14 @@ module IceCube
|
|
74
77
|
def self.to_s_time_format=(format)
|
75
78
|
@to_s_time_format = format
|
76
79
|
end
|
80
|
+
|
81
|
+
# Retain backwards compatibility for schedules exported from older versions
|
82
|
+
# This represents the version number, 11 = 0.11, 1.0 will be 100
|
83
|
+
def self.compatibility
|
84
|
+
@compatibility ||= 11
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.compatibility=(version)
|
88
|
+
@compatibility = version
|
89
|
+
end
|
77
90
|
end
|
data/lib/ice_cube/deprecated.rb
CHANGED
@@ -26,5 +26,13 @@ module IceCube
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.schedule_options(schedule, options)
|
30
|
+
if options[:start_date_override]
|
31
|
+
warn "IceCube: :start_date_override option deprecated. " \
|
32
|
+
"(please use a block { |s| s.start_time = override })"
|
33
|
+
schedule.start_time = options[:start_date_override]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
29
37
|
end
|
30
38
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module IceCube
|
2
|
+
class Enumerator < ::Enumerator
|
3
|
+
|
4
|
+
def initialize(schedule, from_time, to_time)
|
5
|
+
@schedule = schedule
|
6
|
+
@from_time = TimeUtil.ensure_time(from_time)
|
7
|
+
@to_time = TimeUtil.ensure_time(to_time)
|
8
|
+
align_start_time
|
9
|
+
@cursor = @from_time
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
while res = self.find_next && @to_time.nil? || res <= @to_time
|
14
|
+
yield Occurrence.new(res, res + schedule.duration)
|
15
|
+
end
|
16
|
+
raise StopIteration
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_next
|
20
|
+
loop do
|
21
|
+
min_time = recurrence_rules.reduce(nil) do |min_time, rule|
|
22
|
+
catch :limit do
|
23
|
+
new_time = rule.next_time(time, schedule, min_time || @to_time)
|
24
|
+
[min_time, new_time].compact.min
|
25
|
+
end
|
26
|
+
end
|
27
|
+
break nil unless min_time
|
28
|
+
@cursor = min_time + 1
|
29
|
+
next if exception_time?(min_time)
|
30
|
+
break min_time
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def align_start_time
|
37
|
+
if @from_time <= schedule.start_time || full_required?
|
38
|
+
@from_time = schedule.start_time
|
39
|
+
else
|
40
|
+
@from_time += @schedule.start_time.subsec - @from_time.subsec rescue 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return a boolean indicating if any rule needs to be run from the start of time
|
45
|
+
def full_required?
|
46
|
+
recurrence_rules.any?(&:full_required?) ||
|
47
|
+
exception_rules.any?(&:full_required?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def exception_rules
|
51
|
+
schedule.instance_variable_get(:@all_exception_rules)
|
52
|
+
end
|
53
|
+
|
54
|
+
def recurrence_rules
|
55
|
+
@recurrence_rules ||= if recurrence_rules.empty?
|
56
|
+
[SingleOccurrenceRule.new(schedule.start_time)].concat schedule.instance_variable_get(:@all_recurrence_rules)
|
57
|
+
else
|
58
|
+
schedule.instance_variable_get(:@all_recurrence_rules)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -2,21 +2,37 @@ require 'delegate'
|
|
2
2
|
|
3
3
|
module IceCube
|
4
4
|
|
5
|
-
#
|
5
|
+
# Find keys by symbol or string without symbolizing user input
|
6
6
|
# Due to the serialization format of ice_cube, this limited implementation
|
7
7
|
# is entirely sufficient
|
8
8
|
|
9
9
|
class FlexibleHash < SimpleDelegator
|
10
10
|
|
11
|
-
def
|
12
|
-
|
11
|
+
def [](key)
|
12
|
+
key = _match_key(key)
|
13
|
+
super
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def fetch(key)
|
17
|
+
key = _match_key(key)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(key)
|
22
|
+
key = _match_key(key)
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def _match_key(key)
|
29
|
+
return key if __getobj__.has_key? key
|
30
|
+
if Symbol == key.class
|
31
|
+
__getobj__.keys.detect { |k| return k if k == key.to_s }
|
32
|
+
elsif String == key.class
|
33
|
+
__getobj__.keys.detect { |k| return k if k.to_s == key }
|
19
34
|
end
|
35
|
+
key
|
20
36
|
end
|
21
37
|
|
22
38
|
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
class IceCube::StringBuilder
|
2
|
+
|
3
|
+
format :day do |entries|
|
4
|
+
case entries = entries.sort
|
5
|
+
when [0, 6] then 'on Weekends'
|
6
|
+
when [1, 2, 3, 4, 5] then 'on Weekdays'
|
7
|
+
else
|
8
|
+
"on " << build(:sentence, build(:daynames, entries))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
format :dayname do |number|
|
13
|
+
Date::DAYNAMES[number]
|
14
|
+
end
|
15
|
+
|
16
|
+
format :daynames do |entries|
|
17
|
+
entries.map { |wday| build(:dayname, wday) + "s" }
|
18
|
+
end
|
19
|
+
|
20
|
+
format :monthnames do |entries|
|
21
|
+
entries.map { |m| Date::MONTHNAMES[m] }
|
22
|
+
end
|
23
|
+
|
24
|
+
format :count do |number|
|
25
|
+
times = number == 1 ? "time" : "times"
|
26
|
+
"#{number} #{times}"
|
27
|
+
end
|
28
|
+
|
29
|
+
format :day_of_month do |entries|
|
30
|
+
numbered = build(:sentence, build(:ordinals, entries))
|
31
|
+
days = entries.size == 1 ? "day" : "days"
|
32
|
+
"on the #{numbered} #{days} of the month"
|
33
|
+
end
|
34
|
+
|
35
|
+
format :day_of_week do |entries|
|
36
|
+
numbered_weekdays = build(:daynames_of_week, entries).join(" and ")
|
37
|
+
"on the #{numbered_weekdays}"
|
38
|
+
end
|
39
|
+
|
40
|
+
format :daynames_of_week do |entries|
|
41
|
+
entries.map do |occurrence, wday|
|
42
|
+
numbered = build(:ordinal, occurrence)
|
43
|
+
weekday = build(:dayname, wday)
|
44
|
+
"#{numbered} #{weekday}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
format :day_of_year do |entries|
|
49
|
+
numbered = build(:sentence, build(:ordinals, entries))
|
50
|
+
days = entries.size == 1 ? "day" : "days"
|
51
|
+
"on the #{numbered} #{days} of the year"
|
52
|
+
end
|
53
|
+
|
54
|
+
format :hour_of_day do |entries|
|
55
|
+
numbered = build(:sentence, build(:ordinals, entries))
|
56
|
+
hours = entries.size == 1 ? "hour" : "hours"
|
57
|
+
"on the #{numbered} #{hours} of the day"
|
58
|
+
end
|
59
|
+
|
60
|
+
format :minute_of_hour do |entries|
|
61
|
+
numbered = build(:sentence, build(:ordinals, entries))
|
62
|
+
minutes = entries.size == 1 ? "minute" : "minutes"
|
63
|
+
"on the #{numbered} #{minutes} of the hour"
|
64
|
+
end
|
65
|
+
|
66
|
+
format :month_of_year do |entries|
|
67
|
+
"in " << build(:sentence, build(:monthnames, entries))
|
68
|
+
end
|
69
|
+
|
70
|
+
format :second_of_minute do |entries|
|
71
|
+
numbered = build(:sentence, build(:ordinals, entries))
|
72
|
+
seconds = entries.size == 1 ? "second" : "seconds"
|
73
|
+
"on the #{numbered} #{seconds} of the minute"
|
74
|
+
end
|
75
|
+
|
76
|
+
format :time do |time|
|
77
|
+
time.strftime(IceCube.to_s_time_format)
|
78
|
+
end
|
79
|
+
|
80
|
+
format :until do |time|
|
81
|
+
"until " << build(:time, time)
|
82
|
+
end
|
83
|
+
|
84
|
+
format :sentence do |entries|
|
85
|
+
case entries.length
|
86
|
+
when 0 then ''
|
87
|
+
when 1 then entries[0].to_s
|
88
|
+
when 2 then "#{entries[0]} and #{entries[1]}"
|
89
|
+
else "#{entries[0...-1].join(', ')}, and #{entries[-1]}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
format :ordinals do |entries|
|
94
|
+
entries = entries.sort
|
95
|
+
entries.rotate! while entries[0] < 0 if entries.last > 0
|
96
|
+
entries.map { |number| build(:ordinal, number) }
|
97
|
+
end
|
98
|
+
|
99
|
+
format :ordinal do |number|
|
100
|
+
next "last" if number == -1
|
101
|
+
suffix = SPECIAL_SUFFIX[number] || NUMBER_SUFFIX[number.abs % 10]
|
102
|
+
if number < -1
|
103
|
+
number.abs.to_s << suffix << " to last"
|
104
|
+
else
|
105
|
+
number.to_s << suffix
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
format :daily_interval do |i|
|
110
|
+
i == 1 ? "Daily" : "Every #{i} days"
|
111
|
+
end
|
112
|
+
|
113
|
+
format :hourly_interval do |i|
|
114
|
+
i == 1 ? "Hourly" : "Every #{i} hours"
|
115
|
+
end
|
116
|
+
|
117
|
+
format :minutely_interval do |i|
|
118
|
+
i == 1 ? "Minutely" : "Every #{i} minutes"
|
119
|
+
end
|
120
|
+
|
121
|
+
format :monthly_interval do |i|
|
122
|
+
i == 1 ? "Monthly" : "Every #{i} months"
|
123
|
+
end
|
124
|
+
|
125
|
+
format :secondly_interval do |i|
|
126
|
+
i == 1 ? "Secondly" : "Every #{i} seconds"
|
127
|
+
end
|
128
|
+
|
129
|
+
format :weekly_interval do |i|
|
130
|
+
i == 1 ? "Weekly" : "Every #{i} weeks"
|
131
|
+
end
|
132
|
+
|
133
|
+
format :yearly_interval do |i|
|
134
|
+
i == 1 ? "Yearly" : "Every #{i} years"
|
135
|
+
end
|
136
|
+
|
137
|
+
format :exrule do |rule|
|
138
|
+
"not #{rule}"
|
139
|
+
end
|
140
|
+
|
141
|
+
format :extime do |time|
|
142
|
+
"not on #{build(:time, time)}"
|
143
|
+
end
|
144
|
+
|
145
|
+
format :schedule do |s|
|
146
|
+
times = s.recurrence_times_with_start_time - s.extimes
|
147
|
+
pieces = []
|
148
|
+
pieces.concat times.uniq.sort.map { |t| build(:time, t) }
|
149
|
+
pieces.concat s.rrules.map { |t| t.to_s }
|
150
|
+
pieces.concat s.exrules.map { |t| build(:exrule, t) }
|
151
|
+
pieces.concat s.extimes.uniq.sort.map { |t| build(:extime, t) }
|
152
|
+
pieces.join(' / ')
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module IceCube
|
2
|
+
class HashInput
|
3
|
+
|
4
|
+
class Mash
|
5
|
+
def initialize(hash)
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
# Fetch values indifferently by symbol or string key without symbolizing
|
10
|
+
# arbitrary string input
|
11
|
+
#
|
12
|
+
def [](key)
|
13
|
+
@hash.fetch(key) do |key|
|
14
|
+
str_key = key.to_s
|
15
|
+
@hash.each_pair.detect do |sym_key, value|
|
16
|
+
return value if sym_key.to_s == str_key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(hash)
|
23
|
+
@input = Mash.new(hash)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@input[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_rule
|
31
|
+
return nil unless rule_class
|
32
|
+
rule = rule_class.new(interval, week_start)
|
33
|
+
rule.until(limit_time) if limit_time
|
34
|
+
rule.count(limit_count) if limit_count
|
35
|
+
validations.each do |validation, args|
|
36
|
+
rule.send(validation, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def rule_class
|
41
|
+
return @rule_class if @rule_class
|
42
|
+
match = @input[:rule_type].match(/::(\w+Rule)$/)
|
43
|
+
@rule_class = IceCube.const_get(match[1]) if match
|
44
|
+
end
|
45
|
+
|
46
|
+
def interval
|
47
|
+
@input[:interval] || 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def week_start
|
51
|
+
@input[:week_start] || :sunday
|
52
|
+
end
|
53
|
+
|
54
|
+
def limit_time
|
55
|
+
@limit_time ||= TimeUtil.deserialize_time(@input[:until])
|
56
|
+
end
|
57
|
+
|
58
|
+
def limit_count
|
59
|
+
@input[:count]
|
60
|
+
end
|
61
|
+
|
62
|
+
def validations
|
63
|
+
input_validations = Mash.new(@input[:validations] || {})
|
64
|
+
ValidatedRule::VALIDATION_ORDER.each_with_object({}) do |key, output_validations|
|
65
|
+
args = input_validations[key]
|
66
|
+
output_validations[key] = Array(args) if args
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
data/lib/ice_cube/occurrence.rb
CHANGED
@@ -0,0 +1,87 @@
|
|
1
|
+
module IceCube
|
2
|
+
class HashParser
|
3
|
+
|
4
|
+
attr_reader :hash
|
5
|
+
|
6
|
+
def initialize(original_hash)
|
7
|
+
@hash = original_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_schedule
|
11
|
+
data = normalize_keys(hash)
|
12
|
+
schedule = IceCube::Schedule.new parse_time(data[:start_time])
|
13
|
+
apply_duration schedule, data
|
14
|
+
apply_end_time schedule, data
|
15
|
+
apply_rrules schedule, data
|
16
|
+
apply_exrules schedule, data
|
17
|
+
apply_rtimes schedule, data
|
18
|
+
apply_extimes schedule, data
|
19
|
+
yield schedule if block_given?
|
20
|
+
schedule
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def normalize_keys(hash)
|
26
|
+
data = IceCube::FlexibleHash.new(hash.dup)
|
27
|
+
|
28
|
+
if (start_date = data.delete(:start_date))
|
29
|
+
warn "IceCube: :start_date deprecated. (please use :start_time)"
|
30
|
+
data[:start_time] = start_date
|
31
|
+
end
|
32
|
+
|
33
|
+
{:rdates => :rtimes, :exdates => :extimes}.each do |old_key, new_key|
|
34
|
+
if (times = data.delete(old_key))
|
35
|
+
warn "IceCube: :#{old_key} deprecated. (please use :#{new_key})"
|
36
|
+
(data[new_key] ||= []).concat times
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
data
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_duration(schedule, data)
|
44
|
+
return unless data[:duration]
|
45
|
+
schedule.duration = data[:duration].to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_end_time(schedule, data)
|
49
|
+
return unless data[:end_time]
|
50
|
+
schedule.end_time = parse_time(data[:end_time])
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_rrules(schedule, data)
|
54
|
+
return unless data[:rrules]
|
55
|
+
data[:rrules].each do |h|
|
56
|
+
schedule.rrule(IceCube::Rule.from_hash(h))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_exrules(schedule, data)
|
61
|
+
return unless data[:exrules]
|
62
|
+
warn "IceCube: :exrules deprecated. (This will be going away)"
|
63
|
+
data[:exrules].each do |h|
|
64
|
+
schedule.exrule(IceCube::Rule.from_hash(h))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_rtimes(schedule, data)
|
69
|
+
return unless data[:rtimes]
|
70
|
+
data[:rtimes].each do |t|
|
71
|
+
schedule.add_recurrence_time TimeUtil.deserialize_time(t)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def apply_extimes(schedule, data)
|
76
|
+
return unless data[:extimes]
|
77
|
+
data[:extimes].each do |t|
|
78
|
+
schedule.add_exception_time TimeUtil.deserialize_time(t)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def parse_time(time)
|
83
|
+
TimeUtil.deserialize_time(time)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module IceCube
|
2
|
+
class YamlParser < HashParser
|
3
|
+
|
4
|
+
SERIALIZED_START = /start_(?:time|date): .+(?<tz>(?:-|\+)\d{2}:\d{2})$/
|
5
|
+
|
6
|
+
attr_reader :hash
|
7
|
+
|
8
|
+
def initialize(yaml)
|
9
|
+
@hash = YAML::load(yaml)
|
10
|
+
yaml.match(SERIALIZED_START) do |match|
|
11
|
+
start_time = hash[:start_time] || hash[:start_date]
|
12
|
+
hash = FlexibleHash.new(@hash)
|
13
|
+
TimeUtil.restore_deserialized_offset start_time, match[:tz]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/ice_cube/schedule.rb
CHANGED
@@ -153,7 +153,7 @@ module IceCube
|
|
153
153
|
require_terminating_rules
|
154
154
|
enumerate_occurrences(start_time).to_a
|
155
155
|
end
|
156
|
-
|
156
|
+
|
157
157
|
# Emit an enumerator based on the start time
|
158
158
|
def all_occurrences_enumerator
|
159
159
|
enumerate_occurrences(start_time)
|
@@ -340,20 +340,22 @@ module IceCube
|
|
340
340
|
|
341
341
|
# Load the schedule from yaml
|
342
342
|
def self.from_yaml(yaml, options = {})
|
343
|
-
|
344
|
-
|
345
|
-
|
343
|
+
YamlParser.new(yaml).to_schedule do |schedule|
|
344
|
+
Deprecated.schedule_options(schedule, options)
|
345
|
+
yield schedule if block_given?
|
346
346
|
end
|
347
|
-
from_hash hash, options
|
348
347
|
end
|
349
348
|
|
350
349
|
# Convert the schedule to a hash
|
351
350
|
def to_hash
|
352
351
|
data = {}
|
353
|
-
data[:
|
352
|
+
data[:start_time] = TimeUtil.serialize_time(start_time)
|
353
|
+
data[:start_date] = data[:start_time] if IceCube.compatibility <= 11
|
354
354
|
data[:end_time] = TimeUtil.serialize_time(end_time) if end_time
|
355
355
|
data[:rrules] = recurrence_rules.map(&:to_hash)
|
356
|
-
|
356
|
+
if IceCube.compatibility <= 11 && exception_rules.any?
|
357
|
+
data[:exrules] = exception_rules.map(&:to_hash)
|
358
|
+
end
|
357
359
|
data[:rtimes] = recurrence_times.map do |rt|
|
358
360
|
TimeUtil.serialize_time(rt)
|
359
361
|
end
|
@@ -365,28 +367,10 @@ module IceCube
|
|
365
367
|
|
366
368
|
# Load the schedule from a hash
|
367
369
|
def self.from_hash(original_hash, options = {})
|
368
|
-
original_hash
|
369
|
-
|
370
|
-
|
371
|
-
schedule = IceCube::Schedule.new TimeUtil.deserialize_time(data[:start_date])
|
372
|
-
schedule.end_time = schedule.start_time + data[:duration] if data[:duration]
|
373
|
-
schedule.end_time = TimeUtil.deserialize_time(data[:end_time]) if data[:end_time]
|
374
|
-
data[:rrules] && data[:rrules].each { |h| schedule.rrule(IceCube::Rule.from_hash(h)) }
|
375
|
-
data[:exrules] && data[:exrules].each { |h| schedule.exrule(IceCube::Rule.from_hash(h)) }
|
376
|
-
data[:rtimes] && data[:rtimes].each do |t|
|
377
|
-
schedule.add_recurrence_time TimeUtil.deserialize_time(t)
|
378
|
-
end
|
379
|
-
data[:extimes] && data[:extimes].each do |t|
|
380
|
-
schedule.add_exception_time TimeUtil.deserialize_time(t)
|
381
|
-
end
|
382
|
-
# Also serialize old format for backward compat
|
383
|
-
data[:rdates] && data[:rdates].each do |t|
|
384
|
-
schedule.add_recurrence_time TimeUtil.deserialize_time(t)
|
385
|
-
end
|
386
|
-
data[:exdates] && data[:exdates].each do |t|
|
387
|
-
schedule.add_exception_time TimeUtil.deserialize_time(t)
|
370
|
+
HashParser.new(original_hash).to_schedule do |schedule|
|
371
|
+
Deprecated.schedule_options(schedule, options)
|
372
|
+
yield schedule if block_given?
|
388
373
|
end
|
389
|
-
schedule
|
390
374
|
end
|
391
375
|
|
392
376
|
# Determine if the schedule will end
|
@@ -413,25 +397,27 @@ module IceCube
|
|
413
397
|
|
414
398
|
# Find all of the occurrences for the schedule between opening_time
|
415
399
|
# and closing_time
|
400
|
+
# Iteration is unrolled in pairs to skip duplicate times in end of DST
|
416
401
|
def enumerate_occurrences(opening_time, closing_time = nil, &block)
|
417
|
-
opening_time = TimeUtil.
|
418
|
-
closing_time = TimeUtil.
|
402
|
+
opening_time = TimeUtil.match_zone(opening_time, start_time)
|
403
|
+
closing_time = TimeUtil.match_zone(closing_time, start_time)
|
419
404
|
opening_time += start_time.subsec - opening_time.subsec rescue 0
|
420
405
|
reset
|
421
406
|
opening_time = start_time if opening_time < start_time
|
422
|
-
|
423
|
-
# If we have rules with counts, we need to walk from the beginning of time,
|
424
|
-
# otherwise opening_time
|
425
|
-
time = full_required? ? start_time : opening_time
|
407
|
+
t1 = full_required? ? start_time : opening_time
|
426
408
|
e = Enumerator.new do |yielder|
|
427
409
|
loop do
|
428
|
-
|
429
|
-
break
|
430
|
-
|
431
|
-
|
432
|
-
|
410
|
+
break unless (t0 = next_time(t1, closing_time))
|
411
|
+
break if closing_time && t0 > closing_time
|
412
|
+
yielder << (block_given? ? block.call(t0) : t0) if t0 >= opening_time
|
413
|
+
break unless (t1 = next_time(t0 + 1, closing_time))
|
414
|
+
break if closing_time && t1 > closing_time
|
415
|
+
if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?)
|
416
|
+
wind_back_dst
|
417
|
+
next (t1 += 1)
|
433
418
|
end
|
434
|
-
|
419
|
+
yielder << (block_given? ? block.call(t1) : t1) if t1 >= opening_time
|
420
|
+
next (t1 += 1)
|
435
421
|
end
|
436
422
|
end
|
437
423
|
end
|
@@ -443,17 +429,18 @@ module IceCube
|
|
443
429
|
begin
|
444
430
|
new_time = rule.next_time(time, self, min_time || closing_time)
|
445
431
|
[min_time, new_time].compact.min
|
446
|
-
rescue
|
432
|
+
rescue StopIteration
|
447
433
|
min_time
|
448
434
|
end
|
449
435
|
end
|
450
436
|
break nil unless min_time
|
451
|
-
next(time = min_time + 1) if exception_time?(min_time)
|
437
|
+
next (time = min_time + 1) if exception_time?(min_time)
|
452
438
|
break Occurrence.new(min_time, min_time + duration)
|
453
439
|
end
|
454
440
|
end
|
455
441
|
|
456
|
-
#
|
442
|
+
# Indicate if any rule needs to be run from the start of time
|
443
|
+
# If we have rules with counts, we need to walk from the beginning of time
|
457
444
|
def full_required?
|
458
445
|
@all_recurrence_rules.any?(&:full_required?) ||
|
459
446
|
@all_exception_rules.any?(&:full_required?)
|
@@ -497,6 +484,12 @@ module IceCube
|
|
497
484
|
end
|
498
485
|
end
|
499
486
|
|
487
|
+
def wind_back_dst
|
488
|
+
recurrence_rules.each do |rule|
|
489
|
+
rule.skipped_for_dst
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
500
493
|
end
|
501
494
|
|
502
495
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module IceCube
|
2
|
+
class TimeStep
|
3
|
+
SECS = 1
|
4
|
+
MINS = 60
|
5
|
+
HOURS = MINS * 60
|
6
|
+
DAYS = HOURS * 24
|
7
|
+
WEEKS = DAYS * 7
|
8
|
+
MONTHS = {
|
9
|
+
1 => [ 28, 29, 30, 31].map { |m| m * DAYS },
|
10
|
+
2 => [ 59, 60, 61, 62].map { |m| m * DAYS },
|
11
|
+
3 => [ 89, 90, 91, 92].map { |m| m * DAYS },
|
12
|
+
4 => [120, 121, 122, 123].map { |m| m * DAYS },
|
13
|
+
5 => [150, 151, 152, 153].map { |m| m * DAYS },
|
14
|
+
6 => [181, 182, 183, 184].map { |m| m * DAYS },
|
15
|
+
7 => [212, 213, 214, 215].map { |m| m * DAYS },
|
16
|
+
8 => [242, 243, 244, 245].map { |m| m * DAYS },
|
17
|
+
9 => [273, 274, 275, 276].map { |m| m * DAYS },
|
18
|
+
10 => [303, 304, 305, 306].map { |m| m * DAYS },
|
19
|
+
11 => [334, 335, 336, 337].map { |m| m * DAYS },
|
20
|
+
12 => [365, 366] .map { |m| m * DAYS }
|
21
|
+
}
|
22
|
+
YEARS = [365, 366]
|
23
|
+
|
24
|
+
def next(base, validations)
|
25
|
+
end
|
26
|
+
|
27
|
+
def prev(base, validations)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/ice_cube/time_util.rb
CHANGED
@@ -16,6 +16,8 @@ module IceCube
|
|
16
16
|
:november => 11, :december => 12
|
17
17
|
}
|
18
18
|
|
19
|
+
CLOCK_VALUES = [:year, :month, :day, :hour, :min, :sec]
|
20
|
+
|
19
21
|
# Provides a Time.now without the usec, in the reference zone or utc offset
|
20
22
|
def self.now(reference=Time.now)
|
21
23
|
match_zone(Time.at(Time.now.to_i), reference)
|
@@ -72,7 +74,8 @@ module IceCube
|
|
72
74
|
if time_or_hash.is_a?(Time)
|
73
75
|
time_or_hash
|
74
76
|
elsif time_or_hash.is_a?(Hash)
|
75
|
-
|
77
|
+
hash = FlexibleHash.new(time_or_hash)
|
78
|
+
hash[:time].in_time_zone(hash[:zone])
|
76
79
|
end
|
77
80
|
end
|
78
81
|
|
@@ -112,8 +115,8 @@ module IceCube
|
|
112
115
|
|
113
116
|
# Convert a symbol to a numeric month
|
114
117
|
def self.sym_to_month(sym)
|
115
|
-
return wday = sym if (1..12).include? sym
|
116
118
|
MONTHS.fetch(sym) do |k|
|
119
|
+
return wday = sym.to_i if MONTHS.values.any? { |i| i.to_s == sym.to_s }
|
117
120
|
raise ArgumentError, "Expecting Fixnum or Symbol value for month. " \
|
118
121
|
"No such month: #{k.inspect}"
|
119
122
|
end
|
@@ -122,8 +125,8 @@ module IceCube
|
|
122
125
|
|
123
126
|
# Convert a symbol to a wday number
|
124
127
|
def self.sym_to_wday(sym)
|
125
|
-
return sym if (0..6).include? sym
|
126
128
|
DAYS.fetch(sym) do |k|
|
129
|
+
return sym.to_i if DAYS.values.any? { |i| i.to_s == sym.to_s }
|
127
130
|
raise ArgumentError, "Expecting Fixnum or Symbol value for weekday. " \
|
128
131
|
"No such weekday: #{k.inspect}"
|
129
132
|
end
|
@@ -209,6 +212,10 @@ module IceCube
|
|
209
212
|
end
|
210
213
|
end
|
211
214
|
|
215
|
+
def self.same_clock?(t1, t2)
|
216
|
+
CLOCK_VALUES.all? { |i| t1.send(i) == t2.send(i) }
|
217
|
+
end
|
218
|
+
|
212
219
|
# A utility class for safely moving time around
|
213
220
|
class TimeWrapper
|
214
221
|
|
@@ -46,6 +46,14 @@ module IceCube
|
|
46
46
|
@time
|
47
47
|
end
|
48
48
|
|
49
|
+
def skipped_for_dst
|
50
|
+
@uses -= 1 if @uses > 0
|
51
|
+
end
|
52
|
+
|
53
|
+
def dst_adjust?
|
54
|
+
@validations[:interval].any? &:dst_adjust?
|
55
|
+
end
|
56
|
+
|
49
57
|
def to_s
|
50
58
|
builder = StringBuilder.new
|
51
59
|
@validations.each do |name, validations|
|
@@ -132,12 +140,11 @@ module IceCube
|
|
132
140
|
end
|
133
141
|
|
134
142
|
def shift_time_by_validation(res, vals)
|
135
|
-
return unless res.min
|
136
|
-
|
137
|
-
|
138
|
-
wrapper
|
139
|
-
wrapper.
|
140
|
-
wrapper.clear_below(type)
|
143
|
+
return unless (interval = res.min)
|
144
|
+
validation = vals.first
|
145
|
+
wrapper = TimeUtil::TimeWrapper.new(@time, validation.dst_adjust?)
|
146
|
+
wrapper.add(validation.type, interval)
|
147
|
+
wrapper.clear_below(validation.type)
|
141
148
|
|
142
149
|
# Move over DST if blocked, no adjustments
|
143
150
|
if wrapper.to_time <= @time
|
data/lib/ice_cube/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ice_cube
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Crepezzi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -78,10 +78,15 @@ files:
|
|
78
78
|
- lib/ice_cube/builders/ical_builder.rb
|
79
79
|
- lib/ice_cube/builders/string_builder.rb
|
80
80
|
- lib/ice_cube/deprecated.rb
|
81
|
+
- lib/ice_cube/enumerator.rb
|
81
82
|
- lib/ice_cube/errors/count_exceeded.rb
|
82
83
|
- lib/ice_cube/errors/until_exceeded.rb
|
83
84
|
- lib/ice_cube/flexible_hash.rb
|
85
|
+
- lib/ice_cube/formatters/string.rb
|
86
|
+
- lib/ice_cube/hash_input.rb
|
84
87
|
- lib/ice_cube/occurrence.rb
|
88
|
+
- lib/ice_cube/parsers/hash_parser.rb
|
89
|
+
- lib/ice_cube/parsers/yaml_parser.rb
|
85
90
|
- lib/ice_cube/rule.rb
|
86
91
|
- lib/ice_cube/rules/daily_rule.rb
|
87
92
|
- lib/ice_cube/rules/hourly_rule.rb
|
@@ -92,6 +97,8 @@ files:
|
|
92
97
|
- lib/ice_cube/rules/yearly_rule.rb
|
93
98
|
- lib/ice_cube/schedule.rb
|
94
99
|
- lib/ice_cube/single_occurrence_rule.rb
|
100
|
+
- lib/ice_cube/string_helpers.rb
|
101
|
+
- lib/ice_cube/time_step.rb
|
95
102
|
- lib/ice_cube/time_util.rb
|
96
103
|
- lib/ice_cube/validated_rule.rb
|
97
104
|
- lib/ice_cube/validations/count.rb
|
@@ -135,9 +142,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
142
|
version: '0'
|
136
143
|
requirements: []
|
137
144
|
rubyforge_project: ice-cube
|
138
|
-
rubygems_version: 2.2.
|
145
|
+
rubygems_version: 2.2.2
|
139
146
|
signing_key:
|
140
147
|
specification_version: 4
|
141
148
|
summary: Ruby Date Recurrence Library
|
142
149
|
test_files:
|
143
150
|
- spec/spec_helper.rb
|
151
|
+
has_rdoc: true
|