Given two large arrays of ranges...
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]
When I do a logical conjuction...
C = A.mask(B)
Then I expect
describe "Array#mask" do
it{expect(C = A.mask(B)).to eq([0..13, 30..33, 45..53, 65..73, 90..93])}
end
It feels like it should be...
C = A & B
=> []
but that's empty because none of the ranges are identical.
Here's a pictorial example.
.
I've included Infinity in the range because solutions to this problem typically involve converting the Range to an Array or Set.
My current solution This is my current solution with passing tests for speed and accuracy. I was looking for comments and/or suggested improvements. The second test uses the excellent IceCube gem to generate an array of date ranges. There's an implicit assumption in my mask method that date range occurrences within each schedule do not overlap.
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
It feels odd that you can't do this with Ruby's existing core methods? Am I missing something? I find myself calculating range intersections on a fairly regular basis.
It also occurred to me that you could use the same method to find an intersection between two single ranges by passing single item arrays. e.g.
[(54..99)].mask[(65..120)]
I realise I've kind of answered my own question but thought I would leave it here as a reference for others.