ice_cube 0.6.14 → 0.7.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.
Files changed (43) hide show
  1. data/lib/ice_cube.rb +63 -37
  2. data/lib/ice_cube/builders/hash_builder.rb +27 -0
  3. data/lib/ice_cube/builders/ical_builder.rb +59 -0
  4. data/lib/ice_cube/builders/string_builder.rb +74 -0
  5. data/lib/ice_cube/errors/count_exceeded.rb +7 -0
  6. data/lib/ice_cube/errors/until_exceeded.rb +7 -0
  7. data/lib/ice_cube/rule.rb +85 -147
  8. data/lib/ice_cube/rules/daily_rule.rb +5 -27
  9. data/lib/ice_cube/rules/hourly_rule.rb +6 -26
  10. data/lib/ice_cube/rules/minutely_rule.rb +5 -25
  11. data/lib/ice_cube/rules/monthly_rule.rb +6 -30
  12. data/lib/ice_cube/rules/secondly_rule.rb +5 -26
  13. data/lib/ice_cube/rules/weekly_rule.rb +5 -36
  14. data/lib/ice_cube/rules/yearly_rule.rb +8 -34
  15. data/lib/ice_cube/schedule.rb +257 -229
  16. data/lib/ice_cube/single_occurrence_rule.rb +28 -0
  17. data/lib/ice_cube/time_util.rb +202 -76
  18. data/lib/ice_cube/validated_rule.rb +107 -0
  19. data/lib/ice_cube/validations/count.rb +56 -0
  20. data/lib/ice_cube/validations/daily_interval.rb +51 -0
  21. data/lib/ice_cube/validations/day.rb +45 -31
  22. data/lib/ice_cube/validations/day_of_month.rb +44 -44
  23. data/lib/ice_cube/validations/day_of_week.rb +60 -47
  24. data/lib/ice_cube/validations/day_of_year.rb +48 -44
  25. data/lib/ice_cube/validations/hour_of_day.rb +42 -30
  26. data/lib/ice_cube/validations/hourly_interval.rb +50 -0
  27. data/lib/ice_cube/validations/lock.rb +47 -0
  28. data/lib/ice_cube/validations/minute_of_hour.rb +42 -31
  29. data/lib/ice_cube/validations/minutely_interval.rb +50 -0
  30. data/lib/ice_cube/validations/month_of_year.rb +39 -30
  31. data/lib/ice_cube/validations/monthly_interval.rb +47 -0
  32. data/lib/ice_cube/validations/schedule_lock.rb +41 -0
  33. data/lib/ice_cube/validations/second_of_minute.rb +39 -30
  34. data/lib/ice_cube/validations/secondly_interval.rb +50 -0
  35. data/lib/ice_cube/validations/until.rb +49 -0
  36. data/lib/ice_cube/validations/weekly_interval.rb +50 -0
  37. data/lib/ice_cube/validations/yearly_interval.rb +45 -0
  38. data/lib/ice_cube/version.rb +2 -2
  39. data/spec/spec_helper.rb +13 -0
  40. metadata +50 -9
  41. data/lib/ice_cube/rule_occurrence.rb +0 -94
  42. data/lib/ice_cube/validation.rb +0 -44
  43. data/lib/ice_cube/validation_types.rb +0 -137
data/lib/ice_cube.rb CHANGED
@@ -1,48 +1,74 @@
1
- require 'yaml.rb'
2
- require 'set.rb'
3
1
  require 'date'
4
2
 
5
- require 'ice_cube/time_util'
6
-
7
- require 'ice_cube/validation'
8
- require 'ice_cube/validation_types'
9
- require 'ice_cube/rule'
10
- require 'ice_cube/schedule'
11
- require 'ice_cube/rule_occurrence'
3
+ # Use psych if we can
4
+ begin
5
+ require 'psych'
6
+ rescue LoadError
7
+ require 'yaml'
8
+ end
12
9
 
