Skip to content

Instantly share code, notes, and snippets.

@sclinede
Last active August 29, 2015 14:06
Show Gist options
  • Save sclinede/e3ff5d9a509d4d2e873e to your computer and use it in GitHub Desktop.
Save sclinede/e3ff5d9a509d4d2e873e to your computer and use it in GitHub Desktop.
def calc_continuously_paid_months(company)
nodes = []
company.packet_history.where(packet_id: Company::PACKET_PAID_WITH_TEST).each do |packet_history_record|
start_node = {type: :start, value: packet_history_record.valid_from.to_date}
end_node = {type: :end, value: [packet_history_record.valid_to.to_date, Date.today].min}
nodes << start_node << end_node
end
nodes.sort! { |node_1, node_2| node_2[:value] <=> node_1[:value] }
ordered_node_types = [:start, :end]
search_for = search_start_type = ordered_node_types.pop
total_days = 0
prev_node = nodes.shift
calc_intervals = [prev_node[:value]]
while calc_intervals.present? do
node = nodes.shift
if node.nil?
total_days += (calc_intervals.pop - prev_node[:value]).to_i.abs if prev_node.present?
break;
end
prev_node = node && next unless search_for == node[:type]
if node[:type] == search_start_type
break_interval = (node[:value] - prev_node[:value]).to_i.abs
break if break_interval > 365
total_days += (calc_intervals.pop - prev_node[:value]).to_i.abs
calc_intervals << node[:value]
else
prev_node = node
end
search_for = ordered_node_types.unshift(node[:type]).pop
end
total_days
#nodes.each do |node|
# prev_node = node && next if search_for != node[:type]
# # switch node type
# search_for = ordered_node_types.unshift(node[:type]).pop
# if prev_node.present?
# if node[:type] == search_start
# break_interval = (node[:value] - prev_node[:value]).to_i.abs
# break if break_interval > 365
#
# interval_start = calc_intervals.pop
# total_days += (interval_start - prev_node[:value]).to_i.abs
# calc_intervals << node[:value]
# else
# prev_node = node
# end
# else
# calc_intervals << node[:value]
# end
#end
#
#total_days += (calc_intervals.pop - prev_node[:value]).to_i.abs if calc_intervals.present?
end
def calc_continuously_paid_months(company)
nodes = []
company.packet_history.where('packet_id != 0 and valid_from is not null and valid_to is not null').each do |packet_history_record|
start_node = {type: :start, value: packet_history_record.valid_from.to_date}
end_node = {type: :end, value: [packet_history_record.valid_to.to_date, Date.today].min}
nodes << start_node << end_node
end
nodes.sort! { |node_1, node_2| node_2[:value] <=> node_1[:value] }
return 0 if nodes.empty?
ordered_node_types = [:start, :end]
search_for = search_start_type = ordered_node_types.pop
total_days = 0
prev_node = nodes.shift
calc_intervals = [prev_node[:value]]
get_interval = ->(istart, iend) { (istart - iend).to_i.abs }
while calc_intervals.present?
node = nodes.shift
if node.nil?
total_days += get_interval.call(calc_intervals.pop, prev_node[:value]) if prev_node.present?
break
end
unless search_for == node[:type]
prev_node = node
next
end
if node[:type] == search_start_type
break_interval = get_interval.call(node[:value], prev_node[:value])
# finish calc if break between paid packets was too long
if break_interval > 365
prev_node = node
nodes.clear
next
end
total_days += get_interval.call(calc_intervals.pop, prev_node[:value])
calc_intervals << node[:value]
else
prev_node = node
end
search_for = ordered_node_types.unshift(node[:type]).pop
end
(total_days / 30.0).ceil
end
# Класс описывающий дату истории переключения пакетов, объектом является дата начала или окончания размещения.
class DateNode
attr_reader :type, :value
# Public: метод сортирующий даты по убыванию
#
# nodes - Enumerable of Nodes, список дат для сортировки
#
# Returns Enumerable of Nodes, отсортированный по убыванию список дат
def self.sort(nodes)
nodes.sort do |node_1, node_2|
result = node_2.value <=> node_1.value
result == 0 ? node_2.type <=> node_1.type : result
end
end
def initialize(type, value)
fail ArgumentError, 'Wrong node type was given' unless [:start, :end].include?(type)
@type = type
@value = value
end
# Public: является ли дата началом интервала
#
# Returns Boolean
def start?
type == :start
end
# Public: является ли дата концом интервала
#
# Returns Boolean
def end?
type == :end
end
# Public: метод возвращающий интервал времени между датами (нодами), данной и указанной
#
# other_node - DateNode, дата до которой вычисляем интервал времени
#
# Returns Number, интервал времени до указанной даты
def -(other_node)
(value - other_node.value).to_i.abs
end
end # of ContinuouslyPaidCompanies::Node
# Класс - итератор по длительностям *непрерывных* интервалов времени
# между заданными точками начала и окончания интервалов
#
# В основе - следующая идея:
# 1) Сортируем даты переключения пакетов сначала по времени (по убыванию),
# а для равных по времени по типу соответственно (сначала даты окончания, потом даты начала интервалов)
# 2) Последовательно идем по всем датам с последней:
# а) на каждой закрывающей интервал дате - увеличиваем счетчик вложенности временных интервалов,
# б) на каждой открывающей интервал дате - уменьшаем счетчик вложенности временных интервалов,
# в) считаем началом непрерывного интервала - момент когда счетчик вложенности стал равен единице,
# г) считаем концом непрерывного интервала - момент когда счетчик вложенности стал равен нулю
# д) в начале непрерывного интервала запоминаем дату как "дата начала",
# сравниваем "дату окончания" предыдущего интервала с новой "датой начала" и прекращаем работу,
# если длительность перерыва между интервалами получилась слишком большой
# е) в конце непрерывного интервала фиксируем "дату окончания",
# находим длительность интервала от "даты начала" до "даты окончания" и возвращаем как результат итерации
# 3) В результате мы вернули длительности всех непрерывных интервалов, profit.
class DateNodesIntervalDurations
include Enumerable
# В общем случае - максимальный разрыв между интервалами, после которого перестаем искать непрерывные интервалы
# В контексте сервиса - допустимый перерыв между оплаченными размещениями, менее которого мы считаем
# размещения актуальными
MAX_BREAK_IN_DAYS = 365
def initialize(nodes)
fail ArgumentError, 'Wrong nodes number was given' if nodes.size.odd?
# Перебираем даты с конца
@nodes = DateNode.sort(nodes.dup)
@nested_count = 0
end
# Public: итератор по длительностям "непрерывных" промежутков между заданными датами
#
# Returns nothing,
def each
nodes.each do |current_node|
count_nested_intervals current_node
if new_interval_opened?(current_node)
# Задаем дату начала нового интервала
self.interval_start = current_node
# Прерываем поиск интервалов если между ними был слишком большой разрыв
if prev_interval_end.present? && (interval_start - prev_interval_end) > MAX_BREAK_IN_DAYS
@nested_count -= 1
break
end
elsif current_interval_closed?(current_node)
# Задаем дату окончания интервала
self.interval_end = current_node
# Рассчитываем продолжительность интервала и возвращаем в блок
yield (interval_start - interval_end)
end
# Игнорируем даты которые относятся к вложенным или пересекающимся интервалам
# т.к. считаем непрерывные промежутки покрытые как одним, таки несколькими интервалами
end
fail ArgumentError, 'Wrong nodes sequence was given' if @nested_count != 0
end
private
attr_reader :nodes
attr_accessor :interval_end, :interval_start
alias_method :prev_interval_end, :interval_end
# Internal: закрылся ли текущий интервал
#
# Returns nothing
def current_interval_closed?(current_node)
@nested_count == 0 && current_node.start?
end
# Internal: открылся ли новый интервал
#
# Returns nothing
def new_interval_opened?(current_node)
@nested_count == 1 && current_node.end?
end
# Internal: посчитать вложенность текущего интервала
#
# Returns nothing
def count_nested_intervals(current_node)
@nested_count += current_node.end? ? 1 : -1
end
end # of class ContinuouslyPaidCompanies::NodesIterator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment