Last active
August 29, 2015 14:06
-
-
Save sclinede/e3ff5d9a509d4d2e873e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Класс описывающий дату истории переключения пакетов, объектом является дата начала или окончания размещения. | |
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