13
10
  module IceCube
14
-
11
+
12
+ autoload :VERSION, 'ice_cube/version'
13
+
14
+ autoload :TimeUtil, 'ice_cube/time_util'
15
+
16
+ autoload :Rule, 'ice_cube/rule'
17
+ autoload :Schedule, 'ice_cube/schedule'
18
+
19
+ autoload :IcalBuilder, 'ice_cube/builders/ical_builder'
20
+ autoload :HashBuilder, 'ice_cube/builders/hash_builder'
21
+ autoload :StringBuilder, 'ice_cube/builders/string_builder'
22
+
23
+ autoload :CountExceeded, 'ice_cube/errors/count_exceeded'
24
+ autoload :UntilExceeded, 'ice_cube/errors/until_exceeded'
25
+
26
+ autoload :ValidatedRule, 'ice_cube/validated_rule'
27
+ autoload :SingleOccurrenceRule, 'ice_cube/single_occurrence_rule'
28
+
29
+ autoload :SecondlyRule, 'ice_cube/rules/secondly_rule'
30
+ autoload :MinutelyRule, 'ice_cube/rules/minutely_rule'
31
+ autoload :HourlyRule, 'ice_cube/rules/hourly_rule'
15
32
  autoload :DailyRule, 'ice_cube/rules/daily_rule'
16
33
  autoload :WeeklyRule, 'ice_cube/rules/weekly_rule'
17
34
  autoload :MonthlyRule, 'ice_cube/rules/monthly_rule'
18
35
  autoload :YearlyRule, 'ice_cube/rules/yearly_rule'
19
-
20
- autoload :HourlyRule, 'ice_cube/rules/hourly_rule'
21
- autoload :MinutelyRule, 'ice_cube/rules/minutely_rule'
22
- autoload :SecondlyRule, 'ice_cube/rules/secondly_rule'
36
+
37
+ module Validations
38
+
39
+ autoload :Lock, 'ice_cube/validations/lock'
40
+ autoload :ScheduleLock, 'ice_cube/validations/schedule_lock'
41
+
42
+ autoload :Count, 'ice_cube/validations/count'
43
+ autoload :Until, 'ice_cube/validations/until'
44
+
45
+ autoload :SecondlyInterval, 'ice_cube/validations/secondly_interval'
46
+ autoload :MinutelyInterval, 'ice_cube/validations/minutely_interval'
47
+ autoload :DailyInterval, 'ice_cube/validations/daily_interval'
48
+ autoload :WeeklyInterval, 'ice_cube/validations/weekly_interval'
49
+ autoload :MonthlyInterval, 'ice_cube/validations/monthly_interval'
50
+ autoload :YearlyInterval, 'ice_cube/validations/yearly_interval'
51
+ autoload :HourlyInterval, 'ice_cube/validations/hourly_interval'
52
+
53
+ autoload :HourOfDay, 'ice_cube/validations/hour_of_day'
54
+ autoload :MonthOfYear, 'ice_cube/validations/month_of_year'
55
+ autoload :MinuteOfHour, 'ice_cube/validations/minute_of_hour'
56
+ autoload :SecondOfMinute, 'ice_cube/validations/second_of_minute'
57
+ autoload :DayOfMonth, 'ice_cube/validations/day_of_month'
58
+ autoload :DayOfWeek, 'ice_cube/validations/day_of_week'
59
+ autoload :Day, 'ice_cube/validations/day'
60
+ autoload :DayOfYear, 'ice_cube/validations/day_of_year'
61
+
62
+ end
63
+
64
+ # Define some useful constants
65
+ ONE_SECOND = 1
66
+ ONE_MINUTE = ONE_SECOND * 60
67
+ ONE_HOUR = ONE_MINUTE * 60
68
+ ONE_DAY = ONE_HOUR * 24
69
+ ONE_WEEK = ONE_DAY * 7
23
70
 
