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
@@ -1,34 +1,12 @@
1
1
  module IceCube
2
2
 
3
- class DailyRule < Rule
3
+ class DailyRule < ValidatedRule
4
4
 
5
- # TODO repair
6
- # Determine whether this rule occurs on a give date.
7
- def in_interval?(date, start_date)
8
- #make sure we're in a proper interval
9
- day_count = date.to_date - start_date.to_date
10
- day_count % @interval == 0
11
- end
12
-
13
- def to_ical
14
- 'FREQ=DAILY' << to_ical_base
15
- end
16
-
17
- def to_s
18
- to_s_base 'Daily', "Every #{@interval} days"
19
- end
20
-
21
- protected
22
-
23
- def default_jump(date, attempt_count = nil)
24
- goal = date + IceCube::ONE_DAY * @interval
25
- adjust(goal, date)
26
- end
27
-
28
- private
5
+ include Validations::DailyInterval
29
6
 
30
- def initialize(interval)
31
- super(interval)
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:hour, :min, :sec)
32
10
  end
33
11
 
34
12
  end
@@ -1,34 +1,14 @@
1
1
  module IceCube
2
2
 
3
- class HourlyRule < Rule
3
+ class HourlyRule < ValidatedRule
4
4
 
5
- # Determine whether this rule occurs on a give date.
6
- def in_interval?(date, start_date)
7
- #make sure we're in a proper interval
8
- day_count = ((date - start_date) / IceCube::ONE_HOUR).to_i
9
- day_count % @interval == 0
10
- end
5
+ include Validations::HourlyInterval
11
6
 
12
- def to_ical
13
- 'FREQ=HOURLY' << to_ical_base
14
- end
15
-
16
- def to_s
17
- to_s_base 'Hourly', "Every #{@interval} hours"
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:min, :sec)
18
10
  end
19
-
20
- protected
21
-
22
- def default_jump(date, attempt_count = nil)
23
- date + IceCube::ONE_HOUR * @interval
24
- end
25
-
26
- private
27
-
28
- def initialize(interval)
29
- super(interval)
30
- end
31
-
11
+
32
12
  end
33
13
 
34
14
  end
@@ -1,34 +1,14 @@
1
1
  module IceCube
2
2
 
3
- class MinutelyRule < Rule
3
+ class MinutelyRule < ValidatedRule
4
4
 
5
- # Determine whether this rule occurs on a give date.
6
- def in_interval?(date, start_date)
7
- #make sure we're in a proper interval
8
- day_count = ((date - start_date) / IceCube::ONE_MINUTE).to_i
9
- day_count % @interval == 0
10
- end
5
+ include Validations::MinutelyInterval
11
6
 
12
- def to_ical
13
- 'FREQ=MINUTELY' << to_ical_base
14
- end
15
-
16
- def to_s
17
- to_s_base 'Minutely', "Every #{@interval} minutes"
18
- end
19
-
20
- protected
21
-
22
- def default_jump(date, attempt_count = nil)
23
- date + IceCube::ONE_MINUTE
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:sec)
24
10
  end
25
11
 
26
- private
27
-
28
- def initialize(interval)
29
- super(interval)
30
- end
31
-
32
12
  end
33
13
 
34
14
  end
@@ -1,38 +1,14 @@
1
1
  module IceCube
2
2
 
3
- class MonthlyRule < Rule
3
+ class MonthlyRule < ValidatedRule
4
4
 
5
- # Determine for a given date/start_date if this rule occurs or not.
6
- # Month rules occur if we're in a valid interval
7
- # and either (1) we're on a valid day of the week (ie: first sunday of the month)
8
- # or we're on a valid day of the month (1, 15, -1)
9
- # Note: Rollover is not implemented, so the 35th day of the month is invalid.
10
- def in_interval?(date, start_date)
11
- # make sure we're in the proper interval
12
- months_to_start_date = (date.month - start_date.month) + (date.year - start_date.year) * 12
13
- months_to_start_date % @interval == 0
14
- end
15
-
16
- def to_ical
17
- 'FREQ=MONTHLY' << to_ical_base
18
- end
19
-
20
- def to_s
21
- to_s_base 'Monthly', "Every #{@interval} months"
22
- end
23
-
24
- protected
25
-
26
- def default_jump(date, attempt_count = 1)
27
- TimeUtil.date_in_n_months(date, attempt_count * @interval)
28
- end
29
-
30
- private
5
+ include Validations::MonthlyInterval
31
6
 
