class MultiRange

Constants

INDEX_WITH_DEFAULT
VERSION

Attributes

ranges[R]

Public Class Methods

new(ranges) click to toggle source
# File lib/multi_range.rb, line 25
def initialize(ranges)
  if ranges.is_a? MultiRange
    @ranges = ranges.ranges
    @is_float = ranges.is_float?
  else
    @ranges = ranges.map{|s| s.is_a?(Numeric) ? s..s : s }.sort_by(&:begin).freeze
    @is_float = @ranges.any?{|range| range.begin.is_a?(Float) || range.end.is_a?(Float) }
  end
end

Public Instance Methods

&(other) click to toggle source
# File lib/multi_range.rb, line 61
def &(other)
  other_ranges = MultiRange.new(other).merge_overlaps.ranges
  tree = IntervalTree::Tree.new(other_ranges)
  intersected_ranges = merge_overlaps.ranges.flat_map do |range|
    matching_ranges_converted_to_exclusive = tree.search(range) || []

    # The interval tree converts interval endings to exclusive, so we need to restore the original
    matching_ranges = matching_ranges_converted_to_exclusive.map do |matching_range_converted_to_exclusive|
      other_ranges.find do |other_range|
        # Having merged overlaps in each multi_range, there's no need to check the endings,
        # since there will only be one range with each beginning
        other_range.begin == matching_range_converted_to_exclusive.begin
      end
    end

    matching_ranges.map do |matching_range|
      intersect_two_ranges(range, matching_range)
    end
  end
  MultiRange.new(intersected_ranges)
end
Also aliased as: intersection
-(other) click to toggle source
# File lib/multi_range.rb, line 85
def -(other)
  return difference_with_other_multi_range(other) if other.is_a?(MultiRange)

  new_ranges = @ranges.dup
  return MultiRange.new(new_ranges) if not overlaps_with_range?(other)

  changed_size = 0
  @ranges.each_with_index do |range, idx|
    # when this range is smaller than and not overlaps with `other`
    #      range          other
    #   |---------|    |---------|
    next if other.begin > range.end

    # when this range is larger than and not overlaps with `other`
    #      other          range
    #   |---------|    |---------|
    break if other.end < range.begin

    sub_ranges = possible_sub_ranges_of(range, other)
    new_ranges[idx + changed_size, 1] = sub_ranges
    changed_size += sub_ranges.size - 1

    # when the maximum value of this range is larger than that of `other`
    #     range
    # -------------|
    #   other
    # ---------|
    break if other.end <= range.end
  end

  return MultiRange.new(new_ranges)
end
Also aliased as: difference
any?() click to toggle source
# File lib/multi_range.rb, line 142
def any?
  @ranges.any?
end
contain_overlaps?() click to toggle source
# File lib/multi_range.rb, line 183
def contain_overlaps?
  merge_overlaps(false).ranges != ranges
end
difference(other)
Alias for: -
each() { |s| ... } click to toggle source
# File lib/multi_range.rb, line 156
def each
  return to_enum(:each){ size } if !block_given?

  ranges.each do |range|
    range.each{|s| yield(s) }
  end
end
index_with(default = INDEX_WITH_DEFAULT) { |s| ... } click to toggle source
# File lib/multi_range.rb, line 146
def index_with(default = INDEX_WITH_DEFAULT)
  if block_given?
    fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
    return map{|s| [s, yield(s)] }.to_h
  end

  return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
  return map{|s| [s, default] }.to_h
end
intersection(other)
Alias for: &
is_float?() click to toggle source
# File lib/multi_range.rb, line 35
def is_float?
  @is_float
end
map() { |s| ... } click to toggle source
# File lib/multi_range.rb, line 164
def map
  return to_enum(:map){ size } if !block_given?
  return each.map{|s| yield(s) }
end
max() click to toggle source
# File lib/multi_range.rb, line 178
def max
  range = @ranges.last
  return range.max if range
end
merge_overlaps(merge_same_value = true) click to toggle source
# File lib/multi_range.rb, line 39
def merge_overlaps(merge_same_value = true)
  return MultiRange.new([]) if @ranges.size == 0

  new_ranges = []
  current_range = nil

  @ranges.each do |range|
    next current_range = range if current_range == nil
    next if range.end <= current_range.end

    if can_combine?(current_range, range, merge_same_value)
      current_range = range.exclude_end? ? current_range.begin...range.end : current_range.begin..range.end
    else
      new_ranges << current_range
      current_range = range
    end
  end

  new_ranges << current_range
  return MultiRange.new(new_ranges)
end
min() click to toggle source
# File lib/multi_range.rb, line 173
def min
  range = @ranges.first
  return range.min if range
end
overlaps?(other) click to toggle source
# File lib/multi_range.rb, line 127
def overlaps?(other)
  multi_range = merge_overlaps
  return multi_range.ranges != (multi_range - other).ranges
end
sample() click to toggle source
# File lib/multi_range.rb, line 132
def sample
  range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
  return nil if range == nil
  return rand(range)
end
size() click to toggle source
# File lib/multi_range.rb, line 138
def size
  @ranges.inject(0){|sum, v| sum + v.size }
end
to_a() click to toggle source
# File lib/multi_range.rb, line 169
def to_a
  each.to_a
end
union(other)
Alias for: |
|(other) click to toggle source
# File lib/multi_range.rb, line 120
def |(other)
  other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
  return MultiRange.new(@ranges + other_ranges).merge_overlaps
end
Also aliased as: union

Private Instance Methods

can_combine?(range1, range2, merge_same_value) click to toggle source

make sure that range1.begin <= range2.begin

# File lib/multi_range.rb, line 190
def can_combine?(range1, range2, merge_same_value)
  return merge_same_value if range1.end == range2.begin and range1.exclude_end?
  return range1.end >= range2.begin if @is_float
  return range1.end + 1 >= range2.begin
end
difference_with_other_multi_range(other) click to toggle source
# File lib/multi_range.rb, line 196
def difference_with_other_multi_range(other)
  new_multi_range = dup
  other.ranges.each{|range| new_multi_range -= range }
  return new_multi_range
end
intersect_two_ranges(range_a, range_b) click to toggle source
# File lib/multi_range.rb, line 226
def intersect_two_ranges(range_a, range_b)
  ranges = [range_a, range_b]
  start = ranges.map(&:begin).max
  finish = ranges.map(&:end).min
  if ranges.sort_by { |range| [range.end, range.exclude_end? ? 1 : 0] }.first.exclude_end?
    start...finish
  else
    start..finish
  end
end
overlaps_with_range?(range) click to toggle source
# File lib/multi_range.rb, line 219
def overlaps_with_range?(range)
  return false if @ranges.empty?
  return false if range.begin > @ranges.last.end # larger than maximum
  return false if range.end < @ranges.first.begin # smaller than minimum
  return true
end
possible_sub_ranges_of(range, other) click to toggle source
# File lib/multi_range.rb, line 202
def possible_sub_ranges_of(range, other)
  sub_range1 = range.begin...other.begin

  sub_range2_begin = if other.exclude_end?
                       other.end
                     else
                       other.end + (other.end.is_a?(Float) ? Float::EPSILON : 1)
                     end

  sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end

  sub_ranges = []
  sub_ranges << sub_range1 if sub_range1.begin < sub_range1.end
  sub_ranges << sub_range2 if sub_range2.begin < sub_range2.end
  return sub_ranges
end