diff --git a/kernel/src/main/scala/cats/kernel/instances/DoubleInstances.scala b/kernel/src/main/scala/cats/kernel/instances/DoubleInstances.scala index 240a69b54e..77552070bb 100644 --- a/kernel/src/main/scala/cats/kernel/instances/DoubleInstances.scala +++ b/kernel/src/main/scala/cats/kernel/instances/DoubleInstances.scala @@ -36,9 +36,23 @@ class DoubleGroup extends CommutativeGroup[Double] { class DoubleOrder extends Order[Double] with Hash[Double] { - def hash(x: Double): Int = x.hashCode() + // Canonicalize +0.0 / -0.0 so Hash agrees with eqv (primitive ==). + // java.lang.Double.hashCode distinguishes the two zeros via bit patterns. + def hash(x: Double): Int = + java.lang.Double.hashCode(if (x == 0.0) 0.0 else x) + + /** + * Compare using primitive relational operators so +0.0 and -0.0 are equal, + * matching [[eqv]] / IEEE floating equality. `java.lang.Double.compare` + * implements a total order that treats them as different, which made + * `Order[Option[Double]].eqv` disagree with `Order[Double].eqv` (#4807). + * NaN values fall through to `Double.compare` for a stable total order. + */ def compare(x: Double, y: Double): Int = - java.lang.Double.compare(x, y) + if (x < y) -1 + else if (x > y) 1 + else if (x == y) 0 + else java.lang.Double.compare(x, y) override def eqv(x: Double, y: Double): Boolean = x == y override def neqv(x: Double, y: Double): Boolean = x != y diff --git a/kernel/src/main/scala/cats/kernel/instances/FloatInstances.scala b/kernel/src/main/scala/cats/kernel/instances/FloatInstances.scala index 42b0838694..0b0d230117 100644 --- a/kernel/src/main/scala/cats/kernel/instances/FloatInstances.scala +++ b/kernel/src/main/scala/cats/kernel/instances/FloatInstances.scala @@ -47,10 +47,23 @@ class FloatGroup extends CommutativeGroup[Float] { */ class FloatOrder extends Order[Float] with Hash[Float] { - def hash(x: Float): Int = x.hashCode() + // Canonicalize +0.0f / -0.0f so Hash agrees with eqv (primitive ==). + // java.lang.Float.hashCode distinguishes the two zeros via bit patterns. + def hash(x: Float): Int = + java.lang.Float.hashCode(if (x == 0.0f) 0.0f else x) + /** + * Compare using primitive relational operators so +0.0f and -0.0f are equal, + * matching [[eqv]] / IEEE floating equality. `java.lang.Float.compare` + * implements a total order that treats them as different, which made + * `Order[Option[Float]].eqv` disagree with `Order[Float].eqv` (#4807). + * NaN values fall through to `Float.compare` for a stable total order. + */ def compare(x: Float, y: Float): Int = - java.lang.Float.compare(x, y) + if (x < y) -1 + else if (x > y) 1 + else if (x == y) 0 + else java.lang.Float.compare(x, y) override def eqv(x: Float, y: Float): Boolean = x == y override def neqv(x: Float, y: Float): Boolean = x != y diff --git a/tests/shared/src/test/scala/cats/tests/OrderSuite.scala b/tests/shared/src/test/scala/cats/tests/OrderSuite.scala index 9901ade6eb..44f09a65da 100644 --- a/tests/shared/src/test/scala/cats/tests/OrderSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/OrderSuite.scala @@ -76,6 +76,31 @@ class OrderSuite extends CatsSuite { assert(OrderOfCmpSub.gt(OrderSuite.CmpSub(2, "ignored"), OrderSuite.CmpSub(1, "ignored"))) assert(OrderOfCmpSub.eqv(OrderSuite.CmpSub(1, "a"), OrderSuite.CmpSub(1, "b"))) } + + // #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") { + val pos = 0.0 + val neg = -0.0 + assert(Order[Double].eqv(pos, neg)) + assertEquals(Order[Double].compare(pos, neg), 0) + assert(cats.kernel.Hash[Double].hash(pos) === cats.kernel.Hash[Double].hash(neg)) + assert(Order[Option[Double]].eqv(Some(pos), Some(neg))) + assertEquals(Order[Option[Double]].compare(Some(pos), Some(neg)), 0) + assert((Some(pos): Option[Double]) === (Some(neg): Option[Double])) + assert(pos === neg) + } + + test("#4807 Float signed zeros are equal under Order and Option") { + val pos = 0.0f + val neg = -0.0f + assert(Order[Float].eqv(pos, neg)) + assertEquals(Order[Float].compare(pos, neg), 0) + assert(cats.kernel.Hash[Float].hash(pos) === cats.kernel.Hash[Float].hash(neg)) + assert(Order[Option[Float]].eqv(Some(pos), Some(neg))) + assertEquals(Order[Option[Float]].compare(Some(pos), Some(neg)), 0) + assert((Some(pos): Option[Float]) === (Some(neg): Option[Float])) + assert(pos === neg) + } } object OrderSuite {