32
- def initialize(interval)
33
- super(interval)
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:day, :hour, :min, :sec)
34
10
  end
35
-
11
+
36
12
  end
37
13
 
38
14
  end
@@ -1,34 +1,13 @@
1
1
  module IceCube
2
2
 
3
- class SecondlyRule < Rule
3
+ class SecondlyRule < ValidatedRule
4
+
5
+ include Validations::SecondlyInterval
4
6
 
5
- # Determine whether this rule occurs on a give date.
6
- def in_interval?(date, start_date)
7
- #make sure we're in a proper interval
8
- day_count = date - start_date
9
- day_count % @interval == 0
7
+ def initialize(interval = 1)
8
+ interval(interval)
10
9
  end
11
10
 
12
- def to_ical
13
- 'FREQ=SECONDLY' << to_ical_base
14
- end
15
-
16
- def to_s
17
- to_s_base 'Secondly', "Every #{@interval} seconds"
18
- end
19
-
20
- protected
21
-
22
- def default_jump(date, attempt_count = nil)
23
- date + 1
24
- end
25
-
26
- private
27
-
28
- def initialize(interval)
29
- super(interval)
30
- end
31
-
32
11
  end
33
12
 
34
13
  end
@@ -1,45 +1,14 @@
1
1
  module IceCube
2
2
 
3
- class WeeklyRule < Rule
4
-
5
- # Determine whether or not this rule occurs on a given date.
6
- # Weekly rules occurs if we're in one of the interval weeks,
7
- # and we're in a valid day of the week.
8
- def in_interval?(date, start_date)
9
- #make sure we're in the right interval
10
- date = adjust(date, start_date)
3
+ class WeeklyRule < ValidatedRule
11
4
 
12
- date = Date.civil(date.year, date.month, date.day)
13
- start_date = Date.civil(start_date.year, start_date.month, start_date.day)
5
+ include Validations::WeeklyInterval
14
6
 
15
- #Move both to the start of their respective weeks,
16
- #and find the number of full weeks between them
17
- no_weeks = ((date - date.wday) - (start_date - start_date.wday)) / 7
18
-
19
- no_weeks % @interval == 0
20
- end
21
-
22
- def to_ical
23
- 'FREQ=WEEKLY' << to_ical_base
24
- end
25
-
26
- def to_s
27
- to_s_base 'Weekly', "Every #{@interval} weeks"
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:wday, :hour, :min, :sec)
28
10
  end
29
-
30
- protected
31
-
32
- def default_jump(date, attempt_count = nil)
33
- goal = date + 7 * IceCube::ONE_DAY * @interval
34
- adjust(goal, date)
35
- end
36
-
37
- private
38
11
 
39
- def initialize(interval)
40
- super(interval)
41
- end
42
-
43
12
  end
44
13
 
45
14
  end
@@ -1,40 +1,14 @@
1
1
  module IceCube
2
2
 
3
- class YearlyRule < Rule
4
-
5
- # Determine whether or not the rule, given a start_date,
6
- # occurs on a given date.
7
- # Yearly occurs if we're in a proper interval
8
- # and either (1) we're on a day of the year, or (2) we're on a month of the year as specified
9
- # Note: rollover dates don't work, so you can't ask for the 400th day of a year
10
- # and expect to roll into the next year (this might be a possible direction in the future)
11
- def in_interval?(date, start_date)
12
- #make sure we're in the proper interval
13
- (date.year - start_date.year) % @interval == 0
14
- end
15
-
16
- def to_ical
17
- 'FREQ=YEARLY' << to_ical_base
18
- end
19
-
20
- def to_s
21
- to_s_base 'Yearly', "Every #{@interval} years"
22
- end
23
-
24
- protected
25
-
26
- # one year from now, the same month and day of the year
27
- def default_jump(date, attempt_count = 1)
28
- # jump by months since there's no reliable way to jump by year
29
- TimeUtil.date_in_n_months(date, attempt_count * @interval * 12)
30
- end
31
-
32
- private
3
+ class YearlyRule < ValidatedRule
33
4
 
34
- def initialize(interval)
35
- super(interval)
5
+ include Validations::YearlyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:month, :day, :hour, :min, :sec)
36
10
  end
37
-
11
+
38
12
  end
39
-
13
+
40
14
  end
@@ -1,307 +1,335 @@
1
+ require 'yaml'
2
+
1
3
  module IceCube
