lib/timely/temporal_patterns/finder.rb
module Timely
module TemporalPatterns
class Finder
class << self
def patterns(datetimes, options = nil)
return [] if datetimes.blank?
options ||= {}
datetimes = Array.wrap(datetimes).uniq.map(&:to_datetime).sort
if options[:split].is_a?(ActiveSupport::Duration)
find_patterns_split_by_duration(datetimes, options)
elsif options[:split].is_a?(Numeric)
find_patterns_split_by_size(datetimes, options)
else
find_patterns(datetimes, options)
end
end
private
def find_patterns(datetimes, options = {})
frequency_patterns = []
return [] if datetimes.blank?
if frequencies = options[:frequency] || options[:frequencies]
Array.wrap(frequencies).each do |frequency|
unmatched_datetimes = nil
if pattern = frequency_pattern(datetimes, frequency)
frequency_patterns << pattern
unmatched_datetimes = datetimes - pattern[:intervals].flatten
end
break if unmatched_datetimes && unmatched_datetimes.empty?
end
end
if options[:all] || !frequencies
frequency_patterns.concat(frequency_patterns(datetimes))
end
if best_fit = best_pattern(frequency_patterns)
ranges = best_fit[:ranges].map { |r| (r[0]..r[1]) }
frequency = best_fit[:frequency]
unmatched_datetimes = datetimes - best_fit[:intervals].flatten
pattern = Pattern.new(ranges, frequency)
([pattern] + find_patterns(unmatched_datetimes, options)).sort_by(&:first_datetime)
else
datetimes.map { |d| Pattern.new((d..d), 1.day) }.sort_by(&:first_datetime)
end
end
def find_patterns_split_by_duration(datetimes, options = {})
slice_size = options[:split]
slices = []
slice_start = 0
while slice_start < datetimes.size
slice_end = datetimes.index(datetimes[slice_start] + slice_size) || (datetimes.size - 1)
slices << datetimes[slice_start..slice_end]
slice_start = slice_end + 1
end
split_patterns = []
slices.each do |slice|
split_patterns.concat(find_patterns(slice, options))
end
join_patterns(split_patterns)
end
def find_patterns_split_by_size(datetimes, options = {})
slice_size = options[:split]
split_patterns = []
datetimes.each_slice(slice_size) do |slice|
split_patterns.concat(find_patterns(slice, options))
end
join_patterns(split_patterns)
end
def join_patterns(patterns)
split_patterns = patterns.sort_by(&:first_datetime)
joint_patterns = []
while pattern = split_patterns.pop
joint_pattern = pattern
while (next_pattern = split_patterns.pop) && (pattern = joint_pattern.join(next_pattern))
joint_pattern = pattern
end
joint_patterns << joint_pattern
end
joint_patterns.sort_by(&:first_datetime)
end
def frequency_pattern(datetimes, frequency)
return nil if datetimes.blank? || frequency.to_f < 1 || ((datetimes.first + frequency) > datetimes.last)
pattern_intervals = []
pattern_ranges = []
intervals = condition_intervals(datetimes) do |current_date, next_date|
if (current_date + frequency) == next_date
pattern_intervals << [current_date, next_date]
true
else
false
end
end
pattern_ranges = intervals.map { |r| [r.first,r.last] }
{:frequency => frequency, :ranges => pattern_ranges, :intervals => pattern_intervals} unless intervals.blank?
end
def condition_intervals(datetimes, &block)
return [] if datetimes.blank? || !block_given?
datetimes = datetimes.clone
current_datetime = first_datetime = datetimes.shift
last_datetime = nil
while next_datetime = datetimes.delete(datetimes.find { |datetime| block.call(current_datetime, datetime) })
current_datetime = last_datetime = next_datetime
end
(last_datetime ? [(first_datetime..current_datetime)] : []) + condition_intervals(datetimes, &block)
end
def frequency_patterns(datetimes)
return [] if datetimes.blank?
datetimes = datetimes.clone
patterns = {}
while (current_datetime = datetimes.pop)
datetimes.reverse_each do |compared_datetime|
frequency = current_datetime - compared_datetime
patterns[frequency] ||= {:frequency => frequency.days, :ranges => [], :intervals => []}
patterns[frequency][:intervals] << [compared_datetime, current_datetime]
if interval = patterns[frequency][:ranges].find { |i| i[0] == current_datetime }
interval[0] = compared_datetime
else
patterns[frequency][:ranges] << [compared_datetime, current_datetime]
end
end
end
patterns.values
end
def best_pattern(frequency_patterns)
best_fit = nil
frequency_patterns.each do |pattern_hash|
if best_fit.nil? ||
(best_fit[:intervals].count < pattern_hash[:intervals].count) ||
(best_fit[:intervals].count == pattern_hash[:intervals].count && (best_fit[:ranges].count > pattern_hash[:ranges].count ||
(best_fit[:ranges].count == pattern_hash[:ranges].count && best_fit[:frequency] < pattern_hash[:frequency])))
best_fit = pattern_hash
end
end
best_fit
end
end
end
end
end