hiccup 0.5.14 → 0.5.15

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