【问题标题】:How to combine overlapping time ranges (time ranges union)如何组合重叠的时间范围(时间范围联合)
【发布时间】:2011-05-16 12:40:59
【问题描述】:

我有一个包含多个时间范围的数组:

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

我正在考虑一些递归方法,但我需要一些指导......

【问题讨论】:

    标签: ruby-on-rails ruby


    【解决方案1】:

    如果两个范围重叠,则给定一个返回 truthy 的函数:

    def ranges_overlap?(a, b)
      a.include?(b.begin) || b.include?(a.begin)
    end
    

    (此功能由sepp2k and 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
    

    【讨论】:

    • 我认为将其标记为正确答案是公平的。尤其是找到YWCA Hello之后,代码看起来更干净了。
    【解决方案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

    【讨论】:

    • 此算法存在潜在问题。例如,它假设 (1..3) 和 (4..6) 重叠。
    【解决方案3】:

    您可以使用multi_range gem 来完成。

    示例 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] 
    

    示例 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] 
    

    【讨论】:

      【解决方案4】:

      facets gem 有Range.combine 可能有用的方法:http://rdoc.info/github/rubyworks/facets/master/Range#combine-instance_method

      【讨论】:

        【解决方案5】:

        某种可能有帮助的算法:

        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
        

        【讨论】:

        • 谢谢,我找到了一个可行的解决方案,但作为一个新用户,我无法在 8 小时内回答我自己的问题。明天会这样做。
        • 针对特定语言问题的伪代码做得很好!
        【解决方案6】:

        @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)
        

        【讨论】:

          【解决方案7】:

          gem range_operators 通过向 Ruby Range 类添加缺少的功能做得非常出色。它比添加整个 facets gem 小得多。

          在你的情况下,解决方案是 rangify 方法,它被添加到 Array 类中,并且完全符合你的要求。

          【讨论】:

            【解决方案8】:

            我对@9​​87654321@ 的答案进行了小幅更新,以处理涉及开放式数组的边缘情况(使用 ... 运算符而不是 .. 运算符创建)。

            我将名称更改为merge_continuous_ranges,因为虽然0...11..2 这样的范围不重叠,但它们的组合范围是连续的,因此将它们组合起来很有意义:

            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
            

            【讨论】:

              【解决方案9】:

              你不想从数组集合中找到最小的第一个值和最大的最后一个值吗?

              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]
              

              【讨论】:

              • 不,这不是我想要的,我的问题中的第一个输出是错误的(我写了一个范围,但应该是其中两个),这可能会让你感到困惑。无论如何,谢谢你的回答。
              【解决方案10】:

              标记的答案效果很好,除了少数用例。这样的用例之一是

              [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
              

              到目前为止,这为我处理了所有边缘情况。

              【讨论】:

              • 我正在使用
              • 我稍微简化了我的代码,现在这对我有用 def has_overlap?(range_a, range_b) range_a.last &gt; range_b.first &amp;&amp; range_a.first &lt; range_b.last end
              猜你喜欢
              • 2018-12-06
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-03-03
              相关资源
              最近更新 更多