hiccup 0.4.5 → 0.5.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.
- checksums.yaml +4 -4
- data/hiccup.gemspec +1 -0
- data/lib/hiccup/convenience.rb +2 -1
- data/lib/hiccup/enumerable.rb +2 -10
- data/lib/hiccup/enumerable/annually_enumerator.rb +41 -30
- data/lib/hiccup/enumerable/monthly_date_enumerator.rb +14 -0
- data/lib/hiccup/enumerable/monthly_enumerator.rb +82 -46
- data/lib/hiccup/enumerable/schedule_enumerator.rb +68 -32
- data/lib/hiccup/enumerable/weekly_enumerator.rb +67 -17
- data/lib/hiccup/inferable.rb +19 -12
- data/lib/hiccup/inferable/guesser.rb +1 -32
- data/lib/hiccup/version.rb +1 -1
- data/test/enumerable_test.rb +22 -2
- data/test/inferrable_test.rb +17 -0
- data/test/leap_year_test.rb +20 -0
- data/test/test_helper.rb +1 -0
- data/test/weekly_enumerator_test_test.rb +120 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d10125513beb88e0c6c96db75cc928db898ad2f
|
4
|
+
data.tar.gz: fc07262a4ea66ef9d964f69e5a9d69971c2ee557
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72ab32b2d1fbaacf611d09063be2f223f322a4de8af2cb83c950a52b723d2ebdc6b6ae31281459d319d063e1a65d84dabd36befd07245909e9d694cf2c1718a3
|
7
|
+
data.tar.gz: bd3b19597a7ff8444ef1a51ac294e5bf1b56ef9d8d4473419d799adb87662638bd602939b8026a2fd0674592d5e3a21648775f57372921144f0580494df9d376
|
data/hiccup.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_development_dependency "rails", "~> 3.2.8"
|
21
21
|
s.add_development_dependency "turn"
|
22
22
|
s.add_development_dependency "simplecov"
|
23
|
+
s.add_development_dependency "shoulda-context"
|
23
24
|
s.add_development_dependency "pry"
|
24
25
|
|
25
26
|
s.files = `git ls-files`.split("\n")
|
data/lib/hiccup/convenience.rb
CHANGED
data/lib/hiccup/enumerable.rb
CHANGED
@@ -11,16 +11,7 @@ module Hiccup
|
|
11
11
|
|
12
12
|
|
13
13
|
def enumerator
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def occurrences_during_month(year, month)
|
20
|
-
puts "DEPRECATED: `occurrences_during_month` will be removed in 0.5.0. Use `occurrences_between` instead"
|
21
|
-
date1 = Date.new(year, month, 1)
|
22
|
-
date2 = Date.new(year, month, -1)
|
23
|
-
occurrences_between(date1, date2)
|
14
|
+
ScheduleEnumerator.enum_for(self)
|
24
15
|
end
|
25
16
|
|
26
17
|
|
@@ -67,6 +58,7 @@ module Hiccup
|
|
67
58
|
date == first_occurrence_on_or_after(date)
|
68
59
|
end
|
69
60
|
alias :contains? :occurs_on
|
61
|
+
alias :predicts? :occurs_on
|
70
62
|
|
71
63
|
|
72
64
|
|
@@ -4,53 +4,64 @@ module Hiccup
|
|
4
4
|
module Enumerable
|
5
5
|
class AnnuallyEnumerator < ScheduleEnumerator
|
6
6
|
|
7
|
-
|
8
7
|
def initialize(*args)
|
9
8
|
super
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
22
|
-
end
|
9
|
+
@month, @day = start_date.month, start_date.day
|
10
|
+
@february_29 = month == 2 and day == 29
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
|
16
|
+
attr_reader :month, :day, :year
|
17
|
+
|
18
|
+
def february_29?
|
19
|
+
@february_29
|
23
20
|
end
|
24
21
|
|
25
22
|
|
23
|
+
|
24
|
+
def advance!
|
25
|
+
@year += skip
|
26
|
+
to_date!
|
27
|
+
end
|
28
|
+
|
29
|
+
def rewind!
|
30
|
+
@year -= skip
|
31
|
+
to_date!
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
26
36
|
def first_occurrence_on_or_after(date)
|
27
|
-
year
|
28
|
-
|
37
|
+
@year = date.year
|
38
|
+
@year += skip if (date.month > month) or (date.month == month and date.day > day)
|
29
39
|
|
30
|
-
|
31
|
-
year +=
|
40
|
+
remainder = (@year - start_date.year) % skip
|
41
|
+
@year += (skip - remainder) if remainder > 0
|
32
42
|
|
33
|
-
|
34
|
-
year += (skip - remainder) if remainder > 0
|
35
|
-
|
36
|
-
Date.new(year, month, day)
|
43
|
+
to_date!
|
37
44
|
end
|
38
45
|
|
39
46
|
def first_occurrence_on_or_before(date)
|
40
|
-
year
|
41
|
-
|
42
|
-
|
43
|
-
result = Date.new(year, month, day)
|
44
|
-
year -= 1 if result > date
|
47
|
+
@year = date.year
|
48
|
+
@year -= 1 if (date.month < month) or (date.month == month and date.day < day)
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
year -= remainder if remainder > 0
|
50
|
+
remainder = (@year - start_date.year) % skip
|
51
|
+
@year -= remainder if remainder > 0
|
49
52
|
|
53
|
+
to_date!
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
def to_date!
|
59
|
+
return Date.new(year, month, 28) if february_29? and !leap_year?(year)
|
50
60
|
Date.new(year, month, day)
|
51
61
|
end
|
52
62
|
|
53
63
|
|
64
|
+
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
@@ -4,81 +4,117 @@ module Hiccup
|
|
4
4
|
module Enumerable
|
5
5
|
class MonthlyEnumerator < ScheduleEnumerator
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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)
|
7
|
+
def self.for(schedule)
|
8
|
+
if schedule.monthly_pattern.all? { |occurrence| Fixnum === occurrence }
|
9
|
+
MonthlyDateEnumerator
|
10
|
+
else
|
11
|
+
self
|
23
12
|
end
|
24
|
-
result
|
25
13
|
end
|
26
14
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
15
|
+
|
16
|
+
|
17
|
+
def started?
|
18
|
+
!@position.nil?
|
44
19
|
end
|
45
20
|
|
46
21
|
|
47
|
-
private
|
48
22
|
|
23
|
+
protected
|
24
|
+
|
25
|
+
attr_reader :year, :month, :cycle, :last_day_of_month
|
49
26
|
|
50
|
-
|
51
|
-
|
27
|
+
|
28
|
+
|
29
|
+
def advance!
|
30
|
+
@position += 1
|
31
|
+
next_month if @position >= cycle.length
|
32
|
+
|
33
|
+
day = cycle[@position]
|
34
|
+
return self.next if day > last_day_of_month
|
35
|
+
Date.new(year, month, day)
|
52
36
|
end
|
53
37
|
|
38
|
+
def rewind!
|
39
|
+
@position -= 1
|
40
|
+
prev_month if @position < 0
|
41
|
+
|
42
|
+
day = cycle[@position]
|
43
|
+
return self.prev if day > last_day_of_month
|
44
|
+
Date.new(year, month, day)
|
45
|
+
end
|
54
46
|
|
55
|
-
|
56
|
-
|
47
|
+
|
48
|
+
|
49
|
+
def first_occurrence_on_or_after(date)
|
50
|
+
@year, @month = date.year, date.month
|
51
|
+
get_context
|
52
|
+
|
53
|
+
@position = cycle.index { |day| day >= date.day }
|
54
|
+
next_month unless @position
|
57
55
|
|
58
|
-
day =
|
56
|
+
day = cycle[@position]
|
57
|
+
return self.next if day > last_day_of_month
|
58
|
+
Date.new(year, month, day)
|
59
|
+
end
|
60
|
+
|
61
|
+
def first_occurrence_on_or_before(date)
|
62
|
+
@year, @month = date.year, date.month
|
63
|
+
get_context
|
64
|
+
|
65
|
+
@position = cycle.index { |day| day <= date.day }
|
66
|
+
prev_month unless @position
|
67
|
+
|
68
|
+
day = cycle[@position]
|
69
|
+
return self.prev if day > last_day_of_month
|
70
|
+
Date.new(year, month, day)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
def occurrences_in_month(year, month)
|
76
|
+
wday_of_first_of_month = Date.new(year, month, 1).wday
|
77
|
+
monthly_pattern.map do |occurrence|
|
59
78
|
if occurrence.is_a?(Array)
|
60
79
|
ordinal, weekday = occurrence
|
61
|
-
wday_of_first_of_month = Date.new(year, month, 1).wday
|
62
80
|
wday = Date::DAYNAMES.index(weekday)
|
63
81
|
day = wday
|
64
82
|
day = day + 7 if (wday < wday_of_first_of_month)
|
65
83
|
day = day - wday_of_first_of_month
|
66
84
|
day = day + (ordinal * 7) - 6
|
85
|
+
day
|
67
86
|
else
|
68
87
|
occurrence
|
69
88
|
end
|
70
89
|
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
90
|
end
|
75
91
|
|
76
92
|
|
77
|
-
|
78
|
-
|
93
|
+
|
94
|
+
def next_month
|
95
|
+
@position = 0
|
96
|
+
@month += skip
|
97
|
+
@year, @month = year + 1, month - 12 if month > 12
|
98
|
+
get_context
|
99
|
+
end
|
100
|
+
|
101
|
+
def prev_month
|
102
|
+
@position = @cycle.length - 1
|
103
|
+
@month -= skip
|
104
|
+
@year, @month = year - 1, month + 12 if month < 1
|
105
|
+
get_context
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_context
|
109
|
+
@last_day_of_month = [4, 6, 9, 11].member?(month) ? 30 : 31
|
110
|
+
@last_day_of_month = leap_year?(year) ? 29 : 28 if month == 2
|
111
|
+
@cycle = occurrences_in_month(year, month).sort
|
79
112
|
end
|
80
113
|
|
81
114
|
|
115
|
+
|
82
116
|
end
|
83
117
|
end
|
84
118
|
end
|
119
|
+
|
120
|
+
require "hiccup/enumerable/monthly_date_enumerator"
|
@@ -3,63 +3,99 @@ module Hiccup
|
|
3
3
|
module Enumerable
|
4
4
|
class ScheduleEnumerator
|
5
5
|
|
6
|
-
def
|
6
|
+
def self.enum_for(schedule)
|
7
|
+
case schedule.kind
|
8
|
+
when :weekly then WeeklyEnumerator
|
9
|
+
when :annually then AnnuallyEnumerator
|
10
|
+
when :monthly then MonthlyEnumerator.for(schedule)
|
11
|
+
else NeverEnumerator
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(schedule, seed_date)
|
7
18
|
@schedule = schedule
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
19
|
+
@ends = schedule.ends?
|
20
|
+
@seed_date = seed_date
|
21
|
+
@seed_date = seed_date.to_date if seed_date.respond_to?(:to_date)
|
22
|
+
@seed_date = start_date if (seed_date < start_date)
|
23
|
+
@seed_date = end_date if (ends? && seed_date > end_date)
|
24
|
+
@cursor = nil
|
13
25
|
end
|
14
26
|
|
15
|
-
attr_reader :schedule
|
16
|
-
delegate :start_date, :weekly_pattern, :monthly_pattern, :ends?, :end_date, :skip, :to => :schedule
|
27
|
+
attr_reader :schedule, :seed_date, :cursor
|
17
28
|
|
18
29
|
|
19
30
|
|
20
31
|
def next
|
21
|
-
@
|
22
|
-
|
23
|
-
|
24
|
-
first_occurrence_on_or_after(@date)
|
25
|
-
end
|
26
|
-
@current_date = nil if (ends? && @current_date && @current_date > end_date)
|
27
|
-
@current_date
|
32
|
+
@cursor = started? ? advance! : first_occurrence_on_or_after(seed_date)
|
33
|
+
return nil if ends? && @cursor > end_date
|
34
|
+
@cursor
|
28
35
|
end
|
29
36
|
|
30
37
|
def prev
|
31
|
-
@
|
32
|
-
|
33
|
-
|
34
|
-
first_occurrence_on_or_before(@date)
|
35
|
-
end
|
36
|
-
@current_date = nil if (@current_date && @current_date < start_date)
|
37
|
-
@current_date
|
38
|
+
@cursor = started? ? rewind! : first_occurrence_on_or_before(seed_date)
|
39
|
+
return nil if @cursor < start_date
|
40
|
+
@cursor
|
38
41
|
end
|
39
42
|
|
40
43
|
|
41
44
|
|
42
|
-
|
43
|
-
|
45
|
+
def started?
|
46
|
+
!@cursor.nil?
|
47
|
+
end
|
44
48
|
|
45
|
-
def
|
46
|
-
|
49
|
+
def ends?
|
50
|
+
@ends
|
47
51
|
end
|
48
52
|
|
49
|
-
|
50
|
-
|
53
|
+
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
delegate :start_date, :weekly_pattern, :monthly_pattern, :end_date, :skip, :to => :schedule
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
def leap_year?(year)
|
64
|
+
return false unless (year % 4).zero?
|
65
|
+
return (year % 400).zero? if (year % 100).zero?
|
66
|
+
true
|
51
67
|
end
|
52
68
|
|
53
69
|
|
70
|
+
|
54
71
|
# These two methods DO assume that
|
55
72
|
# date is predicted by the given schedule
|
73
|
+
# Subclasses can probably supply more
|
74
|
+
# performant implementations of these.
|
75
|
+
|
76
|
+
def advance!
|
77
|
+
puts "calling ScheduleEnumerator#advance! slow!"
|
78
|
+
first_occurrence_on_or_after(cursor + 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
def rewind!
|
82
|
+
puts "calling ScheduleEnumerator#rewind! slow!"
|
83
|
+
first_occurrence_on_or_before(cursor - 1)
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
# These two methods DO NOT assume that
|
89
|
+
# date is predicted by the given schedule
|
90
|
+
# Subclasses _must_ provide implementations
|
91
|
+
# of these methods.
|
56
92
|
|
57
|
-
def
|
58
|
-
|
93
|
+
def first_occurrence_on_or_after(date)
|
94
|
+
raise NotImplementedError
|
59
95
|
end
|
60
96
|
|
61
|
-
def
|
62
|
-
|
97
|
+
def first_occurrence_on_or_before(date)
|
98
|
+
raise NotImplementedError
|
63
99
|
end
|
64
100
|
|
65
101
|
|
@@ -4,60 +4,110 @@ module Hiccup
|
|
4
4
|
module Enumerable
|
5
5
|
class WeeklyEnumerator < ScheduleEnumerator
|
6
6
|
|
7
|
-
|
8
7
|
def initialize(*args)
|
9
8
|
super
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
@wday_pattern = weekly_pattern.map do |weekday|
|
11
|
+
Date::DAYNAMES.index(weekday)
|
12
|
+
end.sort
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def self.next_occurrence_before(date)
|
20
|
-
date - skip * 7
|
21
|
-
end
|
14
|
+
start_wday = start_date.wday
|
15
|
+
if start_wday <= @wday_pattern.first or start_wday > @wday_pattern.last
|
16
|
+
@base_date = start_date
|
17
|
+
else
|
18
|
+
@base_date = start_date - (start_wday - @wday_pattern.first)
|
22
19
|
end
|
20
|
+
|
21
|
+
@starting_index = wday_pattern.index { |wday| wday >= start_wday } || 0
|
22
|
+
@cycle = calculate_cycle(schedule)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
attr_reader :base_date,
|
30
|
+
:wday_pattern,
|
31
|
+
:starting_index,
|
32
|
+
:cycle,
|
33
|
+
:position
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
def advance!
|
38
|
+
date = cursor + cycle[position]
|
39
|
+
@position = (position + 1) % cycle.length
|
40
|
+
date
|
41
|
+
end
|
42
|
+
|
43
|
+
def rewind!
|
44
|
+
@position = position <= 0 ? cycle.length - 1 : position - 1
|
45
|
+
cursor - cycle[position]
|
23
46
|
end
|
24
47
|
|
25
48
|
|
49
|
+
|
26
50
|
def first_occurrence_on_or_after(date)
|
27
51
|
result = nil
|
28
52
|
wday = date.wday
|
29
|
-
|
30
|
-
wd = Date::DAYNAMES.index(weekday)
|
53
|
+
wday_pattern.each do |wd|
|
31
54
|
wd = wd + 7 if wd < wday
|
32
55
|
days_in_the_future = wd - wday
|
33
56
|
temp = date + days_in_the_future
|
34
57
|
|
35
|
-
remainder = ((temp -
|
58
|
+
remainder = ((temp - base_date) / 7).to_i % skip
|
36
59
|
temp += (skip - remainder) * 7 if remainder > 0
|
37
60
|
|
38
61
|
result = temp if !result || (temp < result)
|
39
62
|
end
|
63
|
+
@position = position_of(result)
|
40
64
|
result
|
41
65
|
end
|
42
66
|
|
43
67
|
def first_occurrence_on_or_before(date)
|
44
68
|
result = nil
|
45
69
|
wday = date.wday
|
46
|
-
|
47
|
-
wd = Date::DAYNAMES.index(weekday)
|
70
|
+
wday_pattern.each do |wd|
|
48
71
|
wd = wd - 7 if wd > wday
|
49
72
|
days_in_the_past = wday - wd
|
50
73
|
temp = date - days_in_the_past
|
51
74
|
|
52
|
-
remainder = ((temp -
|
75
|
+
remainder = ((temp - base_date) / 7).to_i % skip
|
53
76
|
temp -= remainder * 7 if remainder > 0
|
54
77
|
|
55
78
|
result = temp if !result || (temp > result)
|
56
79
|
end
|
80
|
+
@position = position_of(result)
|
57
81
|
result
|
58
82
|
end
|
59
83
|
|
60
84
|
|
85
|
+
|
86
|
+
def calculate_cycle(schedule)
|
87
|
+
cycle = []
|
88
|
+
offset = wday_pattern[starting_index]
|
89
|
+
wdays = wday_pattern.map { |wday| wday - offset }.sort
|
90
|
+
|
91
|
+
while wdays.first <= 0
|
92
|
+
wdays.push (wdays.shift + 7 * skip)
|
93
|
+
end
|
94
|
+
|
95
|
+
cycle = [wdays.first]
|
96
|
+
wdays.each_cons(2) do |wday1, wday2|
|
97
|
+
cycle << (wday2 - wday1)
|
98
|
+
end
|
99
|
+
cycle
|
100
|
+
end
|
101
|
+
|
102
|
+
def position_of(date)
|
103
|
+
date_i = wday_pattern.index(date.wday)
|
104
|
+
position = date_i - starting_index
|
105
|
+
position += wday_pattern.length if position < 0
|
106
|
+
position
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
61
111
|
end
|
62
112
|
end
|
63
113
|
end
|
data/lib/hiccup/inferable.rb
CHANGED
@@ -21,8 +21,10 @@ module Hiccup
|
|
21
21
|
dates = extract_array_of_dates!(dates)
|
22
22
|
enumerator = DatesEnumerator.new(dates)
|
23
23
|
guesser = options.fetch :guesser, Guesser.new(self, options.merge(verbose: verbosity >= 2))
|
24
|
-
|
24
|
+
scorer = options.fetch(:scorer, Scorer.new(options.merge(verbose: verbosity >= 2)))
|
25
25
|
|
26
|
+
dates = []
|
27
|
+
schedules = []
|
26
28
|
confidences = []
|
27
29
|
high_confidence_threshold = 0.6
|
28
30
|
min_confidence_threshold = 0.35
|
@@ -32,10 +34,15 @@ module Hiccup
|
|
32
34
|
|
33
35
|
until enumerator.done?
|
34
36
|
date = enumerator.next
|
35
|
-
|
36
|
-
|
37
|
+
dates << date
|
38
|
+
guesses = guesser.generate_guesses(dates)
|
39
|
+
|
40
|
+
# ! can guess and confidence be nil here??
|
41
|
+
guess, confidence = scorer.pick_best_guess(guesses, dates)
|
42
|
+
|
43
|
+
confidence = confidence.to_f
|
37
44
|
confidences << confidence
|
38
|
-
predicted =
|
45
|
+
predicted = guess.predicts?(date)
|
39
46
|
|
40
47
|
# if the last two confidences are both below a certain
|
41
48
|
# threshhold and both declining, back up to where we
|
@@ -51,20 +58,20 @@ module Hiccup
|
|
51
58
|
|
52
59
|
if predicted && confidence >= min_confidence_threshold
|
53
60
|
iterations_since_last_confident_schedule = 0
|
54
|
-
last_confident_schedule =
|
61
|
+
last_confident_schedule = guess
|
55
62
|
else
|
56
63
|
iterations_since_last_confident_schedule += 1
|
57
64
|
end
|
58
65
|
|
59
|
-
rewind_by = iterations_since_last_confident_schedule ==
|
66
|
+
rewind_by = iterations_since_last_confident_schedule == dates.count ? iterations_since_last_confident_schedule - 1 : iterations_since_last_confident_schedule
|
60
67
|
|
61
68
|
|
62
69
|
|
63
70
|
if verbosity >= 1
|
64
71
|
output = " #{enumerator.index.to_s.rjust(3)} #{date}"
|
65
|
-
output << " #{"[#{
|
66
|
-
output << "~#{(
|
67
|
-
output <<
|
72
|
+
output << " #{"[#{dates.count}]".rjust(5)} => "
|
73
|
+
output << "~#{(confidence * 100).to_i.to_s.rjust(2, "0")} @ "
|
74
|
+
output << guess.humanize.ljust(130)
|
68
75
|
output << " :( move back #{rewind_by}" unless confident
|
69
76
|
puts output
|
70
77
|
end
|
@@ -76,13 +83,13 @@ module Hiccup
|
|
76
83
|
if last_confident_schedule
|
77
84
|
schedules << last_confident_schedule
|
78
85
|
elsif allow_null_schedules
|
79
|
-
|
86
|
+
dates.take(dates.count - rewind_by).each do |date|
|
80
87
|
schedules << self.new(:kind => :never, :start_date => date)
|
81
88
|
end
|
82
89
|
end
|
83
90
|
|
84
91
|
enumerator.rewind_by(rewind_by)
|
85
|
-
|
92
|
+
dates = []
|
86
93
|
confidences = []
|
87
94
|
iterations_since_last_confident_schedule = 0
|
88
95
|
last_confident_schedule = nil
|
@@ -92,7 +99,7 @@ module Hiccup
|
|
92
99
|
if last_confident_schedule
|
93
100
|
schedules << last_confident_schedule
|
94
101
|
elsif allow_null_schedules
|
95
|
-
|
102
|
+
dates.each do |date|
|
96
103
|
schedules << self.new(:kind => :never, :start_date => date)
|
97
104
|
end
|
98
105
|
end
|
@@ -9,46 +9,15 @@ module Hiccup
|
|
9
9
|
@verbose = options.fetch(:verbose, false)
|
10
10
|
@allow_skips = options.fetch(:allow_skips, true)
|
11
11
|
@max_complexity = options.fetch(:max_complexity, 3)
|
12
|
-
@scorer = options.fetch(:scorer, Scorer.new(options))
|
13
|
-
start!
|
14
12
|
end
|
15
13
|
|
16
|
-
attr_reader :
|
14
|
+
attr_reader :max_complexity
|
17
15
|
|
18
16
|
def allow_skips?
|
19
17
|
@allow_skips
|
20
18
|
end
|
21
19
|
|
22
|
-
def start!
|
23
|
-
@dates = []
|
24
|
-
@schedule = nil
|
25
|
-
@confidence = 0
|
26
|
-
end
|
27
|
-
alias :restart! :start!
|
28
|
-
|
29
|
-
|
30
|
-
|
31
20
|
|
32
|
-
def <<(date)
|
33
|
-
@dates << date
|
34
|
-
@schedule, @confidence = best_schedule_for(@dates)
|
35
|
-
date
|
36
|
-
end
|
37
|
-
|
38
|
-
def count
|
39
|
-
@dates.length
|
40
|
-
end
|
41
|
-
|
42
|
-
def predicted?(date)
|
43
|
-
@schedule && @schedule.contains?(date)
|
44
|
-
end
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def best_schedule_for(dates)
|
49
|
-
guesses = generate_guesses(dates)
|
50
|
-
scorer.pick_best_guess(guesses, dates)
|
51
|
-
end
|
52
21
|
|
53
22
|
def generate_guesses(dates)
|
54
23
|
@start_date = dates.first
|
data/lib/hiccup/version.rb
CHANGED
data/test/enumerable_test.rb
CHANGED
@@ -28,6 +28,15 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
28
28
|
assert_equal expected_dates, actual_dates
|
29
29
|
end
|
30
30
|
|
31
|
+
test "annual recurrence with a skip, starting enumeration on an off year" do
|
32
|
+
schedule = Schedule.new({
|
33
|
+
:kind => :annually,
|
34
|
+
:skip => 2,
|
35
|
+
:start_date => Date.new(2009,3,4)})
|
36
|
+
assert_equal "2011-03-04", (schedule.first_occurrence_on_or_after Date.new(2010, 03, 01)).to_s
|
37
|
+
assert_equal "2011-03-04", (schedule.first_occurrence_on_or_before Date.new(2012, 03, 01)).to_s
|
38
|
+
end
|
39
|
+
|
31
40
|
|
32
41
|
|
33
42
|
def test_occurs_on_weekly
|
@@ -79,6 +88,17 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
79
88
|
assert_equal expected_dates, dates, "occurrences_during_month did not correctly observe end date for weekly schedule"
|
80
89
|
end
|
81
90
|
|
91
|
+
test "should keep weekly occurrences during a week together when skipping" do
|
92
|
+
schedule = Schedule.new(
|
93
|
+
:kind => :weekly,
|
94
|
+
:weekly_pattern => %w{Tuesday Thursday},
|
95
|
+
:start_date => Date.new(2013, 10, 2), # Wednesday
|
96
|
+
:skip => 2)
|
97
|
+
|
98
|
+
dates = occurrences_during_month(schedule, 2013, 10).map(&:day)
|
99
|
+
assert_equal [3, 15, 17, 29, 31], dates
|
100
|
+
end
|
101
|
+
|
82
102
|
|
83
103
|
|
84
104
|
def test_monthly_occurrences_during_month
|
@@ -233,7 +253,7 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
233
253
|
:kind => :weekly,
|
234
254
|
:weekly_pattern => %w{Monday},
|
235
255
|
:skip => 3,
|
236
|
-
:start_date => Date.new(2011,1,1)})
|
256
|
+
:start_date => Date.new(2011,1,1)}) # Saturday
|
237
257
|
expected_dates = [[1,3], [1,24], [2,14], [3,7], [3,28]]
|
238
258
|
expected_dates.map! {|pair| Date.new(2011, *pair)}
|
239
259
|
assert_equal expected_dates, schedule.occurrences_between(Date.new(2011,1,1), Date.new(2011,3,31))
|
@@ -349,7 +369,7 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
349
369
|
|
350
370
|
if ENV['PERFORMANCE_TEST']
|
351
371
|
test "performance test" do
|
352
|
-
n =
|
372
|
+
n = 1000
|
353
373
|
|
354
374
|
# Each of these schedules should describe 52 events
|
355
375
|
|
data/test/inferrable_test.rb
CHANGED
@@ -262,4 +262,21 @@ class InferableTest < ActiveSupport::TestCase
|
|
262
262
|
|
263
263
|
|
264
264
|
|
265
|
+
if ENV['PERFORMANCE_TEST']
|
266
|
+
test "performance test" do
|
267
|
+
Benchmark.bm(20) do |x|
|
268
|
+
[10, 25, 50, 100, 250, 500].each do |n|
|
269
|
+
seed = Date.today
|
270
|
+
dates = (0...n).each_with_object([]) { |i, array| array << seed + i * 7 }
|
271
|
+
x.report("#{n} dates") do
|
272
|
+
Schedule.infer(dates)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
|
281
|
+
|
265
282
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class LeapYearTest < ActiveSupport::TestCase
|
5
|
+
include Hiccup
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
test "should correctly determine whether a year is a leap year or not" do
|
10
|
+
enum = Enumerable::ScheduleEnumerator.new(Schedule.new, Date.today)
|
11
|
+
|
12
|
+
assert enum.send(:leap_year?, 1988), "1988 is a leap year"
|
13
|
+
assert enum.send(:leap_year?, 2000), "2000 is a leap year"
|
14
|
+
refute enum.send(:leap_year?, 1998), "1998 is not a leap year"
|
15
|
+
refute enum.send(:leap_year?, 1900), "1900 is not a leap year"
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -0,0 +1,120 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class WeeklyEnumeratorTest < ActiveSupport::TestCase
|
5
|
+
include Hiccup
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
test "should generate a cycle of [7] for something that occurs every week on one day" do
|
10
|
+
assert_equal [7], cycle_for(
|
11
|
+
:start_date => Date.new(2013, 9, 23),
|
12
|
+
:weekly_pattern => ["Monday"])
|
13
|
+
end
|
14
|
+
|
15
|
+
test "should generate a cycle of [21] for something that occurs every _third_ week on one day" do
|
16
|
+
assert_equal [21], cycle_for(
|
17
|
+
:start_date => Date.new(2013, 9, 23),
|
18
|
+
:weekly_pattern => ["Monday"],
|
19
|
+
:skip => 3)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
test "should generate a cycle of [6, 8] for something that occurs every other Saturday and Sunday when the start date is a Sunday" do
|
25
|
+
assert_equal [6, 8], cycle_for(
|
26
|
+
:start_date => Date.new(2013, 9, 22),
|
27
|
+
:weekly_pattern => ["Saturday", "Sunday"],
|
28
|
+
:skip => 2)
|
29
|
+
end
|
30
|
+
|
31
|
+
test "should generate a cycle of [8, 6] for something that occurs every other Saturday and Sunday when the start date is a Saturday" do
|
32
|
+
assert_equal [8, 6], cycle_for(
|
33
|
+
:start_date => Date.new(2013, 9, 28),
|
34
|
+
:weekly_pattern => ["Saturday", "Sunday"],
|
35
|
+
:skip => 2)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
test "should generate a cycle of [2, 2, 10] for something that occurs every other Monday, Wednesday, Friday when the start date is a Monday" do
|
41
|
+
assert_equal [2, 2, 10], cycle_for(
|
42
|
+
:start_date => Date.new(2013, 9, 23),
|
43
|
+
:weekly_pattern => ["Monday", "Wednesday", "Friday"],
|
44
|
+
:skip => 2)
|
45
|
+
end
|
46
|
+
|
47
|
+
test "should generate a cycle of [2, 10, 2] for something that occurs every other Monday, Wednesday, Friday when the start date is a Wednesday" do
|
48
|
+
assert_equal [2, 10, 2], cycle_for(
|
49
|
+
:start_date => Date.new(2013, 9, 25),
|
50
|
+
:weekly_pattern => ["Monday", "Wednesday", "Friday"],
|
51
|
+
:skip => 2)
|
52
|
+
end
|
53
|
+
|
54
|
+
test "should generate a cycle of [10, 2, 2] for something that occurs every other Monday, Wednesday, Friday when the start date is a Friday" do
|
55
|
+
assert_equal [10, 2, 2], cycle_for(
|
56
|
+
:start_date => Date.new(2013, 9, 27),
|
57
|
+
:weekly_pattern => ["Monday", "Wednesday", "Friday"],
|
58
|
+
:skip => 2)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
test "should generate a cycle of [2, 5] for something that occurs every Tuesday and Thursday when the start date is a Friday" do
|
64
|
+
assert_equal [2, 5], cycle_for(
|
65
|
+
:start_date => Date.new(2013, 9, 27),
|
66
|
+
:weekly_pattern => ["Tuesday", "Thursday"])
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
context "#position_of" do
|
72
|
+
setup do
|
73
|
+
@schedule = Schedule.new(
|
74
|
+
:kind => :weekly,
|
75
|
+
:start_date => Date.new(2013, 9, 26), # Thursday
|
76
|
+
:weekly_pattern => ["Tuesday", "Thursday", "Friday"],
|
77
|
+
:skip => 2)
|
78
|
+
end
|
79
|
+
|
80
|
+
should "be a sane test" do
|
81
|
+
assert_equal [1, 11, 2], cycle_for(@schedule)
|
82
|
+
end
|
83
|
+
|
84
|
+
should "find the correct position for the given date" do
|
85
|
+
assert_equal 0, position_of(@schedule, 2013, 9, 26)
|
86
|
+
assert_equal 1, position_of(@schedule, 2013, 9, 27)
|
87
|
+
assert_equal 2, position_of(@schedule, 2013, 10, 8)
|
88
|
+
assert_equal 0, position_of(@schedule, 2013, 10, 10)
|
89
|
+
assert_equal 1, position_of(@schedule, 2013, 10, 11)
|
90
|
+
assert_equal 2, position_of(@schedule, 2013, 10, 22)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def cycle_for(options={})
|
99
|
+
schedule = build_schedule(options)
|
100
|
+
enumerator = schedule.enumerator.new(schedule, Date.today)
|
101
|
+
enumerator.send :calculate_cycle, schedule
|
102
|
+
end
|
103
|
+
|
104
|
+
def position_of(schedule, *args)
|
105
|
+
date = build_date(*args)
|
106
|
+
enumerator = schedule.enumerator.new(schedule, date)
|
107
|
+
enumerator.send :position_of, date
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_schedule(options={})
|
111
|
+
return options if options.is_a? Schedule
|
112
|
+
Schedule.new(options.merge(:kind => :weekly))
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_date(*args)
|
116
|
+
return Date.new(*args) if args.length == 3
|
117
|
+
args.first
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hiccup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Lail
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-09-
|
11
|
+
date: 2013-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - '>='
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: shoulda-context
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: pry
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -131,6 +145,7 @@ files:
|
|
131
145
|
- lib/hiccup/core_ext/hash.rb
|
132
146
|
- lib/hiccup/enumerable.rb
|
133
147
|
- lib/hiccup/enumerable/annually_enumerator.rb
|
148
|
+
- lib/hiccup/enumerable/monthly_date_enumerator.rb
|
134
149
|
- lib/hiccup/enumerable/monthly_enumerator.rb
|
135
150
|
- lib/hiccup/enumerable/never_enumerator.rb
|
136
151
|
- lib/hiccup/enumerable/schedule_enumerator.rb
|
@@ -152,9 +167,11 @@ files:
|
|
152
167
|
- test/humanizable_test.rb
|
153
168
|
- test/ical_serializable_test.rb
|
154
169
|
- test/inferrable_test.rb
|
170
|
+
- test/leap_year_test.rb
|
155
171
|
- test/performance_test.rb
|
156
172
|
- test/test_helper.rb
|
157
173
|
- test/validatable_test.rb
|
174
|
+
- test/weekly_enumerator_test_test.rb
|
158
175
|
homepage: http://boblail.github.com/hiccup/
|
159
176
|
licenses: []
|
160
177
|
metadata: {}
|
@@ -185,6 +202,8 @@ test_files:
|
|
185
202
|
- test/humanizable_test.rb
|
186
203
|
- test/ical_serializable_test.rb
|
187
204
|
- test/inferrable_test.rb
|
205
|
+
- test/leap_year_test.rb
|
188
206
|
- test/performance_test.rb
|
189
207
|
- test/test_helper.rb
|
190
208
|
- test/validatable_test.rb
|
209
|
+
- test/weekly_enumerator_test_test.rb
|