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
@@ -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