2
4
 
3
5
  class Schedule
4
6
 
5
- attr_reader :rdates, :exdates, :start_date
6
- attr_accessor :duration, :end_time
7
+ # Get the start time
8
+ attr_accessor :start_time
9
+ alias :start_date :start_time
10
+ alias :start_date= :start_time=
11
+
12
+ # Get the duration
13
+ attr_accessor :duration
7
14
 
15
+ # Get the end time
16
+ attr_accessor :end_time
8
17
  alias :end_date :end_time
9
- alias :start_time :start_date
10
-
11
- def initialize(start_date, options = {})
12
- @rrule_occurrence_heads = []
13
- @exrule_occurrence_heads = []
14
- @rdates = []
15
- @exdates = []
16
- @start_date = start_date || Time.now
17
- raise ArgumentError.new('Duration cannot be negative') if options[:duration] && options[:duration] < 0
18
- @duration = options[:duration]
19
- raise ArgumentError.new('Start time must be before end time') if options[:end_time] && options[:end_time] < @start_date
18
+
19
+ # Create a new schedule
20
+ def initialize(start_time = nil, options = {})
21
+ @start_time = start_time || Time.now
20
22
  @end_time = options[:end_time]
23
+ @duration = options[:duration]
24
+ @all_recurrence_rules = []
25
+ @all_exception_rules = []
21
26
  end
22
27
 
23
- # Change the current start date
24
- def start_date=(start_date)
25
- @start_date = start_date
26
- @rrule_occurrence_heads.each { |ro| ro.start_date = start_date }
27
- @exrule_occurrence_heads.each { |ro| ro.start_date = start_date }
28
- start_date
28
+ # Add a recurrence time to the schedule
29
+ def add_recurrence_time(time)
30
+ return nil if time.nil?
31
+ rule = SingleOccurrenceRule.new(time)
32
+ add_recurrence_rule rule
33
+ time
29
34
  end
30
- alias :start_time= :start_date=
35
+ alias :rdate :add_recurrence_time
36
+ alias :add_recurrence_date :add_recurrence_time
31
37
 
32
- # Convert the schedule to a hash, reverse of Schedule.from_hash
33
- def to_hash
34
- hash = Hash.new
35
- hash[:start_date] = @start_date
36
- hash[:rrules] = @rrule_occurrence_heads.map { |rr| rr.rule.to_hash }
37
- hash[:exrules] = @exrule_occurrence_heads.map { |ex| ex.rule.to_hash }
38
- hash[:rdates] = @rdates
39
- hash[:exdates] = @exdates
40
- hash[:duration] = @duration
41
- hash[:end_time] = @end_time
42
- hash
43
- end
44
-
45
- # Convert the schedule to yaml, reverse of Schedule.from_yaml
46
- def to_yaml(options = {})
47
- hash = to_hash
48
- hash[:start_date] = TimeUtil.serialize_time(hash[:start_date])
49
- hash[:rdates] = hash[:rdates].map { |t| TimeUtil.serialize_time(t) }
50
- hash[:exdates] = hash[:exdates].map { |t| TimeUtil.serialize_time(t) }
51
- hash[:end_time] = TimeUtil.serialize_time(hash[:end_time])
52
- hash.to_yaml(options)
53
- end
54
-
55
- # Create a schedule from a hash created by instance.to_hash
56
- def self.from_hash(hash, hash_options = {})
57
- options = {}
58
- options[:duration] = hash[:duration] if hash.has_key?(:duration)
59
- options[:end_time] = TimeUtil.deserialize_time(hash[:end_time]) if hash.has_key?(:end_time)
60
- start_date = hash_options[:start_date_override] || TimeUtil.deserialize_time(hash[:start_date])
61
- schedule = Schedule.new(start_date, options)
62
- hash[:rrules].each { |rr| schedule.add_recurrence_rule Rule.from_hash(rr) }
63
- hash[:exrules].each { |ex| schedule.add_exception_rule Rule.from_hash(ex) }
64
- hash[:rdates].each { |rd| schedule.add_recurrence_date TimeUtil.deserialize_time(rd) }
65
- hash[:exdates].each { |ed| schedule.add_exception_date TimeUtil.deserialize_time(ed) }
66
- schedule
38
+ # Add an exception time to the schedule
39
+ def add_exception_time(time)
40
+ return nil if time.nil?
41
+ rule = SingleOccurrenceRule.new(time)
42
+ add_exception_rule rule
43
+ time
67
44
  end
