Вычислить пересечение двух массивов диапазонов (дат) в рубине

Учитывая два больших массива диапазонов ...

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

Когда я выполняю логическое соединение ...

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

но это пустой , потому что ни один из диапазонов не идентичен.

Вот наглядный пример.

Форма волны логической связи.

Я включил Infinity в диапазон, потому что решения этой проблемы обычно включают преобразование диапазона в массив или набор.

Мое текущее решение. Это мое текущее решение, прошедшее тесты на скорость и точность. Я искал комментарии и / или предложения по улучшению. Во втором тесте используется превосходный гем IceCube для создания массива диапазонов дат. В моем методе маски есть неявное предположение, что вхождения диапазона дат в каждом расписании не перекрываются.

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

Я понимаю, что ответил на свой вопрос, но подумал, что оставлю его здесь как ссылку для других.


person Kevin Monk    schedule 12.01.2015    source источник


Ответы (1)


Я не уверен, что правильно понимаю ваш вопрос; Меня немного смущает ваш оператор expect, и я не знаю, почему ваши массивы не одинакового размера. Тем не менее, если вы хотите вычислить пересечение двух диапазонов, мне нравится этот патч обезьяны (из Ruby: пересечение двух диапазонов):

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]

что кажется разумным результатом ...

ИЗМЕНИТЬ

Если вы используете monkeypatch 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]

Вы получаете указанные вами результаты. Не знаю, как он сравнивается с точки зрения производительности, и я не уверен в порядке сортировки ...

person Jacob Brown    schedule 12.01.2015
comment
Спасибо за Ваш ответ. К сожалению, это не работает, когда A и B имеют разную длину ИЛИ если диапазон в A охватывает несколько диапазонов в B. Массивы имеют разные размеры, потому что мой фактический вариант использования - это расписания из гема IceCube. Таким образом, диапазоны могут повторяться в течение дня, месяца, недели или года. В этом конкретном случае я пытаюсь подсчитать количество часов, отработанных в нерабочее время (пн-пт с 7:00 до 19:00) для рабочего графика. - person Kevin Monk; 13.01.2015
comment
P.S. Интересно увидеть метод Array # zip. Я не использовал и не исследовал это раньше. Мне потребовалось время, чтобы осмыслить, что он на самом деле делает, пока я не понял, что это похоже на встречные зубцы застежки-молнии. - person Kevin Monk; 13.01.2015
comment
Это отличное решение, но массивы обычно состоят из 200-500 элементов, поэтому массив продуктов может легко получить длину 500 ^ 2 = 250 тыс. Хотя мне нравится эта концепция. - person Kevin Monk; 13.01.2015
comment
@KevinMonk Похоже, вы действительно не хотите пересечения диапазонов, если вас беспокоит сравнение некоторых диапазонов с потенциально множеством других диапазонов. Вам может быть полезно рассмотреть проект программы, в котором вы используете настраиваемый класс для представления коллекций этих значений Range, а затем опираетесь на этот ответ для реализации менее агностических функций, которые вам нужны, помимо базового пересечения диапазонов. - person Iron Savior; 13.01.2015
comment
@IronSavior Это справедливый вопрос. Я согласен. Это особый «класс» массива. ScheduleArray или ArrayRange или что-то в этом роде. Я получил образование инженера-электронщика, и в мире аппаратного обеспечения это самая простая проблема; это вентиль И с двумя входами. Это заставило меня задуматься, существует ли какое-то решение на основе ввода-вывода, но я недостаточно понимаю, что такое объект ввода-вывода, чтобы его написать. - person Kevin Monk; 13.01.2015
comment
@KevinMonk старается не зацикливаться на том, что он реализуется с точки зрения набора диапазонов. Детали реализации - это средство, а не цель. Я думаю, что это правильный ответ, но помните, что ваша цель - сравнить две серии значений - вы можете обнаружить, что вам нужно использовать одну сторону сравнения для многих итераций другой стороны. Это не так уж сложно, если ваши коллекции диапазонов упорядочены по времени начала. - person Iron Savior; 13.01.2015