ice_cube 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ec72afe66473b31f58d02177fce834635b8d6f6d
4
- data.tar.gz: f2b199ad1c5cddfbdc4f0b435f7caa594e8e0f3b
3
+ metadata.gz: f8d575d65663825eb9be2a82f9e931537009d485
4
+ data.tar.gz: 2e292c2f229a8a7b7aaaf8f8dcaf009412741745
5
5
  SHA512:
6
- metadata.gz: 21eba9fafa80dd036ca6e24f118b4638c46c68000a1a77758467d8cc1a27b9cb0c287980fe2d23fbaeb0b18f35d7ac9f8c6850bb5ff5a5cd2c019dace00d9f43
7
- data.tar.gz: 2d842dc82d6f195642c2ecfd0bb4198dd6d9dced764da87bba2001309d2d4de4672f581c8e3ddc062223c87e9c32c1cb32aa2874cb4a1e4893ba98f578ce73fc
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
@@ -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
@@ -1,7 +1,7 @@
1
1
  module IceCube
2
2
 
3
3
  # An exception for when a count on a Rule is passed
4
- class CountExceeded < Exception
4
+ class CountExceeded < StopIteration
5
5
  end
6
6
 
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module IceCube
2
2
 
3
3
  # An exception for when an until date on a Rule is passed
4
- class UntilExceeded < Exception
4
+ class UntilExceeded < StopIteration
5
5
  end
6
6
 
7
7
  end
@@ -2,21 +2,37 @@ require 'delegate'
2
2
 
3
3
  module IceCube
4
4
 
5
- # A way to symbolize what's necessary on the fly
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 initialize(hash)
12
- @underlying = hash
11
+ def [](key)
12
+ key = _match_key(key)
13
+ super
13
14
  end
14
15
 
15
- def [](key)
16
- case key
17
- when String then @underlying[key] || @underlying[key.to_sym]
18
- else @underlying[key] || @underlying[key.to_s]
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
@@ -93,7 +93,8 @@ module IceCube
93
93
  end
94
94
 
95
95
  def overnight?
96
- midnight = Time.new(start_time.year, start_time.month, start_time.day + 1)
96
+ offset = start_time + 3600 * 24
97
+ midnight = Time.new(offset.year, offset.month, offset.day)
97
98
  midnight < end_time
98
99
  end
99
100
  end
@@ -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
@@ -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
- hash = YAML::load(yaml)
344
- if match = yaml.match(/start_date: .+((?:-|\+)\d{2}:\d{2})$/)
345
- TimeUtil.restore_deserialized_offset(hash[:start_date], match[1])
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[:start_date] = TimeUtil.serialize_time(start_time)
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
- data[:exrules] = exception_rules.map(&:to_hash)
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[:start_date] = options[:start_date_override] if options[:start_date_override]
369
- # And then deserialize
370
- data = IceCube::FlexibleHash.new(original_hash)
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.ensure_time opening_time
418
- closing_time = TimeUtil.ensure_time closing_time
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
- # walk up to the opening time - and off we go
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
- res = next_time(time, closing_time)
429
- break unless res
430
- break if closing_time && res > closing_time
431
- if res >= opening_time
432
- yielder.yield (block_given? ? block.call(res) : res)
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
- time = res + 1
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 CountExceeded, UntilExceeded
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
- # Return a boolean indicating if any rule needs to be run from the start of time
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,10 @@
1
+ module IceCube
2
+ module StringHelpers
3
+
4
+ module_function
5
+
6
+ def nice_number(number)
7
+ end
8
+
9
+ end
10
+ 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
@@ -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
- time_or_hash[:time].in_time_zone(time_or_hash[:zone])
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
- type = vals.first.type # get the jump type
137
- dst_adjust = !vals.first.respond_to?(:dst_adjust?) || vals.first.dst_adjust?
138
- wrapper = TimeUtil::TimeWrapper.new(@time, dst_adjust)
139
- wrapper.add(type, res.min)
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
@@ -29,6 +29,10 @@ module IceCube
29
29
  :limit
30
30
  end
31
31
 
32
+ def dst_adjust?
33
+ false
34
+ end
35
+
32
36
  def validate(time, schedule)
33
37
  raise CountExceeded if rule.uses && rule.uses >= count
34
38
  end
@@ -22,6 +22,10 @@ module IceCube
22
22
  :day
23
23
  end
24
24
 
