hiccup 0.5.14 → 0.5.15

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hiccup.rb +17 -17
  3. data/lib/hiccup/convenience.rb +9 -9
  4. data/lib/hiccup/core_ext/date.rb +7 -7
  5. data/lib/hiccup/core_ext/duration.rb +4 -4
  6. data/lib/hiccup/core_ext/enumerable.rb +2 -2
  7. data/lib/hiccup/core_ext/fixnum.rb +2 -2
  8. data/lib/hiccup/core_ext/hash.rb +2 -2
  9. data/lib/hiccup/enumerable.rb +43 -29
  10. data/lib/hiccup/enumerable/annually_enumerator.rb +23 -23
  11. data/lib/hiccup/enumerable/monthly_date_enumerator.rb +2 -2
  12. data/lib/hiccup/enumerable/monthly_enumerator.rb +40 -40
  13. data/lib/hiccup/enumerable/never_enumerator.rb +8 -8
  14. data/lib/hiccup/enumerable/schedule_enumerator.rb +39 -39
  15. data/lib/hiccup/enumerable/weekly_enumerator.rb +30 -30
  16. data/lib/hiccup/errors.rb +4 -0
  17. data/lib/hiccup/humanizable.rb +19 -19
  18. data/lib/hiccup/inferable.rb +30 -30
  19. data/lib/hiccup/inferable/dates_enumerator.rb +6 -6
  20. data/lib/hiccup/inferable/guesser.rb +21 -21
  21. data/lib/hiccup/inferable/score.rb +7 -7
  22. data/lib/hiccup/inferable/scorer.rb +19 -19
  23. data/lib/hiccup/schedule.rb +10 -10
  24. data/lib/hiccup/serializable/ical.rb +13 -13
  25. data/lib/hiccup/serializers/ical.rb +59 -59
  26. data/lib/hiccup/validatable.rb +23 -23
  27. data/lib/hiccup/version.rb +1 -1
  28. data/test/core_ext_date_test.rb +5 -5
  29. data/test/duration_ext_test.rb +8 -8
  30. data/test/enumerable_test.rb +103 -103
  31. data/test/humanizable_test.rb +24 -24
  32. data/test/ical_serializable_test.rb +29 -29
  33. data/test/inferrable_test.rb +84 -84
  34. data/test/leap_year_test.rb +7 -7
  35. data/test/monthly_enumerator_test.rb +13 -13
  36. data/test/performance_test.rb +7 -7
  37. data/test/validatable_test.rb +1 -1
  38. data/test/weekly_enumerator_test.rb +38 -38
  39. metadata +4 -3
@@ -0,0 +1,4 @@
1
+ module Hiccup
2
+ class UnboundedEnumerationError < RuntimeError
3
+ end
4
+ end
@@ -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
@@ -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