45
+ alias :exdate :add_exception_time
46
+ alias :add_exception_date :add_exception_time
68
47
 
69
- # Create a schedule from a yaml string created by instance.to_yaml
70
- def self.from_yaml(str, hash_options = {})
71
- from_hash(YAML::load(str), hash_options)
48
+ # Add a recurrence rule to the schedule
49
+ def add_recurrence_rule(rule)
50
+ @all_recurrence_rules << rule unless @all_recurrence_rules.include?(rule)
72
51
  end
52
+ alias :rrule :add_recurrence_rule
73
53
 
74
- TIME_FORMAT = '%B %e, %Y'
75
- SEPARATOR = ' / '
76
- NEWLINE = "\n"
54
+ # Remove a recurrence rule
55
+ def remove_recurrence_rule(rule)
56
+ res = @all_recurrence_rules.delete(rule)
57
+ res.nil? ? [] : [res]
58
+ end
77
59
 
78
- # use with caution
79
- # incomplete and not entirely tested - no time representation in dates
80
- # there's a lot that can happen here
81
- def to_s
82
- representation_pieces = []
83
- inc_dates = (@rdates - @exdates).uniq
84
- representation_pieces.concat inc_dates.sort.map { |d| d.strftime(TIME_FORMAT) } unless inc_dates.empty?
85
- representation_pieces.concat @rrule_occurrence_heads.map{ |r| r.rule.to_s } if @rrule_occurrence_heads
86
- representation_pieces.concat @exrule_occurrence_heads.map { |r| 'not ' << r.rule.to_s } if @exrule_occurrence_heads
87
- representation_pieces.concat @exdates.uniq.sort.map { |d| 'not on ' << d.strftime(TIME_FORMAT) } if @exdates
88
- representation_pieces << "until #{end_time.strftime(TIME_FORMAT)}" if @end_time
89
- representation_pieces.join(SEPARATOR)
60
+ # Add an exception rule to the schedule
61
+ def add_exception_rule(rule)
62
+ @all_exception_rules << rule unless @all_exception_rules.include?(rule)
90
63
  end
64
+ alias :exrule :add_exception_rule
91
65
 
92
- def to_ical(force_utc = false)
93
- representation_pieces = ["DTSTART#{TimeUtil.ical_format(@start_date, force_utc)}"]
94
- representation_pieces << "DURATION:#{TimeUtil.ical_duration(@duration)}" if @duration
95
- inc_dates = (@rdates - @exdates).uniq
96
- representation_pieces.concat inc_dates.sort.map { |d| "RDATE#{TimeUtil.ical_format(d, force_utc)}" } if inc_dates.any?
97
- representation_pieces.concat @exdates.uniq.sort.map { |d| "EXDATE#{TimeUtil.ical_format(d, force_utc)}" } if @exdates
98
- representation_pieces.concat @rrule_occurrence_heads.map { |r| "RRULE:#{r.rule.to_ical}" } if @rrule_occurrence_heads
99
- representation_pieces.concat @exrule_occurrence_heads.map { |r| "EXRULE:#{r.rule.to_ical}" } if @exrule_occurrence_heads
100
- representation_pieces << "DTEND#{TimeUtil.ical_format(@end_time, force_utc)}" if @end_time
101
- representation_pieces.join(NEWLINE)
66
+ # Remove an exception rule
67
+ def remove_exception_rule(rule)
68
+ res = @all_exception_rules.delete(rule)
69
+ res.nil? ? [] : [res]
102
70
  end
103
71
 
104
- def occurring_at?(time)
105
- return false if @exdates.include?(time)
106
- return true if @rdates.include?(time)
107
- return false if any_occurring_at?(@exrule_occurrence_heads, time)
108
- any_occurring_at?(@rrule_occurrence_heads, time)
72
+ # Get the recurrence rules
73
+ def recurrence_rules
74
+ @all_recurrence_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
109
75
  end
76
+ alias :rrules :recurrence_rules
110
77
 
111
- # Determine whether a given time adheres to the ruleset of this schedule.
112
- def occurs_at?(date)
113
- return false if @end_time && date > @end_time
114
- dates = occurrences(date)
115
- dates.last == date
78
+ # Get the exception rules
79
+ def exception_rules
80
+ @all_exception_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
116
81
  end
82
+ alias :exrules :exception_rules
117
83
 
