timely 0.0.2 → 0.1.0
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.
- 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
|