ice_cube 0.11.3 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|