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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 358dd23836a721b455842348afbc112ec39d761f
4
- data.tar.gz: 5f54eaa0f43aa11de031123267ee373f54a11b10
3
+ metadata.gz: 7d10125513beb88e0c6c96db75cc928db898ad2f
4
+ data.tar.gz: fc07262a4ea66ef9d964f69e5a9d69971c2ee557
5
5
  SHA512:
6
- metadata.gz: c521bbe940e1d55728835b5d4e9787868f5586af64254f61c4a98a679874a963e8774b3aba260d596ee3c72fac7ccf32ec60cb5b1ef37ace51380d1e7dbe7238
7
- data.tar.gz: caa85c43bcdcaf32653d6c408d431f932a00c166b4ac6150ae95c7acf86185049000efc276ba3a1e934d1664c928f29882dbbc20fa83922590a11562f68197eb
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")
@@ -20,7 +20,8 @@ module Hiccup
20
20
 
21
21
 
22
22
  def ends?
23
- (ends == true) || %w{true 1 t}.member?(ends)
23
+ return %w{true 1 t}.member?(ends) if ends.is_a?(String)
24
+ !!ends
24
25
  end
25
26
 
26
27
 
@@ -11,16 +11,7 @@ module Hiccup
11
11
 
12
12
 
13
13
  def enumerator
14
- "Hiccup::Enumerable::#{kind.to_s.classify}Enumerator".constantize
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
- # Use more efficient iterator methods unless
12
- # we have to care about leap years
13
-
14
- unless start_date.month == 2 && start_date.day == 29
15
- def self.next_occurrence_after(date)
16
- date.next_year(skip)
17
- end
18
-
19
- def self.next_occurrence_before(date)
20
- date.prev_year(skip)
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, month, day = date.year, start_date.month, start_date.day
28
- day = -1 if month == 2 && day == 29
37
+ @year = date.year
38
+ @year += skip if (date.month > month) or (date.month == month and date.day > day)
29
39
 
30
- result = Date.new(year, month, day)
31
- year += 1 if result < date
40
+ remainder = (@year - start_date.year) % skip
41
+ @year += (skip - remainder) if remainder > 0
32
42
 
33
- remainder = (year - start_date.year) % skip
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, month, day = date.year, start_date.month, start_date.day
41
- day = -1 if month == 2 && day == 29
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
- # what if year is before start_date.year?
47
- remainder = (year - start_date.year) % skip
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
@@ -0,0 +1,14 @@
1
+ require 'hiccup/enumerable/schedule_enumerator'
2
+
3
+ module Hiccup
4
+ module Enumerable
5
+ class MonthlyDateEnumerator < MonthlyEnumerator
6
+ protected
7
+
8
+ def occurrences_in_month(year, month)
9
+ monthly_pattern
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -4,81 +4,117 @@ module Hiccup
4
4
  module Enumerable
5
5
  class MonthlyEnumerator < ScheduleEnumerator
6
6
 
7
-
8
- def first_occurrence_on_or_after(date)
9
- result = nil
10
- monthly_pattern.each do |occurrence|
11
- temp = nil
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
- def first_occurrence_on_or_before(date)
28
- result = nil
29
- monthly_pattern.each do |occurrence|
30
- temp = nil
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
- def shift_date_by_months(date, months)
51
- date.next_month(months)
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
- def monthly_occurrence_to_date(occurrence, date)
56
- year, month = date.year, date.month
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 = begin
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
- def months_between(later_date, earlier_date)
78
- ((later_date.year - earlier_date.year) * 12) + (later_date.month - earlier_date.month).to_int
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 initialize(schedule, date)
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
- @date = date
9
- @date = @date.to_date if @date.respond_to?(:to_date)
10
- @date = start_date if (@date < start_date)
11
- @date = end_date if (ends? && @date > end_date)
12
- @current_date = nil
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
- @current_date = if @current_date
22
- next_occurrence_after(@current_date)
23
- else
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
- @current_date = if @current_date
32
- next_occurrence_before(@current_date)
33
- else
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
- # These two methods DO NOT assume that
43
- # date is predicted by the given schedule
45
+ def started?
46
+ !@cursor.nil?
47
+ end
44
48
 
45
- def first_occurrence_on_or_after(date)
46
- raise NotImplementedError
49
+ def ends?
50
+ @ends
47
51
  end
48
52
 
49
- def first_occurrence_on_or_before(date)
50
- raise NotImplementedError
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 next_occurrence_after(date)
58
- first_occurrence_on_or_after(date + 1)
93
+ def first_occurrence_on_or_after(date)
94
+ raise NotImplementedError
59
95
  end
60
96
 
61
- def next_occurrence_before(date)
62
- first_occurrence_on_or_before(date - 1)
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
- # Use more efficient iterator methods if
12
- # weekly_pattern is simple enough
10
+ @wday_pattern = weekly_pattern.map do |weekday|
11
+ Date::DAYNAMES.index(weekday)
12
+ end.sort
13
13
 
14
- if weekly_pattern.length == 1
15
- def self.next_occurrence_after(date)
16
- date + skip * 7
17
- end
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
- weekly_pattern.each do |weekday|
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 - start_date) / 7).to_i % skip
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
- weekly_pattern.each do |weekday|
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 - start_date) / 7).to_i % skip
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
@@ -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
- schedules = []
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
- guesser << date
36
- confidence = guesser.confidence.to_f
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 = guesser.predicted?(date)
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 = guesser.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 == guesser.count ? iterations_since_last_confident_schedule - 1 : 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 << " #{"[#{guesser.count}]".rjust(5)} => "
66
- output << "~#{(guesser.confidence.to_f * 100).to_i.to_s.rjust(2, "0")} @ "
67
- output << guesser.schedule.humanize.ljust(130)
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
- guesser.dates.take(guesser.count - rewind_by).each do |date|
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
- guesser.restart!
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
- guesser.dates.each do |date|
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 :confidence, :schedule, :dates, :scorer, :max_complexity
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
@@ -1,3 +1,3 @@
1
1
  module Hiccup
2
- VERSION = "0.4.5"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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 = 100
372
+ n = 1000
353
373
 
354
374
  # Each of these schedules should describe 52 events
355
375
 
@@ -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
@@ -5,4 +5,5 @@ require "rails/test_help"
5
5
  require "active_support/core_ext"
6
6
  require "turn"
7
7
  require "hiccup/schedule"
8
+ require "shoulda/context"
8
9
  require "pry"
@@ -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.5
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-24 00:00:00.000000000 Z
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