From 9337727cdb2924e7e037af521f16ac0c30fc2e60 Mon Sep 17 00:00:00 2001 From: Grant Hutchins Date: Fri, 12 Jun 2026 17:36:29 -0500 Subject: [PATCH] Return false from == and nil from <=> for incomparable objects Unit subclasses Numeric, so == and <=> routed every non-Numeric operand through #apply_through_coercion, which re-raises as TypeError whenever the operand does not implement #coerce. That made ordinary expressions like `Unit(1) == nil` raise instead of returning false, breaking Ruby's contract that == never raises and that <=> returns nil for incomparable operands. Guard the coercion path with respond_to?(:coerce) so objects that opt into the coercion protocol (e.g. the UnitOne spec helper) still flow through it, while plain objects short-circuit: == returns false and <=> returns nil. Comparable then raises ArgumentError for ordered comparisons such as `Unit(1) > nil`, matching core Numeric behavior. --- lib/unit/class.rb | 8 ++++++-- spec/unit_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/unit/class.rb b/lib/unit/class.rb index 9fbbf66..965a708 100644 --- a/lib/unit/class.rb +++ b/lib/unit/class.rb @@ -118,8 +118,10 @@ def ==(other) other = coerce_numeric(other) a, b = self.normalize, other.normalize a.value == b.value && a.unit == b.unit - else + elsif other.respond_to?(:coerce) apply_through_coercion(other, __method__) + else + false end end @@ -132,8 +134,10 @@ def <=>(other) other = coerce_numeric_compatible(other) a, b = self.normalize, other.normalize a.value <=> b.value - else + elsif other.respond_to?(:coerce) apply_through_coercion(other, __method__) + else + nil end end diff --git a/spec/unit_spec.rb b/spec/unit_spec.rb index c4475e9..5cad4bd 100644 --- a/spec/unit_spec.rb +++ b/spec/unit_spec.rb @@ -62,6 +62,30 @@ expect(Unit(2)).to be > UnitOne.new end + it "should return false from == for objects that are not comparable" do + expect(Unit(1) == nil).to be false + expect(Unit(1) == "string").to be false + expect(Unit(1) == []).to be false + expect(Unit(1) == :symbol).to be false + expect(Unit(1)).not_to eq(nil) + expect(Unit(1)).not_to eq("string") + end + + it "should treat != consistently with ==" do + expect(Unit(1) != nil).to be true + expect(Unit(1) != "string").to be true + end + + it "should return nil from <=> for objects that are not comparable" do + expect(Unit(1) <=> nil).to be_nil + expect(Unit(1) <=> "string").to be_nil + end + + it "should raise ArgumentError for ordered comparison with incomparable objects" do + expect { Unit(1) > nil }.to raise_error(ArgumentError) + expect { Unit(1) < "string" }.to raise_error(ArgumentError) + end + it "should support eql comparison" do expect(Unit(1)).to eql(Unit(1)) expect(Unit(1.0)).not_to eql(Unit(1))