ice_cube 0.6.14 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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