Skip to content

Instantly share code, notes, and snippets.

@fauxparse
Last active July 16, 2023 22:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fauxparse/ba4da9998c29d5cc9f392b671a4965b6 to your computer and use it in GitHub Desktop.
Save fauxparse/ba4da9998c29d5cc9f392b671a4965b6 to your computer and use it in GitHub Desktop.
require 'active_support/core_ext/range/overlaps'
class RangeSet
include Enumerable
def initialize(ranges = [])
@ranges = Set.new(self.class.merge_all(ranges.to_a))
end
def <<(range)
raise ArgumentError, 'RangeSet only accepts Range objects' unless range.is_a?(Range)
@ranges = Set.new(self.class.merge_all(@ranges.to_a + [range]))
end
def +(other)
self.class.new([*@ranges, *other.to_a])
end
def each(&block)
@ranges.each(&block)
end
def include?(value)
@ranges.any? { |r| r.include?(value) }
end
def self.merge_all(ranges)
set = Set.new
queue = ranges.dup
while queue.any?
range = queue.shift
overlapping, not_overlapping = set.partition { |r| r.overlaps?(range) }
if overlapping.empty?
set << range
else
new_range = (overlapping + [range]).reduce { |a, b| merge2(a, b) }
queue << new_range
set = not_overlapping
end
end
set
end
def self.merge2(a, b)
low = [a.begin, b.begin].min
high, exclude_end =
if a.end == b.end
[a.end, a.exclude_end? && b.exclude_end?]
elsif a.end > b.end
[a.end, a.exclude_end?]
else
[b.end, b.exclude_end?]
end
Range.new(low, high, exclude_end)
end
end
require 'date'
RSpec.describe RangeSet do
subject(:range_set) { described_class.new(initializer) }
it 'merges two overlapping ranges' do
expect(described_class.new([1..5, 3..7]).to_a).to eq([1..7])
end
it 'does not merge two non-overlapping ranges' do
expect(described_class.new([1..5, 6..10]).to_a).to eq([1..5, 6..10])
end
it 'does not merge excluded ends' do
expect(described_class.new([1...5, 5..10]).to_a).to eq([1...5, 5..10])
end
it 'correctly merges ranges with identical ends but mismatched exclude_ends' do
expect(described_class.new([1...5, 3..5]).to_a).to eq([1..5])
end
it 'works with dates' do
expect(described_class.new([
Date.new(2023, 4, 1)..Date.new(2023, 7, 27),
Date.new(2023, 7, 2)..Date.new(2023, 9, 8)
]).to_a)
.to eq([Date.new(2023, 4, 1)..Date.new(2023, 9, 8)])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment