Как объединить перекрывающиеся диапазоны времени (объединение временных диапазонов)
У меня есть массив с несколькими диапазонами времени внутри:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00,
Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 09:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
Я хочу получить тот же массив с диапазонами времени перекрывающиеся, поэтому выход для этого случая будет:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
Таким образом, он создает новый временной диапазон, когда перекрываются интервалы времени и т.д. Если они не перекрываются, они будут разделены. Другой пример:
Input:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:00:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
Выход (будет таким же, потому что они не перекрываются):
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:00:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
Я думал о некоторых рекурсивных подходах, но мне нужно руководствоваться здесь...
Ответы
Ответ 1
Для функции, которая возвращает правду, если два диапазона перекрываются:
def ranges_overlap?(a, b)
a.include?(b.begin) || b.include?(a.begin)
end
(эта функция предоставляется sepp2k и steenslag)
и функцию, которая объединяет два перекрывающихся диапазона:
def merge_ranges(a, b)
[a.begin, b.begin].min..[a.end, b.end].max
end
то эта функция, заданная массивом диапазонов, возвращает новый массив с любыми перекрывающимися диапазонами:
def merge_overlapping_ranges(overlapping_ranges)
overlapping_ranges.sort_by(&:begin).inject([]) do |ranges, range|
if !ranges.empty? && ranges_overlap?(ranges.last, range)
ranges[0...-1] + [merge_ranges(ranges.last, range)]
else
ranges + [range]
end
end
end
Ответ 2
Поиск немного Я нашел код, который делает трюк:
def self.merge_ranges(ranges)
ranges = ranges.sort_by {|r| r.first }
*outages = ranges.shift
ranges.each do |r|
lastr = outages[-1]
if lastr.last >= r.first - 1
outages[-1] = lastr.first..[r.last, lastr.last].max
else
outages.push(r)
end
end
outages
end
Пример (работа с диапазонами времени тоже!):
ranges = [1..5, 20..20, 4..11, 40..45, 39..50]
merge_ranges(ranges)
=> [1..11, 20..20, 39..50]
Найдено здесь: http://www.ruby-forum.com/topic/162010
Ответ 3
Графический грань имеет способ Range.combine
, который может быть полезен: http://rdoc.info/github/rubyworks/facets/master/Range#combine-instance_method
Ответ 4
Какой-то алгоритм, который может помочь:
Sort range array by start time (r1, r2, r3, r4, .. rn)
for each range pair [r1, r2], [r2, r3] .. [rn-1, rn]:
if r1_end > r2_start: # they overlap
add [r1_start, r2_end] to new range array
else: # they do not overlap
add [r1] and [r2] to new range array (no changes)
startover with the new range array until no more changes
Ответ 5
Решение, предлагаемое @wayne-conrad, очень хорошее. Я реализовал это для проблемы, я наткнулся. Затем я реализовал итеративную версию и сравнил два. Похоже, итеративная версия быстрее. Примечание. Я использую ActiveSupport
для Range#overlaps?
и временных помощников, но тривиально реализовать версию pure-Ruby.
require 'active_support/all'
module RangesUnifier
extend self
# ranges is an array of ranges, e.g. [1..5, 2..6]
def iterative_call(ranges)
ranges.sort_by(&:begin).reduce([ranges.first]) do |merged_ranges, range|
if merged_ranges.last.overlaps?(range)
merged_ranges[0...-1] << merge_ranges(merged_ranges.last, range)
else
merged_ranges << range
end
end
end
def recursive_call(ranges)
return ranges if ranges.size == 1
if ranges[0].overlaps?(ranges[1])
recursive_call [merge_ranges(ranges[0], ranges[1]), *ranges[2..-1]]
else
[ranges[0], *recursive_call(ranges[1..-1])]
end
end
def merge_ranges(a, b)
[a.begin, b.begin].min..[a.end, b.end].max
end
end
five_hours_ago = 5.hours.ago
four_hours_ago = 4.hours.ago
three_hours_ago = 3.hours.ago
two_hours_ago = 2.hours.ago
one_hour_ago = 1.hour.ago
one_hour_from_now = 1.hour.from_now
two_hours_from_now = 2.hours.from_now
three_hours_from_now = 3.hours.from_now
four_hours_from_now = 4.hours.from_now
five_hours_from_now = 5.hours.from_now
input = [
five_hours_ago..four_hours_ago,
three_hours_ago..two_hours_from_now,
one_hour_ago..one_hour_from_now,
one_hour_from_now..three_hours_from_now,
four_hours_from_now..five_hours_from_now
]
RangesUnifier.iterative_call(input)
#=> [
# 2017-08-21 12:50:50 +0300..2017-08-21 13:50:50 +0300,
# 2017-08-21 14:50:50 +0300..2017-08-21 20:50:50 +0300,
# 2017-08-21 21:50:50 +0300..2017-08-21 22:50:50 +0300
# ]
RangesUnifier.recursive_call(input)
#=> [
# 2017-08-21 12:50:50 +0300..2017-08-21 13:50:50 +0300,
# 2017-08-21 14:50:50 +0300..2017-08-21 20:50:50 +0300,
# 2017-08-21 21:50:50 +0300..2017-08-21 22:50:50 +0300
# ]
n = 100_000
Benchmark.bm do |x|
x.report('iterative') { n.times { RangesUnifier.iterative_call(input) } }
x.report('recursive') { n.times { RangesUnifier.recursive_call(input) } }
end
# =>
# user system total real
# iterative 0.970000 0.000000 0.970000 ( 0.979549)
# recursive 0.540000 0.010000 0.550000 ( 0.546755)
Ответ 6
Не хотите ли вы найти наименьшее первое значение и самое большое последнее значение из набора массивов?
ranges = [Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00,
Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 09:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
union = [ranges.collect(&:first).sort.first, ranges.collect(&:last).sort.last]
Ответ 7
Отмеченный ответ работает хорошо, за исключением нескольких случаев использования. Одним из таких вариантов использования является
[Tue, 21 June 13:30:00 GMT +0:00..Tue, 21 June 15:30:00 GMT +00:00,
Tue, 21 June 14:30:00 GMT +0:00..Tue, 21 June 15:30:00 GMT +00:00]
Условие в ranges_overlap
не обрабатывает этот прецедент. Поэтому я написал это
def ranges_overlap?(a, b)
a.include?(b.begin) || b.include?(a.begin) || a.include?(b.end) || b.include?(a.end)|| (a.begin < b.begin && a.end >= b.end) || (a.begin >= b.begin && a.end < b.end)
end
Это обрабатывает все крайние случаи для меня до сих пор.