From 3d15f571adac6be6ed1ce1fed4cb6e98ea35a1d0 Mon Sep 17 00:00:00 2001 From: Joshua Young Date: Tue, 25 Aug 2026 17:29:08 +1000 Subject: [PATCH] Fix SortedSet initialization and equality on Ruby 4.0 Ruby 4.0's Set initializer can bypass SortedSet's Ruby-level overrides when copying another SortedSet, leaving its separate backing tree empty. Copy the backing tree directly when no transformation block is given, while preserving normal enumeration for transforming initializers. Set's built-in equality also compares inherited storage that SortedSet does not use. Restore SortedSet content and comparator semantics, Set-receiver comparisons, and strict eql? behavior for Hash keys. Apply equivalent behavior to JRuby, including a comparator fallback when Java rejects mixed numeric lookups. --- lib/sorted_set.rb | 23 ++++++++++ lib/sorted_set/jruby.rb | 28 +++++++++++- lib/sorted_set/ruby4.rb | 26 +++++++++++- test/test_sorted_set.rb | 94 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) diff --git a/lib/sorted_set.rb b/lib/sorted_set.rb index 8888889..80a71b2 100644 --- a/lib/sorted_set.rb +++ b/lib/sorted_set.rb @@ -61,3 +61,26 @@ class SortedSet < Set else require_relative 'sorted_set/ruby3' end + +if defined?(JRUBY_VERSION) || defined?(Set::CoreSet) + class Set + # Set's equality cannot see SortedSet's separate backing store. + module SortedSetEquality + def ==(other) + return super unless other.is_a?(SortedSet) + + size == other.size && other.all? { |o| include?(o) } + end + + def eql?(other) + return false if other.is_a?(SortedSet) + + super + end + end + + prepend SortedSetEquality + + private_constant :SortedSetEquality + end +end diff --git a/lib/sorted_set/jruby.rb b/lib/sorted_set/jruby.rb index 9456d86..c36a329 100644 --- a/lib/sorted_set/jruby.rb +++ b/lib/sorted_set/jruby.rb @@ -7,8 +7,13 @@ class SortedSet def initialize(enum = nil, &block) - @tree = java.util.TreeSet.new - super + if block.nil? && enum.instance_of?(self.class) + @tree = enum.instance_variable_get(:@tree).dup + super(nil) + else + @tree = java.util.TreeSet.new + super + end end def add(o) @@ -26,6 +31,10 @@ def delete(o) def include?(o) @tree.contains(o) + rescue Java::JavaLang::ClassCastException + # TreeSet rejects some Ruby-comparable cross-class values, such as + # Integer and Float. + @tree.to_a.any? { |element| (element <=> o) == 0 } end alias member? include? @@ -69,6 +78,21 @@ def hash @tree.to_a.hash end + def ==(other) + return true if equal?(other) + + if other.is_a?(Set) + size == other.size && other.all? { |o| include?(o) } + else + false + end + end + + def eql?(other) + other.instance_of?(self.class) && + @tree.to_a.eql?(other.instance_variable_get(:@tree).to_a) + end + def freeze super end diff --git a/lib/sorted_set/ruby4.rb b/lib/sorted_set/ruby4.rb index bbdcd43..32c06bb 100644 --- a/lib/sorted_set/ruby4.rb +++ b/lib/sorted_set/ruby4.rb @@ -5,8 +5,13 @@ class SortedSet def initialize(enum = nil, &block) - @tree = RBTree.new - super + if block.nil? && enum.instance_of?(self.class) + @tree = enum.instance_variable_get(:@tree).dup + super(nil) + else + @tree = RBTree.new + super + end end def add(o) @@ -63,6 +68,23 @@ def hash @tree.keys.hash end + def ==(other) + return true if equal?(other) + + if other.is_a?(SortedSet) + @tree == other.instance_variable_get(:@tree) + elsif other.is_a?(Set) + size == other.size && other.all? { |o| include?(o) } + else + false + end + end + + def eql?(other) + other.instance_of?(self.class) && + @tree.keys.eql?(other.instance_variable_get(:@tree).keys) + end + def freeze @tree.freeze super diff --git a/test/test_sorted_set.rb b/test/test_sorted_set.rb index 9a3efd2..c558b76 100644 --- a/test/test_sorted_set.rb +++ b/test/test_sorted_set.rb @@ -121,4 +121,98 @@ def test_enumerable_to_sorted_set assert_instance_of(SortedSet, set) assert_equal([-10,-8,-6,-4,-2], set.to_a) end + + def test_new_from_sorted_set + set1 = SortedSet[3,1,2] + set2 = SortedSet.new(set1) + assert_equal([1,2,3], set2.to_a) + assert_nothing_raised { set2.add 4 } + assert_equal([1,2,3,4], set2.to_a) + assert_equal([1,2,3], set1.to_a) + + set2 = SortedSet.new(set1) { |o| o * 2 } + assert_equal([2,4,6], set2.to_a) + + set1.freeze + set3 = SortedSet.new(set1) + assert_equal([1,2,3], set3.to_a) + assert_not_predicate set3, :frozen? + assert_nothing_raised { set3.add 4 } + end + + def test_equality + omit('Ruby 4.0 specific') unless SortedSet.instance_method(:==).owner == SortedSet + + set1 = SortedSet[1,2,3] + set2 = SortedSet[3,2,1] + set3 = SortedSet[1,2] + subclass = Class.new(SortedSet) + set4 = subclass.new([3,2,1]) + + assert_operator(set1, :==, set2) + assert_not_operator(set1, :==, set3) + assert_operator(set1, :eql?, set2) + assert_not_operator(set1, :eql?, set3) + assert_operator(set1, :==, set4) + assert_operator(set4, :==, set1) + assert_not_operator(set1, :eql?, set4) + assert_not_operator(set4, :eql?, set1) + + assert_equal(set1.hash, set2.hash) + + assert_equal(:first, { set1 => :first, set3 => :second }[set2]) + assert_equal(:first, { first: set1, second: set3 }.key(set2)) + + integer_set = SortedSet[1] + float_set = SortedSet[1.0] + assert_operator(integer_set, :==, float_set) + assert_not_operator(integer_set, :eql?, float_set) + assert_nil({ integer_set => :match }[float_set]) + + element_class = Class.new do + include Comparable + + attr_reader :value + + def initialize(value) + @value = value + end + + def <=>(other) + value <=> other.value + end + end + + assert_operator(SortedSet[element_class.new(1)], :==, SortedSet[element_class.new(1)]) + end + + def test_equality_with_set + omit('Ruby 4.0 specific') unless SortedSet.instance_method(:==).owner == SortedSet + + sorted_set = SortedSet[1,2,3] + + assert_operator(SortedSet[], :==, Set[]) + assert_operator(sorted_set, :==, Set[3,2,1]) + assert_not_operator(sorted_set, :==, Set[1,2]) + assert_not_operator(sorted_set, :==, Set[1,2,4]) + assert_operator(SortedSet[1], :==, Set[1.0]) + assert_include(SortedSet[1], 1.0) + assert_not_operator(sorted_set, :==, [1,2,3]) + assert_not_operator(sorted_set, :eql?, Set[1,2,3]) + end + + def test_set_equality_with_sorted_set + omit('Ruby 4.0 specific') unless SortedSet.instance_method(:==).owner == SortedSet + + sorted_set = SortedSet[1,2,3] + + assert_operator(Set[], :==, SortedSet[]) + assert_operator(Set[3,2,1], :==, sorted_set) + assert_not_operator(Set[1,2], :==, sorted_set) + assert_not_operator(Set[1,2,4], :==, sorted_set) + assert_not_operator(Set[1.0], :==, SortedSet[1]) + assert_not_operator(Set[], :eql?, SortedSet[]) + assert_not_operator(Set[1,2,3], :eql?, sorted_set) + assert_nil({ Set[] => :match }[SortedSet[]]) + end end