2015-01-12 35 views
2

給定範圍的兩個大陣......計算(日期)範圍內的兩個數組的交集紅寶石

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

但這是空的because none of the ranges are identical

下面是一個圖例。

Logical conjuction waveform

我已經在範圍內包含Infinity,因爲解決此問題typically involve converting the Range to an Array or Set

當前解決方案 這是我與測試通過的速度和準確性當前的解決方案。我正在尋找意見和/或建議的改進。第二項測試使用優秀的IceCube gem to generate an array of date ranges。在我的掩碼方法中有一個隱含的假設,即每個時間表內的日期範圍出現不重疊。

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

我不確定我是否真的明白你的問題;我對你的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如上發佈,然後做:

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

你會得到你指定的結果。不知道它如何比較性能,我不知道排序順序...

+0

感謝您的回答。不幸的是,當A和B的長度不同或者A的範圍包含B中的多個範圍時,它不起作用。數組的大小不同,因爲我的實際用例是來自IceCube gem的調度。所以範圍可能在一天,一個月,一週或一年中重複出現。在這個特殊情況下,我試圖計算在非高級時間(週一至週五上午7點 - 下午7點)工作時間的工作時間。 –

+0

P.S.有趣的是看到Array#zip方法。我以前沒有使用或調查過。花了我一段時間,讓我的頭在實際上做了什麼,直到我意識到它就像一個拉鍊交錯的牙齒。 –

+0

@KevinMonk,我做了一個小小的改變,產生你指定的輸出... –