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
@@ -0,0 +1,51 @@
|
|
1
|
+
module IceCube
|
2
|
+
|
3
|
+
module Validations::DailyInterval
|
4
|
+
|
5
|
+
# Add a new interval validation
|
6
|
+
def interval(interval)
|
7
|
+
validations_for(:interval) << Validation.new(interval)
|
8
|
+
clobber_base_validations(:wday, :day)
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
# A validation for checking to make sure that a time
|
13
|
+
# is inside of a certain DailyInterval
|
14
|
+
class Validation
|
15
|
+
|
16
|
+
attr_reader :interval
|
17
|
+
|
18
|
+
def initialize(interval)
|
19
|
+
@interval = interval
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_s(builder)
|
23
|
+
builder.base = interval == 1 ? 'Daily' : "Every #{interval} days"
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_hash(builder)
|
27
|
+
builder.validations[:interval] = interval
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_ical(builder)
|
31
|
+
builder['FREQ'] << 'DAILY'
|
32
|
+
end
|
33
|
+
|
34
|
+
def type
|
35
|
+
:day
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(time, schedule)
|
39
|
+
time_date = Date.new(time.year, time.month, time.day)
|
40
|
+
start_date = Date.new(schedule.start_time.year, schedule.start_time.month, schedule.start_time.day)
|
41
|
+
days = time_date - start_date
|
42
|
+
unless days % interval === 0
|
43
|
+
interval - (days % interval)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -1,41 +1,55 @@
|
|
1
|
-
|
1
|
+
require 'date'
|
2
2
|
|
3
|
-
|
3
|
+
module IceCube
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(rule)
|
8
|
-
@days = rule.validations[:day]
|
9
|
-
@rule = rule
|
10
|
-
end
|
5
|
+
module Validations::Day
|
11
6
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def closest(date)
|
18
|
-
return nil if !@days || @days.empty?
|
19
|
-
# turn days into distances
|
20
|
-
days = @days.map do |d|
|
21
|
-
d > date.wday ? (d - date.wday) : (7 - date.wday + d)
|
7
|
+
def day(*days)
|
8
|
+
days.each do |day|
|
9
|
+
day = TimeUtil.symbol_to_day(day) if day.is_a?(Symbol)
|
10
|
+
validations_for(:day) << Validation.new(day)
|
22
11
|
end
|
23
|
-
|
24
|
-
|
25
|
-
goal = date + days.min * IceCube::ONE_DAY
|
26
|
-
self.class.adjust(goal, date)
|
27
|
-
end
|
28
|
-
|
29
|
-
def to_s
|
30
|
-
days_dup = (@days - @rule.validations[:day_of_week].keys if @rule.validations[:day_of_week]) || @days # don't list twice
|
31
|
-
'on ' << self.class.sentence(days_dup.map { |d| Date::DAYNAMES[d] + 's' }) unless days_dup.empty?
|
12
|
+
clobber_base_validations(:wday, :day)
|
13
|
+
self
|
32
14
|
end
|
33
15
|
|
34
|
-
|
35
|
-
|
36
|
-
|
16
|
+
class Validation
|
17
|
+
|
18
|
+
include Validations::Lock
|
19
|
+
|
20
|
+
attr_reader :day
|
21
|
+
alias :value :day
|
22
|
+
|
23
|
+
def initialize(day)
|
24
|
+
@day = day
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_s(builder)
|
28
|
+
builder.piece(:day) << "#{Date::DAYNAMES[day]}s"
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_hash(builder)
|
32
|
+
builder.validations_array(:day) << day
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_ical(builder)
|
36
|
+
ical_day = IcalBuilder.fixnum_to_ical_day(day)
|
37
|
+
# Only add if there aren't others from day_of_week that override
|
38
|
+
if builder['BYDAY'].none? { |b| b.end_with?(ical_day) }
|
39
|
+
builder['BYDAY'] << ical_day
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def type
|
44
|
+
:wday
|
45
|
+
end
|
46
|
+
|
47
|
+
StringBuilder.register_formatter(:day) do |segments|
|
48
|
+
"on #{StringBuilder.sentence(segments)}"
|
49
|
+
end
|
50
|
+
|
37
51
|
end
|
38
|
-
|
52
|
+
|
39
53
|
end
|
40
54
|
|
41
55
|
end
|
@@ -1,52 +1,52 @@
|
|
1
1
|
module IceCube
|
2
2
|
|
3
|
-
|
3
|
+
module Validations::DayOfMonth
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def validate(date)
|
12
|
-
return true if !@days_of_month || @days_of_month.empty?
|
13
|
-
@days_of_month.include?(date.mday) || @days_of_month.include?(date.mday - TimeUtil.days_in_month(date) - 1)
|
14
|
-
end
|
15
|
-
|
16
|
-
def closest(date)
|
17
|
-
return nil if !@days_of_month || @days_of_month.empty?
|
18
|
-
#get some variables we need
|
19
|
-
days_in_month = TimeUtil.days_in_month(date)
|
20
|
-
days_left_in_this_month = days_in_month - date.mday
|
21
|
-
next_month, next_year = date.month == 12 ? [1, date.year + 1] : [date.month + 1, date.year] #clean way to wrap over years
|
22
|
-
days_in_next_month = TimeUtil.days_in_month(Time.utc(next_year, next_month, 1))
|
23
|
-
# create a list of distances
|
24
|
-
distances = []
|
25
|
-
@days_of_month.each do |d|
|
26
|
-
if d > 0
|
27
|
-
distances << d - date.mday #today is 1, we want 20 (19)
|
28
|
-
distances << days_left_in_this_month + d #(364 + 20)
|
29
|
-
elsif d < 0
|
30
|
-
distances << (days_in_month + d + 1) - date.mday #today is 30, we want -1
|
31
|
-
distances << (days_in_next_month + d + 1) + days_left_in_this_month #today is 300, we want -70
|
32
|
-
end
|
5
|
+
include Validations::Lock
|
6
|
+
|
7
|
+
def day_of_month(*days)
|
8
|
+
days.each do |day|
|
9
|
+
validations_for(:day_of_month) << Validation.new(day)
|
33
10
|
end
|
34
|
-
|
35
|
-
|
36
|
-
return nil if distances.empty?
|
37
|
-
# return the start of the proper day
|
38
|
-
goal = date + distances.min * IceCube::ONE_DAY
|
39
|
-
self.class.adjust(goal, date)
|
11
|
+
clobber_base_validations(:day, :wday)
|
12
|
+
self
|
40
13
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
14
|
+
|
15
|
+
class Validation
|
16
|
+
|
17
|
+
include Validations::Lock
|
18
|
+
|
19
|
+
StringBuilder.register_formatter(:day_of_month) do |entries|
|
20
|
+
str = "on the #{StringBuilder.sentence(entries)} "
|
21
|
+
str << (entries.size == 1 ? 'day of the month' : 'days of the month')
|
22
|
+
str
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :day
|
26
|
+
alias :value :day
|
27
|
+
|
28
|
+
def initialize(day)
|
29
|
+
@day = day
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_s(builder)
|
33
|
+
builder.piece(:day_of_month) << StringBuilder.nice_number(day)
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_hash(builder)
|
37
|
+
builder.validations_array(:day_of_month) << day
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_ical(builder)
|
41
|
+
builder['BYMONTHDAY'] << day
|
42
|
+
end
|
43
|
+
|
44
|
+
def type
|
45
|
+
:day
|
46
|
+
end
|
47
|
+
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
end
|
@@ -1,57 +1,70 @@
|
|
1
1
|
module IceCube
|
2
2
|
|
3
|
-
|
3
|
+
module Validations::DayOfWeek
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def validate(date)
|
13
|
-
# is it even one of the valid days?
|
14
|
-
return true if !@days_of_week || @days_of_week.empty?
|
15
|
-
return false unless @days_of_week.has_key?(date.wday) #shortcut
|
16
|
-
# does this fall on one of the occurrences?
|
17
|
-
first_occurrence = ((7 - Time.utc(date.year, date.month, 1).wday) + date.wday) % 7 + 1 #day of first occurrence of a wday in a month
|
18
|
-
this_weekday_in_month_count = ((TimeUtil.days_in_month(date) - first_occurrence + 1) / 7.0).ceil #how many of these in the month
|
19
|
-
nth_occurrence_of_weekday = (date.mday - first_occurrence) / 7 + 1 #what occurrence of the weekday is +date+
|
20
|
-
@days_of_week[date.wday].include?(nth_occurrence_of_weekday) || @days_of_week[date.wday].include?(nth_occurrence_of_weekday - this_weekday_in_month_count - 1)
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
# note - temporary implementation
|
25
|
-
# instead - once we know what weekday starts the month, we should be able to figure out
|
26
|
-
# the rest with basic math
|
27
|
-
def closest(date)
|
28
|
-
return nil if !@days_of_week || @days_of_week.empty?
|
29
|
-
goal = date
|
30
|
-
while (next_date = goal + IceCube::ONE_DAY)
|
31
|
-
# DST hack. If our day starts at midnight, when DST ends it will be pushed to 11 PM on the previous day.
|
32
|
-
check_date = next_date.day == goal.day ? next_date + IceCube::ONE_HOUR : next_date
|
33
|
-
return self.class.adjust(next_date, date) if validate(check_date)
|
34
|
-
goal = next_date
|
5
|
+
def day_of_week(dows)
|
6
|
+
dows.each do |day, occs|
|
7
|
+
occs.each do |occ|
|
8
|
+
day = TimeUtil.symbol_to_day(day) if day.is_a?(Symbol)
|
9
|
+
validations_for(:day_of_week) << Validation.new(day, occ)
|
10
|
+
end
|
35
11
|
end
|
12
|
+
clobber_base_validations :day, :wday
|
13
|
+
self
|
36
14
|
end
|
37
15
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
16
|
+
class Validation
|
17
|
+
|
18
|
+
attr_reader :day, :occ
|
19
|
+
|
20
|
+
StringBuilder.register_formatter(:day_of_week) do |segments|
|
21
|
+
'on the ' + segments.join(' when it is the ')
|
22
|
+
end
|
23
|
+
|
24
|
+
def type
|
25
|
+
:day
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_s(builder)
|
29
|
+
builder.piece(:day_of_week) << "#{StringBuilder.nice_number(occ)} #{Date::DAYNAMES[day]}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_ical(builder)
|
33
|
+
ical_day = IcalBuilder.fixnum_to_ical_day(day)
|
34
|
+
# Delete any with this day and no occ first
|
35
|
+
builder['BYDAY'].delete_if { |d| d == ical_day }
|
36
|
+
builder['BYDAY'] << "#{occ}#{ical_day}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_hash(builder)
|
40
|
+
builder.validations[:day_of_week] ||= {}
|
41
|
+
arr = (builder.validations[:day_of_week][day] ||= [])
|
42
|
+
arr << occ
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(day, occ)
|
46
|
+
@day = day
|
47
|
+
@occ = occ
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate(time, schedule)
|
51
|
+
# count the days to the weekday
|
52
|
+
sum = day >= time.wday ? day - time.wday : 7 - time.wday + day
|
53
|
+
wrapper = TimeUtil::TimeWrapper.new(time)
|
54
|
+
wrapper.add :day, sum
|
55
|
+
# and then count the week until a viable occ
|
56
|
+
loop do
|
57
|
+
which_occ, num_occ = TimeUtil.which_occurrence_in_month(wrapper.to_time, day)
|
58
|
+
this_occ = occ < 0 ? num_occ + occ + 1 : occ
|
59
|
+
break if which_occ == this_occ
|
60
|
+
sum += 7
|
61
|
+
wrapper.add :day, 7 # one week
|
62
|
+
end
|
63
|
+
sum
|
64
|
+
end
|
65
|
+
|
53
66
|
end
|
54
67
|
|
55
68
|
end
|
56
|
-
|
69
|
+
|
57
70
|
end
|
@@ -1,51 +1,55 @@
|
|
1
1
|
module IceCube
|
2
|
-
|
3
|
-
class DayOfYearValidation < Validation
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@days_of_year.include?(date.yday) || @days_of_year.include?(date.yday - TimeUtil.days_in_year(date) - 1)
|
14
|
-
end
|
15
|
-
|
16
|
-
def closest(date)
|
17
|
-
return nil if !@days_of_year || @days_of_year.empty?
|
18
|
-
#get some variables we need
|
19
|
-
days_in_year = TimeUtil.days_in_year(date)
|
20
|
-
days_left_in_this_year = days_in_year - date.yday
|
21
|
-
days_in_next_year = TimeUtil.days_in_year(Time.utc(date.year + 1, 1, 1))
|
22
|
-
# create a list of distances
|
23
|
-
distances = []
|
24
|
-
@days_of_year.each do |d|
|
25
|
-
if d > 0
|
26
|
-
distances << d - date.yday #today is 1, we want 20 (19)
|
27
|
-
distances << days_left_in_this_year + d #(364 + 20)
|
28
|
-
elsif d < 0
|
29
|
-
distances << (days_in_year + d + 1) - date.yday #today is 300, we want -1
|
30
|
-
distances << (days_in_next_year + d + 1) + days_left_in_this_year #today is 300, we want -70
|
31
|
-
end
|
32
|
-
end
|
33
|
-
#return the lowest distance
|
34
|
-
distances = distances.select { |d| d > 0 }
|
35
|
-
return nil if distances.empty?
|
36
|
-
# return the start of the proper day
|
37
|
-
goal = date + distances.min * IceCube::ONE_DAY
|
38
|
-
self.class.adjust(goal, date)
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_s
|
42
|
-
'on the ' << self.class.nice_numbers(@days_of_year) << (@days_of_year.size == 1 ? ' day' : ' days') << ' of the year' unless @days_of_year.empty?
|
3
|
+
module Validations::DayOfYear
|
4
|
+
|
5
|
+
def day_of_year(*days)
|
6
|
+
days.each do |day|
|
7
|
+
validations_for(:day_of_year) << Validation.new(day)
|
8
|
+
end
|
9
|
+
clobber_base_validations(:month, :day, :wday)
|
10
|
+
self
|
43
11
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
12
|
+
|
13
|
+
class Validation
|
14
|
+
|
15
|
+
attr_reader :day
|
16
|
+
|
17
|
+
StringBuilder.register_formatter(:day_of_year) do |entries|
|
18
|
+
str = "on the #{StringBuilder.sentence(entries)} "
|
19
|
+
str << (entries.size == 1 ? 'day of the year' : 'days of the year')
|
20
|
+
str
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(day)
|
24
|
+
@day = day
|
25
|
+
end
|
26
|
+
|
27
|
+
def type
|
28
|
+
:day
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_s(builder)
|
32
|
+
builder.piece(:day_of_year) << StringBuilder.nice_number(day)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_hash(builder)
|
36
|
+
builder.validations_array(:day_of_year) << day
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_ical(builder)
|
40
|
+
builder['BYYEARDAY'] << day
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate(time, schedule)
|
44
|
+
days_in_year = TimeUtil.days_in_year(time)
|
45
|
+
the_day = day < 0 ? day + days_in_year : day
|
46
|
+
# compute the diff
|
47
|
+
diff = the_day - time.yday
|
48
|
+
diff >= 0 ? diff : diff + days_in_year
|
49
|
+
end
|
50
|
+
|
47
51
|
end
|
48
|
-
|
52
|
+
|
49
53
|
end
|
50
54
|
|
51
55
|
end
|