Fix fraction hashCode
The sign must be incorporated into the hash as a fraction with the same
numerical value can have different representations:
-1/3 == 1/-3
Using the sign ensures that -1/3 and 1/3 have different hash codes. This
would not be true if the hash were constructed using only the absolute
values of numerator and denominator.
diff --git a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java
index cee2a6e..3962e2e 100644
--- a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java
+++ b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java
@@ -1019,7 +1019,17 @@
@Override
public int hashCode() {
- return 37 * (37 * 17 + numerator.hashCode()) + denominator.hashCode();
+ // Incorporate the sign and absolute values of the numerator and denominator.
+ // Equivalent to
+ // Arrays.hashCode(new int[] {signum(), numerator.abs(), denominator.abs()})
+ // int hash = 1;
+ // hash = 31 * hash + signum();
+ // hash = 31 * hash + numerator.abs().hashCode();
+ // hash = 31 * hash + denominator.abs().hashCode();
+ // Note: BigInteger.hashCode() * BigInteger.signum() == BigInteger.abs().hashCode().
+ final int numS = numerator.signum();
+ final int denS = denominator.signum();
+ return 31 * (31 * (31 + numS * denS) + numerator.hashCode() * numS) + denominator.hashCode() * denS;
}
/**
diff --git a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java
index 31e7cde..be85fb3 100644
--- a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java
+++ b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java
@@ -706,7 +706,17 @@
@Override
public int hashCode() {
- return 37 * (37 * 17 + numerator) + denominator;
+ // Incorporate the sign and absolute values of the numerator and denominator.
+ // Equivalent to
+ // Arrays.hashCode(new int[] {signum(), Math.abs(numerator), Math.abs(denominator)})
+ // int hash = 1;
+ // hash = 31 * hash + signum();
+ // hash = 31 * hash + Math.abs(numerator);
+ // hash = 31 * hash + Math.abs(denominator);
+ // Note: x * Integer.signum(x) is equivalent to Math.abs(x).
+ final int numS = Integer.signum(numerator);
+ final int denS = Integer.signum(denominator);
+ return 31 * (31 * (31 + numS * denS) + numerator * numS) + denominator * denS;
}
/**
diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java
index e3a611b..4d3377c 100644
--- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java
+++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java
@@ -19,13 +19,12 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
+import java.util.Arrays;
import org.apache.commons.numbers.core.TestUtils;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-
public class BigFractionTest {
private static void assertFraction(long expectedNumerator, long expectedDenominator, BigFraction actual) {
@@ -595,7 +594,6 @@
Assertions.assertEquals(new BigDecimal("0.333"), BigFraction.of(1, 3).bigDecimalValue(3, RoundingMode.DOWN));
}
- @Disabled
@Test
public void testEqualsAndHashCode() {
BigFraction zero = BigFraction.of(0, 1);
@@ -649,6 +647,12 @@
Assertions.assertNotSame(f1, f2, "Do not call this assertion with the same object");
Assertions.assertEquals(f1, f2);
Assertions.assertEquals(f1.hashCode(), f2.hashCode(), "Equal fractions have different hashCode");
+ // Check the hashcode computation.
+ // This is not mandated but is a recommendation.
+ final int expected = Arrays.hashCode(new Object[] {f1.signum(),
+ f1.getNumerator().abs(),
+ f1.getDenominator().abs()});
+ Assertions.assertEquals(expected, f1.hashCode(), "Hashcode not equal to using Arrays.hashCode");
}
@Test
diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java
index 0888a75..ada4269 100644
--- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java
+++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java
@@ -16,13 +16,12 @@
*/
package org.apache.commons.numbers.fraction;
+import java.util.Arrays;
import org.apache.commons.numbers.core.TestUtils;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-
/**
*/
public class FractionTest {
@@ -427,7 +426,6 @@
);
}
- @Disabled
@Test
public void testEqualsAndHashCode() {
Fraction zero = Fraction.of(0, 1);
@@ -492,6 +490,12 @@
Assertions.assertNotSame(f1, f2, "Do not call this assertion with the same object");
Assertions.assertEquals(f1, f2);
Assertions.assertEquals(f1.hashCode(), f2.hashCode(), "Equal fractions have different hashCode");
+ // Check the hashcode computation.
+ // This is not mandated but is a recommendation.
+ final int expected = Arrays.hashCode(new int[] {f1.signum(),
+ Math.abs(f1.getNumerator()),
+ Math.abs(f1.getDenominator())});
+ Assertions.assertEquals(expected, f1.hashCode(), "Hashcode not equal to using Arrays.hashCode");
}
@Test