timely 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Gemfile +13 -0
  2. data/HISTORY.md +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +55 -0
  5. data/Rakefile +150 -4
  6. data/lib/timely/date.rb +3 -3
  7. data/lib/timely/date_chooser.rb +98 -0
  8. data/lib/timely/date_range.rb +51 -0
  9. data/lib/timely/date_time.rb +11 -0
  10. data/lib/timely/rails/date_group.rb +115 -0
  11. data/lib/timely/rails/extensions.rb +43 -0
  12. data/lib/timely/rails/season.rb +99 -0
  13. data/lib/timely/rails.rb +4 -0
  14. data/lib/timely/range.rb +15 -0
  15. data/lib/timely/string.rb +25 -0
  16. data/lib/timely/temporal_patterns.rb +441 -0
  17. data/lib/timely/time.rb +5 -4
  18. data/lib/timely/trackable_date_set.rb +148 -0
  19. data/lib/timely/week_days.rb +128 -0
  20. data/lib/timely.rb +15 -6
  21. data/rails/init.rb +1 -0
  22. data/spec/date_chooser_spec.rb +101 -0
  23. data/spec/date_group_spec.rb +26 -0
  24. data/spec/date_range_spec.rb +40 -0
  25. data/spec/date_spec.rb +15 -15
  26. data/spec/schema.rb +11 -0
  27. data/spec/season_spec.rb +68 -0
  28. data/spec/spec_helper.rb +41 -18
  29. data/spec/string_spec.rb +13 -0
  30. data/spec/time_spec.rb +28 -9
  31. data/spec/trackable_date_set_spec.rb +80 -0
  32. data/spec/week_days_spec.rb +51 -0
  33. data/timely.gemspec +99 -0
  34. metadata +61 -61
  35. data/History.txt +0 -4
  36. data/License.txt +0 -20
  37. data/Manifest.txt +0 -23
  38. data/README.txt +0 -31
  39. data/config/hoe.rb +0 -73
  40. data/config/requirements.rb +0 -15
  41. data/lib/timely/version.rb +0 -9
  42. data/script/console +0 -10
  43. data/script/destroy +0 -14
  44. data/script/generate +0 -14
  45. data/setup.rb +0 -1585
  46. data/spec/spec.opts +0 -1
  47. data/tasks/deployment.rake +0 -34
  48. data/tasks/environment.rake +0 -7
  49. data/tasks/rspec.rake +0 -21
  50. data/tasks/website.rake +0 -9
