ice_cube 0.12.1 → 0.13.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 +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
|