diff --git a/spec/std/range_spec.cr b/spec/std/range_spec.cr index 15b3ec1c62a3..6c502eed349d 100644 --- a/spec/std/range_spec.cr +++ b/spec/std/range_spec.cr @@ -27,6 +27,23 @@ struct RangeSpecIntWrapper end end +struct RangeSpecPredWrapper + include Comparable(self) + + getter value : Int32 + + def initialize(@value) + end + + def pred + RangeSpecPredWrapper.new(@value - 1) + end + + def <=>(other) + value <=> other.value + end +end + private def range_endless_each (2..).each do |x| return x @@ -79,6 +96,174 @@ describe "Range" do (1...5).includes?(5).should be_false end + it "#overlaps?" do + (1..5).overlaps?(4..8).should be_true + (4..8).overlaps?(1..5).should be_true + + (1..5).overlaps?(5..10).should be_true + (1...5).overlaps?(5..10).should be_false + (1..5).overlaps?(6..10).should be_false + (-5...1).overlaps?(1..5).should be_false + (-5..1).overlaps?(1..5).should be_true + + (1...1).overlaps?(1..1).should be_false + (2..1).overlaps?(1..2).should be_false + (2...1).overlaps?(1..2).should be_false + (1..2).overlaps?(2...2).should be_false + + (..5).overlaps?(5..).should be_true + (...5).overlaps?(5..).should be_false + (..5).overlaps?(6..).should be_false + (..5).overlaps?(4..).should be_true + (...5).overlaps?(4..).should be_true + (..5).overlaps?(..4).should be_true + (...5).overlaps?(..4).should be_true + (..5).overlaps?(..6).should be_true + (...5).overlaps?(..6).should be_true + (5..).overlaps?(4..).should be_true + (5..).overlaps?(6..).should be_true + (..).overlaps?(1..2).should be_true + (..).overlaps?(2...2).should be_false + + (1.0..5.5).overlaps?(3.2..7.8).should be_true + (1.0..2.5).overlaps?(3.0..4.0).should be_false + + ('a'..'e').overlaps?('c'..'g').should be_true + ('a'..'c').overlaps?('d'..'f').should be_false + + t1 = Time.local(2024, 10, 1) + t2 = Time.local(2024, 10, 5) + t3 = Time.local(2024, 10, 10) + t4 = Time.local(2024, 10, 15) + (t1..t3).overlaps?(t2..t4).should be_true + (t1..t2).overlaps?(t3..t4).should be_false + + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).overlaps?(RangeSpecIntWrapper.new(3)..RangeSpecIntWrapper.new(7)).should be_true + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).overlaps?(RangeSpecIntWrapper.new(6)..RangeSpecIntWrapper.new(7)).should be_false + end + + it "#union" do + (1..5).union(3..7).should eq(1..7) + (3..7).union(1..5).should eq(1..7) + + (1..10).union(1...15).should eq(1...15) + (1...10).union(1..15).should eq(1..15) + (1...10).union(1..10).should eq(1..10) + + (1...10).union(10..20).should eq(1..20) + (1..5).union(5..10).should eq(1..10) + (1..5).union(6..10).should eq(1..10) + (6..10).union(1..5).should eq(1..10) + (1...6).union(6..10).should eq(1..10) + + (1..5).union(10..20).should be_nil + (1...1).union(1..2).should eq(1..2) + (1..2).union(2...2).should eq(1..2) + + (..5).union(5..).should eq(..) + (...5).union(5..).should eq(..) + (..5).union(6..).should eq(..) + (...5).union(6..).should be_nil + (..5).union(7..).should be_nil + (..5).union(..6).should eq(..6) + (..5).union(4..).should eq(..) + (...5).union(4..).should eq(..) + (..).union(2..3).should eq(..) + (...).union(2..3).should eq(...) + (..).union(2...3).should eq(..) + + ('a'..'c').union('d'..'f').should eq('a'..'f') + ('a'..'e').union('c'..'g').should eq('a'..'g') + ('a'..'c').union('e'..'f').should be_nil + ('a'...'e').union('c'...'g').should eq('a'...'g') + (1.0..2.5).union(3.0..4.0).should be_nil + (1.0..5.5).union(3.2..7.8).should eq(1.0..7.8) + (1.0...5.5).union(3.2...7.8).should eq(1.0...7.8) + (1.0...5.5).union(5.5...7.8).should eq(1.0...7.8) + + t1 = Time.local(2024, 10, 1) + t2 = Time.local(2024, 10, 5) + t3 = Time.local(2024, 10, 10) + t4 = Time.local(2024, 10, 15) + (t1..t2).union(t2..t3).should eq(t1..t3) + (t1..t2).union(t3..).should be_nil + (t1..t2).union(t3..t4).should be_nil + (t1...t2).union(t2...t3).should eq(t1...t3) + + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).union(RangeSpecIntWrapper.new(3)..RangeSpecIntWrapper.new(7)).should eq(RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(7)) + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).union(RangeSpecIntWrapper.new(6)..RangeSpecIntWrapper.new(7)).should eq(RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(7)) + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).union(RangeSpecIntWrapper.new(7)..RangeSpecIntWrapper.new(8)).should be_nil + + (RangeSpecPredWrapper.new(1)..RangeSpecPredWrapper.new(5)).union(RangeSpecPredWrapper.new(6)..RangeSpecPredWrapper.new(7)).should eq(RangeSpecPredWrapper.new(1)..RangeSpecPredWrapper.new(7)) + (RangeSpecPredWrapper.new(1)..RangeSpecPredWrapper.new(5)).union(RangeSpecPredWrapper.new(7)..RangeSpecPredWrapper.new(8)).should be_nil + end + + it "#intersection" do + (1..5).intersection(3..7).should eq(3..5) + (3..7).intersection(1..5).should eq(3..5) + + (1..10).intersection(1...15).should eq(1..10) + (1...10).intersection(1..15).should eq(1...10) + (1...10).intersection(1..10).should eq(1...10) + + (1...10).intersection(7..12).should eq(7...10) + (2..10).intersection(0..8).should eq(2..8) + (1..5).intersection(5..10).should eq(5..5) + + (1..5).intersection(10..20).should be_nil + (1...10).intersection(10..11).should be_nil + (1...1).intersection(1..2).should be_nil + + (..5).intersection(5..).should eq(5..5) + (...5).intersection(5..).should be_nil + (..5).intersection(3..).should eq(3..5) + (..5).intersection(..3).should eq(..3) + (..5).intersection(-5..).should eq(-5..5) + (...5).intersection(-5..).should eq(-5...5) + (..5).intersection(-5...).should eq(-5..5) + (..5).intersection(2..7).should eq(2..5) + (..5).intersection(2...7).should eq(2..5) + (...5).intersection(2...7).should eq(2...5) + (..5).intersection(2..4).should eq(2..4) + (...5).intersection(2..4).should eq(2..4) + (..5).intersection(2...4).should eq(2...4) + (..).intersection(2..3).should eq(2..3) + (..).intersection(2...3).should eq(2...3) + + (1.0..5.5).intersection(3.2..7.8).should eq(3.2..5.5) + (1.0...3.0).intersection(3.0..4.0).should be_nil + (1.0..2.5).intersection(3.0..4.0).should be_nil + + ('a'..'e').intersection('c'..'g').should eq('c'..'e') + ('a'...'e').intersection('c'...'g').should eq('c'...'e') + ('a'..'c').intersection('d'..'f').should be_nil + + t1 = Time.local(2024, 10, 1) + t2 = Time.local(2024, 10, 5) + t3 = Time.local(2024, 10, 10) + t4 = Time.local(2024, 10, 15) + (t1..t3).intersection(t2..t4).should eq(t2..t3) + (t1...t3).intersection(t2...t4).should eq(t2...t3) + (t1..t2).intersection(t3..t4).should be_nil + + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).intersection(RangeSpecIntWrapper.new(3)..RangeSpecIntWrapper.new(7)).should eq(RangeSpecIntWrapper.new(3)..RangeSpecIntWrapper.new(5)) + (RangeSpecIntWrapper.new(1)...RangeSpecIntWrapper.new(5)).intersection(RangeSpecIntWrapper.new(3)..RangeSpecIntWrapper.new(7)).should eq(RangeSpecIntWrapper.new(3)...RangeSpecIntWrapper.new(5)) + (RangeSpecIntWrapper.new(1)..RangeSpecIntWrapper.new(5)).intersection(RangeSpecIntWrapper.new(6)..RangeSpecIntWrapper.new(7)).should be_nil + end + + it "types union and intersection" do + typeof((1..5).union(3..7)).should eq(Range(Int32, Int32)?) + typeof((1..5).union(10..20)).should eq(Range(Int32, Int32)?) + typeof((1..5).intersection(3..7)).should eq(Range(Int32, Int32)?) + typeof((1..5).intersection(10..20)).should eq(Range(Int32, Int32)?) + + typeof((1..5).union(3_i64..7_i64)).should eq(Range(Int32 | Int64, Int32 | Int64)?) + typeof((1..5).intersection(3_i64..7_i64)).should eq(Range(Int32 | Int64, Int32 | Int64)?) + + typeof((..5).union(3..)).should eq(Range(Int32 | Nil, Int32 | Nil)?) + typeof((..5).intersection(3..)).should eq(Range(Int32 | Nil, Int32 | Nil)?) + end + it "does to_s" do (1...5).to_s.should eq("1...5") (1..5).to_s.should eq("1..5") diff --git a/src/range.cr b/src/range.cr index 59d846dbe981..4ce085283dfa 100644 --- a/src/range.cr +++ b/src/range.cr @@ -283,6 +283,121 @@ struct Range(B, E) (@exclusive ? value < end_value : value <= end_value)) end + # Returns `true` if this range and *other* have at least one value in common. + # + # ``` + # (1..5).overlaps?(5..10) # => true + # (1...5).overlaps?(5..10) # => false + # (1...1).overlaps?(1..1) # => false + # ``` + def overlaps?(other : Range) : Bool + return false if empty_without_iterating? + return false if other.empty_without_iterating? + + begin_value = @begin + end_value = @end + other_begin = other.begin + other_end = other.end + + if end_value && other_begin + return false if @exclusive ? end_value <= other_begin : end_value < other_begin + end + + if other_end && begin_value + return false if other.excludes_end? ? other_end <= begin_value : other_end < begin_value + end + + true + end + + # Returns a range that covers all values in this range and *other*. + # + # Returns `nil` if the ranges cannot be represented as a single continuous range. + # + # ``` + # (1..5).union(3..7) # => 1..7 + # (1...10).union(10..20) # => 1..20 + # (1..5).union(10..20) # => nil + # (1..5).union(6..10) # => 1..10 + # ``` + def union(other : Range(OB, OE)) : Range(B | OB, E | OE)? forall OB, OE + self_empty = empty_without_iterating? + other_empty = other.empty_without_iterating? + + return nil if self_empty && other_empty + return Range(B | OB, E | OE).new(other.begin, other.end, other.excludes_end?) if self_empty + return Range(B | OB, E | OE).new(@begin, @end, @exclusive) if other_empty + return nil unless overlaps?(other) || adjacent_to?(other) + + begin_value = @begin + end_value = @end + other_begin = other.begin + other_end = other.end + + union_begin = + if begin_value.nil? || (other_begin && begin_value <= other_begin) + begin_value + else + other_begin + end + + union_end, exclusive = + if end_value.nil? + {end_value, @exclusive} + elsif other_end.nil? + {other_end, other.excludes_end?} + elsif end_value < other_end + {other_end, other.excludes_end?} + elsif end_value > other_end + {end_value, @exclusive} + else + {end_value, @exclusive && other.excludes_end?} + end + + Range(B | OB, E | OE).new(union_begin, union_end, exclusive) + end + + # Returns a range that covers all values common to this range and *other*. + # + # Returns `nil` if the ranges do not overlap. + # + # ``` + # (1..5).intersection(3..7) # => 3..5 + # (1...10).intersection(7..12) # => 7...10 + # (1..5).intersection(10..20) # => nil + # (1...10).intersection(10..) # => nil + # ``` + def intersection(other : Range(OB, OE)) : Range(B | OB, E | OE)? forall OB, OE + return nil unless overlaps?(other) + + begin_value = @begin + end_value = @end + other_begin = other.begin + other_end = other.end + + intersection_begin = + if begin_value.nil? || (other_begin && other_begin >= begin_value) + other_begin + else + begin_value + end + + intersection_end, exclusive = + if end_value.nil? + {other_end, other.excludes_end?} + elsif other_end.nil? + {end_value, @exclusive} + elsif other_end < end_value + {other_end, other.excludes_end?} + elsif end_value < other_end + {end_value, @exclusive} + else + {end_value, @exclusive || other.excludes_end?} + end + + Range(B | OB, E | OE).new(intersection_begin, intersection_end, exclusive) + end + # Same as `includes?`. def covers?(value) includes?(value) @@ -319,6 +434,35 @@ struct Range(B, E) to_s(io) end + protected def empty_without_iterating? : Bool + return false unless end_value = @end + return false unless begin_value = @begin + + @exclusive ? begin_value >= end_value : begin_value > end_value + end + + private def adjacent_to?(other : Range) : Bool + end_value = @end + other_begin = other.begin + + return true if end_value && other_begin && end_value == other_begin + unless excludes_end? + return true if end_value && other_begin && end_value.responds_to?(:succ) && end_value.succ == other_begin + return true if end_value && other_begin && other_begin.responds_to?(:pred) && other_begin.pred == end_value + end + + other_end = other.end + begin_value = @begin + + return true if other_end && begin_value && other_end == begin_value + unless other.excludes_end? + return true if other_end && begin_value && other_end.responds_to?(:succ) && other_end.succ == begin_value + return true if other_end && begin_value && begin_value.responds_to?(:pred) && begin_value.pred == other_end + end + + false + end + # Optimized version of `Enumerable#sum` that runs in O(1) time when `self` is # an `Int` range. def sum(initial)