118
- # Determine whether a given date appears in the times returned by the schedule
119
- def occurs_on?(date)
120
- if defined?(ActiveSupport::TimeWithZone) && @start_date.is_a?(ActiveSupport::TimeWithZone)
121
- return active_support_occurs_on?(date)
84
+ # Get the recurrence times that are on the schedule
85
+ def recurrence_times
86
+ @all_recurrence_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
87
+ end
88
+ alias :rdates :recurrence_times
89
+ alias :recurrence_dates :recurrence_times
90
+
91
+ # Remove a recurrence time
92
+ def remove_recurrence_time(time)
93
+ found = false
94
+ @all_recurrence_rules.delete_if do |rule|
95
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
122
96
  end
123
- # fall back to our own way of doing things
124
- time_format = @start_date.utc? ? :utc : :local
125
- self.occurrences_between(Time.send(time_format, date.year, date.month, date.day, 0, 0, 0), Time.send(time_format, date.year, date.month, date.day, 23, 59, 59)).any?
97
+ time if found
98
+ end
99
+ alias :remove_recurrence_date :remove_recurrence_time
100
+ alias :remove_rdate :remove_recurrence_time
101
+
102
+ # Get the exception times that are on the schedule
103
+ def exception_times
104
+ @all_exception_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
126
105
  end
106
+ alias :exdates :exception_times
107
+ alias :exception_dates :exception_times
127
108
 
128
- def occurs_between?(begin_time, end_time)
129
- if defined?(ActiveSupport::TimeWithZone) && @start_date.is_a?(ActiveSupport::TimeWithZone)
130
- return active_support_occurs_between?(begin_time, end_time)
109
+ # Remove an exception time
110
+ def remove_exception_time(time)
111
+ found = false
112
+ @all_exception_rules.delete_if do |rule|
113
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
131
114
  end
132
- # fall back to our own way of doing things
133
- time_format = @start_date.utc? ? :utc : :local
134
- self.occurrences_between(Time.send(time_format, begin_time.year, begin_time.month, begin_time.day, 0, 0, 0), Time.send(time_format, end_time.year, end_time.month, end_time.day, 23, 59, 59)).any?
115
+ time if found
135
116
  end
117
+ alias :remove_exception_date :remove_exception_time
118
+ alias :remove_exdate :remove_exception_time
136
119
 
137
- # Return all possible occurrences
138
- # In order to make this call, all rules in the schedule must have
139
- # either an until date or an occurrence count
140
- def all_occurrences
141
- find_occurrences { |head| head.all_occurrences }
120
+ # Get all of the occurrences from the start_time up until a
121
+ # given Time
122
+ def occurrences(closing_time)
123
+ find_occurrences(start_time, closing_time)
142
124
  end
143
125
 
144
- # Find all occurrences until a certain date
145
- def occurrences(end_date)
146
- end_date = @end_time if @end_time && @end_time < end_date
147
- find_occurrences { |head| head.upto(end_date) }
126
+ # All of the occurrences
127
+ def all_occurrences
128
+ unless end_time || recurrence_rules.all?(&:terminating?)
129
+ raise ArgumentError.new('Rule must specify either an until date or a count to use #all_occurrences')
130
+ end
131
+ find_occurrences(start_time)
148
132
  end
149
133
 
150
- # Find remaining occurrences
151
- def remaining_occurrences(from = Time.now)
152
- raise ArgumentError.new('Schedule must have an end_time to use remaining_occurrences') unless @end_time
153
- occurrences_between(from, @end_time)
134
+ # Iterate forever
135
+ def each_occurrence(&block)
136
+ find_occurrences(start_time, &block)
137
+ self
154
138
  end
155
139
 
156
- # Find next scheduled occurrence
157
- def next_occurrence(from = Time.now)
158
- next_occurrences(1, from).first
140
+ # The next n occurrences after now
141
+ def next_occurrences(num, from = Time.now)
142
+ find_occurrences(from + 1, nil, num)
159
143
  end
160
144
 
161
- def next_occurrences(n, from = Time.now)
162
- nexts = find_occurrences do |head, exclude_dates|
163
- head.next_occurrences(n, from, exclude_dates)
164
- end
165
- #Grabs the first n occurrences after the from date, remembering that there is still a
166
- #possibility that recurrence dates before the from time could be in the array
167
- nexts.select{|occurrence| occurrence > from}.first(n)
145
+ # The next occurrence after now (overridable)
146
+ def next_occurrence(from = Time.now)
147
+ find_occurrences(from + 1, nil, 1).first
168
148
  end