24
- autoload :DayValidation, 'ice_cube/validations/day'
25
- autoload :DayOfMonthValidation, 'ice_cube/validations/day_of_month'
26
- autoload :DayOfWeekValidation, 'ice_cube/validations/day_of_week'
27
- autoload :DayOfYearValidation, 'ice_cube/validations/day_of_year'
28
- autoload :HourOfDayValidation, 'ice_cube/validations/hour_of_day'
29
- autoload :MinuteOfHourValidation, 'ice_cube/validations/minute_of_hour'
30
- autoload :MonthOfYearValidation, 'ice_cube/validations/month_of_year'
31
- autoload :SecondOfMinuteValidation, 'ice_cube/validations/second_of_minute'
32
-
33
- # if you're reading this code, you've just been iced
34
- # http://brosicingbros.com/
35
-
36
- IceCube::ONE_DAY = 24 * 60 * 60
37
- IceCube::ONE_HOUR = 60 * 60
38
- IceCube::ONE_MINUTE = 60
39
- IceCube::ONE_SECOND = 1
40
-
41
- ICAL_DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
42
- DAYS = { :sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6 }
43
- MONTHS = { :january => 1, :february => 2, :march => 3, :april => 4, :may => 5, :june => 6, :july => 7, :august => 8,
44
- :september => 9, :october => 10, :november => 11, :december => 12 }
45
-
46
- include TimeUtil
71
+ # Formatting
72
+ TO_S_TIME_FORMAT = '%B %e, %Y'
47
73
 
48
74
  end
