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.
- data/Gemfile +13 -0
- data/HISTORY.md +3 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/Rakefile +150 -4
- data/lib/timely/date.rb +3 -3
- data/lib/timely/date_chooser.rb +98 -0
- data/lib/timely/date_range.rb +51 -0
- data/lib/timely/date_time.rb +11 -0
- data/lib/timely/rails/date_group.rb +115 -0
- data/lib/timely/rails/extensions.rb +43 -0
- data/lib/timely/rails/season.rb +99 -0
- data/lib/timely/rails.rb +4 -0
- data/lib/timely/range.rb +15 -0
- data/lib/timely/string.rb +25 -0
- data/lib/timely/temporal_patterns.rb +441 -0
- data/lib/timely/time.rb +5 -4
- data/lib/timely/trackable_date_set.rb +148 -0
- data/lib/timely/week_days.rb +128 -0
- data/lib/timely.rb +15 -6
- data/rails/init.rb +1 -0
- data/spec/date_chooser_spec.rb +101 -0
- data/spec/date_group_spec.rb +26 -0
- data/spec/date_range_spec.rb +40 -0
- data/spec/date_spec.rb +15 -15
- data/spec/schema.rb +11 -0
- data/spec/season_spec.rb +68 -0
- data/spec/spec_helper.rb +41 -18
- data/spec/string_spec.rb +13 -0
- data/spec/time_spec.rb +28 -9
- data/spec/trackable_date_set_spec.rb +80 -0
- data/spec/week_days_spec.rb +51 -0
- data/timely.gemspec +99 -0
- metadata +61 -61
- data/History.txt +0 -4
- data/License.txt +0 -20
- data/Manifest.txt +0 -23
- data/README.txt +0 -31
- data/config/hoe.rb +0 -73
- data/config/requirements.rb +0 -15
- data/lib/timely/version.rb +0 -9
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/setup.rb +0 -1585
- data/spec/spec.opts +0 -1
- data/tasks/deployment.rake +0 -34
- data/tasks/environment.rake +0 -7
- data/tasks/rspec.rake +0 -21
- data/tasks/website.rake +0 -9
data/lib/timely/range.rb
ADDED
@@ -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
|