169
149
 
170
- # Retrieve the first (n) occurrences of the schedule. May return less than
171
- # n results, if the rules end before n results are reached.
172
- def first(n = nil)
173
- dates = find_occurrences { |head| head.first(n || 1) }
174
- n.nil? ? dates.first : dates.slice(0, n)
150
+ # The remaining occurrences (same requirements as all_occurrences)
151
+ def remaining_occurrences(from = Time.now)
152
+ find_occurrences(from)
175
153
  end
176
154
 
177
- # Add a rule of any type as an recurrence in this schedule
178
- def add_recurrence_rule(rule)
179
- raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
180
- @rrule_occurrence_heads << RuleOccurrence.new(rule, @start_date, @end_time)
155
+ # Occurrences between two times
156
+ def occurrences_between(begin_time, closing_time)
157
+ find_occurrences(begin_time, closing_time)
181
158
  end
182
159
 
183
- # Remove a recurrence rule, returns the removed rules, or nil
184
- def remove_recurrence_rule(rule)
185
- raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
186
- deletions = []
187
- @rrule_occurrence_heads.delete_if { |h| deletions << h.rule if h.rule == rule }
188
- deletions
160
+ # Return a boolean indicating if an occurrence falls between
161
+ # two times
162
+ def occurs_between?(begin_time, closing_time)
163
+ !find_occurrences(begin_time, closing_time, 1).empty?
189
164
  end
190
165
 
191
- def rrules
192
- @rrule_occurrence_heads.map { |h| h.rule }
166
+ # Return a boolean indicating if an occurrence falls on a certain date
167
+ def occurs_on?(date)
168
+ begin_time = TimeUtil.beginning_of_date(date)
169
+ closing_time = TimeUtil.end_of_date(date)
170
+ occurs_between?(begin_time, closing_time)
193
171
  end
194
172
 
195
- # Add a rule of any type as an exception to this schedule
196
- def add_exception_rule(rule)
197
- raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
198
- @exrule_occurrence_heads << RuleOccurrence.new(rule, @start_date, @end_time)
173
+ # Determine if the schedule is occurring at a given time
174
+ def occurring_at?(time)
175
+ if duration
176
+ return false if exception_time?(time)
177
+ occurs_between?(time - duration + 1, time)
178
+ else
179
+ occurs_at?(time)
180
+ end
199
181
  end
200
182
 
201
- # Remove an exception rule, returns the removed rule, or nil
202
- def remove_exception_rule(rule)
203
- raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
204
- deletions = []
205
- @exrule_occurrence_heads.delete_if { |h| deletions << h.rule if h.rule == rule }
206
- deletions
183
+ # Determine if the schedule occurs at a specific time
184
+ def occurs_at?(time)
185
+ occurs_between?(time, time)
207
186
  end
208
187
 
209
- def exrules
210
- @exrule_occurrence_heads.map { |h| h.rule }
188
+ # Get the first n occurrences, or the first occurrence if n is skipped
189
+ def first(n = nil)
190
+ occurrences = find_occurrences start_time, nil, n || 1
191
+ n.nil? ? occurrences.first : occurrences
211
192
  end
212
193
 
213
- # Add an individual date to this schedule
214
- def add_recurrence_date(date)
215
- @rdates << date unless date.nil?
194
+ # String serialization
195
+ def to_s
196
+ pieces = []
197
+ ed = exdates; rd = rdates - ed
198
+ pieces.concat rd.sort.map { |t| t.strftime(TO_S_TIME_FORMAT) }
199
+ pieces.concat rrules.map { |t| t.to_s }
200
+ pieces.concat exrules.map { |t| "not #{t.to_s}" }
201
+ pieces.concat ed.sort.map { |t| "not on #{t.strftime(TO_S_TIME_FORMAT)}" }
202
+ pieces << "until #{end_time.strftime(TIME_FORMAT)}" if end_time
203
+ pieces.join(' / ')
216
204
  end
217
205
 
