How to combine overlapping time ranges (time ranges union)

后端 未结 10 1375
渐次进展
渐次进展 2020-12-08 05:19

I have an array with several time ranges inside:

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


        
相关标签:
10条回答
  • 2020-12-08 05:33

    The solution, offered by @wayne-conrad is a very good one. I implemented it for a problem, I stumbled upon. Then I implemented an iterative version and benchmarked the two. It appears, the iterative version is quicker. Note: I use ActiveSupport for Range#overlaps? and the time helpers, but it is trivial to implement a pure-Ruby version.

    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)
    
    0 讨论(0)
  • 2020-12-08 05:34

    The Marked answer works well except for few use cases. One of such use case is

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

    The condition in ranges_overlap does not handle this use case. So I wrote this

    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
    

    This is handling all the edge cases for me so far.

    0 讨论(0)
  • 2020-12-08 05:47

    I made a minor update to the answer from Wayne Conrad to handle edge cases involved with open-ended arrays (created with ... operator instead of .. operator).

    I changed the name to merge_continuous_ranges since while ranges like 0...1 and 1..2 do not overlap, their combined ranges are continuous, so it makes sense to combine them:

    def merge_continuous_ranges(ranges)
      ranges.sort_by(&:begin).inject([]) do |result, range|
        if !result.empty? && ranges_continuous?(result.last, range)
          result[0...-1] + [merge_ranges(result.last, range)]
        else
          result + [range]
        end
      end
    end
    
    def ranges_continuous?(a, b)
      a.include?(b.begin) || b.include?(a.begin) || a.end == b.begin || b.end == a.begin
    end
    
    def merge_ranges(a, b)
      range_begin = [a.begin, b.begin].min
      range_end = [a.end, b.end].max
    
      exclude_end = case a.end <=> b.end
      when -1
        b.exclude_end?
      when 0
        a.exclude_end? && b.exclude_end?
      when 1
        a.exclude_end?
      end
    
      exclude_end ? range_begin...range_end : range_begin..range_end
    end
    
    0 讨论(0)
  • 2020-12-08 05:51

    Searching a little bit I have found a code that does the trick:

    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
    

    A sample (working with time ranges too!):

    ranges = [1..5, 20..20, 4..11, 40..45, 39..50]
    merge_ranges(ranges)
    => [1..11, 20..20, 39..50]
    

    Found here: http://www.ruby-forum.com/topic/162010

    0 讨论(0)
  • 2020-12-08 05:52

    The facets gem has Range.combine method that may be of use: http://rdoc.info/github/rubyworks/facets/master/Range#combine-instance_method

    0 讨论(0)
  • 2020-12-08 05:54

    You can do it by using multi_range gem.

    Example 1:

    ranges = [
      Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 13:00:00 CEST +02:00'),
      Time.parse('Tue, 24 May 2011 16:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
      Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 09:00:00 CEST +02:00'),
      Time.parse('Tue, 24 May 2011 15:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
    ]
    
    MultiRange.new(ranges).merge_overlaps.ranges
    # => [2011-05-24 08:00:00 +0800..2011-05-24 13:00:00 +0800, 2011-05-24 15:30:00 +0800..2011-05-24 18:00:00 +0800] 
    

    Example 2:

    ranges = [
      Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00')..Time.parse('Tue, 24 May 2011 13:00:00 CEST +02:00'),
      Time.parse('Tue, 24 May 2011 16:00:00 CEST +02:00')..Time.parse('Tue, 24 May 2011 18:00:00 CEST +02:00'),
    ]
    
    MultiRange.new(ranges).merge_overlaps.ranges
    # => [2011-05-24 08:00:00 +0800..2011-05-24 13:00:00 +0800, 2011-05-24 16:00:00 +0800..2011-05-24 18:00:00 +0800] 
    
    0 讨论(0)
提交回复
热议问题