diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2025-05-30 20:10:09 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2025-05-30 20:30:44 -0500 |
commit | 7db19d307970b73559239ec343c92c7876510c2a (patch) | |
tree | 8b5515e50178e9c683931606745899e33e33ce23 | |
parent | 4ff3a48a659aec957c5496923a79c4487d9062ec (diff) |
Ensure LinearUnit&Prefix ==/hash obey contracts
Previously, these classes' equals() and hashCode() methods did not obey
the contracts:
For equals(), I considered two values equal even if there was a very
small deviation, in order to avoid floating-point error. This equals
relation is not transitive (i.e. it is possible that a = b && b = c but
a ≠ c), violating the contract of equals.
This also makes it impossible to properly implement hashCode, as if two
values are equal, they must have the same hash code. The solution I had
provided is an ineffective hack, which could mess with hash maps and
sets.
I have changed the implementation to demand exact equality. I have also
provided equalsApproximately() methods to both classes that use the old
behaviour. Hash codes are only really used for hash maps, and the old
implementation doesn't even achieve its purpose, so I did not add a
method to return the old hash behaviour.
-rw-r--r-- | src/main/java/sevenUnits/unit/LinearUnit.java | 15 | ||||
-rw-r--r-- | src/main/java/sevenUnits/unit/UnitPrefix.java | 18 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/DecimalComparison.java | 15 | ||||
-rw-r--r-- | src/test/java/sevenUnits/unit/UnitTest.java | 8 |
4 files changed, 37 insertions, 19 deletions
diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 3c3703c..d453a43 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -227,6 +227,19 @@ public final class LinearUnit extends Unit { return false; final LinearUnit other = (LinearUnit) obj; return Objects.equals(this.getBase(), other.getBase()) + && Double.compare(this.getConversionFactor(), + other.getConversionFactor()) == 0; + } + + /** + * @return true iff this unit and other are equal, + * ignoring small differences caused by floating-point error. + * + * @apiNote This method is not transitive, + * so it cannot be used as an equals method. + */ + public boolean equalsApproximately(final LinearUnit other) { + return Objects.equals(this.getBase(), other.getBase()) && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); } @@ -247,7 +260,7 @@ public final class LinearUnit extends Unit { @Override public int hashCode() { return 31 * this.getBase().hashCode() - + DecimalComparison.hash(this.getConversionFactor()); + + Double.hashCode(this.getConversionFactor()); } /** diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 9035969..ec4be48 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -119,6 +119,22 @@ public final class UnitPrefix implements Nameable { if (!(obj instanceof UnitPrefix)) return false; final UnitPrefix other = (UnitPrefix) obj; + return Double.compare(this.getMultiplier(), + other.getMultiplier()) == 0; + } + + /** + * @return true iff this prefix and other are equal, + * ignoring small differences caused by floating-point error. + * + * @apiNote This method is not transitive, + * so it cannot be used as an equals method. + */ + public boolean equalsApproximately(final UnitPrefix other) { + if (this == other) + return true; + if (other == null) + return false; return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); } @@ -143,7 +159,7 @@ public final class UnitPrefix implements Nameable { */ @Override public int hashCode() { - return DecimalComparison.hash(this.getMultiplier()); + return Double.hashCode(this.getMultiplier()); } /** diff --git a/src/main/java/sevenUnits/utils/DecimalComparison.java b/src/main/java/sevenUnits/utils/DecimalComparison.java index 62c3720..03dd15b 100644 --- a/src/main/java/sevenUnits/utils/DecimalComparison.java +++ b/src/main/java/sevenUnits/utils/DecimalComparison.java @@ -69,7 +69,6 @@ public final class DecimalComparison { * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 - * @see #hash(double) */ public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); @@ -194,7 +193,6 @@ public final class DecimalComparison { * @param b second value to test * @return whether they are equal * @since 2020-09-07 - * @see #hash(double) */ public static final boolean equals(final UncertainDouble a, final UncertainDouble b) { @@ -236,19 +234,6 @@ public final class DecimalComparison { epsilon); } - /** - * Takes the hash code of doubles. Values that are equal according to - * {@link #equals(double, double)} will probably have the same hash code. - * - * @param d double to hash - * @return hash code of double - * @since 2019-10-16 - */ - // TODO reconsider using this - public static final int hash(final double d) { - return Float.hashCode((float) d); - } - // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index 6ac0ebd..f8d3040 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -48,8 +48,12 @@ class UnitTest { final LinearUnit foot = Metric.METRE.times(0.3048) .withName(NameSymbol.of("foot", "ft")); - assertEquals(inch.plus(foot), Metric.METRE.times(0.3302)); - assertEquals(foot.minus(inch), Metric.METRE.times(0.2794)); + assertTrue(inch.plus(foot).equalsApproximately(Metric.METRE.times(0.3302)), + String.format("Expected: %s, Actual: %s", + inch.plus(foot), Metric.METRE.times(0.3302))); + assertTrue(foot.minus(inch).equalsApproximately(Metric.METRE.times(0.2794)), + String.format("Expected: %s, Actual: %s", + foot.minus(inch), Metric.METRE.times(0.2794))); // test with LinearUnitValue final LinearUnitValue value1 = LinearUnitValue.getExact(Metric.METRE, 15); |