218
- # Remove an individual date from this schedule's recurrence dates
219
- # return date that was removed, nil otherwise
220
- def remove_recurrence_date(date)
221
- @rdates.delete(date)
206
+ # Serialize this schedule to_ical
207
+ def to_ical(force_utc = false)
208
+ pieces = []
209
+ pieces << "DTSTART#{IcalBuilder.ical_format(start_time, force_utc)}"
210
+ pieces << "DURATION:#{IcalBuilder.ical_duration(duration)}" if duration
211
+ pieces.concat recurrence_rules.map { |r| "RRULE:#{r.to_ical}" }
212
+ pieces.concat exception_rules.map { |r| "EXRULE:#{r.to_ical}" }
213
+ pieces.concat recurrence_times.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" }
214
+ pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" }
215
+ pieces << "DTEND#{IcalBuilder.ical_format(end_time, force_utc)}" if end_time
216
+ pieces.join("\n")
222
217
  end
223
218
 
224
- # Add an individual date exception to this schedule
225
- def add_exception_date(date)
226
- @exdates << date unless date.nil?
219
+ # Convert the schedule to yaml
220
+ def to_yaml(*args)
221
+ defined?(Psych) ? Psych::dump(to_hash) : YAML::dump(to_hash, *args)
227
222
  end
228
223
 
229
- # Remove an individual date exception from this schedule's exception dates
230
- # return date that was removed, nil otherwise
231
- def remove_exception_date(date)
232
- @exdates.delete(date)
224
+ # Load the schedule from yaml
225
+ def self.from_yaml(yaml, options = {})
226
+ from_hash defined?(Psych) ? Psych::load(yaml) : YAML::load(yaml), options
233
227
  end
234
228
 
235
- def occurrences_between(begin_time, end_time)
236
- # adjust to the propert end date
237
- end_time = @end_time if @end_time && @end_time < end_time
238
- # collect the occurrences
239
- include_dates, exclude_dates = SortedSet.new(@rdates), Set.new(@exdates)
240
- @rrule_occurrence_heads.each do |rrule_occurrence_head|
241
- include_dates.merge(rrule_occurrence_head.between(begin_time, end_time))
229
+ # Convert the schedule to a hash
230
+ def to_hash
231
+ data = {}
232
+ data[:start_date] = TimeUtil.serialize_time(start_time)
233
+ data[:end_time] = end_time if end_time
234
+ data[:duration] = duration if duration
235
+ data[:rrules] = recurrence_rules.map(&:to_hash)
236
+ data[:exrules] = exception_rules.map(&:to_hash)
237
+ data[:rdates] = recurrence_times.map do |rt|
238
+ TimeUtil.serialize_time(rt)
242
239
  end
243
- @exrule_occurrence_heads.each do |exrule_occurrence_head|
244
- exclude_dates.merge(exrule_occurrence_head.between(begin_time, end_time))
240
+ data[:exdates] = exception_times.map do |et|
241
+ TimeUtil.serialize_time(et)
245
242
  end
246
- # reject all of the ones outside of the range
247
- include_dates.reject! { |date| exclude_dates.include?(date) || date < begin_time || date > end_time }
248
- include_dates.to_a
243
+ data
244
+ end
245
+
246
+ # Load the schedule from a hash
247
+ def self.from_hash(data, options = {})
248
+ data[:start_date] = options[:start_date_override] if options[:start_date_override]
249
+ # And then deserialize
250
+ schedule = IceCube::Schedule.new TimeUtil.deserialize_time(data[:start_date])
251
+ schedule.duration = data[:duration] if data[:duration]
252
+ schedule.end_time = TimeUtil.deserialize_time(data[:end_time]) if data[:end_time]
253
+ data[:rrules] && data[:rrules].each { |h| schedule.rrule(IceCube::Rule.from_hash(h)) }
254
+ data[:exrules] && data[:exrules].each { |h| schedule.exrule(IceCube::Rule.from_hash(h)) }
255
+ data[:rdates] && data[:rdates].each do |t|
256
+ schedule.add_recurrence_time TimeUtil.deserialize_time(t)
257
+ end
258
+ data[:exdates] && data[:exdates].each do |t|
259
+ schedule.add_exception_time TimeUtil.deserialize_time(t)
260
+ end
261
+ schedule
249
262
  end
250
263
 
251
- alias :rdate :add_recurrence_date
252
- alias :rrule :add_recurrence_rule
253
- alias :exdate :add_exception_date
254
- alias :exrule :add_exception_rule
255
- alias :recurrence_dates :rdates
256
- alias :exception_dates :exdates
257
- alias :remove_rdate :remove_recurrence_date
258
- alias :remove_exdate :remove_exception_date
259
- alias :remove_rrule :remove_recurrence_rule
260
- alias :remove_exrule :remove_exception_rule
261
-
262
264
  private