@@ -0,0 +1,27 @@
1
+ module IceCube
2
+
3
+ class HashBuilder
4
+
5
+ def initialize(rule = nil)
6
+ @hash = { :validations => {}, :rule_type => rule.class.name }
7
+ end
8
+
9
+ def validations
10
+ @hash[:validations]
11
+ end
12
+
13
+ def []=(key, value)
14
+ @hash[key] = value
15
+ end
16
+
17
+ def validations_array(type)
18
+ validations[type] ||= []
19
+ end
20
+
21
+ def to_hash
22
+ @hash
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,59 @@
1
+ module IceCube
2
+
3
+ class IcalBuilder
4
+
5
+ ICAL_DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
6
+
7
+ def initialize
8
+ @hash = {}
9
+ end
10
+
11
+ def self.fixnum_to_ical_day(num)
12
+ ICAL_DAYS[num]
13
+ end
14
+
15
+ def [](key)
16
+ @hash[key] ||= []
17
+ end
18
+
19
+ # Build for a single rule entry
20
+ def to_s
21
+ arr = []
22
+ if freq = @hash.delete('FREQ')
23
+ arr << "FREQ=#{freq.join(',')}"
24
+ end
25
+ arr.concat(@hash.map do |key, value|
26
+ if value.is_a?(Array)
27
+ "#{key}=#{value.join(',')}"
28
+ end
29
+ end.compact)
30
+ arr.join(';')
31
+ end
32
+
33
+ def self.ical_utc_format(time)
34
+ time = time.dup.utc
35
+ "#{time.strftime('%Y%m%dT%H%M%SZ')}" # utc time
36
+ end
37
+
38
+ def self.ical_format(time, force_utc)
39
+ time = time.dup.utc if force_utc
40
+ if time.utc?
41
+ ":#{time.strftime('%Y%m%dT%H%M%SZ')}" # utc time
42
+ else
43
+ ";TZID=#{time.strftime('%Z:%Y%m%dT%H%M%S')}" # local time specified
44
+ end
45
+ end
46
+
47
+ def self.ical_duration(duration)
48
+ hours = duration / 3600; duration %= 3600
49
+ minutes = duration / 60; duration %= 60
50
+ repr = ''
51
+ repr << "#{hours}H" if hours > 0
52
+ repr << "#{minutes}M" if minutes > 0
53
+ repr << "#{duration}S" if duration > 0
54
+ "PT#{repr}"
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,74 @@
1
+ module IceCube
2
+
3
+ class StringBuilder
4
+
5
+ attr_writer :base
6
+
7
+ def initialize
8
+ @types = {}
9
+ end
10
+
11
+ def piece(type, prefix = nil, suffix = nil)
12
+ @types[type] ||= []
13
+ end
14
+
15
+ def to_s
16
+ str = @base || ''
17
+ res = @types.map do |type, segments|
18
+ if f = self.class.formatter(type)
19
+ str << ' ' + f.call(segments)
20
+ else
21
+ next if segments.empty?
22
+ str << ' ' + self.class.sentence(segments)
23
+ end
24
+ end
25
+ str
26
+ end
27
+
28
+ class << self
29
+
30
+ def formatter(type)
31
+ @formatters[type]
32
+ end
33
+
34
+ def register_formatter(type, &formatter)
35
+ @formatters ||= {}
36
+ @formatters[type] = formatter
37
+ end
38
+
39
+ end
40
+
41
+ class << self
42
+
43
+ NUMBER_SUFFIX = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th']
44
+ SPECIAL_SUFFIX = { 11 => 'th', 12 => 'th', 13 => 'th', 14 => 'th' }
45
+
46
+ # influenced by ActiveSupport's to_sentence
47
+ def sentence(array)
48
+ case array.length
49
+ when 0 ; ''
50
+ when 1 ; array[0].to_s
51
+ when 2 ; "#{array[0]} and #{array[1]}"
52
+ else ; "#{array[0...-1].join(', ')}, and #{array[-1]}"
53
+ end
54
+ end
55
+
56
+ def nice_number(number)
57
+ if number == -1
58
+ 'last'
59
+ elsif number < -1
60
+ suffix = SPECIAL_SUFFIX.include?(number) ?
61
+ SPECIAL_SUFFIX[number] : NUMBER_SUFFIX[number.abs % 10]
62
+ number.abs.to_s << suffix << ' to last'
63
+ else
64
+ suffix = SPECIAL_SUFFIX.include?(number) ?
65
+ SPECIAL_SUFFIX[number] : NUMBER_SUFFIX[number.abs % 10]
66
+ number.to_s << suffix
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,7 @@
1
+ module IceCube
2
+
3
+ # An exception for when a count on a Rule is passed
4
+ class CountExceeded < Exception
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module IceCube
2
+
3
+ # An exception for when an until date on a Rule is passed
4
+ class UntilExceeded < Exception
5
+ end
6
+
7
+ end
data/lib/ice_cube/rule.rb CHANGED
@@ -1,171 +1,109 @@
1
+ require 'yaml'
2
+
1
3
  module IceCube
2
-
4
+
3
5
  class Rule
4
-
5
- attr_reader :occurrence_count, :until_date
6
- attr_reader :interval
7
- attr_reader :validations
8
-
9
- include ValidationTypes
10
- include Comparable
11
6
 
12
- # Compare based on hash representations
13
- def <=>(other)
14
- to_hash <=> other.to_hash
15
- end
7
+ attr_reader :uses
16
8
 
17
- def to_hash
18
- hash = Hash.new
19
- hash[:rule_type] = self.class.name
20
- hash[:interval] = @interval
21
- hash[:until] = @until_date ? TimeUtil.serialize_time(@until_date) : @until_date
22
- hash[:count] = @occurrence_count
23
- hash[:validations] = @validations
24
- hash
25
- end
26
-
27
- def self.from_hash(hash)
28
- rule = hash[:rule_type].split('::').inject(Object) { |namespace, const_name| namespace.const_get(const_name) }.new(hash[:interval])
29
- rule.count(hash[:count]) if hash[:count]
30
- rule.until(hash[:until]) if hash[:until]
31
- hash[:validations].each do |validation, data|
32
- data.is_a?(Array) ? rule.send(validation, *data) : rule.send(validation, data)
33
- end
34
- rule
35
- end
36
-
37
- def to_yaml(options = {})
38
- to_hash.to_yaml(options)
9
+ # Is this a terminating schedule?
10
+ def terminating?
11
+ until_time || occurrence_count
39
12
  end
