timely 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -12
- data/README.md +5 -0
- data/Rakefile +1 -139
- data/gemfiles/rails3.gemfile +8 -0
- data/gemfiles/rails4.gemfile +9 -0
- data/lib/timely.rb +7 -3
- data/lib/timely/date.rb +20 -0
- data/lib/timely/date_chooser.rb +10 -5
- data/lib/timely/date_range.rb +47 -10
- data/lib/timely/rails.rb +10 -3
- data/lib/timely/rails/calendar_tag.rb +52 -0
- data/lib/timely/rails/date.rb +5 -0
- data/lib/timely/rails/date_group.rb +68 -99
- data/lib/timely/rails/date_range_validity_module.rb +27 -0
- data/lib/timely/rails/date_time.rb +20 -0
- data/lib/timely/rails/extensions.rb +23 -11
- data/lib/timely/rails/period.rb +31 -0
- data/lib/timely/rails/season.rb +65 -75
- data/lib/timely/railtie.rb +7 -0
- data/lib/timely/temporal_patterns/finder.rb +152 -0
- data/lib/timely/temporal_patterns/frequency.rb +108 -0
- data/lib/timely/temporal_patterns/interval.rb +67 -0
- data/lib/timely/temporal_patterns/pattern.rb +160 -0
- data/lib/timely/time_since.rb +17 -0
- data/lib/timely/version.rb +3 -0
- data/spec/calendar_tag_spec.rb +29 -0
- data/spec/date_chooser_spec.rb +36 -27
- data/spec/date_group_spec.rb +9 -9
- data/spec/date_range_spec.rb +58 -20
- data/spec/date_spec.rb +20 -12
- data/spec/extensions_spec.rb +32 -0
- data/spec/rails/date_spec.rb +16 -0
- data/spec/rails/date_time_spec.rb +20 -0
- data/spec/rails/period_spec.rb +17 -0
- data/spec/schema.rb +5 -0
- data/spec/season_spec.rb +21 -24
- data/spec/spec_helper.rb +5 -20
- data/spec/string_spec.rb +4 -3
- data/spec/support/coverage.rb +26 -0
- data/spec/temporal_patterns_spec.rb +28 -0
- data/spec/time_since_spec.rb +24 -0
- data/spec/time_spec.rb +14 -14
- data/spec/trackable_date_set_spec.rb +14 -14
- data/spec/week_days_spec.rb +18 -18
- data/timely.gemspec +34 -98
- metadata +244 -21
- data/lib/timely/temporal_patterns.rb +0 -441
@@ -0,0 +1,152 @@
|
|
1
|
+
module Timely
|
2
|
+
module TemporalPatterns
|
3
|
+
class Finder
|
4
|
+
class << self
|
5
|
+
def patterns(datetimes, options = nil)
|
6
|
+
return [] if datetimes.blank?
|
7
|
+
options ||= {}
|
8
|
+
|
9
|
+
datetimes = Array.wrap(datetimes).uniq.map(&:to_datetime).sort
|
10
|
+
|
11
|
+
if options[:split].is_a?(ActiveSupport::Duration)
|
12
|
+
find_patterns_split_by_duration(datetimes, options)
|
13
|
+
elsif options[:split].is_a?(Numeric)
|
14
|
+
find_patterns_split_by_size(datetimes, options)
|
15
|
+
else
|
16
|
+
find_patterns(datetimes, options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def find_patterns(datetimes, options = {})
|
23
|
+
frequency_patterns = []
|
24
|
+
|
25
|
+
return [] if datetimes.blank?
|
26
|
+
|
27
|
+
if frequencies = options[:frequency] || options[:frequencies]
|
28
|
+
Array.wrap(frequencies).each do |frequency|
|
29
|
+
unmatched_datetimes = nil
|
30
|
+
if pattern = frequency_pattern(datetimes, frequency)
|
31
|
+
frequency_patterns << pattern
|
32
|
+
unmatched_datetimes = datetimes - pattern[:intervals].flatten
|
33
|
+
end
|
34
|
+
break if unmatched_datetimes && unmatched_datetimes.empty?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if options[:all] || !frequencies
|
39
|
+
frequency_patterns.concat(frequency_patterns(datetimes))
|
40
|
+
end
|
41
|
+
|
42
|
+
if best_fit = best_pattern(frequency_patterns)
|
43
|
+
ranges = best_fit[:ranges].map { |r| (r[0]..r[1]) }
|
44
|
+
frequency = best_fit[:frequency]
|
45
|
+
unmatched_datetimes = datetimes - best_fit[:intervals].flatten
|
46
|
+
pattern = Pattern.new(ranges, frequency)
|
47
|
+
([pattern] + find_patterns(unmatched_datetimes, options)).sort_by(&:first_datetime)
|
48
|
+
else
|
49
|
+
datetimes.map { |d| Pattern.new((d..d), 1.day) }.sort_by(&:first_datetime)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_patterns_split_by_duration(datetimes, options = {})
|
54
|
+
slice_size = options[:split]
|
55
|
+
slices = []
|
56
|
+
slice_start = 0
|
57
|
+
while slice_start < datetimes.size
|
58
|
+
slice_end = datetimes.index(datetimes[slice_start] + slice_size) || (datetimes.size - 1)
|
59
|
+
slices << datetimes[slice_start..slice_end]
|
60
|
+
slice_start = slice_end + 1
|
61
|
+
end
|
62
|
+
split_patterns = []
|
63
|
+
slices.each do |slice|
|
64
|
+
split_patterns.concat(find_patterns(slice, options))
|
65
|
+
end
|
66
|
+
join_patterns(split_patterns)
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_patterns_split_by_size(datetimes, options = {})
|
70
|
+
slice_size = options[:split]
|
71
|
+
split_patterns = []
|
72
|
+
datetimes.each_slice(slice_size) do |slice|
|
73
|
+
split_patterns.concat(find_patterns(slice, options))
|
74
|
+
end
|
75
|
+
join_patterns(split_patterns)
|
76
|
+
end
|
77
|
+
|
78
|
+
def join_patterns(patterns)
|
79
|
+
split_patterns = patterns.sort_by(&:first_datetime)
|
80
|
+
joint_patterns = []
|
81
|
+
while pattern = split_patterns.pop
|
82
|
+
joint_pattern = pattern
|
83
|
+
while (next_pattern = split_patterns.pop) && (pattern = joint_pattern.join(next_pattern))
|
84
|
+
joint_pattern = pattern
|
85
|
+
end
|
86
|
+
joint_patterns << joint_pattern
|
87
|
+
end
|
88
|
+
joint_patterns.sort_by(&:first_datetime)
|
89
|
+
end
|
90
|
+
|
91
|
+
def frequency_pattern(datetimes, frequency)
|
92
|
+
return nil if datetimes.blank? || frequency.to_f < 1 || ((datetimes.first + frequency) > datetimes.last)
|
93
|
+
pattern_intervals = []
|
94
|
+
pattern_ranges = []
|
95
|
+
intervals = condition_intervals(datetimes) do |current_date, next_date|
|
96
|
+
if (current_date + frequency) == next_date
|
97
|
+
pattern_intervals << [current_date, next_date]
|
98
|
+
true
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
pattern_ranges = intervals.map { |r| [r.first,r.last] }
|
104
|
+
{:frequency => frequency, :ranges => pattern_ranges, :intervals => pattern_intervals} unless intervals.blank?
|
105
|
+
end
|
106
|
+
|
107
|
+
def condition_intervals(datetimes, &block)
|
108
|
+
return [] if datetimes.blank? || !block_given?
|
109
|
+
datetimes = datetimes.clone
|
110
|
+
current_datetime = first_datetime = datetimes.shift
|
111
|
+
last_datetime = nil
|
112
|
+
while next_datetime = datetimes.delete(datetimes.find { |datetime| block.call(current_datetime, datetime) })
|
113
|
+
current_datetime = last_datetime = next_datetime
|
114
|
+
end
|
115
|
+
(last_datetime ? [(first_datetime..current_datetime)] : []) + condition_intervals(datetimes, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
def frequency_patterns(datetimes)
|
119
|
+
return [] if datetimes.blank?
|
120
|
+
datetimes = datetimes.clone
|
121
|
+
patterns = {}
|
122
|
+
while (current_datetime = datetimes.pop)
|
123
|
+
datetimes.reverse_each do |compared_datetime|
|
124
|
+
frequency = current_datetime - compared_datetime
|
125
|
+
patterns[frequency] ||= {:frequency => frequency.days, :ranges => [], :intervals => []}
|
126
|
+
patterns[frequency][:intervals] << [compared_datetime, current_datetime]
|
127
|
+
if interval = patterns[frequency][:ranges].find { |i| i[0] == current_datetime }
|
128
|
+
interval[0] = compared_datetime
|
129
|
+
else
|
130
|
+
patterns[frequency][:ranges] << [compared_datetime, current_datetime]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
patterns.values
|
135
|
+
end
|
136
|
+
|
137
|
+
def best_pattern(frequency_patterns)
|
138
|
+
best_fit = nil
|
139
|
+
frequency_patterns.each do |pattern_hash|
|
140
|
+
if best_fit.nil? ||
|
141
|
+
(best_fit[:intervals].count < pattern_hash[:intervals].count) ||
|
142
|
+
(best_fit[:intervals].count == pattern_hash[:intervals].count && (best_fit[:ranges].count > pattern_hash[:ranges].count ||
|
143
|
+
(best_fit[:ranges].count == pattern_hash[:ranges].count && best_fit[:frequency] < pattern_hash[:frequency])))
|
144
|
+
best_fit = pattern_hash
|
145
|
+
end
|
146
|
+
end
|
147
|
+
best_fit
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Timely
|
2
|
+
module TemporalPatterns
|
3
|
+
class Frequency
|
4
|
+
UNITS = [:second, :minute, :hour, :day, :week, :fortnight, :month, :year]
|
5
|
+
|
6
|
+
attr_accessor :duration
|
7
|
+
|
8
|
+
def initialize(duration)
|
9
|
+
self.duration = duration
|
10
|
+
end
|
11
|
+
|
12
|
+
def duration=(duration)
|
13
|
+
raise ArgumentError, "Frequency (#{duration}) must be a duration" unless duration.is_a?(ActiveSupport::Duration)
|
14
|
+
raise ArgumentError, "Frequency (#{duration}) must be positive" unless duration > 0
|
15
|
+
@duration = self.class.parse(duration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(other)
|
19
|
+
self.duration <=> other.duration
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"every " + duration.parts.
|
24
|
+
reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
|
25
|
+
sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
|
26
|
+
map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" if val.nonzero?}.compact.
|
27
|
+
to_sentence(:locale => :en)
|
28
|
+
end
|
29
|
+
|
30
|
+
def unit
|
31
|
+
parts = self.class.decompose(duration)
|
32
|
+
parts.size == 1 && parts.first.last == 1? parts.first.first : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def min_unit
|
36
|
+
parts = self.class.decompose(duration)
|
37
|
+
{parts.last.first => parts.last.last}
|
38
|
+
end
|
39
|
+
|
40
|
+
def max_unit
|
41
|
+
parts = self.class.decompose(duration)
|
42
|
+
{parts.first.first => parts.first.last}
|
43
|
+
end
|
44
|
+
|
45
|
+
def units
|
46
|
+
self.class.decompose_to_hash(duration)
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def singular_units
|
51
|
+
UNITS.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
def plural_units
|
55
|
+
UNITS.map { |unit| unit.to_s.pluralize.to_sym }
|
56
|
+
end
|
57
|
+
|
58
|
+
def unit_durations
|
59
|
+
UNITS.map { |unit| 1.call(unit) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid_units
|
63
|
+
singular_units + plural_units
|
64
|
+
end
|
65
|
+
|
66
|
+
def valid_unit?(unit)
|
67
|
+
valid_units.include?(unit.to_s.to_sym)
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse(duration)
|
71
|
+
parsed = 0.seconds
|
72
|
+
decompose(duration).each do |part|
|
73
|
+
parsed += part.last.send(part.first)
|
74
|
+
end
|
75
|
+
parsed
|
76
|
+
end
|
77
|
+
|
78
|
+
def decompose(duration)
|
79
|
+
whole = duration
|
80
|
+
parts = []
|
81
|
+
plural_units.reverse_each do |unit|
|
82
|
+
if whole >= (one_unit = 1.send(unit))
|
83
|
+
current_unit_value = ((whole / one_unit).floor)
|
84
|
+
if current_unit_value > 0
|
85
|
+
parts << [unit, current_unit_value]
|
86
|
+
whole -= current_unit_value.send(unit)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
parts
|
91
|
+
end
|
92
|
+
|
93
|
+
def decompose_to_hash(duration)
|
94
|
+
decompose(duration).inject({}) do |hash, unit|
|
95
|
+
hash[unit.first] = unit.last
|
96
|
+
hash
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def method_missing(method, *args, &block) #:nodoc:
|
104
|
+
duration.send(method, *args, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Timely
|
2
|
+
module TemporalPatterns
|
3
|
+
class Interval
|
4
|
+
attr_accessor :first_datetime, :last_datetime
|
5
|
+
|
6
|
+
def self.surrounding(intervals)
|
7
|
+
first_datetime = nil
|
8
|
+
last_datetime = nil
|
9
|
+
intervals.each do |i|
|
10
|
+
first_datetime = i.first_datetime if first_datetime.nil? || i.first_datetime < first_datetime
|
11
|
+
last_datetime = i.last_datetime if last_datetime.nil? || i.last_datetime > last_datetime
|
12
|
+
end
|
13
|
+
new(first_datetime, last_datetime)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(first_datetime, last_datetime = nil)
|
17
|
+
self.first_datetime = first_datetime
|
18
|
+
self.last_datetime = last_datetime || first_datetime
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_datetime=(first_datetime)
|
22
|
+
@first_datetime = first_datetime.to_datetime
|
23
|
+
end
|
24
|
+
|
25
|
+
def last_datetime=(last_datetime)
|
26
|
+
@last_datetime = last_datetime.to_datetime
|
27
|
+
end
|
28
|
+
|
29
|
+
def range
|
30
|
+
(first_datetime..last_datetime)
|
31
|
+
end
|
32
|
+
|
33
|
+
def datetimes
|
34
|
+
range.to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
self.range == other.range
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
if first_datetime == last_datetime
|
43
|
+
"on #{first_datetime}#{first_datetime == first_datetime.beginning_of_day ? "" : " at #{first_datetime.strftime("%I:%M %p")}"}"
|
44
|
+
elsif first_datetime == first_datetime.beginning_of_month && last_datetime == last_datetime.end_of_month
|
45
|
+
if first_datetime.month == last_datetime.month
|
46
|
+
"during #{first_datetime.strftime('%b %Y')}"
|
47
|
+
else
|
48
|
+
"from #{first_datetime.strftime('%b %Y')} to #{last_datetime.strftime('%b %Y')}"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
"from #{first_datetime}#{first_datetime == first_datetime.beginning_of_day ? "" : " at #{first_datetime.strftime("%I:%M %p")}"} "+
|
52
|
+
"to #{last_datetime}#{last_datetime == last_datetime.beginning_of_day ? "" : " at #{last_datetime.strftime("%I:%M %p")}"}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def date_time_to_s(datetime)
|
57
|
+
datetime.strftime("%I:%M %p")
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def method_missing(method, *args, &block) #:nodoc:
|
63
|
+
range.send(method, *args, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'active_support/core_ext/integer/inflections' # ordinalize
|
2
|
+
|
3
|
+
module Timely
|
4
|
+
module TemporalPatterns
|
5
|
+
class Pattern
|
6
|
+
attr_reader :intervals, :frequency
|
7
|
+
|
8
|
+
def initialize(ranges, frequency)
|
9
|
+
@intervals = Array.wrap(ranges).map { |r| Interval.new(r.first, r.last) }.sort_by(&:first_datetime)
|
10
|
+
@frequency = Frequency.new(frequency)
|
11
|
+
fix_frequency
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convert each interval to a list of datetimes
|
15
|
+
def datetimes
|
16
|
+
intervals.map do |interval|
|
17
|
+
datetimes = []
|
18
|
+
datetime = interval.first_datetime
|
19
|
+
while datetime <= interval.last_datetime
|
20
|
+
datetimes << datetime
|
21
|
+
datetime = datetime + frequency.duration
|
22
|
+
end
|
23
|
+
datetimes
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def ranges
|
28
|
+
intervals.map { |i| (i.first_datetime..i.last_datetime) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def first_datetime
|
32
|
+
surrounding_interval.first_datetime
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_datetime
|
36
|
+
surrounding_interval.last_datetime
|
37
|
+
end
|
38
|
+
|
39
|
+
def surrounding_interval
|
40
|
+
Interval.surrounding(intervals)
|
41
|
+
end
|
42
|
+
alias_method :interval, :surrounding_interval # backwards compatibility
|
43
|
+
|
44
|
+
def match?(datetimes)
|
45
|
+
datetimes = Array.wrap(datetimes).map(&:to_datetime)
|
46
|
+
intervals.each do |interval|
|
47
|
+
current_datetime = interval.first_datetime
|
48
|
+
while current_datetime <= interval.last_datetime
|
49
|
+
datetimes.delete_if { |datetime| datetime == current_datetime }
|
50
|
+
return true if datetimes.empty?
|
51
|
+
current_datetime = current_datetime + frequency.duration
|
52
|
+
end
|
53
|
+
end
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def <=>(other)
|
58
|
+
self.intervals.count <=> other.intervals.count
|
59
|
+
end
|
60
|
+
|
61
|
+
# Join with other IF same frequency AND same number of intervals
|
62
|
+
def join(other)
|
63
|
+
return nil unless self.frequency == other.frequency
|
64
|
+
|
65
|
+
expanded_datetimes = self.datetimes.map { |datetimes_within_an_interval|
|
66
|
+
back_one = datetimes_within_an_interval.first - frequency.duration
|
67
|
+
forward_one = datetimes_within_an_interval.last + frequency.duration
|
68
|
+
|
69
|
+
[back_one] + datetimes_within_an_interval + [forward_one]
|
70
|
+
}
|
71
|
+
|
72
|
+
joint_ranges = []
|
73
|
+
|
74
|
+
# Look for overlaps, where an overlap may be 'off by 1' -- hence the 'expanded_datetimes'
|
75
|
+
# ...but start with other and join to each of it's intervals.
|
76
|
+
#
|
77
|
+
# Remember that 'pattern.datetimes' returns a list of datetimes per interval
|
78
|
+
other.datetimes.each do |other_datetimes_within_an_interval|
|
79
|
+
|
80
|
+
joinable_datetimes = expanded_datetimes.find { |expanded_datetimes_within_an_interval|
|
81
|
+
other_datetimes_within_an_interval.any? { |d|
|
82
|
+
expanded_datetimes_within_an_interval.include?(d)
|
83
|
+
}
|
84
|
+
}
|
85
|
+
break unless joinable_datetimes
|
86
|
+
|
87
|
+
# Joint ranges should be those that overlap
|
88
|
+
#
|
89
|
+
# This is buggy, because joinable_datetimes is a list of datetimes per interval that overlap
|
90
|
+
# Excluding the first doesn't make sense
|
91
|
+
#
|
92
|
+
# Instead, we should exclude the first AND last for each element within joinable_datetimes
|
93
|
+
joint_datetimes = (other_datetimes_within_an_interval + joinable_datetimes[1...-1]).sort
|
94
|
+
joint_ranges << (joint_datetimes.first..joint_datetimes.last)
|
95
|
+
end
|
96
|
+
|
97
|
+
# This seems to be trying to say "Only join when we got one for each interval of self"
|
98
|
+
# ...it also seems too restrictive...
|
99
|
+
#
|
100
|
+
# What if other includes multiple intervals of self?
|
101
|
+
# Then we don't need same number of intervals
|
102
|
+
#
|
103
|
+
# Also might be wrong in other ways, it's tricky to tell
|
104
|
+
if joint_ranges.size == self.intervals.size
|
105
|
+
Pattern.new(joint_ranges, frequency.duration)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
single_date_intervals, multiple_dates_intervals = intervals.partition { |i| i.first_datetime == i.last_datetime}
|
111
|
+
patterns_strings = if multiple_dates_intervals.empty?
|
112
|
+
single_date_intervals.map(&:to_s)
|
113
|
+
else
|
114
|
+
interval_surrounding_multiple_dates = Interval.surrounding(multiple_dates_intervals)
|
115
|
+
|
116
|
+
multiple_dates_intervals_string = case frequency.unit
|
117
|
+
when :years
|
118
|
+
"every #{multiple_dates_intervals.map { |i| "#{i.first_datetime.day.ordinalize} of #{i.first_datetime.strftime('%B')}" }.uniq.to_sentence} #{interval_surrounding_multiple_dates}"
|
119
|
+
when :months
|
120
|
+
"every #{multiple_dates_intervals.map { |i| i.first_datetime.day.ordinalize }.uniq.to_sentence} of the month #{interval_surrounding_multiple_dates}"
|
121
|
+
when :weeks
|
122
|
+
weekdays = multiple_dates_intervals.map { |i| i.first_datetime.strftime('%A') }.uniq
|
123
|
+
if weekdays.count == 7
|
124
|
+
"every day #{interval_surrounding_multiple_dates}"
|
125
|
+
else
|
126
|
+
"every #{weekdays.to_sentence} #{interval_surrounding_multiple_dates}"
|
127
|
+
end
|
128
|
+
when :days
|
129
|
+
if multiple_dates_intervals.any? { |i| i.first_datetime != i.first_datetime.beginning_of_day }
|
130
|
+
"every day at #{multiple_dates_intervals.map { |i| i.first_datetime.strftime("%I:%M %p") }.to_sentence} #{interval_surrounding_multiple_dates}"
|
131
|
+
else
|
132
|
+
"every day #{interval_surrounding_multiple_dates}"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
"#{frequency} #{multiple_dates_intervals.map(&:to_s).to_sentence}"
|
136
|
+
end
|
137
|
+
[multiple_dates_intervals_string] + single_date_intervals.map(&:to_s)
|
138
|
+
end
|
139
|
+
patterns_strings.to_sentence
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Fix the time units inconsistency problem
|
145
|
+
# 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
|
146
|
+
def fix_frequency
|
147
|
+
return unless frequency.duration > 1.month
|
148
|
+
if frequency.duration < 12.months
|
149
|
+
if intervals.all? { |i| i.first_datetime.day == i.last_datetime.day }
|
150
|
+
frequency.duration = frequency.units[:months].months
|
151
|
+
end
|
152
|
+
else
|
153
|
+
if intervals.all? { |i| i.first_datetime.month == i.last_datetime.month && i.first_datetime.day == i.last_datetime.day }
|
154
|
+
frequency.duration = (frequency.duration / 12.months).floor.years
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|