lib/multi_range.rb in multi_range-2.1.1 vs lib/multi_range.rb in multi_range-2.2.0

- old
+ new

@@ -1,236 +1,242 @@ -# frozen_string_literal: true - -require 'multi_range/version' -require 'roulette-wheel-selection' -require 'interval_tree' - -if not Range.method_defined?(:size) - warn "Please backports Range#size method to use multi_range gem.\n" \ - "You can use backports gem and add the following lines to your program:\n" \ - "require 'backports/1.9.2/float/infinity'\n" \ - "require 'backports/2.0.0/range/size'" -end - -if not Enumerable.method_defined?(:to_h) - warn "Please backports Enumerable#to_h method to use multi_range gem.\n" \ - "You can use backports gem and add the following lines to your program:\n" \ - "require 'backports/2.1.0/enumerable/to_h'" -end - -class MultiRange - INDEX_WITH_DEFAULT = Object.new - - attr_reader :ranges - - 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 - - def is_float? - @is_float - end - - 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 - - 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 - - alias intersection & - - 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 - - alias difference - - - def |(other) - other_ranges = other.is_a?(MultiRange) ? other.ranges : [other] - return MultiRange.new(@ranges + other_ranges).merge_overlaps - end - - alias union | - - def overlaps?(other) - multi_range = merge_overlaps - return multi_range.ranges != (multi_range - other).ranges - end - - def sample - range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h) - return nil if range == nil - return rand(range) - end - - def size - @ranges.inject(0){|sum, v| sum + v.size } - end - - def any? - @ranges.any? - end - - 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 - - def each - return to_enum(:each){ size } if !block_given? - - ranges.each do |range| - range.each{|s| yield(s) } - end - end - - def map - return to_enum(:map){ size } if !block_given? - return each.map{|s| yield(s) } - end - - def to_a - each.to_a - end - - def min - range = @ranges.first - return range.min if range - end - - def max - range = @ranges.last - return range.max if range - end - - def contain_overlaps? - merge_overlaps(false).ranges != ranges - end - - private - - # make sure that range1.begin <= range2.begin - 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 - - def difference_with_other_multi_range(other) - new_multi_range = dup - other.ranges.each{|range| new_multi_range -= range } - return new_multi_range - end - - 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 - - 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 - - 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 -end +# frozen_string_literal: true + +require 'multi_range/version' +require 'roulette-wheel-selection' +require 'interval_tree' + +if not Range.method_defined?(:size) + warn "Please backports Range#size method to use multi_range gem.\n" \ + "You can use backports gem and add the following lines to your program:\n" \ + "require 'backports/1.9.2/float/infinity'\n" \ + "require 'backports/2.0.0/range/size'" +end + +if not Enumerable.method_defined?(:to_h) + warn "Please backports Enumerable#to_h method to use multi_range gem.\n" \ + "You can use backports gem and add the following lines to your program:\n" \ + "require 'backports/2.1.0/enumerable/to_h'" +end + +class MultiRange + INDEX_WITH_DEFAULT = Object.new + + attr_reader :ranges + + 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 + + def is_float? + @is_float + end + + 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 + + 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| + # A workaround for the issue: https://github.com/greensync/interval-tree/issues/17 + query = (range.first == range.last) ? range.first : range + matching_ranges_converted_to_exclusive = tree.search(query) || [] + + # 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 + + alias intersection & + + 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 + + alias difference - + + def |(other) + other_ranges = other.is_a?(MultiRange) ? other.ranges : [other] + return MultiRange.new(@ranges + other_ranges).merge_overlaps + end + + alias union | + + def overlaps?(other) + multi_range = merge_overlaps + return multi_range.ranges != (multi_range - other).ranges + end + + def sample + range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h) + return nil if range == nil + return rand(range) + end + + def size + @ranges.inject(0){|sum, v| sum + v.size } + end + + def any? + @ranges.any? + end + + 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 + + def each + return to_enum(:each){ size } if !block_given? + + ranges.each do |range| + range.each{|s| yield(s) } + end + end + + def map + return to_enum(:map){ size } if !block_given? + return each.map{|s| yield(s) } + end + + def to_a + each.to_a + end + + def min + range = @ranges.first + return range.min if range + end + + def max + range = @ranges.last + return range.max if range + end + + def contain_overlaps? + merge_overlaps(false).ranges != ranges + end + + private + + # make sure that range1.begin <= range2.begin + 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 + + def difference_with_other_multi_range(other) + new_multi_range = dup + other.ranges.each{|range| new_multi_range -= range } + return new_multi_range + end + + 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.is_a?(Float) ? other.end.next_float : other.end + 1 + end + + sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end + + sub_ranges = [] + sub_ranges << sub_range1 if not empty_range?(sub_range1) + sub_ranges << sub_range2 if not empty_range?(sub_range2) + return sub_ranges + end + + 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 + + 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 + + def empty_range?(range) + range.begin > range.end || (range.begin == range.end && range.exclude_end?) + end +end