40
13
 
41
- def self.from_yaml(str)
42
- from_hash(YAML::load(str))
14
+ def ==(rule)
15
+ hash = to_hash
16
+ hash && hash == rule.to_hash
43
17
  end
44
-
45
- # create a new daily rule
46
- def self.daily(interval = 1)
47
- DailyRule.new(interval)
18
+
19
+ def hash
20
+ h = to_hash
21
+ h.nil? ? super : h.hash
48
22
  end
49
23
 
50
- # create a new weekly rule
51
- def self.weekly(interval = 1)
52
- WeeklyRule.new(interval)
24
+ # Expected to be overridden by subclasses
25
+ def to_ical
26
+ nil
53
27
  end
54
28
 
55
- # create a new monthly rule
56
- def self.monthly(interval = 1)
57
- MonthlyRule.new(interval)
29
+ # Yaml implementation
30
+ def to_yaml(*args)
31
+ defined?(Psych) ? Psych::dump(to_hash) : YAML::dump(to_hash, *args)
58
32
  end
59
33
 
60
- # create a new yearly rule
61
- def self.yearly(interval = 1)
62
- YearlyRule.new(interval)
34
+ # From yaml
35
+ def self.from_yaml(yaml)
36
+ from_hash defined?(Psych) ? Psych::load(yaml) : YAML::load(yaml)
63
37
  end
64
-
65
- # create a new hourly rule
66
- def self.hourly(interval = 1)
67
- HourlyRule.new(interval)
38
+
39
+ # Expected to be overridden by subclasses
40
+ def to_hash
41
+ nil
68
42
  end
69
-
70
- # create a new minutely rule
71
- def self.minutely(interval = 1)
72
- MinutelyRule.new(interval)
43
+
44
+ # Convert from a hash and create a rule
45
+ def self.from_hash(hash)
46
+ return nil unless match = hash[:rule_type].match(/\:\:(.+?)Rule/)
47
+ rule = IceCube::Rule.send(match[1].downcase.to_sym, hash[:interval] || 1)
48
+ rule.until(TimeUtil.deserialize_time(hash[:until])) if hash[:until]
49
+ rule.count(hash[:count]) if hash[:count]
50
+ hash[:validations] && hash[:validations].each do |key, value|
51
+ key = key.to_sym unless key.is_a?(Symbol)
52
+ value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value)
53
+ end
54
+ rule
73
55
  end
74
-
75
- # create a new secondly rule
76
- def self.secondly(interval = 1)
77
- SecondlyRule.new(interval)
56
+
57
+ # Reset the uses on the rule to 0
58
+ def reset
59
+ @uses = 0
78
60
  end
79
-
80
- # Set the time when this rule will no longer be effective
81
- def until(until_date)
82
- raise ArgumentError.new('Cannot specify until and count on the same rule') if @count #as per rfc
83
- @until_date = until_date
84
- self
61
+
62
+ def next_time(time, schedule, closing_time)
85
63
  end
86
-
87
- # set the number of occurrences after which this rule is no longer effective
88
- def count(count)
89
- raise ArgumentError.new('Argument must be a positive integer') unless Integer(count) && count >= 0
90
- @occurrence_count = count
91
- self
64
+
65
+ def on?(time, schedule)
66
+ next_time(time, schedule, time) == time
92
67
  end
93
-
94
- def validate_single_date(date)
95
- @validation_types.values.all? do |validation|
96
- response = validation.send(:validate, date)
97
- response.nil? || response
68
+
69
+ # Convenience methods for creating Rules
70
+ class << self
71
+
72
+ # Secondly Rule
73
+ def secondly(interval = 1)
74
+ SecondlyRule.new(interval)
98
75
  end
