hiccup 0.3.0 → 0.4.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/README.mdown +2 -2
- data/lib/hiccup/core_ext/date.rb +0 -18
- data/lib/hiccup/enumerable/annually_enumerator.rb +56 -0
- data/lib/hiccup/enumerable/monthly_enumerator.rb +84 -0
- data/lib/hiccup/enumerable/never_enumerator.rb +19 -0
- data/lib/hiccup/enumerable/schedule_enumerator.rb +67 -0
- data/lib/hiccup/enumerable/weekly_enumerator.rb +63 -0
- data/lib/hiccup/enumerable.rb +34 -189
- data/lib/hiccup/{inferrable.rb → inferable.rb} +153 -30
- data/lib/hiccup/schedule.rb +1 -1
- data/lib/hiccup/version.rb +1 -1
- data/test/duration_ext_test.rb +1 -0
- data/test/enumerable_test.rb +78 -33
- data/test/inferrable_test.rb +57 -49
- metadata +11 -6
data/README.mdown
CHANGED
@@ -13,7 +13,7 @@ class Schedule
|
|
13
13
|
hiccup :enumerable,
|
14
14
|
:validatable,
|
15
15
|
:humanizable,
|
16
|
-
:
|
16
|
+
:inferable,
|
17
17
|
:serializable => [:ical]
|
18
18
|
|
19
19
|
end
|
@@ -60,7 +60,7 @@ Mixes in ActiveModel validations for recurrence models
|
|
60
60
|
|
61
61
|
Represents a recurring pattern in a human-readable string
|
62
62
|
|
63
|
-
###
|
63
|
+
### Inferable
|
64
64
|
|
65
65
|
Infers a schedule from an array of dates
|
66
66
|
|
data/lib/hiccup/core_ext/date.rb
CHANGED
@@ -3,24 +3,6 @@ module Hiccup
|
|
3
3
|
module Date
|
4
4
|
|
5
5
|
|
6
|
-
def get_months_since(earlier_date)
|
7
|
-
((self.year - earlier_date.year) * 12) + (self.month - earlier_date.month).to_int
|
8
|
-
end
|
9
|
-
|
10
|
-
def get_years_since(earlier_date)
|
11
|
-
(self.year - earlier_date.year)
|
12
|
-
end
|
13
|
-
|
14
|
-
|
15
|
-
def get_months_until(later_date)
|
16
|
-
later_date.months_since(self)
|
17
|
-
end
|
18
|
-
|
19
|
-
def get_years_until(later_date)
|
20
|
-
later_date.years_since(self)
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
6
|
|
25
7
|
def get_nth_wday_of_month
|
26
8
|
(day - 1) / 7 + 1
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'hiccup/enumerable/schedule_enumerator'
|
2
|
+
|
3
|
+
module Hiccup
|
4
|
+
module Enumerable
|
5
|
+
class AnnuallyEnumerator < ScheduleEnumerator
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
|
11
|
+
# Use more efficient iterator methods unless
|
12
|
+
# we have to care about leap years
|
13
|
+
|
14
|
+
unless start_date.month == 2 && start_date.day == 29
|
15
|
+
def self.next_occurrence_after(date)
|
16
|
+
date.next_year(skip)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.next_occurrence_before(date)
|
20
|
+
date.prev_year(skip)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def first_occurrence_on_or_after(date)
|
27
|
+
year, month, day = date.year, start_date.month, start_date.day
|
28
|
+
day = -1 if month == 2 && day == 29
|
29
|
+
|
30
|
+
result = Date.new(year, month, day)
|
31
|
+
year += 1 if result < date
|
32
|
+
|
33
|
+
remainder = (year - start_date.year) % skip
|
34
|
+
year += (skip - remainder) if remainder > 0
|
35
|
+
|
36
|
+
Date.new(year, month, day)
|
37
|
+
end
|
38
|
+
|
39
|
+
def first_occurrence_on_or_before(date)
|
40
|
+
year, month, day = date.year, start_date.month, start_date.day
|
41
|
+
day = -1 if month == 2 && day == 29
|
42
|
+
|
43
|
+
result = Date.new(year, month, day)
|
44
|
+
year -= 1 if result > date
|
45
|
+
|
46
|
+
# what if year is before start_date.year?
|
47
|
+
remainder = (year - start_date.year) % skip
|
48
|
+
year -= remainder if remainder > 0
|
49
|
+
|
50
|
+
Date.new(year, month, day)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'hiccup/enumerable/schedule_enumerator'
|
2
|
+
|
3
|
+
module Hiccup
|
4
|
+
module Enumerable
|
5
|
+
class MonthlyEnumerator < ScheduleEnumerator
|
6
|
+
|
7
|
+
|
8
|
+
def first_occurrence_on_or_after(date)
|
9
|
+
result = nil
|
10
|
+
monthly_pattern.each do |occurrence|
|
11
|
+
temp = nil
|
12
|
+
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the future
|
13
|
+
temp = monthly_occurrence_to_date(occurrence, shift_date_by_months(date, i))
|
14
|
+
break if temp && (temp >= date)
|
15
|
+
end
|
16
|
+
next unless temp
|
17
|
+
|
18
|
+
remainder = months_between(temp, start_date) % skip
|
19
|
+
temp = monthly_occurrence_to_date(occurrence, shift_date_by_months(temp, skip - remainder)) if remainder > 0
|
20
|
+
next unless temp
|
21
|
+
|
22
|
+
result = temp if !result || (temp < result)
|
23
|
+
end
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def first_occurrence_on_or_before(date)
|
28
|
+
result = nil
|
29
|
+
monthly_pattern.each do |occurrence|
|
30
|
+
temp = nil
|
31
|
+
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the past
|
32
|
+
temp = monthly_occurrence_to_date(occurrence, shift_date_by_months(date, -i))
|
33
|
+
break if temp && (temp <= date)
|
34
|
+
end
|
35
|
+
next unless temp
|
36
|
+
|
37
|
+
remainder = months_between(temp, start_date) % skip
|
38
|
+
temp = monthly_occurrence_to_date(occurrence, shift_date_by_months(temp, -remainder)) if remainder > 0
|
39
|
+
next unless temp
|
40
|
+
|
41
|
+
result = temp if !result || (temp > result)
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
|
50
|
+
def shift_date_by_months(date, months)
|
51
|
+
date.next_month(months)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def monthly_occurrence_to_date(occurrence, date)
|
56
|
+
year, month = date.year, date.month
|
57
|
+
|
58
|
+
day = begin
|
59
|
+
if occurrence.is_a?(Array)
|
60
|
+
ordinal, weekday = occurrence
|
61
|
+
wday_of_first_of_month = Date.new(year, month, 1).wday
|
62
|
+
wday = Date::DAYNAMES.index(weekday)
|
63
|
+
day = wday
|
64
|
+
day = day + 7 if (wday < wday_of_first_of_month)
|
65
|
+
day = day - wday_of_first_of_month
|
66
|
+
day = day + (ordinal * 7) - 6
|
67
|
+
else
|
68
|
+
occurrence
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
last_day_of_month = Date.new(year, month, -1).day
|
73
|
+
(day > last_day_of_month) ? nil : Date.new(year, month, day)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def months_between(later_date, earlier_date)
|
78
|
+
((later_date.year - earlier_date.year) * 12) + (later_date.month - earlier_date.month).to_int
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'hiccup/enumerable/schedule_enumerator'
|
2
|
+
|
3
|
+
module Hiccup
|
4
|
+
module Enumerable
|
5
|
+
class NeverEnumerator < ScheduleEnumerator
|
6
|
+
|
7
|
+
|
8
|
+
def first_occurrence_on_or_after(date)
|
9
|
+
date if date == start_date
|
10
|
+
end
|
11
|
+
|
12
|
+
def first_occurrence_on_or_before(date)
|
13
|
+
date
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
module Hiccup
|
3
|
+
module Enumerable
|
4
|
+
class ScheduleEnumerator
|
5
|
+
|
6
|
+
def initialize(schedule, date)
|
7
|
+
@schedule = schedule
|
8
|
+
@date = date
|
9
|
+
@date = @date.to_date if @date.respond_to?(:to_date)
|
10
|
+
@current_date = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :schedule
|
14
|
+
delegate :start_date, :weekly_pattern, :monthly_pattern, :ends?, :end_date, :skip, :to => :schedule
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
def next
|
19
|
+
@current_date = if @current_date
|
20
|
+
next_occurrence_after(@current_date)
|
21
|
+
else
|
22
|
+
first_occurrence_on_or_after(@date)
|
23
|
+
end
|
24
|
+
@current_date = nil if (ends? && @current_date && @current_date > end_date)
|
25
|
+
@current_date
|
26
|
+
end
|
27
|
+
|
28
|
+
def prev
|
29
|
+
@current_date = if @current_date
|
30
|
+
next_occurrence_before(@current_date)
|
31
|
+
else
|
32
|
+
first_occurrence_on_or_before(@date)
|
33
|
+
end
|
34
|
+
@current_date = nil if (@current_date && @current_date < start_date)
|
35
|
+
@current_date
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
# These two methods DO NOT assume that
|
41
|
+
# date is predicted by the given schedule
|
42
|
+
|
43
|
+
def first_occurrence_on_or_after(date)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def first_occurrence_on_or_before(date)
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# These two methods DO assume that
|
53
|
+
# date is predicted by the given schedule
|
54
|
+
|
55
|
+
def next_occurrence_after(date)
|
56
|
+
first_occurrence_on_or_after(date + 1)
|
57
|
+
end
|
58
|
+
|
59
|
+
def next_occurrence_before(date)
|
60
|
+
first_occurrence_on_or_before(date - 1)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'hiccup/enumerable/schedule_enumerator'
|
2
|
+
|
3
|
+
module Hiccup
|
4
|
+
module Enumerable
|
5
|
+
class WeeklyEnumerator < ScheduleEnumerator
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
|
11
|
+
# Use more efficient iterator methods if
|
12
|
+
# weekly_pattern is simple enough
|
13
|
+
|
14
|
+
if weekly_pattern.length == 1
|
15
|
+
def self.next_occurrence_after(date)
|
16
|
+
date + skip * 7
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.next_occurrence_before(date)
|
20
|
+
date - skip * 7
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def first_occurrence_on_or_after(date)
|
27
|
+
result = nil
|
28
|
+
wday = date.wday
|
29
|
+
weekly_pattern.each do |weekday|
|
30
|
+
wd = Date::DAYNAMES.index(weekday)
|
31
|
+
wd = wd + 7 if wd < wday
|
32
|
+
days_in_the_future = wd - wday
|
33
|
+
temp = date + days_in_the_future
|
34
|
+
|
35
|
+
remainder = ((temp - start_date) / 7).to_i % skip
|
36
|
+
temp += (skip - remainder) * 7 if remainder > 0
|
37
|
+
|
38
|
+
result = temp if !result || (temp < result)
|
39
|
+
end
|
40
|
+
result
|
41
|
+
end
|
42
|
+
|
43
|
+
def first_occurrence_on_or_before(date)
|
44
|
+
result = nil
|
45
|
+
wday = date.wday
|
46
|
+
weekly_pattern.each do |weekday|
|
47
|
+
wd = Date::DAYNAMES.index(weekday)
|
48
|
+
wd = wd - 7 if wd > wday
|
49
|
+
days_in_the_past = wday - wd
|
50
|
+
temp = date - days_in_the_past
|
51
|
+
|
52
|
+
remainder = ((temp - start_date) / 7).to_i % skip
|
53
|
+
temp -= remainder * 7 if remainder > 0
|
54
|
+
|
55
|
+
result = temp if !result || (temp > result)
|
56
|
+
end
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/hiccup/enumerable.rb
CHANGED
@@ -1,173 +1,65 @@
|
|
1
|
-
require "hiccup/convenience"
|
2
1
|
require "hiccup/core_ext/date"
|
3
|
-
require "hiccup/
|
2
|
+
require "hiccup/enumerable/annually_enumerator"
|
3
|
+
require "hiccup/enumerable/monthly_enumerator"
|
4
|
+
require "hiccup/enumerable/never_enumerator"
|
5
|
+
require "hiccup/enumerable/weekly_enumerator"
|
4
6
|
|
5
7
|
|
6
8
|
module Hiccup
|
7
9
|
module Enumerable
|
8
|
-
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def enumerator
|
14
|
+
@enumerator ||= "Hiccup::Enumerable::#{kind.to_s.classify}Enumerator".constantize
|
15
|
+
end
|
16
|
+
|
17
|
+
def kind=(value)
|
18
|
+
super
|
19
|
+
@enumerator = nil
|
20
|
+
end
|
9
21
|
|
10
22
|
|
11
23
|
|
12
24
|
def occurrences_during_month(year, month)
|
13
25
|
date1 = Date.new(year, month, 1)
|
14
|
-
date2 =
|
26
|
+
date2 = Date.new(year, month, -1)
|
15
27
|
occurrences_between(date1, date2)
|
16
28
|
end
|
17
29
|
|
18
30
|
|
19
31
|
|
20
32
|
def occurrences_between(earlier_date, later_date)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
occurrences << occurrence
|
28
|
-
occurrence = next_occurrence_after(occurrence)
|
29
|
-
end
|
30
|
-
end
|
33
|
+
earlier_date = start_date if earlier_date < start_date
|
34
|
+
|
35
|
+
occurrences = []
|
36
|
+
enum = enumerator.new(self, earlier_date)
|
37
|
+
while (occurrence = enum.next) && (occurrence <= later_date)
|
38
|
+
occurrences << occurrence
|
31
39
|
end
|
40
|
+
occurrences
|
32
41
|
end
|
33
42
|
|
34
43
|
|
35
44
|
|
36
45
|
def first_occurrence_on_or_after(date)
|
37
|
-
date =
|
38
|
-
|
39
|
-
|
40
|
-
result = nil
|
41
|
-
case kind
|
42
|
-
when :never
|
43
|
-
result = date if date == self.start_date # (date > self.start_date) ? nil : self.start_date
|
44
|
-
|
45
|
-
when :weekly
|
46
|
-
wday = date.wday
|
47
|
-
weekly_pattern.each do |weekday|
|
48
|
-
wd = Date::DAYNAMES.index(weekday)
|
49
|
-
wd = wd + 7 if wd < wday
|
50
|
-
days_in_the_future = wd - wday
|
51
|
-
temp = days_in_the_future.days.after(date)
|
52
|
-
|
53
|
-
remainder = ((temp - start_date) / 7).to_i % skip
|
54
|
-
temp = (skip - remainder).weeks.after(temp) if (remainder > 0)
|
55
|
-
|
56
|
-
result = temp if !result || (temp < result)
|
57
|
-
end
|
58
|
-
|
59
|
-
when :monthly
|
60
|
-
monthly_pattern.each do |occurrence|
|
61
|
-
temp = nil
|
62
|
-
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the future
|
63
|
-
temp = monthly_occurrence_to_date(occurrence, i.months.after(date))
|
64
|
-
break if temp && (temp >= date)
|
65
|
-
end
|
66
|
-
next unless temp
|
67
|
-
|
68
|
-
remainder = months_between(temp, start_date) % skip
|
69
|
-
temp = monthly_occurrence_to_date(occurrence, (skip - remainder).months.after(temp)) if (remainder > 0)
|
70
|
-
next unless temp
|
71
|
-
|
72
|
-
result = temp if !result || (temp < result)
|
73
|
-
end
|
74
|
-
|
75
|
-
when :annually
|
76
|
-
result, try_to_use_2_29 = if((start_date.month == 2) && (start_date.day == 29))
|
77
|
-
[Date.new(date.year, 2, 28), true]
|
78
|
-
else
|
79
|
-
[Date.new(date.year, start_date.month, start_date.day), false]
|
80
|
-
end
|
81
|
-
result = 1.year.after(result) if (result < date)
|
82
|
-
|
83
|
-
remainder = years_between(result, start_date) % skip
|
84
|
-
result = (skip - remainder).years.after(result) if (remainder > 0)
|
85
|
-
|
86
|
-
if try_to_use_2_29
|
87
|
-
begin
|
88
|
-
date = Date.new(result.year, 2, 29)
|
89
|
-
rescue
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
result = nil if (self.ends? && result && result > self.end_date)
|
95
|
-
result
|
46
|
+
date = start_date if (date < start_date)
|
47
|
+
enumerator.new(self, date).next
|
96
48
|
end
|
97
49
|
|
98
|
-
|
99
|
-
|
100
50
|
def next_occurrence_after(date)
|
101
|
-
first_occurrence_on_or_after(1
|
51
|
+
first_occurrence_on_or_after(date + 1)
|
102
52
|
end
|
103
53
|
|
104
54
|
|
105
55
|
|
106
56
|
def first_occurrence_on_or_before(date)
|
107
|
-
date =
|
108
|
-
|
109
|
-
|
110
|
-
result = nil
|
111
|
-
case kind
|
112
|
-
when :never
|
113
|
-
result = date # (date > self.start_date) ? nil : self.start_date
|
114
|
-
|
115
|
-
when :weekly
|
116
|
-
wday = date.wday
|
117
|
-
weekly_pattern.each do |weekday|
|
118
|
-
wd = Date::DAYNAMES.index(weekday)
|
119
|
-
wd = wd - 7 if wd > wday
|
120
|
-
days_in_the_past = wday - wd
|
121
|
-
temp = days_in_the_past.days.before(date)
|
122
|
-
|
123
|
-
remainder = ((temp - start_date) / 7).to_i % skip
|
124
|
-
temp = remainder.weeks.before(temp) if (remainder > 0)
|
125
|
-
|
126
|
-
result = temp if !result || (temp > result)
|
127
|
-
end
|
128
|
-
|
129
|
-
when :monthly
|
130
|
-
monthly_pattern.each do |occurrence|
|
131
|
-
temp = nil
|
132
|
-
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the past
|
133
|
-
temp = monthly_occurrence_to_date(occurrence, i.months.before(date))
|
134
|
-
break if temp && (temp <= date)
|
135
|
-
end
|
136
|
-
next unless temp
|
137
|
-
|
138
|
-
remainder = months_between(temp, start_date) % skip
|
139
|
-
temp = monthly_occurrence_to_date(occurrence, remainder.months.before(temp)) if (remainder > 0)
|
140
|
-
next unless temp
|
141
|
-
|
142
|
-
result = temp if !result || (temp > result)
|
143
|
-
end
|
144
|
-
|
145
|
-
when :annually
|
146
|
-
result, try_to_use_2_29 = if((start_date.month == 2) && (start_date.day == 29))
|
147
|
-
[Date.new(date.year, 2, 28), true]
|
148
|
-
else
|
149
|
-
[Date.new(date.year, start_date.month, start_date.day), false]
|
150
|
-
end
|
151
|
-
result = 1.year.before(result) if (result < date)
|
152
|
-
|
153
|
-
remainder = years_between(result, start_date) % skip
|
154
|
-
result = remainder.years.before(result) if (remainder > 0)
|
155
|
-
if try_to_use_2_29
|
156
|
-
begin
|
157
|
-
date = Date.new(result.year, 2, 29)
|
158
|
-
rescue
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
result = nil if (result && result < self.start_date)
|
164
|
-
result
|
57
|
+
date = end_date if (ends? && date > end_date)
|
58
|
+
enumerator.new(self, date).prev
|
165
59
|
end
|
166
60
|
|
167
|
-
|
168
|
-
|
169
61
|
def first_occurrence_before(date)
|
170
|
-
first_occurrence_on_or_before(1
|
62
|
+
first_occurrence_on_or_before(date - 1)
|
171
63
|
end
|
172
64
|
|
173
65
|
|
@@ -181,68 +73,21 @@ module Hiccup
|
|
181
73
|
|
182
74
|
|
183
75
|
def n_occurrences_before(limit, date)
|
184
|
-
n_occurrences_on_or_before(limit, 1
|
76
|
+
n_occurrences_on_or_before(limit, date - 1)
|
185
77
|
end
|
186
78
|
|
187
79
|
def n_occurrences_on_or_before(limit, date)
|
80
|
+
date = end_date if (ends? && date > end_date)
|
81
|
+
|
188
82
|
occurrences = []
|
189
|
-
|
190
|
-
while occurrence && occurrences.length < limit
|
83
|
+
enum = enumerator.new(self, date)
|
84
|
+
while (occurrence = enum.prev) && occurrences.length < limit
|
191
85
|
occurrences << occurrence
|
192
|
-
occurrence = first_occurrence_before(occurrence)
|
193
86
|
end
|
194
87
|
occurrences
|
195
88
|
end
|
196
89
|
|
197
90
|
|
198
91
|
|
199
|
-
def monthly_occurrence_to_date(occurrence, date)
|
200
|
-
year, month = date.year, date.month
|
201
|
-
|
202
|
-
day = begin
|
203
|
-
if occurrence.is_a?(Array)
|
204
|
-
ordinal, weekday = occurrence
|
205
|
-
wday_of_first_of_month = Date.new(year, month, 1).wday
|
206
|
-
wday = Date::DAYNAMES.index(weekday)
|
207
|
-
day = wday
|
208
|
-
day = day + 7 if (wday < wday_of_first_of_month)
|
209
|
-
day = day - wday_of_first_of_month
|
210
|
-
day = day + (ordinal * 7) - 6
|
211
|
-
else
|
212
|
-
occurrence
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
last_day_of_month = Date.new(year, month, -1).day
|
217
|
-
(day > last_day_of_month) ? nil : Date.new(year, month, day)
|
218
|
-
end
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
private
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
def months_between(date1, date2)
|
227
|
-
later_date, earlier_date = sort_dates(date1, date2)
|
228
|
-
later_date.get_months_since(earlier_date)
|
229
|
-
end
|
230
|
-
|
231
|
-
def weeks_between(date1, date2)
|
232
|
-
later_date, earlier_date = sort_dates(date1, date2)
|
233
|
-
later_date.get_weeks_since(earlier_date)
|
234
|
-
end
|
235
|
-
|
236
|
-
def years_between(date1, date2)
|
237
|
-
later_date, earlier_date = sort_dates(date1, date2)
|
238
|
-
later_date.get_years_since(earlier_date)
|
239
|
-
end
|
240
|
-
|
241
|
-
def sort_dates(date1, date2)
|
242
|
-
(date1 > date2) ? [date1, date2] : [date2, date1]
|
243
|
-
end
|
244
|
-
|
245
|
-
|
246
|
-
|
247
92
|
end
|
248
93
|
end
|
@@ -2,29 +2,138 @@ require 'active_support/concern'
|
|
2
2
|
require 'active_support/core_ext/date/conversions'
|
3
3
|
require 'hiccup/core_ext/enumerable'
|
4
4
|
require 'hiccup/core_ext/hash'
|
5
|
+
require "hiccup/core_ext/date"
|
5
6
|
|
6
7
|
|
7
8
|
module Hiccup
|
8
|
-
module
|
9
|
+
module Inferable
|
9
10
|
extend ActiveSupport::Concern
|
10
11
|
|
11
12
|
module ClassMethods
|
12
13
|
|
14
|
+
def infer(dates, options={})
|
15
|
+
dates = extract_array_of_dates!(dates)
|
16
|
+
|
17
|
+
enumerator = DatesEnumerator.new(dates)
|
18
|
+
guesser = Guesser.new(self, options)
|
19
|
+
confidences = []
|
20
|
+
confidence_threshold = 0.6
|
21
|
+
rewind_by = 0
|
22
|
+
|
23
|
+
until enumerator.done?
|
24
|
+
date = enumerator.next
|
25
|
+
guesser << date
|
26
|
+
confidences << guesser.confidence.to_f
|
27
|
+
|
28
|
+
if guesser.predicted?(date)
|
29
|
+
rewind_by = 0
|
30
|
+
else
|
31
|
+
rewind_by += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# puts "date: #{date}, confidences: #{confidences}, rewind_by: #{rewind_by}" if options[:verbose]
|
35
|
+
|
36
|
+
# if the last two confidences are both below a certain
|
37
|
+
# threshhold and both declining, back up to where we
|
38
|
+
# started to go wrong and start a new schedule.
|
39
|
+
|
40
|
+
if confidences.length >= 3 &&
|
41
|
+
confidences[-1] < confidence_threshold &&
|
42
|
+
confidences[-2] < confidence_threshold &&
|
43
|
+
confidences[-1] < confidences[-2] &&
|
44
|
+
confidences[-2] < confidences[-3]
|
45
|
+
|
46
|
+
rewind_by -= 1 if rewind_by == guesser.count
|
47
|
+
enumerator.rewind_by(rewind_by)
|
48
|
+
guesser.restart!
|
49
|
+
confidences = []
|
50
|
+
rewind_by = 0
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
guesser.stop!
|
55
|
+
|
56
|
+
guesser.guesses
|
57
|
+
end
|
13
58
|
|
14
59
|
|
15
|
-
|
60
|
+
|
61
|
+
def extract_array_of_dates!(dates)
|
62
|
+
raise_invalid_dates_error! unless dates.respond_to?(:each)
|
63
|
+
dates.map { |date| assert_date!(date) }.sort
|
64
|
+
end
|
65
|
+
|
66
|
+
def assert_date!(date)
|
67
|
+
return date if date.is_a?(Date)
|
68
|
+
date.to_date rescue raise_invalid_dates_error!
|
69
|
+
end
|
70
|
+
|
71
|
+
def raise_invalid_dates_error!
|
72
|
+
raise ArgumentError.new("Inferable.infer expects to receive a collection of dates")
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
class Guesser
|
82
|
+
|
83
|
+
def initialize(klass, options={})
|
84
|
+
@klass = klass
|
16
85
|
@verbose = options.fetch(:verbose, false)
|
17
|
-
|
18
|
-
|
19
|
-
guess, score = pick_best_guess(guesses, dates)
|
20
|
-
guess
|
86
|
+
@guesses = []
|
87
|
+
start!
|
21
88
|
end
|
22
89
|
|
90
|
+
attr_reader :guesses, :confidence
|
91
|
+
|
92
|
+
def start!
|
93
|
+
save_current_guess!
|
94
|
+
reset_growing_understanding!
|
95
|
+
end
|
96
|
+
alias :restart! :start!
|
23
97
|
|
98
|
+
def stop!
|
99
|
+
start!
|
100
|
+
end
|
101
|
+
|
102
|
+
def save_current_guess!
|
103
|
+
@guesses << @schedule if @schedule
|
104
|
+
end
|
105
|
+
|
106
|
+
def reset_growing_understanding!
|
107
|
+
@dates = []
|
108
|
+
@schedule = nil
|
109
|
+
@confidence = 0
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
def <<(date)
|
115
|
+
@dates << date
|
116
|
+
@schedule, @confidence = best_schedule_for(@dates)
|
117
|
+
end
|
118
|
+
|
119
|
+
def count
|
120
|
+
@dates.length
|
121
|
+
end
|
122
|
+
|
123
|
+
def predicted?(date)
|
124
|
+
@schedule && @schedule.contains?(date)
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
def best_schedule_for(dates)
|
130
|
+
guesses = generate_guesses(dates)
|
131
|
+
pick_best_guess(guesses, dates)
|
132
|
+
end
|
24
133
|
|
25
134
|
def generate_guesses(dates)
|
26
|
-
@start_date = dates.
|
27
|
-
@end_date = dates.
|
135
|
+
@start_date = dates.first
|
136
|
+
@end_date = dates.last
|
28
137
|
[].tap do |guesses|
|
29
138
|
guesses.concat generate_yearly_guesses(dates)
|
30
139
|
guesses.concat generate_monthly_guesses(dates)
|
@@ -43,7 +152,7 @@ module Hiccup
|
|
43
152
|
|
44
153
|
[].tap do |guesses|
|
45
154
|
(1...5).each do |skip|
|
46
|
-
guesses <<
|
155
|
+
guesses << @klass.new.tap do |schedule|
|
47
156
|
schedule.kind = :annually
|
48
157
|
schedule.start_date = start_date
|
49
158
|
schedule.end_date = @end_date
|
@@ -75,7 +184,7 @@ module Hiccup
|
|
75
184
|
[].tap do |guesses|
|
76
185
|
(1...5).each do |skip|
|
77
186
|
enumerate_by_popularity(days_by_popularity) do |days|
|
78
|
-
guesses <<
|
187
|
+
guesses << @klass.new.tap do |schedule|
|
79
188
|
schedule.kind = :monthly
|
80
189
|
schedule.start_date = @start_date
|
81
190
|
schedule.end_date = @end_date
|
@@ -85,7 +194,7 @@ module Hiccup
|
|
85
194
|
end
|
86
195
|
|
87
196
|
enumerate_by_popularity(patterns_by_popularity) do |patterns|
|
88
|
-
guesses <<
|
197
|
+
guesses << @klass.new.tap do |schedule|
|
89
198
|
schedule.kind = :monthly
|
90
199
|
schedule.start_date = @start_date
|
91
200
|
schedule.end_date = @end_date
|
@@ -114,7 +223,7 @@ module Hiccup
|
|
114
223
|
|
115
224
|
(1...5).each do |skip|
|
116
225
|
enumerate_by_popularity(wdays_by_popularity) do |wdays|
|
117
|
-
guesses <<
|
226
|
+
guesses << @klass.new.tap do |schedule|
|
118
227
|
schedule.kind = :weekly
|
119
228
|
schedule.start_date = @start_date
|
120
229
|
schedule.end_date = @end_date
|
@@ -158,8 +267,7 @@ module Hiccup
|
|
158
267
|
puts ""
|
159
268
|
end
|
160
269
|
|
161
|
-
|
162
|
-
best_guess || Schedule.new(kind: :never)
|
270
|
+
scored_guesses.reject { |(guess, score)| score.to_f < 0.333 }.first
|
163
271
|
end
|
164
272
|
|
165
273
|
def score_guess(guess, input_dates)
|
@@ -194,22 +302,6 @@ module Hiccup
|
|
194
302
|
|
195
303
|
|
196
304
|
|
197
|
-
def extract_array_of_dates!(dates)
|
198
|
-
raise_invalid_dates_error! unless dates.respond_to?(:each)
|
199
|
-
dates.map { |date| assert_date!(date) }.sort
|
200
|
-
end
|
201
|
-
|
202
|
-
def assert_date!(date)
|
203
|
-
return date if date.is_a?(Date)
|
204
|
-
date.to_date rescue raise_invalid_dates_error!
|
205
|
-
end
|
206
|
-
|
207
|
-
def raise_invalid_dates_error!
|
208
|
-
raise ArgumentError.new("Inferrable.infer expects to receive a collection of dates")
|
209
|
-
end
|
210
|
-
|
211
|
-
|
212
|
-
|
213
305
|
class Score < Struct.new(:prediction_rate, :brick_rate, :complexity_rate)
|
214
306
|
|
215
307
|
# as brick rate rises, our confidence in this guess drops
|
@@ -244,6 +336,37 @@ module Hiccup
|
|
244
336
|
end
|
245
337
|
|
246
338
|
|
339
|
+
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
|
344
|
+
class DatesEnumerator
|
345
|
+
|
346
|
+
def initialize(dates)
|
347
|
+
@dates = dates
|
348
|
+
@last_index = @dates.length - 1
|
349
|
+
@index = -1
|
350
|
+
end
|
351
|
+
|
352
|
+
def done?
|
353
|
+
@index == @last_index
|
354
|
+
end
|
355
|
+
|
356
|
+
def next
|
357
|
+
@index += 1
|
358
|
+
raise OutOfRangeException if @index > @last_index
|
359
|
+
@dates[@index]
|
360
|
+
end
|
361
|
+
|
362
|
+
def rewind_by(n)
|
363
|
+
@index -= n
|
364
|
+
@index = -1 if @index < -1
|
365
|
+
end
|
366
|
+
|
247
367
|
end
|
368
|
+
|
369
|
+
|
370
|
+
|
248
371
|
end
|
249
372
|
end
|
data/lib/hiccup/schedule.rb
CHANGED
data/lib/hiccup/version.rb
CHANGED
data/test/duration_ext_test.rb
CHANGED
data/test/enumerable_test.rb
CHANGED
@@ -3,6 +3,7 @@ require "test_helper"
|
|
3
3
|
|
4
4
|
class EnumerableTest < ActiveSupport::TestCase
|
5
5
|
include Hiccup
|
6
|
+
PERFORMANCE_TEST = false
|
6
7
|
|
7
8
|
|
8
9
|
|
@@ -18,6 +19,18 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
18
19
|
|
19
20
|
|
20
21
|
|
22
|
+
test "annual recurrence with a skip" do
|
23
|
+
schedule = Schedule.new({
|
24
|
+
:kind => :annually,
|
25
|
+
:skip => 2,
|
26
|
+
:start_date => Date.new(2009,3,4)})
|
27
|
+
expected_dates = %w{2009-03-04 2011-03-04 2013-03-04}
|
28
|
+
actual_dates = schedule.occurrences_between(Date.new(2009, 01, 01), Date.new(2013, 12, 31)).map(&:to_s)
|
29
|
+
assert_equal expected_dates, actual_dates
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
21
34
|
def test_occurs_on_weekly
|
22
35
|
schedule = Schedule.new({
|
23
36
|
:kind => :weekly,
|
@@ -277,6 +290,7 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
277
290
|
});
|
278
291
|
|
279
292
|
assert_equal [Date.new(2010, 2, 28)], schedule.occurrences_during_month(2010, 2)
|
293
|
+
assert_equal [Date.new(2012, 2, 29)], schedule.occurrences_during_month(2012, 2)
|
280
294
|
end
|
281
295
|
|
282
296
|
|
@@ -286,7 +300,7 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
286
300
|
:kind => :monthly,
|
287
301
|
:monthly_pattern => [31],
|
288
302
|
:start_date => Date.new(2008, 2, 29)
|
289
|
-
})
|
303
|
+
})
|
290
304
|
|
291
305
|
assert_equal [Date.new(2010, 1, 31)], schedule.occurrences_during_month(2010, 1)
|
292
306
|
assert_equal [], schedule.occurrences_during_month(2010, 2)
|
@@ -294,38 +308,69 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
294
308
|
|
295
309
|
|
296
310
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
311
|
+
if PERFORMANCE_TEST
|
312
|
+
test "performance test" do
|
313
|
+
n = 100
|
314
|
+
|
315
|
+
# Each of these schedules should describe 52 events
|
316
|
+
|
317
|
+
Benchmark.bm(20) do |x|
|
318
|
+
x.report("weekly (simple):") do
|
319
|
+
n.times do
|
320
|
+
Schedule.new(
|
321
|
+
:kind => :weekly,
|
322
|
+
:weekly_pattern => ["Friday"],
|
323
|
+
:start_date => Date.new(2009, 1, 1)) \
|
324
|
+
.occurrences_between(Date.new(2009, 1, 1), Date.new(2009, 12, 31))
|
325
|
+
end
|
326
|
+
end
|
327
|
+
x.report("weekly (complex):") do
|
328
|
+
n.times do
|
329
|
+
Schedule.new(
|
330
|
+
:kind => :weekly,
|
331
|
+
:weekly_pattern => ["Monday", "Wednesday", "Friday"],
|
332
|
+
:start_date => Date.new(2009, 1, 1)) \
|
333
|
+
.occurrences_between(Date.new(2009, 1, 1), Date.new(2009, 5, 2))
|
334
|
+
end
|
335
|
+
end
|
336
|
+
x.report("monthly (simple):") do
|
337
|
+
n.times do
|
338
|
+
Schedule.new(
|
339
|
+
:kind => :monthly,
|
340
|
+
:monthly_pattern => [[2, "Monday"]],
|
341
|
+
:start_date => Date.new(2009, 1, 1)) \
|
342
|
+
.occurrences_between(Date.new(2009, 1, 1), Date.new(2013, 4, 30))
|
343
|
+
end
|
344
|
+
end
|
345
|
+
x.report("monthly (complex):") do
|
346
|
+
n.times do
|
347
|
+
Schedule.new(
|
348
|
+
:kind => :monthly,
|
349
|
+
:monthly_pattern => [[2, "Monday"], [4, "Monday"]],
|
350
|
+
:start_date => Date.new(2009, 1, 1)) \
|
351
|
+
.occurrences_between(Date.new(2009, 1, 1), Date.new(2011, 3, 1))
|
352
|
+
end
|
353
|
+
end
|
354
|
+
x.report("yearly:") do
|
355
|
+
n.times do
|
356
|
+
Schedule.new(
|
357
|
+
:kind => :annually,
|
358
|
+
:start_date => Date.new(1960, 3, 15)) \
|
359
|
+
.occurrences_between(Date.new(1960, 1, 1), Date.new(2011, 12, 31))
|
360
|
+
end
|
361
|
+
end
|
362
|
+
x.report("yearly (2/29):") do
|
363
|
+
n.times do
|
364
|
+
Schedule.new(
|
365
|
+
:kind => :annually,
|
366
|
+
:start_date => Date.new(1960, 2, 29)) \
|
367
|
+
.occurrences_between(Date.new(1960, 1, 1), Date.new(2011, 12, 31))
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|
373
|
+
end
|
329
374
|
|
330
375
|
|
331
376
|
|
data/test/inferrable_test.rb
CHANGED
@@ -3,7 +3,7 @@ require "test_helper"
|
|
3
3
|
# Ideas: see 3/4/10, 3/5/11, 3/4/12 as closer to
|
4
4
|
# a pattern than 3/4/10, 9/15/11, 3/4/12.
|
5
5
|
|
6
|
-
class
|
6
|
+
class InferableTest < ActiveSupport::TestCase
|
7
7
|
include Hiccup
|
8
8
|
|
9
9
|
|
@@ -46,7 +46,7 @@ class InferrableTest < ActiveSupport::TestCase
|
|
46
46
|
# If bricks and fails were equal, the annual recurrence would be the
|
47
47
|
# preferred guess, but the monthly one makes more sense.
|
48
48
|
# It is better to brick than to fail.
|
49
|
-
schedule = Schedule.infer(dates)
|
49
|
+
schedule = Schedule.infer(dates).first
|
50
50
|
assert_equal :monthly, schedule.kind
|
51
51
|
end
|
52
52
|
|
@@ -58,8 +58,8 @@ class InferrableTest < ActiveSupport::TestCase
|
|
58
58
|
|
59
59
|
test "should infer an annual" do
|
60
60
|
dates = %w{2010-3-4 2011-3-4 2012-3-4}
|
61
|
-
|
62
|
-
assert_equal "Every year on March 4",
|
61
|
+
schedules = Schedule.infer(dates)
|
62
|
+
assert_equal ["Every year on March 4"], schedules.map(&:humanize)
|
63
63
|
end
|
64
64
|
|
65
65
|
|
@@ -67,22 +67,22 @@ class InferrableTest < ActiveSupport::TestCase
|
|
67
67
|
|
68
68
|
test "should infer a schedule that occurs every other year" do
|
69
69
|
dates = %w{2010-3-4 2012-3-4 2014-3-4}
|
70
|
-
|
71
|
-
assert_equal "Every other year on March 4",
|
70
|
+
schedules = Schedule.infer(dates)
|
71
|
+
assert_equal ["Every other year on March 4"], schedules.map(&:humanize)
|
72
72
|
end
|
73
73
|
|
74
74
|
# ... where some of the input is wrong
|
75
75
|
|
76
76
|
test "should infer a yearly schedule when one of the dates was rescheduled" do
|
77
77
|
dates = %w{2010-3-4 2011-9-15 2012-3-4 2013-3-4}
|
78
|
-
|
79
|
-
assert_equal "Every year on March 4",
|
78
|
+
schedules = Schedule.infer(dates)
|
79
|
+
assert_equal ["Every year on March 4"], schedules.map(&:humanize)
|
80
80
|
end
|
81
81
|
|
82
82
|
test "should infer a yearly schedule when the first date was rescheduled" do
|
83
83
|
dates = %w{2010-3-6 2011-3-4 2012-3-4 2013-3-4}
|
84
|
-
|
85
|
-
assert_equal "Every year on March 4",
|
84
|
+
schedules = Schedule.infer(dates)
|
85
|
+
assert_equal ["Every year on March 4"], schedules.map(&:humanize)
|
86
86
|
end
|
87
87
|
|
88
88
|
|
@@ -93,24 +93,24 @@ class InferrableTest < ActiveSupport::TestCase
|
|
93
93
|
|
94
94
|
test "should infer a monthly schedule that occurs on a date" do
|
95
95
|
dates = %w{2012-2-4 2012-3-4 2012-4-4}
|
96
|
-
|
97
|
-
assert_equal "The 4th of every month",
|
96
|
+
schedules = Schedule.infer(dates)
|
97
|
+
assert_equal ["The 4th of every month"], schedules.map(&:humanize)
|
98
98
|
|
99
99
|
dates = %w{2012-2-17 2012-3-17 2012-4-17}
|
100
|
-
|
101
|
-
assert_equal "The 17th of every month",
|
100
|
+
schedules = Schedule.infer(dates)
|
101
|
+
assert_equal ["The 17th of every month"], schedules.map(&:humanize)
|
102
102
|
end
|
103
103
|
|
104
104
|
test "should infer a monthly schedule that occurs on a weekday" do
|
105
105
|
dates = %w{2012-7-9 2012-8-13 2012-9-10}
|
106
|
-
|
107
|
-
assert_equal "The second Monday of every month",
|
106
|
+
schedules = Schedule.infer(dates)
|
107
|
+
assert_equal ["The second Monday of every month"], schedules.map(&:humanize)
|
108
108
|
end
|
109
109
|
|
110
110
|
test "should infer a schedule that occurs several times a month" do
|
111
111
|
dates = %w{2012-7-9 2012-7-23 2012-8-13 2012-8-27 2012-9-10 2012-9-24}
|
112
|
-
|
113
|
-
assert_equal "The second Monday and fourth Monday of every month",
|
112
|
+
schedules = Schedule.infer(dates)
|
113
|
+
assert_equal ["The second Monday and fourth Monday of every month"], schedules.map(&:humanize)
|
114
114
|
end
|
115
115
|
|
116
116
|
|
@@ -118,8 +118,8 @@ class InferrableTest < ActiveSupport::TestCase
|
|
118
118
|
|
119
119
|
test "should infer a schedule that occurs every third month" do
|
120
120
|
dates = %w{2012-2-4 2012-5-4 2012-8-4}
|
121
|
-
|
122
|
-
assert_equal "The 4th of every third month",
|
121
|
+
schedules = Schedule.infer(dates)
|
122
|
+
assert_equal ["The 4th of every third month"], schedules.map(&:humanize)
|
123
123
|
end
|
124
124
|
|
125
125
|
|
@@ -127,33 +127,33 @@ class InferrableTest < ActiveSupport::TestCase
|
|
127
127
|
|
128
128
|
test "should infer a monthly (by day) schedule when one day was rescheduled" do
|
129
129
|
dates = %w{2012-10-02 2012-11-02 2012-12-03}
|
130
|
-
|
131
|
-
assert_equal "The 2nd of every month",
|
130
|
+
schedules = Schedule.infer(dates)
|
131
|
+
assert_equal ["The 2nd of every month"], schedules.map(&:humanize)
|
132
132
|
end
|
133
133
|
|
134
134
|
test "should infer a monthly (by day) schedule when the first day was rescheduled" do
|
135
135
|
dates = %w{2012-10-03 2012-11-02 2012-12-02}
|
136
|
-
|
137
|
-
assert_equal "The 2nd of every month",
|
136
|
+
schedules = Schedule.infer(dates)
|
137
|
+
assert_equal ["The 2nd of every month"], schedules.map(&:humanize)
|
138
138
|
end
|
139
139
|
|
140
140
|
|
141
141
|
test "should infer a monthly (by weekday) schedule when one day was rescheduled" do
|
142
142
|
dates = %w{2012-10-02 2012-11-06 2012-12-05} # 1st Tuesday, 1st Tuesday, 1st Wednesday
|
143
|
-
|
144
|
-
assert_equal "The first Tuesday of every month",
|
143
|
+
schedules = Schedule.infer(dates)
|
144
|
+
assert_equal ["The first Tuesday of every month"], schedules.map(&:humanize)
|
145
145
|
end
|
146
146
|
|
147
147
|
test "should infer a monthly (by weekday) schedule when the first day was rescheduled" do
|
148
148
|
dates = %w{2012-10-03 2012-11-01 2012-12-06} # 1st Wednesday, 1st Thursday, 1st Thursday
|
149
|
-
|
150
|
-
assert_equal "The first Thursday of every month",
|
149
|
+
schedules = Schedule.infer(dates)
|
150
|
+
assert_equal ["The first Thursday of every month"], schedules.map(&:humanize)
|
151
151
|
end
|
152
152
|
|
153
153
|
test "should infer a monthly (by weekday) schedule when the first day was rescheduled 2" do
|
154
154
|
dates = %w{2012-10-11 2012-11-01 2012-12-06} # 2nd Thursday, 1st Thursday, 1st Thursday
|
155
|
-
|
156
|
-
assert_equal "The first Thursday of every month",
|
155
|
+
schedules = Schedule.infer(dates)
|
156
|
+
assert_equal ["The first Thursday of every month"], schedules.map(&:humanize)
|
157
157
|
end
|
158
158
|
|
159
159
|
|
@@ -164,14 +164,14 @@ class InferrableTest < ActiveSupport::TestCase
|
|
164
164
|
|
165
165
|
test "should infer a weekly schedule" do
|
166
166
|
dates = %w{2012-3-4 2012-3-11 2012-3-18}
|
167
|
-
|
168
|
-
assert_equal "Every Sunday",
|
167
|
+
schedules = Schedule.infer(dates)
|
168
|
+
assert_equal ["Every Sunday"], schedules.map(&:humanize)
|
169
169
|
end
|
170
170
|
|
171
171
|
test "should infer a schedule that occurs several times a week" do
|
172
172
|
dates = %w{2012-3-6 2012-3-8 2012-3-13 2012-3-15 2012-3-20 2012-3-22}
|
173
|
-
|
174
|
-
assert_equal "Every Tuesday and Thursday",
|
173
|
+
schedules = Schedule.infer(dates)
|
174
|
+
assert_equal ["Every Tuesday and Thursday"], schedules.map(&:humanize)
|
175
175
|
end
|
176
176
|
|
177
177
|
|
@@ -179,8 +179,8 @@ class InferrableTest < ActiveSupport::TestCase
|
|
179
179
|
|
180
180
|
test "should infer weekly recurrence for something that occurs every other week" do
|
181
181
|
dates = %w{2012-3-6 2012-3-8 2012-3-20 2012-3-22}
|
182
|
-
|
183
|
-
assert_equal "Tuesday and Thursday of every other week",
|
182
|
+
schedules = Schedule.infer(dates)
|
183
|
+
assert_equal ["Tuesday and Thursday of every other week"], schedules.map(&:humanize)
|
184
184
|
end
|
185
185
|
|
186
186
|
|
@@ -188,14 +188,14 @@ class InferrableTest < ActiveSupport::TestCase
|
|
188
188
|
|
189
189
|
test "should infer a weekly schedule (missing dates)" do
|
190
190
|
dates = %w{2012-3-4 2012-3-11 2012-3-25}
|
191
|
-
|
192
|
-
assert_equal "Every Sunday",
|
191
|
+
schedules = Schedule.infer(dates)
|
192
|
+
assert_equal ["Every Sunday"], schedules.map(&:humanize)
|
193
193
|
end
|
194
194
|
|
195
195
|
test "should infer a schedule that occurs several times a week (missing dates)" do
|
196
196
|
dates = %w{2012-3-6 2012-3-8 2012-3-15 2012-3-20 2012-3-27 2012-3-29}
|
197
|
-
|
198
|
-
assert_equal "Every Tuesday and Thursday",
|
197
|
+
schedules = Schedule.infer(dates)
|
198
|
+
assert_equal ["Every Tuesday and Thursday"], schedules.map(&:humanize)
|
199
199
|
end
|
200
200
|
|
201
201
|
|
@@ -203,14 +203,14 @@ class InferrableTest < ActiveSupport::TestCase
|
|
203
203
|
|
204
204
|
test "should infer a weekly schedule when one day was rescheduled" do
|
205
205
|
dates = %w{2012-10-02 2012-10-09 2012-10-15} # a Tuesday, a Tuesday, and a Monday
|
206
|
-
|
207
|
-
assert_equal "Every Tuesday",
|
206
|
+
schedules = Schedule.infer(dates)
|
207
|
+
assert_equal ["Every Tuesday"], schedules.map(&:humanize)
|
208
208
|
end
|
209
209
|
|
210
210
|
test "should infer a weekly schedule when the first day was rescheduled" do
|
211
211
|
dates = %w{2012-10-07 2012-10-10 2012-10-17} # a Sunday, a Wednesday, and a Wednesday
|
212
|
-
|
213
|
-
assert_equal "Every Wednesday",
|
212
|
+
schedules = Schedule.infer(dates)
|
213
|
+
assert_equal ["Every Wednesday"], schedules.map(&:humanize)
|
214
214
|
end
|
215
215
|
|
216
216
|
|
@@ -225,20 +225,28 @@ class InferrableTest < ActiveSupport::TestCase
|
|
225
225
|
]
|
226
226
|
|
227
227
|
arbitrary_date_ranges.each do |dates|
|
228
|
-
|
229
|
-
fail "There should be no pattern to the dates #{dates}, but Hiccup guessed \"#{
|
228
|
+
schedules = Schedule.infer(dates)
|
229
|
+
fail "There should be no pattern to the dates #{dates}, but Hiccup guessed \"#{schedules.map(&:humanize)}\"" if schedules.any?
|
230
230
|
end
|
231
231
|
end
|
232
232
|
|
233
233
|
|
234
234
|
|
235
|
+
test "should infer multiple schedules from mixed input" do
|
236
|
+
dates = %w{2012-11-05 2012-11-12 2012-11-19 2012-11-28 2012-12-05 2012-12-12} # three Mondays then three Wednesdays
|
237
|
+
schedules = Schedule.infer(dates)
|
238
|
+
assert_equal ["Every Monday", "Every Wednesday"],
|
239
|
+
schedules.map(&:humanize)
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
|
235
244
|
test "should diabolically complex schedules" do
|
236
245
|
dates = %w{2012-11-06 2012-11-08 2012-11-15 2012-11-20 2012-11-27 2012-11-29 2013-02-05 2013-02-14 2013-02-21 2013-02-19 2013-02-26 2013-05-07 2013-05-09 2013-05-16 2013-05-28 2013-05-21 2013-05-30}
|
237
|
-
|
238
|
-
assert_equal "The first Tuesday, second Thursday, third Thursday, third Tuesday, fourth Tuesday, and fifth Thursday of every third month",
|
246
|
+
schedules = Schedule.infer(dates)
|
247
|
+
assert_equal ["The first Tuesday, second Thursday, third Thursday, third Tuesday, fourth Tuesday, and fifth Thursday of every third month"], schedules.map(&:humanize)
|
239
248
|
end
|
240
249
|
|
241
250
|
|
242
251
|
|
243
|
-
|
244
252
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hiccup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -145,8 +145,13 @@ files:
|
|
145
145
|
- lib/hiccup/core_ext/fixnum.rb
|
146
146
|
- lib/hiccup/core_ext/hash.rb
|
147
147
|
- lib/hiccup/enumerable.rb
|
148
|
+
- lib/hiccup/enumerable/annually_enumerator.rb
|
149
|
+
- lib/hiccup/enumerable/monthly_enumerator.rb
|
150
|
+
- lib/hiccup/enumerable/never_enumerator.rb
|
151
|
+
- lib/hiccup/enumerable/schedule_enumerator.rb
|
152
|
+
- lib/hiccup/enumerable/weekly_enumerator.rb
|
148
153
|
- lib/hiccup/humanizable.rb
|
149
|
-
- lib/hiccup/
|
154
|
+
- lib/hiccup/inferable.rb
|
150
155
|
- lib/hiccup/schedule.rb
|
151
156
|
- lib/hiccup/serializable/ical.rb
|
152
157
|
- lib/hiccup/serializers/ical.rb
|
@@ -174,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
179
|
version: '0'
|
175
180
|
segments:
|
176
181
|
- 0
|
177
|
-
hash: -
|
182
|
+
hash: -1083227062532008741
|
178
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
184
|
none: false
|
180
185
|
requirements:
|
@@ -183,10 +188,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
183
188
|
version: '0'
|
184
189
|
segments:
|
185
190
|
- 0
|
186
|
-
hash: -
|
191
|
+
hash: -1083227062532008741
|
187
192
|
requirements: []
|
188
193
|
rubyforge_project: hiccup
|
189
|
-
rubygems_version: 1.8.
|
194
|
+
rubygems_version: 1.8.24
|
190
195
|
signing_key:
|
191
196
|
specification_version: 3
|
192
197
|
summary: A library for working with things that recur
|