Как объединить перекрывающиеся временные диапазоны (объединение временных диапазонов) - PullRequest
18 голосов
/ 16 мая 2011

У меня есть массив с несколькими временными диапазонами внутри:

[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]

Таким образом, создается новый временной диапазон, когда временные диапазоны перекрываются, и так далее.Если они не перекрываются, они будут разделены.Другой пример:

Ввод:

[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]

Я думал в некотором рекурсивном подходе, но мне нужно некоторое руководство здесь ...

Ответы [ 8 ]

30 голосов
/ 16 мая 2011

С учетом функции, которая возвращает truey , если два диапазона перекрываются:

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
5 голосов
/ 17 мая 2011

Немного поиска Я нашел код, который делает трюк:

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

2 голосов
/ 11 июня 2014

Драгоценный камень граней имеет метод Range.combine, который может быть полезен: http://rdoc.info/github/rubyworks/facets/master/Range#combine-instance_method

1 голос
/ 21 августа 2017

Решение, предлагаемое @ wayne-conrad, очень хорошее. Я реализовал это для проблемы, на которую я наткнулся. Затем я реализовал итерационную версию и сравнил их. Оказывается, итеративная версия быстрее. Примечание: я использую ActiveSupport для Range#overlaps? и помощников по времени, но реализовать чистую версию 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)
1 голос
/ 16 мая 2011

Какой-то алгоритм, который может помочь:

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
0 голосов
/ 25 марта 2018

gem range_operators делает замечательную работу, добавляя недостающие возможности в класс Ruby Range. Это намного меньше, чем добавление целых граней драгоценных камней.

В вашем случае решением будет метод rangify, который добавляется к классу Array и будет делать именно то, что вы ищете.

0 голосов
/ 21 июня 2016

Отмеченный ответ работает хорошо, за исключением нескольких случаев использования.Одним из таких вариантов использования является

[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

Это пока что обрабатывает все крайние случаи для меня.

0 голосов
/ 16 мая 2011

Разве вы не хотите найти наименьшее первое значение и наибольшее последнее значение из набора массивов?

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]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...