如何组合重叠时间范围(时间范围联合)

时间:2011-05-16 12:41:00

标签: ruby-on-rails ruby

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

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

我在想一些递归方法,但我需要一些指导...

10 个答案:

答案 0 :(得分:30)

如果两个范围重叠,则给出一个返回 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

答案 1 :(得分:5)

稍微搜索一下,我找到了一个可以解决问题的代码:

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 :(得分:3)

您可以使用multi_range宝石来做到这一点。

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

答案 3 :(得分:2)

答案 4 :(得分:1)

某种可能有用的算法:

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 :(得分:1)

@ wayne-conrad提供的解决方案非常好。我为一个问题实现了它,我偶然发现了。然后我实现了一个迭代版本并对两者进行了基准测试。看来,迭代版本更快。注意:我使用public class ExampleActivity extends Activity{ private AudioManager mAudioManager; private BroadcastReceiver mBroadcastReceiver; @Override public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { super.onCreate(savedInstanceState, persistentState); setContentView(R.layout.example_activity); initActivity(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); registerReceiver(mBroadcastReceiver, intentFilter); } @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mBroadcastReceiver); } private void initActivity() { initBroadcasrReceiver(); initAudioManager(); } private void initBroadcasrReceiver() { mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { int scoAudioState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); if(scoAudioState == AudioManager.SCO_AUDIO_STATE_CONNECTED) { // This method should be called only after SCO is connected! mAudioManager.setBluetoothScoOn(true); } } } }; } private void initAudioManager() { mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); resetSco(); mAudioManager.startBluetoothSco(); } private void resetSco() { mAudioManager.setMode(AudioManager.MODE_NORMAL); mAudioManager.stopBluetoothSco(); mAudioManager.setBluetoothScoOn(false); mAudioManager.setSpeakerphoneOn(false); mAudioManager.setWiredHeadsetOn(false); }} 作为ActiveSupport和时间助手,但实现纯Ruby版本是微不足道的。

Range#overlaps?

答案 6 :(得分:0)

你不想只想找到数组中最小的第一个值和最后一个值吗?

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 :(得分:0)

标记的答案很有效,除了少数用例。其中一个用例是

[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

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

答案 8 :(得分:0)

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

我的情况是解决方案是rangify方法,它被添加到Array类中,并且可以完全按照您的要求进行操作。

答案 9 :(得分:0)

我对Wayne Conrad的答案进行了小幅更新,以处理与开放式数组有关的边缘情况(使用...运算符而不是..运算符创建)。

我将名称更改为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