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 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