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.
- data/lib/ice_cube.rb +63 -37
- data/lib/ice_cube/builders/hash_builder.rb +27 -0
- data/lib/ice_cube/builders/ical_builder.rb +59 -0
- data/lib/ice_cube/builders/string_builder.rb +74 -0
- data/lib/ice_cube/errors/count_exceeded.rb +7 -0
- data/lib/ice_cube/errors/until_exceeded.rb +7 -0
- data/lib/ice_cube/rule.rb +85 -147
- data/lib/ice_cube/rules/daily_rule.rb +5 -27
- data/lib/ice_cube/rules/hourly_rule.rb +6 -26
- data/lib/ice_cube/rules/minutely_rule.rb +5 -25
- data/lib/ice_cube/rules/monthly_rule.rb +6 -30
- data/lib/ice_cube/rules/secondly_rule.rb +5 -26
- data/lib/ice_cube/rules/weekly_rule.rb +5 -36
- data/lib/ice_cube/rules/yearly_rule.rb +8 -34
- data/lib/ice_cube/schedule.rb +257 -229
- data/lib/ice_cube/single_occurrence_rule.rb +28 -0
- data/lib/ice_cube/time_util.rb +202 -76
- data/lib/ice_cube/validated_rule.rb +107 -0
- data/lib/ice_cube/validations/count.rb +56 -0
- data/lib/ice_cube/validations/daily_interval.rb +51 -0
- data/lib/ice_cube/validations/day.rb +45 -31
- data/lib/ice_cube/validations/day_of_month.rb +44 -44
- data/lib/ice_cube/validations/day_of_week.rb +60 -47
- data/lib/ice_cube/validations/day_of_year.rb +48 -44
- data/lib/ice_cube/validations/hour_of_day.rb +42 -30
- data/lib/ice_cube/validations/hourly_interval.rb +50 -0
- data/lib/ice_cube/validations/lock.rb +47 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +42 -31
- data/lib/ice_cube/validations/minutely_interval.rb +50 -0
- data/lib/ice_cube/validations/month_of_year.rb +39 -30
- data/lib/ice_cube/validations/monthly_interval.rb +47 -0
- data/lib/ice_cube/validations/schedule_lock.rb +41 -0
- data/lib/ice_cube/validations/second_of_minute.rb +39 -30
- data/lib/ice_cube/validations/secondly_interval.rb +50 -0
- data/lib/ice_cube/validations/until.rb +49 -0
- data/lib/ice_cube/validations/weekly_interval.rb +50 -0
- data/lib/ice_cube/validations/yearly_interval.rb +45 -0
- data/lib/ice_cube/version.rb +2 -2
- data/spec/spec_helper.rb +13 -0
- metadata +50 -9
- data/lib/ice_cube/rule_occurrence.rb +0 -94
- data/lib/ice_cube/validation.rb +0 -44
- data/lib/ice_cube/validation_types.rb +0 -137
@@ -1,34 +1,12 @@
|
|
1
1
|
module IceCube
|
2
2
|
|
3
|
-
class DailyRule <
|
3
|
+
class DailyRule < ValidatedRule
|
4
4
|
|
5
|
-
|
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
|
-
|
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 <
|
3
|
+
class HourlyRule < ValidatedRule
|
4
4
|
|
5
|
-
|
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
|
13
|
-
|
14
|
-
|
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 <
|
3
|
+
class MinutelyRule < ValidatedRule
|
4
4
|
|
5
|
-
|
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
|
13
|
-
|
14
|
-
|
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 <
|
3
|
+
class MonthlyRule < ValidatedRule
|
4
4
|
|
5
|
-
|
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
|
-
|
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 <
|
3
|
+
class SecondlyRule < ValidatedRule
|
4
|
+
|
5
|
+
include Validations::SecondlyInterval
|
4
6
|
|
5
|
-
|
6
|
-
|
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 <
|
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
|
-
|
13
|
-
start_date = Date.civil(start_date.year, start_date.month, start_date.day)
|
5
|
+
include Validations::WeeklyInterval
|
14
6
|
|
15
|
-
|
16
|
-
|
17
|
-
|
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 <
|
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
|
-
|
35
|
-
|
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
|
data/lib/ice_cube/schedule.rb
CHANGED
@@ -1,307 +1,335 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
1
3
|
module IceCube
|
2
4
|
|
3
5
|
class Schedule
|
4
6
|
|
5
|
-
|
6
|
-
attr_accessor :
|
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
|
-
|
10
|
-
|
11
|
-
def initialize(
|
12
|
-
@
|
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
|
-
#
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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 :
|
35
|
+
alias :rdate :add_recurrence_time
|
36
|
+
alias :add_recurrence_date :add_recurrence_time
|
31
37
|
|
32
|
-
#
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
#
|
70
|
-
def
|
71
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
-
#
|
79
|
-
|
80
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
#
|
112
|
-
def
|
113
|
-
|
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
|
-
#
|
119
|
-
def
|
120
|
-
|
121
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
-
#
|
138
|
-
#
|
139
|
-
|
140
|
-
|
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
|
-
#
|
145
|
-
def
|
146
|
-
|
147
|
-
|
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
|
-
#
|
151
|
-
def
|
152
|
-
|
153
|
-
|
134
|
+
# Iterate forever
|
135
|
+
def each_occurrence(&block)
|
136
|
+
find_occurrences(start_time, &block)
|
137
|
+
self
|
154
138
|
end
|
155
139
|
|
156
|
-
#
|
157
|
-
def
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
#
|
171
|
-
|
172
|
-
|
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
|
-
#
|
178
|
-
def
|
179
|
-
|
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
|
-
#
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
192
|
-
|
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
|
-
#
|
196
|
-
def
|
197
|
-
|
198
|
-
|
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
|
-
#
|
202
|
-
def
|
203
|
-
|
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
|
-
|
210
|
-
|
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
|
-
#
|
214
|
-
def
|
215
|
-
|
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
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
#
|
225
|
-
def
|
226
|
-
|
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
|
-
#
|
230
|
-
|
231
|
-
|
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
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
244
|
-
|
240
|
+
data[:exdates] = exception_times.map do |et|
|
241
|
+
TimeUtil.serialize_time(et)
|
245
242
|
end
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
#
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
#
|
291
|
-
#
|
292
|
-
def
|
293
|
-
|
294
|
-
|
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
|