99
- end
100
-
101
- # The key to speed - extremely educated guesses
102
- # This spidering behavior will go through look for the next suggestion
103
- # by constantly moving the farthest back value forward
104
- def next_suggestion(date)
105
- # get the next date recommendation set
106
- suggestions = {};
107
- @validation_types.each { |k, validation| suggestions[k] = validation.send(:closest, date) }
108
- compact_suggestions = suggestions.values.compact
109
- # find the next date to go to
110
- if compact_suggestions.empty?
111
- attempt_count = 0
112
- loop do
113
- # keep going through rule suggestions
114
- next_date = self.default_jump(date, attempt_count += 1)
115
- return next_date if !next_date.nil? && validate_single_date(next_date)
116
- end
117
- else
118
- loop do
119
- compact_suggestions = suggestions.values.compact
120
- min_suggestion = compact_suggestions.min
121
- # validate all against the minimum
122
- return min_suggestion if validate_single_date(min_suggestion)
123
- # move anything that is the minimum to its next closest
124
- @validation_types.each do |k, validation|
125
- suggestions[k] = validation.send(:closest, min_suggestion) if min_suggestion == suggestions[k]
126
- end
127
- end
76
+
77
+ # Minutely Rule
78
+ def minutely(interval = 1)
79
+ MinutelyRule.new(interval)
128
80
  end
129
- end
130
-
131
- attr_reader :validations
132
-
133
- private
134
-
135
- def adjust(goal, date)
136
- return goal if goal.utc_offset == date.utc_offset
137
- goal - goal.utc_offset + date.utc_offset
138
- end
139
-
140
- # get a very meaningful string representation of this rule
141
- def to_s_base(singular, plural)
142
- representation = ''
143
- representation = (@interval == 1 ? singular : plural)
144
- representation << @validation_types.values.map { |v| ' ' + v.send(:to_s) }.join()
145
- representation << " #{occurrence_count} #{@occurrence_count == 1 ? 'time' : 'times'}" if @occurrence_count
146
- representation
147
- end
148
-
149
- # get the icalendar representation of this rule logic
150
- # Note: UNTIL dates are always in UTC as per iCalendar
151
- def to_ical_base
152
- representation = ''
153
- representation << ";INTERVAL=#{@interval}" if @interval > 1
154
- @validation_types.values.each do |v|
155
- representation << ';' << v.send(:to_ical)
156
- end
157
- representation << ";COUNT=#{@occurrence_count}" if @occurrence_count
158
- representation << ";UNTIL=#{TimeUtil.ical_utc_format(@until_date)}" if @until_date
159
- representation
160
- end
161
-
162
- # Set the interval for the rule. Depending on the type of rule,
163
- # interval means every (n) weeks, months, etc. starting on the start_date's
164
- def initialize(interval = 1)
165
- throw ArgumentError.new('Interval must be > 0') unless interval > 0
166
- @validations = {}
167
- @validation_types = {}
168
- @interval = interval
81
+
82
+ # Hourly Rule
83
+ def hourly(interval = 1)
84
+ HourlyRule.new(interval)
85
+ end
86
+
87
+ # Daily Rule
88
+ def daily(interval = 1)
89
+ DailyRule.new(interval)
90
+ end
91
+
92
+ # Weekly Rule
93
+ def weekly(interval = 1)
94
+ WeeklyRule.new(interval)
95
+ end
96
+
97
+ # Monthly Rule
98
+ def monthly(interval = 1)
99
+ MonthlyRule.new(interval)
100
+ end
101
+
102
+ # Yearly Rule
103
+ def yearly(interval = 1)
104
+ YearlyRule.new(interval)
105
+ end
106
+
169
107
  end
170
108
 
171
109
  end