Skip to content

fix(kernel): treat +0.0 and -0.0 as equal in Double/Float Order - #4893

Open
arimu1 wants to merge 2 commits into
typelevel:mainfrom
arimu1:fix/4807-option-signed-zero-eq
Open

arimu1 wants to merge 2 commits into
typelevel:mainfrom
arimu1:fix/4807-option-signed-zero-eq

Conversation

@arimu1

@arimu1 arimu1 commented Aug 8, 2026

Copy link
Copy Markdown

Summary

Fixes #4807.

Order[Double].eqv / Order[Float].eqv use primitive ==, so positive and negative zero compare equal:

0.0 === -0.0 // true

compare previously delegated to java.lang.Double.compare / Float.compare, which implement a total order that distinguishes signed zeros. OptionOrder.eqv is derived from compare (compare == 0), so:

Option(0.0) === Option(-0.0) // was false

That disagreed with both bare === and Scala's Option(0.0) == Option(-0.0).

Change

  • DoubleOrder / FloatOrder.compare: use < / > / == so +0 and -0 are equal (matching eqv and IEEE floating equality). Fall back to java.lang.*.compare only when both relational and equality checks fail (NaN total ordering).
  • hash: canonicalize ±0 to +0 before hashing so Hash agrees with eqv for zeros (bit-pattern hashes previously differed).

Alternative considered

Making eqv use Double.compare(x, y) == 0 would also restore compare/eqv consistency, but would make 0.0 === -0.0 false and would make NaN === NaN true — a larger break from primitive == and from the behavior reported in #4807.

Tests

  • testsJVM/testOnly cats.tests.OrderSuite — 91/91 (Corretto 8 / Scala 2.13), including #4807 cases for Double and Float (bare ===, Order.compare/eqv, Hash, and Option ===/Order).

Checklist

  • Related issue linked (Fixes #4807)
  • Tests added / updated
  • Binary-compatible change (method bodies only)

Order[Double].compare used java.lang.Double.compare, which distinguishes
signed zeros, while eqv used primitive ==. OptionOrder.eqv derives from
compare, so Option(0.0) === Option(-0.0) was false despite 0.0 === -0.0.

Align compare with IEEE floating equality (+0 equals -0) and fall back
to Double/Float.compare only for NaN total ordering. Canonicalize zero
in hash so Hash agrees with eqv.

Fixes typelevel#4807

@johnynek johnynek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should merge this.

Thank you

@johnynek

Copy link
Copy Markdown
Contributor

I think the native CI flaked.

Can you merge origin/main and push to the branch. Let's see if CI passes then.

@arimu1

arimu1 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Merged main into the branch and pushed to retrigger CI.

@johnynek

Copy link
Copy Markdown
Contributor

@satorg what do you think? Can we click merge?

Comment on lines +52 to +55
if (x < y) -1
else if (x > y) 1
else if (x == y) 0
else java.lang.Double.compare(x, y)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first thing that Double.compare does is this:

public static int compare(double d1, double d2) {
    if (d1 < d2)
        return -1;           // Neither val is NaN, thisVal is smaller
    if (d1 > d2)
        return 1;            // Neither val is NaN, thisVal is larger

    ...

Is there a reason for repeating those < and > comparisons in our code?
How about this:

def compare(x: Double, y: Double): Int =
  if (x == y) 0
  else java.lang.Double.compare(x, y)

}

// #4807: +0.0 and -0.0 must agree across eqv, compare, and derived Option eqv
test("#4807 Double signed zeros are equal under Order and Option") {

@satorg satorg Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests are pretty comprehensive, thank you!
However, I'd suggest several improvements, because:

  1. Tests for Doube and Float are identical.
  2. The tests always have 0.0 as LHS and -0.0 as RHS. However, we probably should be checking -0.0 vs 0.0 too in order to prevent asymmetry.

That suggests that the checks could probably be extracted into a generic function and then run for

  • (0.0, -0.0)
  • (-0.0, 0.0)
  • (0.0f, -0.0f)
  • (-0.0f, 0.0f)

Also, it probably makes sense to split the tests according the type classes being tested:

  • Order checks should stay in OrderSuite
  • Hash checks in HashSuite
  • Eq checks in EqSuite.

I realize that this specific PR is focused on the specific issue, but long-term it may be confusing to see tests for Hash or Eq here in OrderSuite.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I’m not sure it’s generally a good idea to include the issue number in the test name. Test names should clearly describe what is being tested, rather than the issue that prompted the PR. If cross-referencing is useful (which it probably is) – adding the issue number to a code comment should be sufficient.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

catsSyntaxEq returns false for equality of positive and negative zero in Option

3 participants