25
+ def dst_adjust?
26
+ true
27
+ end
28
+
25
29
  def validate(step_time, schedule)
26
30
  t0, t1 = schedule.start_time, step_time
27
31
  days = Date.new(t1.year, t1.month, t1.day) -
@@ -31,6 +31,10 @@ module IceCube
31
31
  :wday
32
32
  end
33
33
 
34
+ def dst_adjust?
35
+ true
36
+ end
37
+
34
38
  def build_s(builder)
35
39
  builder.piece(:day) << day
36
40
  end
@@ -28,6 +28,10 @@ module IceCube
28
28
  :day
29
29
  end
30
30
 
31
+ def dst_adjust?
32
+ true
33
+ end
34
+
31
35
  def build_s(builder)
32
36
  builder.piece(:day_of_month) << StringBuilder.nice_number(day)
33
37
  end
@@ -26,6 +26,10 @@ module IceCube
26
26
  :day
27
27
  end
28
28
 
29
+ def dst_adjust?
30
+ true
31
+ end
32
+
29
33
  def validate(step_time, schedule)
30
34
  wday = step_time.wday
31
35
  offset = (day < wday) ? (7 - wday + day) : (day - wday)
@@ -25,6 +25,10 @@ module IceCube
25
25
  :day
26
26
  end
27
27
 
28
+ def dst_adjust?
29
+ true
30
+ end
31
+
28
32
  def validate(step_time, schedule)
29
33
  days_in_year = TimeUtil.days_in_year(step_time)
30
34
  yday = day < 0 ? day + days_in_year : day
@@ -29,6 +29,10 @@ module IceCube
29
29
  :hour
30
30
  end
31
31
 
32
+ def dst_adjust?
33
+ true
34
+ end
35
+
32
36
  def build_s(builder)
33
37
  builder.piece(:hour_of_day) << StringBuilder.nice_number(hour)
34
38
  end
@@ -28,6 +28,10 @@ module IceCube
28
28
  :min
29
29
  end
30
30
 
31
+ def dst_adjust?
32
+ false
33
+ end
34
+
31
35
  def build_s(builder)
32
36
  builder.piece(:minute_of_hour) << StringBuilder.nice_number(minute)
33
37
  end
@@ -29,6 +29,10 @@ module IceCube
29
29
  :month
30
30
  end
31
31
 
32
+ def dst_adjust?
33
+ true
34
+ end
35
+
32
36
  def build_s(builder)
33
37
  builder.piece(:month_of_year) << Date::MONTHNAMES[month]
34
38
  end
@@ -21,6 +21,10 @@ module IceCube
21
21
  :month
22
22
  end
23
23
 
24
+ def dst_adjust?
25
+ true
26
+ end
27
+
24
28
  def validate(step_time, schedule)
25
29
  t0, t1 = schedule.start_time, step_time
26
30
  months = (t1.month - t0.month) +
@@ -28,6 +28,10 @@ module IceCube
28
28
  :sec
29
29
  end
30
30
 
31
+ def dst_adjust?
32
+ false
33
+ end
34
+
31
35
  def build_s(builder)
32
36
  builder.piece(:second_of_minute) << StringBuilder.nice_number(second)
33
37
  end
@@ -29,6 +29,10 @@ module IceCube
29
29
  :limit
30
30
  end
31
31
 
32
+ def dst_adjust?
33
+ false
34
+ end
35
+
32
36
  def validate(step_time, schedule)
33
37
  raise UntilExceeded if step_time > time
34
38
  end
@@ -29,6 +29,10 @@ module IceCube
29
29
  :day
30
30
  end
31
31
 
32
+ def dst_adjust?
33
+ true
34
+ end
35
+
32
36
  def validate(step_time, schedule)
33
37
  t0, t1 = schedule.start_time, step_time
34
38
  d0 = Date.new(t0.year, t0.month, t0.day)
@@ -20,6 +20,10 @@ module IceCube
20
20
  :year
21
21
  end
22
22
 
23
+ def dst_adjust?
24
+ true
25
+ end
26
+
23
27
  def validate(step_time, schedule)
24
28
  years = step_time.year - schedule.start_time.year
25
29
  offset = (years % interval).nonzero?
@@ -1,5 +1,5 @@
1
1
  module IceCube
2
2
 
3
- VERSION = '0.11.3'
3
+ VERSION = '0.12.0'
4
4
 
5
5
  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.11.3
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-02-07 00:00:00.000000000 Z
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.0
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