Рубин: пересечение двух диапазонов
В ruby, учитывая два диапазона дат, я хочу диапазон, представляющий пересечение двух диапазонов дат, или ноль, если пересечения нет. Например:
(Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
=> Mon, 10 Jan 2011..Sat, 15 Jan 2011
Изменить: Должен был сказать, что я хочу, чтобы он работал и для DateTime, поэтому интервал может быть уменьшен до минут и секунд:
(DateTime.new(2011,1,1,22,45)..Date.new(2011,2,15)) & (Date.new(2011,1,1)..Date.new(2011,2,15))
=> Sat, 01 Jan 2011 22:45:00 +0000..Tue, 15 Feb 2011
11 ответов
require 'date'
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
p (Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
#<Date: 2011-01-10 ((2455572j,0s,0n),+0s,2299161j)>..#<Date: 2011-01-15 ((2455577j,0s,0n),+0s,2299161j)>
Вы можете попробовать это, чтобы получить диапазон, представляющий пересечение
range1 = Date.new(2011,12,1)..Date.new(2011,12,10)
range2 = Date.new(2011,12,4)..Date.new(2011,12,12)
inters = range1.to_a & range2.to_a
intersected_range = inters.min..inters.max
Преобразование вашего примера:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
inters = self.to_a & other.to_a
inters.empty? ? nil : inters.min..inters.max
end
alias_method :&, :intersection
end
Вы можете использовать
overlaps?
с
Range
начиная с рельсов v3
# For dates, make sure you have the correct format
first_range = first_start.to_date..first_end.to_date
second_range = second_start.to_date..second_end.to_date
intersection = first_range.overlaps?(second_range) # => Boolean
# Example with numbers
(1..7).overlaps?(3..5) # => true
Подробнее в документах
Я нашел это: http://www.postal-code.com/binarycode/2009/06/06/better-range-intersection-in-ruby/ который является довольно хорошим началом, но не работает для дат. Я немного подправил это:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil
new_max = self.cover?(other.max) ? other.max : other.cover?(max) ? max : nil
new_min && new_max ? new_min..new_max : nil
end
alias_method :&, :intersection
end
Я пропустил тесты, но в основном это тесты из поста, измененного для дат. Это работает для ruby 1.9.2.
У кого-нибудь есть лучшее решение?
Я испек это решение для восходящих диапазонов, также позаботившись об исключающих конечных ситуациях:
intersect_ranges = ->(r1, r2) do
new_end = [r1.end, r2.end].min
new_begin = [r1.begin, r2.begin].max
exclude_end = (r2.exclude_end? && new_end == r2.end) || (r1.exclude_end? && new_end == r1.end)
valid = (new_begin <= new_end && !exclude_end)
valid ||= (new_begin < new_end && exclude_end))
valid ? Range.new(new_begin, new_end, exclude_end) : nil
end
Я также немного обеспокоен тем, что вы, ребята, добавляете его в сам класс Range, поскольку поведение пересекающихся диапазонов не определяется единообразно. (Как насчет пересечения 1...4 и 4...1? Почему ноль, когда пересечения нет; мы могли бы также сказать, что это пустой диапазон: 1...1)
У меня есть раз [[start, end], ...]
и я хочу удалить некоторые временные диапазоны из каждого начального временного диапазона, вот что я сделал:
def exclude_intersecting_time_ranges(initial_times, other_times)
initial_times.map { |initial_time|
other_times.each do |other_time|
next unless initial_time
# Other started after initial ended
next if other_time.first >= initial_time.last
# Other ended before initial started
next if other_time.last <= initial_time.first
# if other time started before and ended after after, no hour is counted
if other_time.first <= initial_time.first && other_time.last >= initial_time.last
initial_time = nil
# if other time range is inside initial time range, split in two time ranges
elsif initial_time.first < other_time.first && initial_time.last > other_time.last
initial_times.push([other_time.last, initial_time.last])
initial_time = [initial_time.first, other_time.first]
# if start time of other time range is before initial time range
elsif other_time.first <= initial_time.first
initial_time = [other_time.last, initial_time.last]
# if end time of other time range if after initial time range
elsif other_time.last >= initial_time.last
initial_time = [initial_time.first, other_time.first]
end
end
initial_time
}.compact
end
Попробуйте что-то вроде этого
require 'date'
sample = Date.parse('2011-01-01')
sample1 = Date.parse('2011-01-15')
sample2 = Date.parse('2010-12-19')
sample3 = Date.parse('2011-01-11')
puts (sample..sample1).to_a & (sample2..sample3).to_a
Это даст вам массив дат пересечения!
Поскольку этот вопрос связан с тем, как объединить перекрывающиеся временные диапазоны (объединение временных диапазонов), я также хотел опубликовать свой вывод о геме range_operators здесь, потому что, если он помог мне в той же ситуации.
Вы также можете использовать Set для достижения этой цели, что приводит к более элегантному коду.
require 'date'
class Range
def intersection(other)
intersecting_set = to_set & other.to_set
intersecting_set.first && (intersecting_set.min..intersecting_set.max)
end
alias_method :&, :intersection
end
pry(main)> p (Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
Mon, 10 Jan 2011..Sat, 15 Jan 2011
=> Mon, 10 Jan 2011..Sat, 15 Jan 2011
Range#intersection
который обрабатываетDateTime
объекты тоже.
Инициализатор
# Determines the overlap of two ranges.
#
# Returns: Range
#
def intersection( second_range )
raise ArgumentError, "must be another Range" unless second_range.is_a?( Range )
return nil unless self.overlaps?( second_range )
intersection_start_at = [ self.first, second_range.first ].compact.max
intersection_end_at = [ self.end, second_range.end ].compact.min
( intersection_start_at..intersection_end_at )
end
alias_method :&, :intersection
Применение
(12.hours.ago..6.hours.ago).intersection(10.hours.ago..8.hours.ago)
#=> Thu, 22 Jun 2023 01:45:33.072270000 MDT -06:00..Thu, 22 Jun 2023 03:45:33.072282000 MDT -06:00
И на конкретном примере ОП:
(DateTime.new(2011,1,1,22,45)..Date.new(2011,2,15)) & (Date.new(2011,1,1)..Date.new(201
1,2,15))
#=> Sat, 01 Jan 2011 22:45:00 +0000..Tue, 15 Feb 2011
Я бы перевел их в массив, так как массивы знают операцию пересечения:
(Date.new(2011,1,1)..Date.new(2011,1,15)).to_a & (Date.new(2011,1,10)..Date.new(2011,2,15)).to_a
Конечно, это возвращает массив. Поэтому, если вам нужен Enumerator (диапазон кажется невозможным, поскольку они больше не являются последовательными значениями), просто бросьте to_enum
в конце.