timely 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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