ice_cube 0.12.1 → 0.13.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 +2 -2
- data/lib/ice_cube/occurrence.rb +1 -1
- data/lib/ice_cube/parsers/hash_parser.rb +6 -2
- data/lib/ice_cube/parsers/ical_parser.rb +90 -0
- data/lib/ice_cube/rule.rb +5 -0
- data/lib/ice_cube/schedule.rb +44 -18
- data/lib/ice_cube/time_util.rb +46 -27
- data/lib/ice_cube/validated_rule.rb +21 -11
- data/lib/ice_cube/validations/day.rb +1 -3
- data/lib/ice_cube/validations/day_of_month.rb +1 -3
- data/lib/ice_cube/validations/fixed_value.rb +95 -0
- data/lib/ice_cube/validations/hour_of_day.rb +1 -3
- data/lib/ice_cube/validations/minute_of_hour.rb +1 -3
- data/lib/ice_cube/validations/month_of_year.rb +1 -3
- data/lib/ice_cube/validations/schedule_lock.rb +1 -3
- data/lib/ice_cube/validations/second_of_minute.rb +1 -3
- data/lib/ice_cube/validations/weekly_interval.rb +1 -1
- data/lib/ice_cube/version.rb +1 -1
- data/spec/spec_helper.rb +11 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b2874169ce6feed9740297977d1dc7a4de82c83
|
4
|
+
data.tar.gz: 6b7c7b5c35167429740adb2f64eced33413b3e93
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d9ec04b5840a45acd23ad7f41e44491ff6cb66f85348d32504ec3116955513eb2e4ec3c8e6281c958d2f6957ec7f08ff67e126a4853618fa748da84d5705638
|
7
|
+
data.tar.gz: e47242b8441a7eb029ebd4c128030eb004110d539aed236ad1b55a8c02f378483fdf925c93ccbf4c794dd990d6a5b80bd3f9a1728c4d5c6e8f748c3a1f33cce8
|
data/lib/ice_cube.rb
CHANGED
@@ -18,6 +18,7 @@ module IceCube
|
|
18
18
|
|
19
19
|
autoload :HashParser, 'ice_cube/parsers/hash_parser'
|
20
20
|
autoload :YamlParser, 'ice_cube/parsers/yaml_parser'
|
21
|
+
autoload :IcalParser, 'ice_cube/parsers/ical_parser'
|
21
22
|
|
22
23
|
autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
|
23
24
|
autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'
|
@@ -34,8 +35,7 @@ module IceCube
|
|
34
35
|
autoload :YearlyRule, 'ice_cube/rules/yearly_rule'
|
35
36
|
|
36
37
|
module Validations
|
37
|
-
|
38
|
-
autoload :Lock, 'ice_cube/validations/lock'
|
38
|
+
autoload :FixedValue, 'ice_cube/validations/fixed_value'
|
39
39
|
autoload :ScheduleLock, 'ice_cube/validations/schedule_lock'
|
40
40
|
|
41
41
|
autoload :Count, 'ice_cube/validations/count'
|
data/lib/ice_cube/occurrence.rb
CHANGED
@@ -84,7 +84,7 @@ module IceCube
|
|
84
84
|
# time formats and is only used when ActiveSupport is available.
|
85
85
|
#
|
86
86
|
def to_s(format=nil)
|
87
|
-
if format && to_time.public_method(:to_s).arity
|
87
|
+
if format && to_time.public_method(:to_s).arity != 0
|
88
88
|
t0, t1 = start_time.to_s(format), end_time.to_s(format)
|
89
89
|
else
|
90
90
|
t0, t1 = start_time.to_s, end_time.to_s
|
@@ -53,7 +53,9 @@ module IceCube
|
|
53
53
|
def apply_rrules(schedule, data)
|
54
54
|
return unless data[:rrules]
|
55
55
|
data[:rrules].each do |h|
|
56
|
-
|
56
|
+
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)
|
57
|
+
|
58
|
+
schedule.rrule(rrule)
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -61,7 +63,9 @@ module IceCube
|
|
61
63
|
return unless data[:exrules]
|
62
64
|
warn "IceCube: :exrules is deprecated, and will be removed in a future release. at: #{ caller[0] }"
|
63
65
|
data[:exrules].each do |h|
|
64
|
-
|
66
|
+
rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h)
|
67
|
+
|
68
|
+
schedule.exrule(rrule)
|
65
69
|
end
|
66
70
|
end
|
67
71
|
|
@@ -0,0 +1,90 @@
|
|
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(&:to_i)
|
44
|
+
when 'BYMINUTE'
|
45
|
+
params[:validations][:minute_of_hour] = value.split(',').collect(&:to_i)
|
46
|
+
when 'BYHOUR'
|
47
|
+
params[:validations][:hour_of_day] = value.split(',').collect(&: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(&:to_i)
|
65
|
+
when 'BYMONTH'
|
66
|
+
params[:validations][:month_of_year] = value.split(',').collect(&:to_i)
|
67
|
+
when 'BYYEARDAY'
|
68
|
+
params[:validations][:day_of_year] = value.split(',').collect(&: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
|
+
|
77
|
+
# WKST only valid for weekly rules
|
78
|
+
params.delete(:wkst) unless params[:freq] == 'weekly'
|
79
|
+
|
80
|
+
rule = Rule.send(*params.values_at(:freq, :interval, :wkst).compact)
|
81
|
+
rule.count(params[:count]) if params[:count]
|
82
|
+
rule.until(params[:until]) if params[:until]
|
83
|
+
params[:validations].each do |key, value|
|
84
|
+
value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value)
|
85
|
+
end
|
86
|
+
|
87
|
+
rule
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/ice_cube/rule.rb
CHANGED
@@ -27,6 +27,11 @@ module IceCube
|
|
27
27
|
raise MethodNotImplemented, "Expected to be overrridden by subclasses"
|
28
28
|
end
|
29
29
|
|
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
|
+
|
30
35
|
# Yaml implementation
|
31
36
|
def to_yaml(*args)
|
32
37
|
YAML::dump(to_hash, *args)
|
data/lib/ice_cube/schedule.rb
CHANGED
@@ -167,28 +167,28 @@ module IceCube
|
|
167
167
|
|
168
168
|
# The next n occurrences after now
|
169
169
|
def next_occurrences(num, from = nil)
|
170
|
-
from
|
170
|
+
from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
|
171
171
|
enumerate_occurrences(from + 1, nil).take(num)
|
172
172
|
end
|
173
173
|
|
174
174
|
# The next occurrence after now (overridable)
|
175
175
|
def next_occurrence(from = nil)
|
176
|
-
from
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
nil
|
181
|
-
end
|
176
|
+
from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
|
177
|
+
enumerate_occurrences(from + 1, nil).next
|
178
|
+
rescue StopIteration
|
179
|
+
nil
|
182
180
|
end
|
183
181
|
|
184
182
|
# The previous occurrence from a given time
|
185
183
|
def previous_occurrence(from)
|
184
|
+
from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
|
186
185
|
return nil if from <= start_time
|
187
186
|
enumerate_occurrences(start_time, from - 1).to_a.last
|
188
187
|
end
|
189
188
|
|
190
189
|
# The previous n occurrences before a given time
|
191
190
|
def previous_occurrences(num, from)
|
191
|
+
from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
|
192
192
|
return [] if from <= start_time
|
193
193
|
a = enumerate_occurrences(start_time, from - 1).to_a
|
194
194
|
a.size > num ? a[-1*num,a.size] : a
|
@@ -214,12 +214,10 @@ module IceCube
|
|
214
214
|
|
215
215
|
# Return a boolean indicating if an occurrence falls between two times
|
216
216
|
def occurs_between?(begin_time, closing_time)
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
false
|
222
|
-
end
|
217
|
+
enumerate_occurrences(begin_time, closing_time).next
|
218
|
+
true
|
219
|
+
rescue StopIteration
|
220
|
+
false
|
223
221
|
end
|
224
222
|
|
225
223
|
# Return a boolean indicating if an occurrence is occurring between two
|
@@ -235,7 +233,7 @@ module IceCube
|
|
235
233
|
|
236
234
|
# Return a boolean indicating if an occurrence falls on a certain date
|
237
235
|
def occurs_on?(date)
|
238
|
-
date = TimeUtil.ensure_date
|
236
|
+
date = TimeUtil.ensure_date(date)
|
239
237
|
begin_time = TimeUtil.beginning_of_date(date, start_time)
|
240
238
|
closing_time = TimeUtil.end_of_date(date, start_time)
|
241
239
|
occurs_between?(begin_time, closing_time)
|
@@ -243,6 +241,7 @@ module IceCube
|
|
243
241
|
|
244
242
|
# Determine if the schedule is occurring at a given time
|
245
243
|
def occurring_at?(time)
|
244
|
+
time = TimeUtil.match_zone(time, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
|
246
245
|
if duration > 0
|
247
246
|
return false if exception_time?(time)
|
248
247
|
occurs_between?(time - duration + 1, time)
|
@@ -256,7 +255,7 @@ module IceCube
|
|
256
255
|
# @param [Time] closing_time - the last time to consider
|
257
256
|
# @return [Boolean] whether or not the schedules conflict at all
|
258
257
|
def conflicts_with?(other_schedule, closing_time = nil)
|
259
|
-
closing_time = TimeUtil.ensure_time
|
258
|
+
closing_time = TimeUtil.ensure_time(closing_time)
|
260
259
|
unless terminating? || other_schedule.terminating? || closing_time
|
261
260
|
raise ArgumentError, "One or both schedules must be terminating to use #conflicts_with?"
|
262
261
|
end
|
@@ -333,6 +332,11 @@ module IceCube
|
|
333
332
|
pieces.join("\n")
|
334
333
|
end
|
335
334
|
|
335
|
+
# Load the schedule from ical
|
336
|
+
def self.from_ical(ical, options = {})
|
337
|
+
IcalParser.schedule_from_ical(ical, options)
|
338
|
+
end
|
339
|
+
|
336
340
|
# Convert the schedule to yaml
|
337
341
|
def to_yaml(*args)
|
338
342
|
YAML::dump(to_hash, *args)
|
@@ -404,10 +408,10 @@ module IceCube
|
|
404
408
|
opening_time = TimeUtil.match_zone(opening_time, start_time)
|
405
409
|
closing_time = TimeUtil.match_zone(closing_time, start_time)
|
406
410
|
opening_time += start_time.subsec - opening_time.subsec rescue 0
|
407
|
-
reset
|
408
411
|
opening_time = start_time if opening_time < start_time
|
409
|
-
|
410
|
-
|
412
|
+
Enumerator.new do |yielder|
|
413
|
+
reset
|
414
|
+
t1 = full_required? ? start_time : realign(opening_time)
|
411
415
|
loop do
|
412
416
|
break unless (t0 = next_time(t1, closing_time))
|
413
417
|
break if closing_time && t0 > closing_time
|
@@ -492,6 +496,28 @@ module IceCube
|
|
492
496
|
end
|
493
497
|
end
|
494
498
|
|
499
|
+
# If any rule has validations for values within the period, (overriding the
|
500
|
+
# interval from start time, e.g. `day[_of_week]`), and the opening time is
|
501
|
+
# offset from the interval multiplier such that it might miss the first
|
502
|
+
# correct occurrence (e.g. repeat is every N weeks, but selecting from end
|
503
|
+
# of week N-1, the first jump would go to end of week N and miss any
|
504
|
+
# earlier validations in the week). This realigns the opening time to
|
505
|
+
# the start of the interval's correct period (e.g. move to start of week N)
|
506
|
+
# TODO: check if this is needed for validations other than `:wday`
|
507
|
+
#
|
508
|
+
def realign(opening_time)
|
509
|
+
time = TimeUtil::TimeWrapper.new(opening_time)
|
510
|
+
recurrence_rules.each do |rule|
|
511
|
+
wday_validations = rule.other_interval_validations.select { |v| v.type == :wday } or next
|
512
|
+
interval = rule.base_interval_validation.validate(opening_time, self).to_i
|
513
|
+
offset = wday_validations
|
514
|
+
.map { |v| v.validate(opening_time, self).to_i }
|
515
|
+
.reduce(0) { |least, i| i > 0 && i <= interval && (i < least || least == 0) ? i : least }
|
516
|
+
time.add(rule.base_interval_type, 7 - time.to_time.wday) if offset > 0
|
517
|
+
end
|
518
|
+
time.to_time
|
519
|
+
end
|
520
|
+
|
495
521
|
end
|
496
522
|
|
497
523
|
end
|
data/lib/ice_cube/time_util.rb
CHANGED
@@ -11,6 +11,11 @@ module IceCube
|
|
11
11
|
:thursday => 4, :friday => 5, :saturday => 6
|
12
12
|
}
|
13
13
|
|
14
|
+
ICAL_DAYS = {
|
15
|
+
'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday,
|
16
|
+
'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday
|
17
|
+
}
|
18
|
+
|
14
19
|
MONTHS = {
|
15
20
|
:january => 1, :february => 2, :march => 3, :april => 4, :may => 5,
|
16
21
|
:june => 6, :july => 7, :august => 8, :september => 9, :october => 10,
|
@@ -24,21 +29,34 @@ module IceCube
|
|
24
29
|
match_zone(Time.at(Time.now.to_i), reference)
|
25
30
|
end
|
26
31
|
|
27
|
-
def self.
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def self.build_in_zone(args, reference)
|
33
|
+
if reference.respond_to?(:time_zone)
|
34
|
+
reference.time_zone.local(*args)
|
35
|
+
elsif reference.utc?
|
36
|
+
Time.utc(*args)
|
37
|
+
elsif reference.zone
|
38
|
+
Time.local(*args)
|
31
39
|
else
|
32
|
-
|
33
|
-
time.utc
|
34
|
-
elsif reference.zone
|
35
|
-
time.getlocal
|
36
|
-
else
|
37
|
-
time.getlocal(reference.utc_offset)
|
38
|
-
end
|
40
|
+
Time.new(*args << reference.utc_offset)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
44
|
+
def self.match_zone(input_time, reference)
|
45
|
+
return unless time = ensure_time(input_time)
|
46
|
+
time = if reference.respond_to? :time_zone
|
47
|
+
time.in_time_zone(reference.time_zone)
|
48
|
+
else
|
49
|
+
if reference.utc?
|
50
|
+
time.utc
|
51
|
+
elsif reference.zone
|
52
|
+
time.getlocal
|
53
|
+
else
|
54
|
+
time.getlocal(reference.utc_offset)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
(Date === input_time) ? beginning_of_date(time, reference) : time
|
58
|
+
end
|
59
|
+
|
42
60
|
# Ensure that this is either nil, or a time
|
43
61
|
def self.ensure_time(time, date_eod = false)
|
44
62
|
case time
|
@@ -96,25 +114,13 @@ module IceCube
|
|
96
114
|
end
|
97
115
|
|
98
116
|
# Get the beginning of a date
|
99
|
-
def self.beginning_of_date(date, reference=
|
100
|
-
|
101
|
-
reference ||= Time.local(*args)
|
102
|
-
if reference.respond_to?(:time_zone) && reference.time_zone
|
103
|
-
reference.time_zone.local(*args)
|
104
|
-
else
|
105
|
-
match_zone(Time.new(*args << reference.utc_offset), reference)
|
106
|
-
end
|
117
|
+
def self.beginning_of_date(date, reference=Time.now)
|
118
|
+
build_in_zone([date.year, date.month, date.day, 0, 0, 0], reference)
|
107
119
|
end
|
108
120
|
|
109
121
|
# Get the end of a date
|
110
|
-
def self.end_of_date(date, reference=
|
111
|
-
|
112
|
-
reference ||= Time.local(*args)
|
113
|
-
if reference.respond_to?(:time_zone) && reference.time_zone
|
114
|
-
reference.time_zone.local(*args)
|
115
|
-
else
|
116
|
-
match_zone(Time.new(*args << reference.utc_offset), reference)
|
117
|
-
end
|
122
|
+
def self.end_of_date(date, reference=Time.now)
|
123
|
+
build_in_zone([date.year, date.month, date.day, 23, 59, 59], reference)
|
118
124
|
end
|
119
125
|
|
120
126
|
# Convert a symbol to a numeric month
|
@@ -146,12 +152,25 @@ module IceCube
|
|
146
152
|
end
|
147
153
|
end
|
148
154
|
|
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
|
+
|
149
162
|
# Convert weekday from base sunday to the schedule's week start.
|
150
163
|
def self.normalize_wday(wday, week_start)
|
151
164
|
(wday - sym_to_wday(week_start)) % 7
|
152
165
|
end
|
153
166
|
deprecated_alias :normalize_weekday, :normalize_wday
|
154
167
|
|
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
|
+
|
155
174
|
# Return the count of the number of times wday appears in the month,
|
156
175
|
# and which of those time falls on
|
157
176
|
def self.which_occurrence_in_month(time, wday)
|
@@ -30,10 +30,24 @@ module IceCube
|
|
30
30
|
:interval
|
31
31
|
]
|
32
32
|
|
33
|
+
attr_reader :validations
|
34
|
+
|
33
35
|
def initialize(interval = 1, *)
|
34
36
|
@validations = Hash.new
|
35
37
|
end
|
36
38
|
|
39
|
+
def base_interval_validation
|
40
|
+
@validations[:interval].first
|
41
|
+
end
|
42
|
+
|
43
|
+
def other_interval_validations
|
44
|
+
Array(@validations[base_interval_validation.type])
|
45
|
+
end
|
46
|
+
|
47
|
+
def base_interval_type
|
48
|
+
base_interval_validation.type
|
49
|
+
end
|
50
|
+
|
37
51
|
# Compute the next time after (or including) the specified time in respect
|
38
52
|
# to the given schedule
|
39
53
|
def next_time(time, schedule, closing_time)
|
@@ -106,6 +120,7 @@ module IceCube
|
|
106
120
|
end
|
107
121
|
|
108
122
|
private
|
123
|
+
|
109
124
|
def normalized_interval(interval)
|
110
125
|
int = interval.to_i
|
111
126
|
raise ArgumentError, "'#{interval}' is not a valid input for interval. Please pass an integer." unless int > 0
|
@@ -127,15 +142,11 @@ module IceCube
|
|
127
142
|
|
128
143
|
def validation_accepts_or_updates_time?(validations_for_type)
|
129
144
|
res = validated_results(validations_for_type)
|
130
|
-
|
131
|
-
if res.
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
res.reject! { |r| r.nil? || r == 0 || r === true }
|
136
|
-
shift_time_by_validation(res, validations_for_type)
|
137
|
-
false
|
138
|
-
end
|
145
|
+
return true if res.any? { |r| r.nil? || r == 0 }
|
146
|
+
return nil if res.all? { |r| r == true }
|
147
|
+
res.reject! { |r| r == true }
|
148
|
+
shift_time_by_validation(res, validations_for_type.first)
|
149
|
+
false
|
139
150
|
end
|
140
151
|
|
141
152
|
def validated_results(validations_for_type)
|
@@ -144,9 +155,8 @@ module IceCube
|
|
144
155
|
end
|
145
156
|
end
|
146
157
|
|
147
|
-
def shift_time_by_validation(res,
|
158
|
+
def shift_time_by_validation(res, validation)
|
148
159
|
return unless (interval = res.min)
|
149
|
-
validation = vals.first
|
150
160
|
wrapper = TimeUtil::TimeWrapper.new(@time, validation.dst_adjust?)
|
151
161
|
wrapper.add(validation.type, interval)
|
152
162
|
wrapper.clear_below(validation.type)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module IceCube
|
2
|
+
|
3
|
+
# This abstract validation class is used by the various "fixed-time" (e.g.
|
4
|
+
# day, day_of_month, hour_of_day) Validation and ScheduleLock::Validation
|
5
|
+
# modules. It is not a standalone rule validation module like the others.
|
6
|
+
#
|
7
|
+
# Given the including Validation's defined +type+ field, it will lock to the
|
8
|
+
# specified +value+ or else the corresponding time unit from the schedule's
|
9
|
+
# start_time
|
10
|
+
#
|
11
|
+
class Validations::FixedValue
|
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 = start % INTERVALS[type] if start < 0
|
90
|
+
start
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -39,7 +39,7 @@ module IceCube
|
|
39
39
|
d1 = Date.new(t1.year, t1.month, t1.day)
|
40
40
|
days = (d1 - TimeUtil.normalize_wday(d1.wday, week_start)) -
|
41
41
|
(d0 - TimeUtil.normalize_wday(d0.wday, week_start))
|
42
|
-
offset = ((days / 7) % interval).nonzero?
|
42
|
+
offset = ((days.to_i / 7) % interval).nonzero?
|
43
43
|
(interval - offset) * 7 if offset
|
44
44
|
end
|
45
45
|
|
data/lib/ice_cube/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -7,6 +7,8 @@ end
|
|
7
7
|
|
8
8
|
require File.dirname(__FILE__) + '/../lib/ice_cube'
|
9
9
|
|
10
|
+
IceCube.compatibility = 12
|
11
|
+
|
10
12
|
DAY = Time.utc(2010, 3, 1)
|
11
13
|
WEDNESDAY = Time.utc(2010, 6, 23, 5, 0, 0)
|
12
14
|
|
@@ -17,6 +19,9 @@ WORLD_TIME_ZONES = [
|
|
17
19
|
]
|
18
20
|
|
19
21
|
RSpec.configure do |config|
|
22
|
+
Dir[File.dirname(__FILE__) + '/support/**/*'].each { |f| require f }
|
23
|
+
|
24
|
+
config.include WarningHelpers
|
20
25
|
|
21
26
|
config.around :each, :if_active_support_time => true do |example|
|
22
27
|
example.run if defined? ActiveSupport
|
@@ -50,4 +55,10 @@ RSpec.configure do |config|
|
|
50
55
|
end
|
51
56
|
end
|
52
57
|
|
58
|
+
config.around :each, expect_warnings: true do |example|
|
59
|
+
capture_warnings do
|
60
|
+
example.run
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
53
64
|
end
|
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.13.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:
|
11
|
+
date: 2015-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- lib/ice_cube/flexible_hash.rb
|
84
84
|
- lib/ice_cube/occurrence.rb
|
85
85
|
- lib/ice_cube/parsers/hash_parser.rb
|
86
|
+
- lib/ice_cube/parsers/ical_parser.rb
|
86
87
|
- lib/ice_cube/parsers/yaml_parser.rb
|
87
88
|
- lib/ice_cube/rule.rb
|
88
89
|
- lib/ice_cube/rules/daily_rule.rb
|
@@ -102,6 +103,7 @@ files:
|
|
102
103
|
- lib/ice_cube/validations/day_of_month.rb
|
103
104
|
- lib/ice_cube/validations/day_of_week.rb
|
104
105
|
- lib/ice_cube/validations/day_of_year.rb
|
106
|
+
- lib/ice_cube/validations/fixed_value.rb
|
105
107
|
- lib/ice_cube/validations/hour_of_day.rb
|
106
108
|
- lib/ice_cube/validations/hourly_interval.rb
|
107
109
|
- lib/ice_cube/validations/lock.rb
|
@@ -137,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
139
|
version: '0'
|
138
140
|
requirements: []
|
139
141
|
rubyforge_project: ice-cube
|
140
|
-
rubygems_version: 2.
|
142
|
+
rubygems_version: 2.4.6
|
141
143
|
signing_key:
|
142
144
|
specification_version: 4
|
143
145
|
summary: Ruby Date Recurrence Library
|