在Ruby中计算两个日期范围数组的交集

3
给定两个大范围数组...
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]

当我进行逻辑连接logical conjuction时...

C = A.mask(B)

然后我期望

describe "Array#mask" do
  it{expect(C = A.mask(B)).to eq([0..13, 30..33, 45..53, 65..73, 90..93])}
end

感觉应该是这样的...

C = A & B
=> []

但是这是空的 因为没有一个范围是相同的

这里有一个图示例。

Logical conjuction waveform.

我已经在范围内包含了无限大,因为解决这个问题通常涉及将范围转换为数组或集合我的当前解决方案 这是我的当前解决方案,通过速度和准确性测试。我正在寻找评论和/或建议改进。第二个测试使用优秀的IceCube gem来生成日期范围数组。在我的掩码方法中有一个隐含的假设,即每个计划中的日期范围出现不重叠。
require 'pry'
require 'rspec'
require 'benchmark'
require 'chronic'
require 'ice_cube'
require 'active_support'
require 'active_support/core_ext/numeric'
require 'active_support/core_ext/date/calculations'

A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]

class Array
  def mask(other)
    a_down = self.map{|r| [:a, r.max]}
    a_up = self.map{|r| [:a, r.min]}

    b_down = other.map{|r| [:b, r.max]}
    b_up = other.map{|r| [:b, r.min]}

    up = a_up + b_up
    down = a_down + b_down

    a, b, start, result = false, false, nil, []
    ticks = (up + down).sort_by{|i| i[1]}
    ticks.each do |tick|
      tick[0] == :a ? a = !a : b = !b
      result << (start..tick[1]) if !start.nil?
      start = a & b ? tick[1] : nil
    end
    return result
  end
end

describe "Array#mask" do
  context "simple integer array" do
    it{expect(C = A.mask(B)).to eq([0..13, 30..33, 45..53, 65..73, 90..93])}
  end

  context "larger date ranges from IceCube schedule" do
    it "should take less than 0.1 seconds" do
      year = Time.now..(Time.now + 52.weeks)
      non_premium_schedule = IceCube::Schedule.new(Time.at(0)) do |s|
        s.duration = 12.hours
        s.add_recurrence_rule IceCube::Rule.weekly.day(:monday, :tuesday, :wednesday, :thursday, :friday).hour_of_day(7).minute_of_hour(0)
      end
      rota_schedule = IceCube::Schedule.new(Time.at(0)) do |s|
        s.duration = 7.hours
        s.add_recurrence_rule IceCube::Rule.weekly(2).day(:tuesday).hour_of_day(15).minute_of_hour(30)
      end
      np = non_premium_schedule.occurrences_between(year.min, year.max).map{|d| d..d+non_premium_schedule.duration}
      rt = rota_schedule.occurrences_between(year.min, year.max).map{|d| d..d+rota_schedule.duration}
      expect(Benchmark.realtime{np.mask(rt)}).to be < 0.1
    end
  end
end

感觉用Ruby现有的核心方法无法做到这一点?我是错过了什么吗?我经常需要计算范围交集。

我也想到了,你可以使用相同的方法通过传递单个项数组来查找两个单一范围之间的交集。例如:

[(54..99)].mask[(65..120)]

我意识到我已经回答了自己的问题,但我觉得把它留在这里作为其他人的参考。

1个回答

4

我不确定我是否真正理解了你的问题;我有点困惑你的 expect 语句,并且我不知道为什么你的数组大小不同。话虽如此,如果你想计算两个范围的交集,我喜欢这个猴子补丁(来自Ruby: intersection between two ranges):

class Range
  def intersection(other)
    return nil if (self.max < other.begin or other.max < self.begin) 
    [self.begin, other.begin].max..[self.max, other.max].min
  end
  alias_method :&, :intersection
end

然后你可以这样做:

A = [0..23, 30..53, 60..83, 0..0, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]

A.zip(B).map { |x, y| x & y }
# => [0..13, 30..33, nil, nil, 90..93]

这似乎是一个合理的结果...

编辑

如果你按照上面的方式对Range进行了monkeypatch,并且执行以下操作:

# your initial data
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]

A.product(B).map {|x, y| x & y }.compact
# => [0..13, 30..33, 45..53, 65..73, 90..93]

您将获得您指定的结果。我不知道它在性能方面如何比较,并且对排序顺序也不确定...


感谢您的回答。不幸的是,当A和B的长度不同时,或者A中的范围覆盖了B中的多个范围时,它无法工作。数组的大小不同,因为我的实际用例是来自IceCube gem的时间表。因此,这些范围可能会在一天、一个月、一周或一年内重复出现。在这种特殊情况下,我正在尝试计算非高峰时间(周一至周五上午7点至下午7点)的工作时间。 - Kevin Monk
有趣的是看到Array#zip方法。我以前从未使用或调查过它。花了我一段时间才理解它实际上是像拉链的交错齿一样的东西。 - Kevin Monk
这是一个不错的解决方案,但数组通常有200-500个元素,因此乘积数组很容易达到500 ^ 2 = 250k的长度。虽然我喜欢这个概念。 - Kevin Monk
@KevinMonk 如果你担心要比较一些范围与可能有很多其他范围的交集,那么似乎你并不真正想要范围的交集。你可能会受益于考虑使用自定义类来表示这些范围值的集合,并在此答案的基础上构建更少不可知功能的实现,以满足你的需求。 - Iron Savior
@IronSavior 这是一个很好的观点,我同意。这是一种特定的“类”数组。ScheduleArray或ArrayRange之类的东西。我接受了电子工程师的培训,在硬件世界中,这是最简单的问题之一;它是一个两输入AND门。这让我想知道是否有一些基于IO的解决方案,但我对IO对象的理解不足以编写它。 - Kevin Monk
@KevinMonk 不要过于关注它以一系列范围的形式实现的细节。实施细节是手段,而不是目的。我认为这个答案走在了正确的轨道上,但请记住你的目的是比较两个数值序列 - 你可能会发现你必须对另一侧的比较使用很多次。如果你的范围集合按开始时间排序,这不会太难。 - Iron Savior

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接