ice_cube 0.16.2 → 0.16.3
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 +1 -3
- data/lib/ice_cube/i18n.rb +10 -7
- data/lib/ice_cube/input_alignment.rb +89 -0
- data/lib/ice_cube/null_i18n.rb +12 -6
- data/lib/ice_cube/occurrence.rb +25 -23
- data/lib/ice_cube/parsers/ical_parser.rb +7 -4
- data/lib/ice_cube/rule.rb +4 -11
- data/lib/ice_cube/rules/daily_rule.rb +9 -0
- data/lib/ice_cube/rules/hourly_rule.rb +9 -0
- data/lib/ice_cube/rules/minutely_rule.rb +9 -0
- data/lib/ice_cube/rules/monthly_rule.rb +9 -0
- data/lib/ice_cube/rules/secondly_rule.rb +9 -0
- data/lib/ice_cube/rules/weekly_rule.rb +10 -1
- data/lib/ice_cube/rules/yearly_rule.rb +9 -0
- data/lib/ice_cube/schedule.rb +10 -9
- data/lib/ice_cube/single_occurrence_rule.rb +4 -0
- data/lib/ice_cube/time_util.rb +26 -16
- data/lib/ice_cube/validated_rule.rb +10 -19
- data/lib/ice_cube/validations/count.rb +1 -2
- data/lib/ice_cube/validations/daily_interval.rb +5 -1
- data/lib/ice_cube/validations/day.rb +6 -2
- data/lib/ice_cube/validations/day_of_month.rb +5 -0
- data/lib/ice_cube/validations/hour_of_day.rb +23 -0
- data/lib/ice_cube/validations/hourly_interval.rb +2 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +16 -0
- data/lib/ice_cube/validations/minutely_interval.rb +2 -0
- data/lib/ice_cube/validations/month_of_year.rb +5 -0
- data/lib/ice_cube/validations/monthly_interval.rb +4 -1
- data/lib/ice_cube/validations/schedule_lock.rb +4 -0
- data/lib/ice_cube/validations/second_of_minute.rb +19 -3
- data/lib/ice_cube/validations/secondly_interval.rb +2 -0
- data/lib/ice_cube/validations/until.rb +1 -2
- data/lib/ice_cube/validations/weekly_interval.rb +0 -2
- data/lib/ice_cube/version.rb +1 -1
- data/spec/spec_helper.rb +32 -9
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: afc29d6bebdb8a25e7926863560b6b73214887d7
|
4
|
+
data.tar.gz: 850cad27d4f06dd30052222e4c30f6fa8b3b50e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 351c83281b34657ff2a64c8fc03e023265803ba8776d6f6a98c90fbbf04f14c0e0c9a7d7ee40f5a8c739cea31283b60c429b37521f7fe4e7ea61bc2175123c46
|
7
|
+
data.tar.gz: d02fba63d82b7a26077cadf400a0bd45172d6d047c26533164f786654b925e0b6e28902cefec8ada0a2df10f08596237c848271ffbb85094323ec515a71f2431
|
data/lib/ice_cube.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
require 'date'
|
2
2
|
require 'ice_cube/deprecated'
|
3
|
-
require 'ice_cube/i18n'
|
4
|
-
|
5
|
-
IceCube::I18n.detect_backend!
|
6
3
|
|
7
4
|
module IceCube
|
8
5
|
|
@@ -10,6 +7,7 @@ module IceCube
|
|
10
7
|
|
11
8
|
autoload :TimeUtil, 'ice_cube/time_util'
|
12
9
|
autoload :FlexibleHash, 'ice_cube/flexible_hash'
|
10
|
+
autoload :I18n, 'ice_cube/i18n'
|
13
11
|
|
14
12
|
autoload :Rule, 'ice_cube/rule'
|
15
13
|
autoload :Schedule, 'ice_cube/schedule'
|
data/lib/ice_cube/i18n.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
require 'ice_cube/null_i18n'
|
2
|
+
|
1
3
|
module IceCube
|
2
4
|
module I18n
|
5
|
+
|
6
|
+
LOCALES_PATH = File.expand_path(File.join('..', '..', '..', 'config', 'locales'), __FILE__)
|
7
|
+
|
3
8
|
def self.t(*args)
|
4
9
|
backend.t(*args)
|
5
10
|
end
|
@@ -9,16 +14,14 @@ module IceCube
|
|
9
14
|
end
|
10
15
|
|
11
16
|
def self.backend
|
12
|
-
@backend
|
17
|
+
@backend ||= detect_backend!
|
13
18
|
end
|
14
19
|
|
15
20
|
def self.detect_backend!
|
16
|
-
|
17
|
-
::I18n
|
18
|
-
|
19
|
-
|
20
|
-
require 'ice_cube/null_i18n'
|
21
|
-
@backend = NullI18n
|
21
|
+
::I18n.load_path += Dir[File.join(LOCALES_PATH, '*.yml')]
|
22
|
+
::I18n
|
23
|
+
rescue NameError
|
24
|
+
NullI18n
|
22
25
|
end
|
23
26
|
end
|
24
27
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module IceCube
|
2
|
+
class InputAlignment
|
3
|
+
|
4
|
+
def initialize(rule, value, rule_part)
|
5
|
+
@rule = rule
|
6
|
+
@value = value
|
7
|
+
@rule_part = rule_part
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :rule, :value, :rule_part
|
11
|
+
|
12
|
+
def verify(freq, options={}, &block)
|
13
|
+
@rule.validations[:interval] or return
|
14
|
+
|
15
|
+
case @rule
|
16
|
+
when DailyRule
|
17
|
+
verify_wday_alignment(freq, &block)
|
18
|
+
when MonthlyRule
|
19
|
+
verify_month_alignment(freq, &block)
|
20
|
+
else
|
21
|
+
verify_freq_alignment(freq, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def interval_validation
|
28
|
+
@interval_validation ||= @rule.validations[:interval].first
|
29
|
+
end
|
30
|
+
|
31
|
+
def interval_value
|
32
|
+
@interval_value ||= (rule_part == :interval) ? value : interval_validation.interval
|
33
|
+
end
|
34
|
+
|
35
|
+
def fixed_validations
|
36
|
+
@fixed_validations ||= @rule.validations.values.flatten.select { |v|
|
37
|
+
interval_type = (v.type == :wday ? :day : v.type)
|
38
|
+
v.class < Validations::FixedValue &&
|
39
|
+
interval_type == rule.base_interval_validation.type
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify_freq_alignment(freq)
|
44
|
+
interval_validation.type == freq or return
|
45
|
+
(last_validation = fixed_validations.min_by(&:value)) or return
|
46
|
+
|
47
|
+
alignment = (value - last_validation.value) % interval_validation.interval
|
48
|
+
return if alignment.zero?
|
49
|
+
|
50
|
+
validation_values = fixed_validations.map(&:value).join(', ')
|
51
|
+
if rule_part == :interval
|
52
|
+
message = "interval(#{value}) " \
|
53
|
+
"must be a multiple of " \
|
54
|
+
"intervals in #{last_validation.key}(#{validation_values})"
|
55
|
+
else
|
56
|
+
message = "intervals in #{last_validation.key}(#{validation_values}, #{value}) " \
|
57
|
+
"must be multiples of " \
|
58
|
+
"interval(#{interval_validation.interval})"
|
59
|
+
end
|
60
|
+
|
61
|
+
yield ArgumentError.new(message)
|
62
|
+
end
|
63
|
+
|
64
|
+
def verify_month_alignment(_freq)
|
65
|
+
return if interval_value == 1 || (interval_value % 12).zero?
|
66
|
+
return if fixed_validations.empty?
|
67
|
+
|
68
|
+
message = "month_of_year can only be used with interval(1) or multiples of interval(12)"
|
69
|
+
|
70
|
+
yield ArgumentError.new(message)
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify_wday_alignment(freq)
|
74
|
+
return if interval_value == 1
|
75
|
+
|
76
|
+
if freq == :wday
|
77
|
+
return if (interval_value % 7).zero?
|
78
|
+
return if Array(@rule.validations[:day]).empty?
|
79
|
+
message = "day can only be used with multiples of interval(7)"
|
80
|
+
else
|
81
|
+
(fixed_validation = fixed_validations.first) or return
|
82
|
+
message = "#{fixed_validation.key} can only be used with interval(1)"
|
83
|
+
end
|
84
|
+
|
85
|
+
yield ArgumentError.new(message)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/lib/ice_cube/null_i18n.rb
CHANGED
@@ -7,13 +7,19 @@ module IceCube
|
|
7
7
|
|
8
8
|
base = base[options[:count] == 1 ? "one" : "other"] if options[:count]
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
case base
|
11
|
+
when Hash
|
12
|
+
base.each_with_object({}) do |(k, v), hash|
|
13
|
+
hash[k.is_a?(String) ? k.to_sym : k] = v
|
13
14
|
end
|
15
|
+
when Array
|
16
|
+
base.each_with_index.each_with_object({}) do |(v, k), hash|
|
17
|
+
hash[k] = v
|
18
|
+
end
|
19
|
+
else
|
20
|
+
return base unless base.include?('%{')
|
21
|
+
base % options
|
14
22
|
end
|
15
|
-
|
16
|
-
options.reduce(base) { |result, (find, replace)| result.gsub("%{#{find}}", "#{replace}") }
|
17
23
|
end
|
18
24
|
|
19
25
|
def self.l(date_or_time, options = {})
|
@@ -22,7 +28,7 @@ module IceCube
|
|
22
28
|
end
|
23
29
|
|
24
30
|
def self.config
|
25
|
-
@config ||= YAML.
|
31
|
+
@config ||= YAML.load_file(File.join(IceCube::I18n::LOCALES_PATH, 'en.yml'))['en']
|
26
32
|
end
|
27
33
|
end
|
28
34
|
end
|
data/lib/ice_cube/occurrence.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'forwardable'
|
2
1
|
require 'delegate'
|
3
2
|
|
4
3
|
module IceCube
|
@@ -20,18 +19,16 @@ module IceCube
|
|
20
19
|
# Time.now - Occurrence.new(start_time) # => 3600
|
21
20
|
#
|
22
21
|
class Occurrence < SimpleDelegator
|
22
|
+
include Comparable
|
23
23
|
|
24
24
|
# Report class name as 'Time' to thwart type checking.
|
25
25
|
def self.name
|
26
26
|
'Time'
|
27
27
|
end
|
28
28
|
|
29
|
-
# Optimize for common methods to avoid method_missing
|
30
|
-
extend Forwardable
|
31
|
-
def_delegators :start_time, :to_i, :<=>, :==
|
32
|
-
def_delegators :to_range, :cover?, :include?, :each, :first, :last
|
33
|
-
|
34
29
|
attr_reader :start_time, :end_time
|
30
|
+
alias first start_time
|
31
|
+
alias last end_time
|
35
32
|
|
36
33
|
def initialize(start_time, end_time=nil)
|
37
34
|
@start_time = start_time
|
@@ -39,29 +36,34 @@ module IceCube
|
|
39
36
|
__setobj__ @start_time
|
40
37
|
end
|
41
38
|
|
39
|
+
def to_i
|
40
|
+
@start_time.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def <=>(other)
|
44
|
+
@start_time <=> other
|
45
|
+
end
|
46
|
+
|
42
47
|
def is_a?(klass)
|
43
48
|
klass == ::Time || super
|
44
49
|
end
|
45
50
|
alias_method :kind_of?, :is_a?
|
46
51
|
|
47
|
-
def intersects?
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
else
|
62
|
-
cover? other
|
63
|
-
end
|
52
|
+
def intersects?(other)
|
53
|
+
return cover?(other) unless other.is_a?(Occurrence) || other.is_a?(Range)
|
54
|
+
|
55
|
+
this_start = first + 1
|
56
|
+
this_end = last # exclude end boundary
|
57
|
+
other_start = other.first + 1
|
58
|
+
other_end = other.last + 1
|
59
|
+
|
60
|
+
!(this_end < other_start || this_start > other_end)
|
61
|
+
end
|
62
|
+
|
63
|
+
def cover?(other)
|
64
|
+
to_range.cover?(other)
|
64
65
|
end
|
66
|
+
alias_method :include?, :cover?
|
65
67
|
|
66
68
|
def comparable_time
|
67
69
|
start_time
|
@@ -7,12 +7,15 @@ module IceCube
|
|
7
7
|
(property, tzid) = property.split(';')
|
8
8
|
case property
|
9
9
|
when 'DTSTART'
|
10
|
-
data[:start_time] =
|
10
|
+
data[:start_time] = TimeUtil.deserialize_time(value)
|
11
11
|
when 'DTEND'
|
12
|
-
data[:end_time] =
|
12
|
+
data[:end_time] = TimeUtil.deserialize_time(value)
|
13
|
+
when 'RDATE'
|
14
|
+
data[:rtimes] ||= []
|
15
|
+
data[:rtimes] += value.split(',').map { |v| TimeUtil.deserialize_time(v) }
|
13
16
|
when 'EXDATE'
|
14
17
|
data[:extimes] ||= []
|
15
|
-
data[:extimes] += value.split(',').map{|v|
|
18
|
+
data[:extimes] += value.split(',').map { |v| TimeUtil.deserialize_time(v) }
|
16
19
|
when 'DURATION'
|
17
20
|
data[:duration] # FIXME
|
18
21
|
when 'RRULE'
|
@@ -41,7 +44,7 @@ module IceCube
|
|
41
44
|
when 'COUNT'
|
42
45
|
params[:count] = value.to_i
|
43
46
|
when 'UNTIL'
|
44
|
-
params[:until] =
|
47
|
+
params[:until] = TimeUtil.deserialize_time(value).utc
|
45
48
|
when 'WKST'
|
46
49
|
params[:week_start] = TimeUtil.ical_day_to_symbol(value)
|
47
50
|
when 'BYSECOND'
|
data/lib/ice_cube/rule.rb
CHANGED
@@ -19,11 +19,9 @@ module IceCube
|
|
19
19
|
until_time || occurrence_count
|
20
20
|
end
|
21
21
|
|
22
|
-
def ==(
|
23
|
-
|
24
|
-
|
25
|
-
hash && hash == rule.to_hash
|
26
|
-
end
|
22
|
+
def ==(other)
|
23
|
+
return false unless other.is_a? Rule
|
24
|
+
hash == other.hash
|
27
25
|
end
|
28
26
|
|
29
27
|
def hash
|
@@ -31,7 +29,7 @@ module IceCube
|
|
31
29
|
end
|
32
30
|
|
33
31
|
def to_ical
|
34
|
-
raise MethodNotImplemented, "Expected to be
|
32
|
+
raise MethodNotImplemented, "Expected to be overridden by subclasses"
|
35
33
|
end
|
36
34
|
|
37
35
|
# Convert from ical string and create a rule
|
@@ -60,11 +58,6 @@ module IceCube
|
|
60
58
|
next_time(time, schedule, time).to_i == time.to_i
|
61
59
|
end
|
62
60
|
|
63
|
-
# Whether this rule requires a full run
|
64
|
-
def full_required?
|
65
|
-
!@count.nil?
|
66
|
-
end
|
67
|
-
|
68
61
|
class << self
|
69
62
|
|
70
63
|
# Convert from a hash and create a rule
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class DailyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
# include Validations::DayOfYear # n/a
|
13
|
+
|
5
14
|
include Validations::DailyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class HourlyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
include Validations::DayOfYear
|
13
|
+
|
5
14
|
include Validations::HourlyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class MinutelyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
include Validations::DayOfYear
|
13
|
+
|
5
14
|
include Validations::MinutelyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class MonthlyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
# include Validations::DayOfYear # n/a
|
13
|
+
|
5
14
|
include Validations::MonthlyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class SecondlyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
include Validations::DayOfYear
|
13
|
+
|
5
14
|
include Validations::SecondlyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class WeeklyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
# include Validations::DayOfMonth # n/a
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
# include Validations::DayOfYear # n/a
|
13
|
+
|
5
14
|
include Validations::WeeklyInterval
|
6
15
|
|
7
16
|
attr_reader :week_start
|
@@ -28,7 +37,7 @@ module IceCube
|
|
28
37
|
time = TimeUtil::TimeWrapper.new(start_time)
|
29
38
|
offset = wday_offset(step_time, start_time)
|
30
39
|
time.add(:day, offset)
|
31
|
-
time.to_time
|
40
|
+
super step_time, time.to_time
|
32
41
|
end
|
33
42
|
|
34
43
|
# Calculate how many days to the first wday validation in the correct
|
@@ -2,6 +2,15 @@ module IceCube
|
|
2
2
|
|
3
3
|
class YearlyRule < ValidatedRule
|
4
4
|
|
5
|
+
include Validations::HourOfDay
|
6
|
+
include Validations::MinuteOfHour
|
7
|
+
include Validations::SecondOfMinute
|
8
|
+
include Validations::DayOfMonth
|
9
|
+
include Validations::DayOfWeek
|
10
|
+
include Validations::Day
|
11
|
+
include Validations::MonthOfYear
|
12
|
+
include Validations::DayOfYear
|
13
|
+
|
5
14
|
include Validations::YearlyInterval
|
6
15
|
|
7
16
|
def initialize(interval = 1)
|
data/lib/ice_cube/schedule.rb
CHANGED
@@ -340,9 +340,9 @@ module IceCube
|
|
340
340
|
IcalParser.schedule_from_ical(ical, options)
|
341
341
|
end
|
342
342
|
|
343
|
-
#
|
344
|
-
def
|
345
|
-
|
343
|
+
# Hook for YAML.dump, enables to_yaml
|
344
|
+
def encode_with(coder)
|
345
|
+
coder.represent_object nil, to_hash
|
346
346
|
end
|
347
347
|
|
348
348
|
# Load the schedule from yaml
|
@@ -371,6 +371,7 @@ module IceCube
|
|
371
371
|
end
|
372
372
|
data
|
373
373
|
end
|
374
|
+
alias_method :to_h, :to_hash
|
374
375
|
|
375
376
|
# Load the schedule from a hash
|
376
377
|
def self.from_hash(original_hash, options = {})
|
@@ -383,7 +384,7 @@ module IceCube
|
|
383
384
|
# Determine if the schedule will end
|
384
385
|
# @return [Boolean] true if ending, false if repeating forever
|
385
386
|
def terminating?
|
386
|
-
|
387
|
+
@all_recurrence_rules.all?(&:terminating?)
|
387
388
|
end
|
388
389
|
|
389
390
|
def hash
|
@@ -423,7 +424,7 @@ module IceCube
|
|
423
424
|
def enumerate_occurrences(opening_time, closing_time = nil, options = {})
|
424
425
|
opening_time = TimeUtil.match_zone(opening_time, start_time)
|
425
426
|
closing_time = TimeUtil.match_zone(closing_time, start_time)
|
426
|
-
opening_time +=
|
427
|
+
opening_time += TimeUtil.subsec(start_time) - TimeUtil.subsec(opening_time)
|
427
428
|
opening_time = start_time if opening_time < start_time
|
428
429
|
spans = options[:spans] == true && duration != 0
|
429
430
|
Enumerator.new do |yielder|
|
@@ -445,12 +446,12 @@ module IceCube
|
|
445
446
|
# Get the next time after (or including) a specific time
|
446
447
|
def next_time(time, closing_time)
|
447
448
|
loop do
|
448
|
-
min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |
|
449
|
+
min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |best_time, rule|
|
449
450
|
begin
|
450
|
-
new_time = rule.next_time(time, start_time,
|
451
|
-
[
|
451
|
+
new_time = rule.next_time(time, start_time, best_time || closing_time)
|
452
|
+
[best_time, new_time].compact.min
|
452
453
|
rescue StopIteration
|
453
|
-
|
454
|
+
best_time
|
454
455
|
end
|
455
456
|
end
|
456
457
|
break unless min_time
|
data/lib/ice_cube/time_util.rb
CHANGED
@@ -170,20 +170,13 @@ module IceCube
|
|
170
170
|
|
171
171
|
# Convert wday number to day symbol
|
172
172
|
def self.wday_to_sym(wday)
|
173
|
-
return
|
173
|
+
return wday if DAYS.keys.include? wday
|
174
174
|
DAYS.invert.fetch(wday) do |i|
|
175
175
|
raise ArgumentError, "Expecting Integer value for weekday. " \
|
176
176
|
"No such wday number: #{i.inspect}"
|
177
177
|
end
|
178
178
|
end
|
179
179
|
|
180
|
-
# Convert a symbol to an ical day (SU, MO)
|
181
|
-
def self.week_start(sym)
|
182
|
-
raise ArgumentError, "Invalid day: #{str}" unless DAYS.keys.include?(sym)
|
183
|
-
day = sym.to_s.upcase[0..1]
|
184
|
-
day
|
185
|
-
end
|
186
|
-
|
187
180
|
# Convert weekday from base sunday to the schedule's week start.
|
188
181
|
def self.normalize_wday(wday, week_start)
|
189
182
|
(wday - sym_to_wday(week_start)) % 7
|
@@ -260,8 +253,19 @@ module IceCube
|
|
260
253
|
end
|
261
254
|
end
|
262
255
|
|
263
|
-
|
264
|
-
|
256
|
+
# Handle discrepancies between various time types
|
257
|
+
# - Time has subsec
|
258
|
+
# - DateTime does not
|
259
|
+
# - ActiveSupport::TimeWithZone can wrap either type, depending on version
|
260
|
+
# or if `parse` or `now`/`local` was used to build it.
|
261
|
+
def self.subsec(time)
|
262
|
+
if time.respond_to?(:subsec)
|
263
|
+
time.subsec
|
264
|
+
elsif time.respond_to?(:sec_fraction)
|
265
|
+
time.sec_fraction
|
266
|
+
else
|
267
|
+
0.0
|
268
|
+
end
|
265
269
|
end
|
266
270
|
|
267
271
|
# A utility class for safely moving time around
|
@@ -271,7 +275,7 @@ module IceCube
|
|
271
275
|
@dst_adjust = dst_adjust
|
272
276
|
@base = time
|
273
277
|
if dst_adjust
|
274
|
-
@time = Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec +
|
278
|
+
@time = Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + TimeUtil.subsec(time))
|
275
279
|
else
|
276
280
|
@time = time
|
277
281
|
end
|
@@ -307,7 +311,17 @@ module IceCube
|
|
307
311
|
end
|
308
312
|
end
|
309
313
|
|
310
|
-
|
314
|
+
def hour=(value)
|
315
|
+
@time += (value * ONE_HOUR) - (@time.hour * ONE_HOUR)
|
316
|
+
end
|
317
|
+
|
318
|
+
def min=(value)
|
319
|
+
@time += (value * ONE_MINUTE) - (@time.min * ONE_MINUTE)
|
320
|
+
end
|
321
|
+
|
322
|
+
def sec=(value)
|
323
|
+
@time += (value) - (@time.sec)
|
324
|
+
end
|
311
325
|
|
312
326
|
def clear_sec
|
313
327
|
@time.sec > 0 ? @time -= @time.sec : @time
|
@@ -335,10 +349,6 @@ module IceCube
|
|
335
349
|
@time += ONE_DAY
|
336
350
|
end
|
337
351
|
|
338
|
-
def clear_year
|
339
|
-
@time
|
340
|
-
end
|
341
|
-
|
342
352
|
end
|
343
353
|
|
344
354
|
end
|
@@ -1,18 +1,11 @@
|
|
1
|
+
require 'ice_cube/input_alignment'
|
2
|
+
|
1
3
|
module IceCube
|
2
4
|
|
3
5
|
class ValidatedRule < Rule
|
4
6
|
|
5
7
|
include Validations::ScheduleLock
|
6
8
|
|
7
|
-
include Validations::HourOfDay
|
8
|
-
include Validations::MinuteOfHour
|
9
|
-
include Validations::SecondOfMinute
|
10
|
-
include Validations::DayOfMonth
|
11
|
-
include Validations::DayOfWeek
|
12
|
-
include Validations::Day
|
13
|
-
include Validations::MonthOfYear
|
14
|
-
include Validations::DayOfYear
|
15
|
-
|
16
9
|
include Validations::Count
|
17
10
|
include Validations::Until
|
18
11
|
|
@@ -51,10 +44,6 @@ module IceCube
|
|
51
44
|
Array(@validations[base_interval_validation.type])
|
52
45
|
end
|
53
46
|
|
54
|
-
def base_interval_type
|
55
|
-
base_interval_validation.type
|
56
|
-
end
|
57
|
-
|
58
47
|
# Compute the next time after (or including) the specified time in respect
|
59
48
|
# to the given start time
|
60
49
|
def next_time(time, start_time, closing_time)
|
@@ -74,12 +63,8 @@ module IceCube
|
|
74
63
|
start_time
|
75
64
|
end
|
76
65
|
|
77
|
-
def
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
|
-
def dst_adjust?
|
82
|
-
@validations[:interval].any?(&:dst_adjust?)
|
66
|
+
def full_required?
|
67
|
+
!occurrence_count.nil?
|
83
68
|
end
|
84
69
|
|
85
70
|
def to_s
|
@@ -193,6 +178,12 @@ module IceCube
|
|
193
178
|
VALIDATION_ORDER & @validations.keys
|
194
179
|
end
|
195
180
|
|
181
|
+
def verify_alignment(value, freq, rule_part)
|
182
|
+
InputAlignment.new(self, value, rule_part).verify(freq) do |error|
|
183
|
+
yield error
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
196
187
|
end
|
197
188
|
|
198
189
|
end
|
@@ -4,14 +4,13 @@ module IceCube
|
|
4
4
|
|
5
5
|
# Value reader for limit
|
6
6
|
def occurrence_count
|
7
|
-
@count
|
7
|
+
(arr = @validations[:count]) && (val = arr[0]) && val.count
|
8
8
|
end
|
9
9
|
|
10
10
|
def count(max)
|
11
11
|
unless max.nil? || max.is_a?(Integer)
|
12
12
|
raise ArgumentError, "Expecting Integer or nil value for count, got #{max.inspect}"
|
13
13
|
end
|
14
|
-
@count = max
|
15
14
|
replace_validations_for(:count, max && [Validation.new(max, self)])
|
16
15
|
self
|
17
16
|
end
|
@@ -4,7 +4,11 @@ module IceCube
|
|
4
4
|
|
5
5
|
# Add a new interval validation
|
6
6
|
def interval(interval)
|
7
|
-
|
7
|
+
interval = normalized_interval(interval)
|
8
|
+
verify_alignment(interval, :wday, :interval) { |error| raise error }
|
9
|
+
verify_alignment(interval, :day, :interval) { |error| raise error }
|
10
|
+
|
11
|
+
@interval = interval
|
8
12
|
replace_validations_for(:interval, [Validation.new(@interval)])
|
9
13
|
clobber_base_validations(:wday, :day)
|
10
14
|
self
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'date'
|
2
|
-
|
3
1
|
module IceCube
|
4
2
|
|
5
3
|
module Validations::Day
|
@@ -12,6 +10,8 @@ module IceCube
|
|
12
10
|
raise ArgumentError, "expecting Integer or Symbol value for day, got #{day.inspect}"
|
13
11
|
end
|
14
12
|
day = TimeUtil.sym_to_wday(day)
|
13
|
+
verify_alignment(day, :wday, :day) { |error| raise error }
|
14
|
+
|
15
15
|
validations_for(:day) << Validation.new(day)
|
16
16
|
end
|
17
17
|
clobber_base_validations(:wday, :day)
|
@@ -27,6 +27,10 @@ module IceCube
|
|
27
27
|
@day = day
|
28
28
|
end
|
29
29
|
|
30
|
+
def key
|
31
|
+
:day
|
32
|
+
end
|
33
|
+
|
30
34
|
def type
|
31
35
|
:wday
|
32
36
|
end
|
@@ -7,6 +7,7 @@ module IceCube
|
|
7
7
|
unless day.is_a?(Integer)
|
8
8
|
raise ArgumentError, "expecting Integer value for day, got #{day.inspect}"
|
9
9
|
end
|
10
|
+
verify_alignment(day, :day, :day_of_month) { |error| raise error }
|
10
11
|
validations_for(:day_of_month) << Validation.new(day)
|
11
12
|
end
|
12
13
|
clobber_base_validations(:day, :wday)
|
@@ -22,6 +23,10 @@ module IceCube
|
|
22
23
|
@day = day
|
23
24
|
end
|
24
25
|
|
26
|
+
def key
|
27
|
+
:day_of_month
|
28
|
+
end
|
29
|
+
|
25
30
|
def type
|
26
31
|
:day
|
27
32
|
end
|
@@ -8,12 +8,31 @@ module IceCube
|
|
8
8
|
unless hour.is_a?(Integer)
|
9
9
|
raise ArgumentError, "expecting Integer value for hour, got #{hour.inspect}"
|
10
10
|
end
|
11
|
+
|
12
|
+
verify_alignment(hour, :hour, :hour_of_day) { |error| raise error }
|
13
|
+
|
11
14
|
validations_for(:hour_of_day) << Validation.new(hour)
|
12
15
|
end
|
13
16
|
clobber_base_validations(:hour)
|
14
17
|
self
|
15
18
|
end
|
16
19
|
|
20
|
+
def realign(opening_time, start_time)
|
21
|
+
return super unless validations[:hour_of_day]
|
22
|
+
freq = base_interval_validation.interval
|
23
|
+
|
24
|
+
first_hour = Array(validations[:hour_of_day]).min_by(&:value)
|
25
|
+
time = TimeUtil::TimeWrapper.new(start_time, false)
|
26
|
+
if freq > 1
|
27
|
+
offset = first_hour.validate(opening_time, start_time)
|
28
|
+
time.add(:hour, offset - freq)
|
29
|
+
else
|
30
|
+
time.hour = first_hour.value
|
31
|
+
end
|
32
|
+
|
33
|
+
super opening_time, time.to_time
|
34
|
+
end
|
35
|
+
|
17
36
|
class Validation < Validations::FixedValue
|
18
37
|
|
19
38
|
attr_reader :hour
|
@@ -23,6 +42,10 @@ module IceCube
|
|
23
42
|
@hour = hour
|
24
43
|
end
|
25
44
|
|
45
|
+
def key
|
46
|
+
:hour_of_day
|
47
|
+
end
|
48
|
+
|
26
49
|
def type
|
27
50
|
:hour
|
28
51
|
end
|
@@ -3,6 +3,8 @@ module IceCube
|
|
3
3
|
module Validations::HourlyInterval
|
4
4
|
|
5
5
|
def interval(interval)
|
6
|
+
verify_alignment(interval, :hour, :interval) { |error| raise error }
|
7
|
+
|
6
8
|
@interval = normalized_interval(interval)
|
7
9
|
replace_validations_for(:interval, [Validation.new(@interval)])
|
8
10
|
clobber_base_validations(:hour)
|
@@ -7,12 +7,24 @@ module IceCube
|
|
7
7
|
unless minute.is_a?(Integer)
|
8
8
|
raise ArgumentError, "expecting Integer value for minute, got #{minute.inspect}"
|
9
9
|
end
|
10
|
+
|
11
|
+
verify_alignment(minute, :min, :minute_of_hour) { |error| raise error }
|
12
|
+
|
10
13
|
validations_for(:minute_of_hour) << Validation.new(minute)
|
11
14
|
end
|
12
15
|
clobber_base_validations(:min)
|
13
16
|
self
|
14
17
|
end
|
15
18
|
|
19
|
+
def realign(opening_time, start_time)
|
20
|
+
return super unless validations[:minute_of_hour]
|
21
|
+
|
22
|
+
first_minute = validations[:minute_of_hour].min_by(&:value)
|
23
|
+
time = TimeUtil::TimeWrapper.new(start_time, false)
|
24
|
+
time.min = first_minute.value
|
25
|
+
super opening_time, time.to_time
|
26
|
+
end
|
27
|
+
|
16
28
|
class Validation < Validations::FixedValue
|
17
29
|
|
18
30
|
attr_reader :minute
|
@@ -22,6 +34,10 @@ module IceCube
|
|
22
34
|
@minute = minute
|
23
35
|
end
|
24
36
|
|
37
|
+
def key
|
38
|
+
:minute_of_hour
|
39
|
+
end
|
40
|
+
|
25
41
|
def type
|
26
42
|
:min
|
27
43
|
end
|
@@ -3,6 +3,8 @@ module IceCube
|
|
3
3
|
module Validations::MinutelyInterval
|
4
4
|
|
5
5
|
def interval(interval)
|
6
|
+
verify_alignment(interval, :min, :interval) { |error| raise error }
|
7
|
+
|
6
8
|
@interval = normalized_interval(interval)
|
7
9
|
replace_validations_for(:interval, [Validation.new(@interval)])
|
8
10
|
clobber_base_validations(:min)
|
@@ -8,6 +8,7 @@ module IceCube
|
|
8
8
|
raise ArgumentError, "expecting Integer or Symbol value for month, got #{month.inspect}"
|
9
9
|
end
|
10
10
|
month = TimeUtil.sym_to_month(month)
|
11
|
+
verify_alignment(month, :month, :month_of_year) { |error| raise error }
|
11
12
|
validations_for(:month_of_year) << Validation.new(month)
|
12
13
|
end
|
13
14
|
clobber_base_validations :month
|
@@ -23,6 +24,10 @@ module IceCube
|
|
23
24
|
@month = month
|
24
25
|
end
|
25
26
|
|
27
|
+
def key
|
28
|
+
:month_of_year
|
29
|
+
end
|
30
|
+
|
26
31
|
def type
|
27
32
|
:month
|
28
33
|
end
|
@@ -3,7 +3,10 @@ module IceCube
|
|
3
3
|
module Validations::MonthlyInterval
|
4
4
|
|
5
5
|
def interval(interval)
|
6
|
-
|
6
|
+
interval = normalized_interval(interval)
|
7
|
+
verify_alignment(interval, :month, :interval) { |error| raise error }
|
8
|
+
|
9
|
+
@interval = interval
|
7
10
|
replace_validations_for(:interval, [Validation.new(@interval)])
|
8
11
|
clobber_base_validations(:month)
|
9
12
|
self
|
@@ -4,15 +4,27 @@ module IceCube
|
|
4
4
|
|
5
5
|
def second_of_minute(*seconds)
|
6
6
|
seconds.flatten.each do |second|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
unless second.is_a?(Integer)
|
8
|
+
raise ArgumentError, "Expecting Integer value for second, got #{second.inspect}"
|
9
|
+
end
|
10
|
+
|
11
|
+
verify_alignment(second, :sec, :second_of_minute) { |error| raise error }
|
12
|
+
|
10
13
|
validations_for(:second_of_minute) << Validation.new(second)
|
11
14
|
end
|
12
15
|
clobber_base_validations :sec
|
13
16
|
self
|
14
17
|
end
|
15
18
|
|
19
|
+
def realign(opening_time, start_time)
|
20
|
+
return super unless validations[:second_of_minute]
|
21
|
+
|
22
|
+
first_second = Array(validations[:second_of_minute]).min_by(&:value)
|
23
|
+
time = TimeUtil::TimeWrapper.new(start_time, false)
|
24
|
+
time.sec = first_second.value
|
25
|
+
super opening_time, time.to_time
|
26
|
+
end
|
27
|
+
|
16
28
|
class Validation < Validations::FixedValue
|
17
29
|
|
18
30
|
attr_reader :second
|
@@ -22,6 +34,10 @@ module IceCube
|
|
22
34
|
@second = second
|
23
35
|
end
|
24
36
|
|
37
|
+
def key
|
38
|
+
:second_of_minute
|
39
|
+
end
|
40
|
+
|
25
41
|
def type
|
26
42
|
:sec
|
27
43
|
end
|
@@ -3,6 +3,8 @@ module IceCube
|
|
3
3
|
module Validations::SecondlyInterval
|
4
4
|
|
5
5
|
def interval(interval)
|
6
|
+
verify_alignment(interval, :sec, :interval) { |error| raise error }
|
7
|
+
|
6
8
|
@interval = normalized_interval(interval)
|
7
9
|
replace_validations_for(:interval, [Validation.new(@interval)])
|
8
10
|
clobber_base_validations(:sec)
|
@@ -6,12 +6,11 @@ module IceCube
|
|
6
6
|
|
7
7
|
# Value reader for limit
|
8
8
|
def until_time
|
9
|
-
@until
|
9
|
+
(arr = @validations[:until]) && (val = arr[0]) && val.time
|
10
10
|
end
|
11
11
|
deprecated_alias :until_date, :until_time
|
12
12
|
|
13
13
|
def until(time)
|
14
|
-
@until = time
|
15
14
|
replace_validations_for(:until, time.nil? ? nil : [Validation.new(time)])
|
16
15
|
self
|
17
16
|
end
|
data/lib/ice_cube/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "bundler/setup"
|
2
2
|
require 'ice_cube'
|
3
|
+
require 'timeout'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'simplecov'
|
@@ -19,6 +20,17 @@ WORLD_TIME_ZONES = [
|
|
19
20
|
'Pacific/Auckland', # +1200 / +1300
|
20
21
|
]
|
21
22
|
|
23
|
+
# TODO: enable warnings here and update specs to call IceCube objects correctly
|
24
|
+
def Object.const_missing(sym)
|
25
|
+
case sym
|
26
|
+
when :Schedule, :Rule, :Occurrence, :TimeUtil, :ONE_DAY, :ONE_HOUR, :ONE_MINUTE
|
27
|
+
# warn "Use IceCube::#{sym}", caller[0]
|
28
|
+
IceCube.const_get(sym)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
22
34
|
RSpec.configure do |config|
|
23
35
|
# Enable flags like --only-failures and --next-failure
|
24
36
|
config.example_status_persistence_file_path = ".rspec_status"
|
@@ -29,6 +41,8 @@ RSpec.configure do |config|
|
|
29
41
|
|
30
42
|
Dir[File.dirname(__FILE__) + '/support/**/*'].each { |f| require f }
|
31
43
|
|
44
|
+
config.warnings = true
|
45
|
+
|
32
46
|
config.include WarningHelpers
|
33
47
|
|
34
48
|
config.before :each do |example|
|
@@ -37,15 +51,18 @@ RSpec.configure do |config|
|
|
37
51
|
end
|
38
52
|
end
|
39
53
|
|
40
|
-
config.around :each do |example|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
54
|
+
config.around :each, system_time_zone: true do |example|
|
55
|
+
orig_zone = ENV['TZ']
|
56
|
+
ENV['TZ'] = example.metadata[:system_time_zone]
|
57
|
+
example.run
|
58
|
+
ENV['TZ'] = orig_zone
|
59
|
+
end
|
60
|
+
|
61
|
+
config.around :each, locale: true do |example|
|
62
|
+
orig_locale = I18n.locale
|
63
|
+
I18n.locale = example.metadata[:locale]
|
64
|
+
example.run
|
65
|
+
I18n.locale = orig_locale
|
49
66
|
end
|
50
67
|
|
51
68
|
config.around :each, expect_warnings: true do |example|
|
@@ -53,4 +70,10 @@ RSpec.configure do |config|
|
|
53
70
|
example.run
|
54
71
|
end
|
55
72
|
end
|
73
|
+
|
74
|
+
config.around :each do |example|
|
75
|
+
Timeout.timeout(example.metadata.fetch(:timeout, 1)) do
|
76
|
+
example.run
|
77
|
+
end
|
78
|
+
end
|
56
79
|
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.16.
|
4
|
+
version: 0.16.3
|
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: 2018-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -62,6 +62,7 @@ files:
|
|
62
62
|
- lib/ice_cube/errors/until_exceeded.rb
|
63
63
|
- lib/ice_cube/flexible_hash.rb
|
64
64
|
- lib/ice_cube/i18n.rb
|
65
|
+
- lib/ice_cube/input_alignment.rb
|
65
66
|
- lib/ice_cube/null_i18n.rb
|
66
67
|
- lib/ice_cube/occurrence.rb
|
67
68
|
- lib/ice_cube/parsers/hash_parser.rb
|
@@ -120,8 +121,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
121
|
- !ruby/object:Gem::Version
|
121
122
|
version: '0'
|
122
123
|
requirements: []
|
123
|
-
rubyforge_project:
|
124
|
-
rubygems_version: 2.6.
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 2.6.14
|
125
126
|
signing_key:
|
126
127
|
specification_version: 4
|
127
128
|
summary: Ruby Date Recurrence Library
|