263
265
 
264
- # We know that start_date is a time with zone - so check referencing
265
- # The date in that time zone
266
- def active_support_occurs_on?(date)
267
- time = Time.zone.parse(date.to_s) # date.to_time.in_time_zone(@start_date.time_zone)
268
- occurrences_between(time.beginning_of_day, time.end_of_day).any?
266
+ # Reset all rules for another run
267
+ def reset
268
+ @all_recurrence_rules.each(&:reset)
269
+ @all_exception_rules.each(&:reset)
269
270
  end
270
271
 
271
- def active_support_occurs_between?(start_time, end_time)
272
- time_start = Time.zone.parse(start_time.to_s) # date.to_time.in_time_zone(@start_date.time_zone)
273
- time_end = Time.zone.parse(end_time.to_s) # date.to_time.in_time_zone(@end_date.time_zone)
274
-
275
- occurrences_between(time_start.beginning_of_day, time_end.end_of_day).any?
276
- end
277
-
278
- # tell if, from a list of rule_occurrence heads, a certain time is occurring
279
- def any_occurring_at?(what, time)
280
- return false if @start_date && time < @start_date
281
- what.any? do |occurrence_head|
282
- # time is less than now and duration is less than that distance
283
- possibilities = occurrence_head.between(@start_date, time)
284
- possibilities.any? do |possibility|
285
- possibility + (@duration || 0) >= time
272
+ # Find all of the occurrences for the schedule between opening_time
273
+ # and closing_time
274
+ def find_occurrences(opening_time, closing_time = nil, limit = nil, &block)
275
+ reset
276
+ answers = []
277
+ # ensure the bounds are proper
278
+ if end_time
279
+ closing_time = end_time unless closing_time && closing_time < @end_time
280
+ end
281
+ opening_time = start_time if opening_time < start_time
282
+ # And off we go
283
+ time = opening_time
284
+ loop do
285
+ res = next_time(time, closing_time)
286
+ break unless res
287
+ break if closing_time && res > closing_time
288
+ block_given? ? block.call(res) : (answers << res)
289
+ break if limit && answers.length == limit
290
+ time = res + 1
291
+ end
292
+ # and return our answers
293
+ answers
294
+ end
295
+
296
+ # Get the next time after (or including) a specific time
297
+ def next_time(time, closing_time)
298
+ min_time = nil
299
+ loop do
300
+ @all_recurrence_rules.each do |rule|
301
+ begin
302
+ if res = rule.next_time(time, self, closing_time)
303
+ if min_time.nil? || res < min_time
304
+ min_time = res
305
+ end
306
+ end
307
+ # Certain exceptions mean this rule no longer wants to play
308
+ rescue CountExceeded, UntilExceeded
309
+ next
310
+ end
311
+ end
312
+ # If there is no match, return nil
313
+ return nil unless min_time
314
+ # Now make sure that its not an exception_time, and if it is
315
+ # then keep looking
316
+ if exception_time?(min_time)
317
+ time = min_time + 1
318
+ min_time = nil
319
+ next
286
320
  end
321
+ # Break, we're done
322
+ break
287
323
  end
324
+ min_time
288
325
  end
289
326
 
290
- # Find all occurrences (following rules and exceptions) from the schedule's start date to end_date.
291
- # Use custom methods to say when to end
292
- def find_occurrences
293
- exclude_dates, include_dates = Set.new(@exdates), SortedSet.new(@rdates)
294
- # walk through each exrule, adding it to the exclude dates
295
- @exrule_occurrence_heads.each do |exrule_occurrence_head|
296
- exclude_dates.merge(yield(exrule_occurrence_head))
297
- end
298
- # walk through each rule, adding it to dates
299
- @rrule_occurrence_heads.each do |rrule_occurrence_head|
300
- include_dates.merge(yield(rrule_occurrence_head, exclude_dates))
327
+ # Return a boolean indicating whether or not a specific time
328
+ # is excluded from the schedule
329
+ def exception_time?(time)
330
+ @all_exception_rules.any? do |rule|
331
+ rule.on?(time, self)
301
332
  end
302
- #return a unique list of dates
303
- include_dates.reject! { |date| exclude_dates.include?(date) }
304
- include_dates.to_a
305
333
  end
306
334
 
307
335
  end