hiccup 0.5.14 → 0.5.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/hiccup.rb +17 -17
- data/lib/hiccup/convenience.rb +9 -9
- data/lib/hiccup/core_ext/date.rb +7 -7
- data/lib/hiccup/core_ext/duration.rb +4 -4
- data/lib/hiccup/core_ext/enumerable.rb +2 -2
- data/lib/hiccup/core_ext/fixnum.rb +2 -2
- data/lib/hiccup/core_ext/hash.rb +2 -2
- data/lib/hiccup/enumerable.rb +43 -29
- data/lib/hiccup/enumerable/annually_enumerator.rb +23 -23
- data/lib/hiccup/enumerable/monthly_date_enumerator.rb +2 -2
- data/lib/hiccup/enumerable/monthly_enumerator.rb +40 -40
- data/lib/hiccup/enumerable/never_enumerator.rb +8 -8
- data/lib/hiccup/enumerable/schedule_enumerator.rb +39 -39
- data/lib/hiccup/enumerable/weekly_enumerator.rb +30 -30
- data/lib/hiccup/errors.rb +4 -0
- data/lib/hiccup/humanizable.rb +19 -19
- data/lib/hiccup/inferable.rb +30 -30
- data/lib/hiccup/inferable/dates_enumerator.rb +6 -6
- data/lib/hiccup/inferable/guesser.rb +21 -21
- data/lib/hiccup/inferable/score.rb +7 -7
- data/lib/hiccup/inferable/scorer.rb +19 -19
- data/lib/hiccup/schedule.rb +10 -10
- data/lib/hiccup/serializable/ical.rb +13 -13
- data/lib/hiccup/serializers/ical.rb +59 -59
- data/lib/hiccup/validatable.rb +23 -23
- data/lib/hiccup/version.rb +1 -1
- data/test/core_ext_date_test.rb +5 -5
- data/test/duration_ext_test.rb +8 -8
- data/test/enumerable_test.rb +103 -103
- data/test/humanizable_test.rb +24 -24
- data/test/ical_serializable_test.rb +29 -29
- data/test/inferrable_test.rb +84 -84
- data/test/leap_year_test.rb +7 -7
- data/test/monthly_enumerator_test.rb +13 -13
- data/test/performance_test.rb +7 -7
- data/test/validatable_test.rb +1 -1
- data/test/weekly_enumerator_test.rb +38 -38
- metadata +4 -3
data/lib/hiccup/humanizable.rb
CHANGED
@@ -5,9 +5,9 @@ require "hiccup/core_ext/fixnum"
|
|
5
5
|
module Hiccup
|
6
6
|
module Humanizable
|
7
7
|
include Convenience
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
|
9
|
+
|
10
|
+
|
11
11
|
def humanize(format: "%Y-%m-%d")
|
12
12
|
case kind
|
13
13
|
when :never; start_date.strftime(format)
|
@@ -17,13 +17,13 @@ module Hiccup
|
|
17
17
|
else; "Invalid"
|
18
18
|
end
|
19
19
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
|
21
|
+
|
22
|
+
|
23
23
|
private
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
|
25
|
+
|
26
|
+
|
27
27
|
def weekly_humanize
|
28
28
|
weekdays_sentece = weekly_pattern.map(&:humanize).to_sentence
|
29
29
|
if skip == 1 || weekly_pattern.length == 1
|
@@ -32,18 +32,18 @@ module Hiccup
|
|
32
32
|
sentence(weekdays_sentece, "of every", ordinal, "week")
|
33
33
|
end
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
def monthly_humanize
|
37
37
|
monthly_occurrences = monthly_pattern.map(&method(:monthly_occurrence_to_s)).to_sentence
|
38
38
|
sentence("The", monthly_occurrences, "of every", ordinal, "month")
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def yearly_humanize
|
42
42
|
sentence("Every", ordinal, "year on", self.start_date.strftime('%B'), self.start_date.strftime('%e').strip)
|
43
43
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
|
45
|
+
|
46
|
+
|
47
47
|
def monthly_occurrence_to_s(monthly_occurrence)
|
48
48
|
if monthly_occurrence.is_a?(Array)
|
49
49
|
_skip, weekday = monthly_occurrence
|
@@ -53,16 +53,16 @@ module Hiccup
|
|
53
53
|
monthly_occurrence < 0 ? "last day" : monthly_occurrence.ordinalize
|
54
54
|
end
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
def ordinal
|
58
58
|
skip && skip.human_ordinalize(1 => nil, 2 => "other")
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
def sentence(*array)
|
62
62
|
array.compact.join(" ")
|
63
63
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
|
65
|
+
|
66
|
+
|
67
67
|
end
|
68
68
|
end
|
data/lib/hiccup/inferable.rb
CHANGED
@@ -11,43 +11,43 @@ require 'hiccup/inferable/score'
|
|
11
11
|
module Hiccup
|
12
12
|
module Inferable
|
13
13
|
extend ActiveSupport::Concern
|
14
|
-
|
14
|
+
|
15
15
|
module ClassMethods
|
16
|
-
|
16
|
+
|
17
17
|
def infer(dates, options={})
|
18
18
|
allow_null_schedules = options.fetch(:allow_null_schedules, false)
|
19
19
|
verbosity = options.fetch(:verbosity, (options[:verbose] ? 1 : 0)) # 0, 1, or 2
|
20
|
-
|
20
|
+
|
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
26
|
dates = []
|
27
27
|
schedules = []
|
28
28
|
confidences = []
|
29
29
|
high_confidence_threshold = 0.6
|
30
30
|
min_confidence_threshold = 0.35
|
31
|
-
|
31
|
+
|
32
32
|
last_confident_schedule = nil
|
33
33
|
iterations_since_last_confident_schedule = 0
|
34
|
-
|
34
|
+
|
35
35
|
until enumerator.done?
|
36
36
|
date = enumerator.next
|
37
37
|
dates << date
|
38
38
|
guesses = guesser.generate_guesses(dates)
|
39
|
-
|
39
|
+
|
40
40
|
# ! can guess and confidence be nil here??
|
41
41
|
guess, confidence = scorer.pick_best_guess(guesses, dates)
|
42
|
-
|
42
|
+
|
43
43
|
confidence = confidence.to_f
|
44
44
|
confidences << confidence
|
45
45
|
predicted = guess.predicts?(date)
|
46
|
-
|
46
|
+
|
47
47
|
# if the last two confidences are both below a certain
|
48
48
|
# threshhold and both declining, back up to where we
|
49
49
|
# started to go wrong and start a new schedule.
|
50
|
-
|
50
|
+
|
51
51
|
confident = !(confidences.length >= 3 && (
|
52
52
|
(confidences[-1] < high_confidence_threshold &&
|
53
53
|
confidences[-2] < high_confidence_threshold &&
|
@@ -55,18 +55,18 @@ module Hiccup
|
|
55
55
|
confidences[-2] < confidences[-3]) ||
|
56
56
|
(confidences[-1] < min_confidence_threshold &&
|
57
57
|
confidences[-2] < min_confidence_threshold)))
|
58
|
-
|
58
|
+
|
59
59
|
if predicted && confidence >= min_confidence_threshold
|
60
60
|
iterations_since_last_confident_schedule = 0
|
61
61
|
last_confident_schedule = guess
|
62
62
|
else
|
63
63
|
iterations_since_last_confident_schedule += 1
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
rewind_by = iterations_since_last_confident_schedule == dates.count ? iterations_since_last_confident_schedule - 1 : iterations_since_last_confident_schedule
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
|
68
|
+
|
69
|
+
|
70
70
|
if verbosity >= 1
|
71
71
|
output = " #{enumerator.index.to_s.rjust(3)} #{date}"
|
72
72
|
output << " #{"[#{dates.count}]".rjust(5)} => "
|
@@ -75,11 +75,11 @@ module Hiccup
|
|
75
75
|
output << " :( move back #{rewind_by}" unless confident
|
76
76
|
puts output
|
77
77
|
end
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
|
79
|
+
|
80
|
+
|
81
81
|
unless confident
|
82
|
-
|
82
|
+
|
83
83
|
if last_confident_schedule
|
84
84
|
schedules << last_confident_schedule
|
85
85
|
elsif allow_null_schedules
|
@@ -87,7 +87,7 @@ module Hiccup
|
|
87
87
|
schedules << self.new(:kind => :never, :start_date => date)
|
88
88
|
end
|
89
89
|
end
|
90
|
-
|
90
|
+
|
91
91
|
enumerator.rewind_by(rewind_by)
|
92
92
|
dates = []
|
93
93
|
confidences = []
|
@@ -95,7 +95,7 @@ module Hiccup
|
|
95
95
|
last_confident_schedule = nil
|
96
96
|
end
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
if last_confident_schedule
|
100
100
|
schedules << last_confident_schedule
|
101
101
|
elsif allow_null_schedules
|
@@ -103,28 +103,28 @@ module Hiccup
|
|
103
103
|
schedules << self.new(:kind => :never, :start_date => date)
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
schedules
|
108
108
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
|
110
|
+
|
111
|
+
|
112
112
|
def extract_array_of_dates!(dates)
|
113
113
|
raise_invalid_dates_error! unless dates.respond_to?(:each)
|
114
114
|
dates.map { |date| assert_date!(date) }.sort
|
115
115
|
end
|
116
|
-
|
116
|
+
|
117
117
|
def assert_date!(date)
|
118
118
|
return date if date.is_a?(Date)
|
119
119
|
date.to_date rescue raise_invalid_dates_error!
|
120
120
|
end
|
121
|
-
|
121
|
+
|
122
122
|
def raise_invalid_dates_error!
|
123
123
|
raise ArgumentError.new("Inferable.infer expects to receive a collection of dates")
|
124
124
|
end
|
125
|
-
|
126
|
-
|
127
|
-
|
125
|
+
|
126
|
+
|
127
|
+
|
128
128
|
end
|
129
129
|
end
|
130
130
|
end
|
@@ -1,30 +1,30 @@
|
|
1
1
|
module Hiccup
|
2
2
|
module Inferable
|
3
3
|
class DatesEnumerator
|
4
|
-
|
4
|
+
|
5
5
|
def initialize(dates)
|
6
6
|
@dates = dates
|
7
7
|
@last_index = @dates.length - 1
|
8
8
|
@index = -1
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
attr_reader :index
|
12
|
-
|
12
|
+
|
13
13
|
def done?
|
14
14
|
@index == @last_index
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def next
|
18
18
|
@index += 1
|
19
19
|
raise OutOfRangeException if @index > @last_index
|
20
20
|
@dates[@index]
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def rewind_by(n)
|
24
24
|
@index -= n
|
25
25
|
@index = -1 if @index < -1
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -3,22 +3,22 @@ require 'hiccup/inferable/scorer'
|
|
3
3
|
module Hiccup
|
4
4
|
module Inferable
|
5
5
|
class Guesser
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(klass, options={})
|
8
8
|
@klass = klass
|
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
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
attr_reader :max_complexity
|
15
|
-
|
15
|
+
|
16
16
|
def allow_skips?
|
17
17
|
@allow_skips
|
18
18
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
|
20
|
+
|
21
|
+
|
22
22
|
def generate_guesses(dates)
|
23
23
|
@start_date = dates.first
|
24
24
|
@end_date = dates.last
|
@@ -28,7 +28,7 @@ module Hiccup
|
|
28
28
|
guesses.concat generate_weekly_guesses(dates)
|
29
29
|
end
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
def generate_yearly_guesses(dates)
|
33
33
|
histogram_of_patterns = dates.to_histogram do |date|
|
34
34
|
[date.month, date.day]
|
@@ -37,7 +37,7 @@ module Hiccup
|
|
37
37
|
highest_popularity = patterns_by_popularity.keys.max # => 5
|
38
38
|
most_popular = patterns_by_popularity[highest_popularity].first # => a
|
39
39
|
start_date = Date.new(@start_date.year, *most_popular)
|
40
|
-
|
40
|
+
|
41
41
|
[].tap do |guesses|
|
42
42
|
skip_range.each do |skip|
|
43
43
|
guesses << @klass.new.tap do |schedule|
|
@@ -49,16 +49,16 @@ module Hiccup
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
def generate_monthly_guesses(dates)
|
54
54
|
histogram_of_patterns = dates.to_histogram do |date|
|
55
55
|
[date.get_nth_wday_of_month, Date::DAYNAMES[date.wday]]
|
56
56
|
end
|
57
57
|
patterns_by_popularity = histogram_of_patterns.flip
|
58
|
-
|
58
|
+
|
59
59
|
histogram_of_days = dates.to_histogram(&:day)
|
60
60
|
days_by_popularity = histogram_of_days.flip
|
61
|
-
|
61
|
+
|
62
62
|
if @verbose
|
63
63
|
puts "",
|
64
64
|
" monthly analysis:",
|
@@ -68,7 +68,7 @@ module Hiccup
|
|
68
68
|
" histogram (day): #{histogram_of_days.inspect}",
|
69
69
|
" by_popularity (day): #{days_by_popularity.inspect}"
|
70
70
|
end
|
71
|
-
|
71
|
+
|
72
72
|
[].tap do |guesses|
|
73
73
|
skip_range.each do |skip|
|
74
74
|
enumerate_by_popularity(days_by_popularity) do |days|
|
@@ -81,7 +81,7 @@ module Hiccup
|
|
81
81
|
schedule.monthly_pattern = days
|
82
82
|
end
|
83
83
|
end
|
84
|
-
|
84
|
+
|
85
85
|
enumerate_by_popularity(patterns_by_popularity) do |patterns|
|
86
86
|
next if patterns.length > max_complexity
|
87
87
|
guesses << @klass.new.tap do |schedule|
|
@@ -95,14 +95,14 @@ module Hiccup
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def generate_weekly_guesses(dates)
|
100
100
|
[].tap do |guesses|
|
101
101
|
histogram_of_wdays = dates.to_histogram do |date|
|
102
102
|
Date::DAYNAMES[date.wday]
|
103
103
|
end
|
104
104
|
wdays_by_popularity = histogram_of_wdays.flip
|
105
|
-
|
105
|
+
|
106
106
|
if @verbose
|
107
107
|
puts "",
|
108
108
|
" weekly analysis:",
|
@@ -110,7 +110,7 @@ module Hiccup
|
|
110
110
|
" histogram: #{histogram_of_wdays.inspect}",
|
111
111
|
" by_popularity: #{wdays_by_popularity.inspect}"
|
112
112
|
end
|
113
|
-
|
113
|
+
|
114
114
|
skip_range.each do |skip|
|
115
115
|
enumerate_by_popularity(wdays_by_popularity) do |wdays|
|
116
116
|
next if wdays.length > max_complexity
|
@@ -125,14 +125,14 @@ module Hiccup
|
|
125
125
|
end
|
126
126
|
end
|
127
127
|
end
|
128
|
-
|
128
|
+
|
129
129
|
def skip_range
|
130
130
|
return 1..1 unless allow_skips?
|
131
131
|
1...5
|
132
132
|
end
|
133
|
-
|
134
|
-
|
135
|
-
|
133
|
+
|
134
|
+
|
135
|
+
|
136
136
|
# Expects a hash of values grouped by popularity
|
137
137
|
# Yields the most popular values first, and then
|
138
138
|
# increasingly less popular values
|
@@ -143,7 +143,7 @@ module Hiccup
|
|
143
143
|
yield values_by_popularity.values_at(*at_popularities).flatten(1)
|
144
144
|
end
|
145
145
|
end
|
146
|
-
|
146
|
+
|
147
147
|
end
|
148
148
|
end
|
149
149
|
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
module Hiccup
|
2
2
|
module Inferable
|
3
|
-
|
3
|
+
|
4
4
|
class Score < Struct.new(:prediction_rate, :brick_rate, :complexity_rate)
|
5
|
-
|
5
|
+
|
6
6
|
# as brick rate rises, our confidence in this guess drops
|
7
7
|
def brick_penalty
|
8
8
|
brick_penalty = brick_rate * 0.33
|
9
9
|
brick_penalty = 1 if brick_penalty > 1
|
10
10
|
brick_penalty
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
# as the complexity rises, our confidence in this guess drops
|
14
14
|
# this hash table is a stand-in for a proper formala
|
15
15
|
#
|
@@ -18,7 +18,7 @@ module Hiccup
|
|
18
18
|
def complexity_penalty
|
19
19
|
complexity_rate
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
# our confidence is weakened by bricks and complexity
|
23
23
|
def confidence
|
24
24
|
confidence = 1.0
|
@@ -26,13 +26,13 @@ module Hiccup
|
|
26
26
|
confidence *= (1 - complexity_penalty)
|
27
27
|
confidence
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
# a number between 0 and 1
|
31
31
|
def to_f
|
32
32
|
prediction_rate * confidence
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
end
|
38
38
|
end
|