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