@@ -0,0 +1,15 @@
1
+ module Timely
2
+ module Range
3
+ def to_date_range
4
+ DateRange.new(self.first, self.last)
5
+ end
6
+
7
+ def days_from(date = Date.today)
8
+ (date + self.first)..(date + self.last)
9
+ end
10
+ end
11
+ end
12
+
13
+ class Range
14
+ include Timely::Range
15
+ end
@@ -0,0 +1,25 @@
1
+ module Timely
2
+ module String
3
+ # fmt e.g. '%d/%m/%Y'
4
+ # By default it will try to guess the format
5
+ # If using ActiveSupport you can pass in a symbol for the DATE_FORMATS
6
+ def to_date(fmt = nil)
7
+ if fmt
8
+ fmt = Date::DATE_FORMATS[fmt] if fmt.is_a?(Symbol) && defined?(Date::DATE_FORMATS)
9
+ parsed = ::Date._strptime(self, fmt)
10
+ parsed[:year] = parsed[:year] + 2000 if parsed[:year] < 1000
11
+ ::Date.new(*parsed.values_at(:year, :mon, :mday))
12
+ else
13
+ ::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday))
14
+ end
15
+ rescue NoMethodError, ArgumentError => e
16
+ raise DateFormatException, "Date #{self} is invalid or not formatted correctly."
17
+ end
18
+ end
19
+
20
+ class DateFormatException < Exception; end
21
+ end
22
+
23
+ class String
24
+ include Timely::String
25
+ end
@@ -0,0 +1,441 @@
1
+ module TemporalPatterns
2
+ class Frequency
3
+ UNITS = [:second, :minute, :hour, :day, :week, :fortnight, :month, :year]
4
+
5
+ attr_accessor :duration
6
+
7
+ def initialize(duration)
8
+ self.duration = duration
9
+ end
10
+
11
+ def duration=(duration)
12
+ raise ArgumentError, "Frequency (#{duration}) must be a duration" unless duration.is_a?(ActiveSupport::Duration)
13
+ raise ArgumentError, "Frequency (#{duration}) must be positive" unless duration > 0
14
+ @duration = self.class.parse(duration)
15
+ end
16
+
17
+ def <=>(other)
18
+ self.duration <=> other.duration
19
+ end
20
+
21
+ def to_s
22
+ "every #{duration.inspect}"
23
+ end
24
+
25
+ def unit
26
+ parts = self.class.decompose(duration)
27
+ parts.size == 1 && parts.first.last == 1? parts.first.first : nil
28
+ end
29
+
30
+ def min_unit
31
+ parts = self.class.decompose(duration)
32
+ {parts.last.first => parts.last.last}
33
+ end
34
+
35
+ def max_unit
36
+ parts = self.class.decompose(duration)
37
+ {parts.first.first => parts.first.last}
38
+ end
39
+
40
+ def units
41
+ self.class.decompose_to_hash(duration)
42
+ end
43
+
44
+ class << self
45
+ def singular_units
46
+ UNITS.dup
47
+ end
48
+
49
+ def plural_units
50
+ UNITS.map { |unit| unit.to_s.pluralize.to_sym }
51
+ end
52
+
53
+ def unit_durations
54
+ UNITS.map { |unit| 1.call(unit) }
55
+ end
56
+
57
+ def valid_units
58
+ singular_units + plural_units
59
+ end
60
+
61
+ def valid_unit?(unit)
62
+ valid_units.include?(unit.to_s.to_sym)
63
+ end
64
+
65
+ def parse(duration)
66
+ parsed = 0.seconds
67
+ decompose(duration).each do |part|
68
+ parsed += part.last.send(part.first)
69
+ end
70
+ parsed
71
+ end
72
+
73
+ def decompose(duration)
74
+ whole = duration
75
+ parts = []
76
+ plural_units.reverse_each do |unit|
77
+ if whole >= (one_unit = 1.send(unit))
78
+ current_unit_value = ((whole / one_unit).floor)
79
+ if current_unit_value > 0
80
+ parts << [unit, current_unit_value]
81
+ whole -= current_unit_value.send(unit)
82
+ end
83
+ end
84
+ end
85
+ parts
86
+ end
87
+
88
+ def decompose_to_hash(duration)
89
+ decompose(duration).inject({}) do |hash, unit|
90
+ hash[unit.first] = unit.last
91
+ hash
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def method_missing(method, *args, &block) #:nodoc:
99
+ duration.send(method, *args, &block)
100
+ end
101
+ end
102
+
103
+ class Interval
104
+ attr_accessor :first_datetime, :last_datetime
105
+
106
+ def initialize(first_datetime, last_datetime = nil)
107
+ self.first_datetime = first_datetime
108
+ self.last_datetime = last_datetime || first_datetime
109
+ end
110
+
111
+ def first_datetime=(first_datetime)
112
+ @first_datetime = first_datetime.to_datetime
113
+ end
114
+
115
+ def last_datetime=(last_datetime)
116
+ @last_datetime = last_datetime.to_datetime
117
+ end
118
+
119
+ def range
120
+ (first_datetime..last_datetime)
121
+ end
122
+
123
+ def datetimes
124
+ range.to_a
125
+ end
126
+
127
+ def ==(other)
128
+ self.range == other.range
129
+ end
130
+
131
+ def to_s
132
+ if first_datetime == last_datetime
133
+ "on #{first_datetime}#{first_datetime == first_datetime.beginning_of_day ? "" : " at #{first_datetime.strftime("%I:%M %p")}"}"
134
+ elsif first_datetime == first_datetime.beginning_of_month && last_datetime == last_datetime.end_of_month
135
+ if first_datetime.month == last_datetime.month
136
+ "during #{first_datetime.strftime('%b %Y')}"
137
+ else
138
+ "from #{first_datetime.strftime('%b %Y')} to #{last_datetime.strftime('%b %Y')}"
139
+ end
140
+ else
141
+ "from #{first_datetime}#{first_datetime == first_datetime.beginning_of_day ? "" : " at #{first_datetime.strftime("%I:%M %p")}"} "+
142
+ "to #{last_datetime}#{last_datetime == last_datetime.beginning_of_day ? "" : " at #{last_datetime.strftime("%I:%M %p")}"}"
143
+ end
144
+ end
145
+
146
+ def date_time_to_s(datetime)
147
+ datetime.strftime("%I:%M %p")
148
+ end
149
+
150
+ private
151
+
152
+ def method_missing(method, *args, &block) #:nodoc:
153
+ range.send(method, *args, &block)
154
+ end
155
+ end
156
+
157
+ class Pattern
158
+ attr_reader :intervals, :frequency
159
+
160
+ def initialize(ranges, frequency)
161
+ @intervals = Array.wrap(ranges).map { |r| Interval.new(r.first, r.last) }.sort_by(&:first_datetime)
162
+ @frequency = Frequency.new(frequency)
163
+ fix_frequency
164
+ end
165
+
166
+ def datetimes
167
+ intervals.map do |interval|
168
+ datetimes = []
169
+ datetime = interval.first_datetime
170
+ while datetime <= interval.last_datetime
171
+ datetimes << datetime
172
+ datetime = datetime + frequency.duration
173
+ end
174
+ datetimes
175
+ end
176
+ end
177
+
178
+ def ranges
179
+ intervals.map { |i| (i.first_datetime..i.last_datetime) }
180
+ end
181
+
182
+ def first_datetime
183
+ interval.first_datetime
184
+ end
185
+
186
+ def last_datetime
187
+ interval.last_datetime
188
+ end
189
+
190
+ def interval
191
+ first_datetime = nil
192
+ last_datetime = nil
193
+ intervals.each do |i|
194
+ first_datetime = i.first_datetime if first_datetime.nil? || i.first_datetime < first_datetime
195
+ last_datetime = i.last_datetime if last_datetime.nil? || i.last_datetime > last_datetime
196
+ end
197
+ Interval.new(first_datetime, last_datetime)
198
+ end
199
+
200
+ def match?(datetimes)
201
+ datetimes = Array.wrap(datetimes).map(&:to_datetime)
202
+ intervals.each do |interval|
203
+ current_datetime = interval.first_datetime
204
+ while current_datetime <= interval.last_datetime
205
+ datetimes.delete_if { |datetime| datetime == current_datetime }
206
+ return true if datetimes.empty?
207
+ current_datetime = current_datetime + frequency.duration
208
+ end
209
+ end
210
+ false
211
+ end
212
+
213
+ def <=>(other)
214
+ self.intervals.count <=> other.intervals.count
215
+ end
216
+
217
+ def join(other)
218
+ if self.frequency == other.frequency
219
+ expanded_datetimes = self.datetimes.map do |datetimes|
220
+ datetimes.unshift(datetimes.first - frequency.duration)
221
+ datetimes << (datetimes.last + frequency.duration)
222
+ end
223
+ joint_ranges = []
224
+ other.datetimes.each do |datetimes|
225
+ if joinable_datetimes = expanded_datetimes.find { |ed| datetimes.any? { |d| ed.include?(d) } }
226
+ joint_datetimes = (datetimes + joinable_datetimes[1...-1]).sort
227
+ joint_ranges << (joint_datetimes.first..joint_datetimes.last)
228
+ else
229
+ break
230
+ end
231
+ end
232
+ unless joint_ranges.size != self.intervals.size
233
+ Pattern.new(joint_ranges, frequency.duration)
234
+ end
235
+ end
236
+ end
237
+
238
+ def to_s
239
+ single_date_intervals, multiple_dates_intervals = intervals.partition { |i| i.first_datetime == i.last_datetime}
240
+ patterns_strings = if multiple_dates_intervals.empty?
241
+ single_date_intervals.map(&:to_s)
242
+ else
243
+ multiple_dates_intervals_first_datetime = nil
244
+ multiple_dates_intervals_last_datetime = nil
245
+ multiple_dates_intervals.each do |i|
246
+ multiple_dates_intervals_first_datetime = i.first_datetime if multiple_dates_intervals_first_datetime.nil? || i.first_datetime < multiple_dates_intervals_first_datetime
247
+ multiple_dates_intervals_last_datetime = i.last_datetime if multiple_dates_intervals_last_datetime.nil? || i.last_datetime > multiple_dates_intervals_last_datetime
248
+ end
249
+ multiple_dates_interval = Interval.new(multiple_dates_intervals_first_datetime, multiple_dates_intervals_last_datetime)
250
+ multiple_dates_intervals_string = case frequency.unit
251
+ when :years
252
+ "every #{multiple_dates_intervals.map { |i| "#{i.first_datetime.day.ordinalize} of #{i.first_datetime.strftime('%B')}" }.uniq.to_sentence} #{multiple_dates_interval}"
253
+ when :months
254
+ "every #{multiple_dates_intervals.map { |i| i.first_datetime.day.ordinalize }.uniq.to_sentence} of the month #{multiple_dates_interval}"
255
+ when :weeks
256
+ weekdays = multiple_dates_intervals.map { |i| i.first_datetime.strftime('%A') }.uniq
257
+ if weekdays.count == 7
258
+ "every day #{multiple_dates_interval}"
259
+ else
260
+ "every #{weekdays.to_sentence} #{multiple_dates_interval}"
261
+ end
262
+ when :days
263
+ if multiple_dates_intervals.any? { |i| i.first_datetime != i.first_datetime.beginning_of_day }
264
+ "every day at #{multiple_dates_intervals.map { |i| i.first_datetime.strftime("%I:%M %p") }.to_sentence} #{multiple_dates_interval}"
265
+ else
266
+ "every day #{multiple_dates_interval}"
267
+ end
268
+ else
269
+ "#{frequency} #{multiple_dates_intervals.map(&:to_s).to_sentence}"
270
+ end
271
+ [multiple_dates_intervals_string] + single_date_intervals.map(&:to_s)
272
+ end
273
+ patterns_strings.to_sentence
274
+ end
275
+
276
+ private
277
+
278
+ # Fix the time units inconsistency problem
279
+ # e.g.: a year isn't exactly 12 months, it's a little bit more, but it is commonly considered to be equal to 12 months
280
+ def fix_frequency
281
+ if frequency.duration > 12.months
282
+ if intervals.all? { |i| i.first_datetime.month == i.last_datetime.month && i.first_datetime.day == i.last_datetime.day }
283
+ frequency.duration = (frequency.duration / 12.months).floor.years
284
+ end
285
+ elsif frequency.duration > 1.month && frequency.duration < 12.months
286
+ if intervals.all? { |i| i.first_datetime.day == i.last_datetime.day }
287
+ frequency.duration = frequency.units[:months].months
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ class Finder
294
+ class << self
295
+ def patterns(datetimes, options = nil)
296
+ return [] if datetimes.blank?
297
+ options ||= {}
298
+
299
+ datetimes = Array.wrap(datetimes).uniq.map(&:to_datetime).sort
300
+
301
+ if options[:split].is_a?(ActiveSupport::Duration)
302
+ find_patterns_split_by_duration(datetimes, options)
303
+ elsif options[:split].is_a?(Numeric)
304
+ find_patterns_split_by_size(datetimes, options)
305
+ else
306
+ find_patterns(datetimes, options)
307
+ end
308
+ end
309
+
310
+ private
311
+
312
+ def find_patterns(datetimes, options = {})
313
+ frequency_patterns = []
314
+
315
+ return [] if datetimes.blank?
316
+
317
+ if frequencies = options[:frequency] || options[:frequencies]
318
+ Array.wrap(frequencies).each do |frequency|
319
+ unmatched_datetimes = nil
320
+ if pattern = frequency_pattern(datetimes, frequency)
321
+ frequency_patterns << pattern
322
+ unmatched_datetimes = datetimes - pattern[:intervals].flatten
323
+ end
324
+ break if unmatched_datetimes && unmatched_datetimes.empty?
325
+ end
326
+ end
327
+
328
+ if options[:all] || !frequencies
329
+ frequency_patterns.concat(frequency_patterns(datetimes))
330
+ end
331
+
332
+ if best_fit = best_pattern(frequency_patterns)
333
+ ranges = best_fit[:ranges].map { |r| (r[0]..r[1]) }
334
+ frequency = best_fit[:frequency]
335
+ unmatched_datetimes = datetimes - best_fit[:intervals].flatten
336
+ pattern = Pattern.new(ranges, frequency)
337
+ ([pattern] + find_patterns(unmatched_datetimes, options)).sort_by(&:first_datetime)
338
+ else
339
+ datetimes.map { |d| Pattern.new((d..d), 1.day) }.sort_by(&:first_datetime)
340
+ end
341
+ end
342
+
343
+ def find_patterns_split_by_duration(datetimes, options = {})
344
+ slice_size = options[:split]
345
+ slices = []
346
+ slice_start = 0
347
+ while slice_start < datetimes.size
348
+ slice_end = datetimes.index(datetimes[slice_start] + slice_size) || (datetimes.size - 1)
349
+ slices << datetimes[slice_start..slice_end]
350
+ slice_start = slice_end + 1
351
+ end
352
+ split_patterns = []
353
+ slices.each do |slice|
354
+ split_patterns.concat(find_patterns(slice, options))
355
+ end
356
+ join_patterns(split_patterns)
357
+ end
358
+
359
+ def find_patterns_split_by_size(datetimes, options = {})
360
+ slice_size = options[:split]
361
+ split_patterns = []
362
+ datetimes.each_slice(slice_size) do |slice|
363
+ split_patterns.concat(find_patterns(slice, options))
364
+ end
365
+ join_patterns(split_patterns)
366
+ end
367
+
368
+ def join_patterns(patterns)
369
+ split_patterns = patterns.sort_by(&:first_datetime)
370
+ joint_patterns = []
371
+ while pattern = split_patterns.pop
372
+ joint_pattern = pattern
373
+ while (next_pattern = split_patterns.pop) && (pattern = joint_pattern.join(next_pattern))
374
+ joint_pattern = pattern
375
+ end
376
+ joint_patterns << joint_pattern
377
+ end
378
+ joint_patterns.sort_by(&:first_datetime)
379
+ end
380
+
381
+ def frequency_pattern(datetimes, frequency)
382
+ return nil if datetimes.blank? || frequency.to_f < 1 || ((datetimes.first + frequency) > datetimes.last)
383
+ pattern_intervals = []
384
+ pattern_ranges = []
385
+ intervals = condition_intervals(datetimes) do |current_date, next_date|
386
+ if (current_date + frequency) == next_date
387
+ pattern_intervals << [current_date, next_date]
388
+ true
389
+ else
390
+ false
391
+ end
392
+ end
393
+ pattern_ranges = intervals.map { |r| [r.first,r.last] }
394
+ {:frequency => frequency, :ranges => pattern_ranges, :intervals => pattern_intervals} unless intervals.blank?
395
+ end
396
+
397
+ def condition_intervals(datetimes, &block)
398
+ return [] if datetimes.blank? || !block_given?
399
+ datetimes = datetimes.clone
400
+ current_datetime = first_datetime = datetimes.shift
401
+ last_datetime = nil
402
+ while next_datetime = datetimes.delete(datetimes.find { |datetime| block.call(current_datetime, datetime) })
403
+ current_datetime = last_datetime = next_datetime
404
+ end
405
+ (last_datetime ? [(first_datetime..current_datetime)] : []) + condition_intervals(datetimes, &block)
406
+ end
407
+
408
+ def frequency_patterns(datetimes)
409
+ return [] if datetimes.blank?
410
+ datetimes = datetimes.clone
411
+ patterns = {}
412
+ while (current_datetime = datetimes.pop)
413
+ datetimes.reverse_each do |compared_datetime|
414
+ frequency = current_datetime - compared_datetime
415
+ patterns[frequency] ||= {:frequency => frequency.days, :ranges => [], :intervals => []}
416
+ patterns[frequency][:intervals] << [compared_datetime, current_datetime]
417
+ if interval = patterns[frequency][:ranges].find { |i| i[0] == current_datetime }
418
+ interval[0] = compared_datetime
419
+ else
420
+ patterns[frequency][:ranges] << [compared_datetime, current_datetime]
421
+ end
422
+ end
423
+ end
424
+ patterns.values
425
+ end
426
+
427
+ def best_pattern(frequency_patterns)
428
+ best_fit = nil
429
+ frequency_patterns.each do |pattern_hash|
430
+ if best_fit.nil? ||
431
+ (best_fit[:intervals].count < pattern_hash[:intervals].count) ||
432
+ (best_fit[:intervals].count == pattern_hash[:intervals].count && (best_fit[:ranges].count > pattern_hash[:ranges].count ||
433
+ (best_fit[:ranges].count == pattern_hash[:ranges].count && best_fit[:frequency] < pattern_hash[:frequency])))
434
+ best_fit = pattern_hash
435
+ end
436
+ end
437
+ best_fit
438
+ end
439
+ end
440
+ end
441
+ end
data/lib/timely/time.rb CHANGED
@@ -5,16 +5,17 @@ module Timely
5
5
  date = year
