timely 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +14 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +1 -12
  7. data/README.md +5 -0
  8. data/Rakefile +1 -139
  9. data/gemfiles/rails3.gemfile +8 -0
  10. data/gemfiles/rails4.gemfile +9 -0
  11. data/lib/timely.rb +7 -3
  12. data/lib/timely/date.rb +20 -0
  13. data/lib/timely/date_chooser.rb +10 -5
  14. data/lib/timely/date_range.rb +47 -10
  15. data/lib/timely/rails.rb +10 -3
  16. data/lib/timely/rails/calendar_tag.rb +52 -0
  17. data/lib/timely/rails/date.rb +5 -0
  18. data/lib/timely/rails/date_group.rb +68 -99
  19. data/lib/timely/rails/date_range_validity_module.rb +27 -0
  20. data/lib/timely/rails/date_time.rb +20 -0
  21. data/lib/timely/rails/extensions.rb +23 -11
  22. data/lib/timely/rails/period.rb +31 -0
  23. data/lib/timely/rails/season.rb +65 -75
  24. data/lib/timely/railtie.rb +7 -0
  25. data/lib/timely/temporal_patterns/finder.rb +152 -0
  26. data/lib/timely/temporal_patterns/frequency.rb +108 -0
  27. data/lib/timely/temporal_patterns/interval.rb +67 -0
  28. data/lib/timely/temporal_patterns/pattern.rb +160 -0
  29. data/lib/timely/time_since.rb +17 -0
  30. data/lib/timely/version.rb +3 -0
  31. data/spec/calendar_tag_spec.rb +29 -0
  32. data/spec/date_chooser_spec.rb +36 -27
  33. data/spec/date_group_spec.rb +9 -9
  34. data/spec/date_range_spec.rb +58 -20
  35. data/spec/date_spec.rb +20 -12
  36. data/spec/extensions_spec.rb +32 -0
  37. data/spec/rails/date_spec.rb +16 -0
  38. data/spec/rails/date_time_spec.rb +20 -0
  39. data/spec/rails/period_spec.rb +17 -0
  40. data/spec/schema.rb +5 -0
  41. data/spec/season_spec.rb +21 -24
  42. data/spec/spec_helper.rb +5 -20
  43. data/spec/string_spec.rb +4 -3
  44. data/spec/support/coverage.rb +26 -0
  45. data/spec/temporal_patterns_spec.rb +28 -0
  46. data/spec/time_since_spec.rb +24 -0
  47. data/spec/time_spec.rb +14 -14
  48. data/spec/trackable_date_set_spec.rb +14 -14
  49. data/spec/week_days_spec.rb +18 -18
  50. data/timely.gemspec +34 -98
  51. metadata +244 -21
  52. data/lib/timely/temporal_patterns.rb +0 -441
@@ -1,441 +0,0 @@
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