hiccup 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|