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