6
6
  year, month, day = date.year, date.month, date.day
7
7
  end
8
-
8
+
9
9
  raise ArgumentError, "Year, month, and day needed" unless [year, month, day].all?
10
-
10
+
11
11
  ::Time.local(year, month, day, hour, min, sec)
12
12
  end
13
-
13
+
14
14
  alias_method :on, :on_date
15
15
  end
16
16
  end
17
17
 
18
18
  class Time
19
19
  include Timely::Time
20
- end
20
+ end
21
+
@@ -0,0 +1,148 @@
1
+ require 'set'
2
+
3
+ # Track a set of dates (usually a range)
4
+ #
5
+ # Tracking means to remember whether each date has been worked on, or 'done'.
6
+ #
7
+ # range = Date.current..(Date.current+10)
8
+ # my_dates = TrackableDateSet.new(range)
9
+ #
10
+ # my_dates.set_date_done!(Date.current)
11
+ # my_dates.set_dates_done!([Date.current+1, Date.current+2])
12
+ # my_dates.set_all_done!
13
+ #
14
+ #
15
+ #
16
+ # As well as tracking status of individual dates, you can also remember whether
17
+ # any action has been applied or not across the whole set:
18
+ #
19
+ # my_dates = TrackableDateSet.new(Date.current..(Date.current+10))
20
+ #
21
+ # my_dates.apply_action(:minimum_nights_surcharge)
22
+ # my_dates.action_applied?(:minimum_nights_surcharge) # will be true
23
+ #
24
+ module Timely
25
+ class TrackableDateSet
26
+ attr_reader :start_date, :end_date, :dates_to_do
27
+
28
+ # Pass in dates as array, range or any kind of enumerable
29
+ def initialize(dates)
30
+ # Sort so that start/end date are correct
31
+ sorted_dates = dates.sort
32
+
33
+ # Have to do this, because Set doesn't respond to :last
34
+ # ...but .sort returns an array which does
35
+ @start_date = sorted_dates.first
36
+ @end_date = sorted_dates.last
37
+
38
+ @dates = Set.new(sorted_dates)
39
+ # track dates_to_do instead of dates_done... better fits common access patterns
40
+ @dates_to_do = @dates.dup
41
+ end
42
+
43
+ def self.new_for_date(date, opts={})
44
+ duration = opts[:duration] || 1
45
+ TrackableDateSet.new(date..(date + duration - 1))
46
+ end
47
+
48
+ # Todo: remove
49
+ # Initialize from a date + duration
50
+ def self.from_params(date_string, duration_string=nil)
51
+ duration = duration_string.to_i
52
+ duration = 1 if duration == 0
53
+ new_for_date(date_string.to_date, :duration => duration)
54
+ end
55
+
56
+ def dates
57
+ @dates
58
+ end
59
+
60
+ # Find the set of dates which are YET to do
61
+ def find_to_do
62
+ @dates_to_do
63
+ end
64
+
65
+ # Find which dates ARE done
66
+ def dates_done
67
+ @dates - @dates_to_do
68
+ end
69
+ alias_method :find_done, :dates_done
70
+
71
+ # Yield each date to do
72
+ def each_date_to_do
73
+ # Sort method needed as Ruby 1.8 set's aren't ordered
74
+ @dates_to_do.sort.each{|date| yield date}
75
+ end
76
+
77
+ # Yield each date in the whole set
78
+ def each_date
79
+ # Sort method needed as Ruby 1.8 set's aren't ordered
80
+ @dates.sort.each{|date| yield date}
81
+ end
82
+
83
+ # Set dates as done
84
+ def set_dates_done!(date_enum)
85
+ @dates_to_do.subtract(date_enum)
86
+ end
87
+
88
+ def set_date_done!(date)
89
+ @dates_to_do.delete(date)
90
+ end
91
+
92
+ # Set all done!
93
+ def set_all_done!
94
+ @dates_to_do.clear
95
+ end
96
+
97
+ def has_done?(date_or_date_range)
98
+ if date_or_date_range.is_a?(Enumerable)
99
+ @dates_to_do.none?{|date_to_do| date_or_date_range.include?(date_to_do)}
100
+ else
101
+ !@dates_to_do.include? date_or_date_range
102
+ end
103
+ end
104
+
105
+ def all_done?
106
+ @dates_to_do.empty?
107
+ end
108
+
109
+ # Do something once within this tracked period
110
+ #
111
+ # Will only consider job done when opts[:job_done] is true
112
+ #
113
+ # action_name => Name to track
114
+ # {:job_done_when} => Block to call, passed result of yield
115
+ def do_once(action_name, opts={})
116
+ return if action_applied?(action_name)
117
+ result = yield
118
+
119
+ job_done = opts[:job_done_when].blank? || opts[:job_done_when].call(result)
120
+ apply_action(action_name) if job_done
121
+ end
122
+
123
+ # Remember an action has been applied across the whole date set
124
+ def apply_action(action)
125
+ actions_applied << action
126
+ end
127
+
128
+ # Check if an action has been applied
129
+ def action_applied?(action)
130
+ actions_applied.include? action
131
+ end
132
+
133
+ def duration
134
+ @dates.size
135
+ end
136
+ alias_method :number_of_nights, :duration
137
+
138
+ # Can't say whole_period anymore... it's not necessarily sequential dates
139
+ # def whole_period
140
+ # self.dates
141
+ # end
142
+
143
+ private
144
+ def actions_applied
145
+ @actions_applied ||= Set.new
146
+ end
147
+ end
148
+ end