Skip to content

Commit

Permalink
Add BigDecimal equality using compareTo checks
Browse files Browse the repository at this point in the history
  • Loading branch information
ac183 committed Nov 20, 2021
1 parent b53172f commit 156b101
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 2 deletions.
30 changes: 30 additions & 0 deletions docs/_manual/12-bigdecimal-compareto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: "`BigDecimal` equality using `compareTo(BigDecimal val)`"
permalink: /manual/bigdecimal-compareto/
---
The `Comparable` interface strongly recommends but does not require that implementations consider two objects equal using `compareTo` whenever they are equal using `equals` and vice versa. `BigDecimal` is a class where this is not applied.

{% highlight java %}
BigDecimal zero = new BigDecimal("0");
BigDecimal alsoZero = new BigDecimal("0.0");

// prints true - zero is the same as zero
System.out.println(zero.compareTo(alsoZero) == 0);
// prints false - zero is not the same as zero
System.out.println(zero.equals(alsoZero));
{% endhighlight %}

This is because `BigDecimal` can have multiple representations of the same value. It uses an unscaled value and a scale so, for example, the value of 1 can be represented as unscaled value 1 with scale of 0 (the number of places after the decimal point) or as unscaled value 10 with scale of 1 resolving to 1.0. Its `equals` and `hashCode` methods use both of these attributes in their calculation rather than the resolved value.

If your class contains any `BigDecimal` fields, and you would like comparably equal values to be considered the same, then the `equals` method must use `compareTo` for the check and the `hashCode` calculation must derive the same value for all `BigDecimal` instances that are equal using `compareTo` (taking care if it is a nullable field).

EqualsVerifier can check this by adding `usingBigDecimalCompareTo()`:

{% highlight java %}
EqualsVerifier.forClass(FooWithComparablyEqualBigDecimalFields.class)
.usingBigDecimalCompareTo()
.verify();
{% endhighlight %}

There is more information on `compareTo` and `equals` in the `Comparable` Javadoc and Effective Java's chapter on implementing `Comparable`.
There is more information on `BigDecimal` in its Javadoc (and its representation can be seen by printing `unscaledValue()` and `scale()`).
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public final class ConfiguredEqualsVerifier implements EqualsVerifierApi<Void> {
private final EnumSet<Warning> warningsToSuppress;
private final FactoryCache factoryCache;
private boolean usingGetClass;
private boolean usingBigDecimalCompareTo;

/** Constructor. */
public ConfiguredEqualsVerifier() {
Expand Down Expand Up @@ -90,6 +91,13 @@ public ConfiguredEqualsVerifier usingGetClass() {
return this;
}

/** {@inheritDoc} */
@Override
public ConfiguredEqualsVerifier usingBigDecimalCompareTo() {
usingBigDecimalCompareTo = true;
return this;
}

/**
* Factory method. For general use.
*
Expand All @@ -102,7 +110,8 @@ public <T> SingleTypeEqualsVerifierApi<T> forClass(Class<T> type) {
type,
EnumSet.copyOf(warningsToSuppress),
factoryCache,
usingGetClass
usingGetClass,
usingBigDecimalCompareTo
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,12 @@ public <S> EqualsVerifierApi<T> withGenericPrefabValues(
* @see Warning#STRICT_INHERITANCE
*/
public EqualsVerifierApi<T> usingGetClass();

/**
* Checks that equality of {@code BigDecimal} fields is implemented using {@code compareTo}
* rather than {@code equals}.
*
* @return {@code this}, for easy method chaining.
*/
public EqualsVerifierApi<T> usingBigDecimalCompareTo();
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ public MultipleTypeEqualsVerifierApi usingGetClass() {
return this;
}

/** {@inheritDoc} */
@Override
public MultipleTypeEqualsVerifierApi usingBigDecimalCompareTo() {
ev.usingBigDecimalCompareTo();
return this;
}

/**
* Removes the given type or types from the list of types to verify.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class SingleTypeEqualsVerifierApi<T> implements EqualsVerifierApi<T> {

private EnumSet<Warning> warningsToSuppress = EnumSet.noneOf(Warning.class);
private boolean usingGetClass = false;
private boolean usingBigDecimalCompareTo = false;
private boolean hasRedefinedSuperclass = false;
private Class<? extends T> redefinedSubclass = null;
private FactoryCache factoryCache = new FactoryCache();
Expand Down Expand Up @@ -53,17 +54,21 @@ public SingleTypeEqualsVerifierApi(Class<T> type) {
* @param factoryCache Factories that can be used to create values.
* @param usingGetClass Whether {@code getClass} is used in the implementation of the {@code
* equals} method, instead of an {@code instanceof} check.
* @param usingBigDecimalCompareTo Whether {@code compareTo} is used for {@code BigDecimal}
* field equality check in the implementation of the {@code equals} method.
*/
public SingleTypeEqualsVerifierApi(
Class<T> type,
EnumSet<Warning> warningsToSuppress,
FactoryCache factoryCache,
boolean usingGetClass
boolean usingGetClass,
boolean usingBigDecimalCompareTo
) {
this(type);
this.warningsToSuppress = EnumSet.copyOf(warningsToSuppress);
this.factoryCache = this.factoryCache.merge(factoryCache);
this.usingGetClass = usingGetClass;
this.usingBigDecimalCompareTo = usingBigDecimalCompareTo;
}

/**
Expand Down Expand Up @@ -128,6 +133,13 @@ public SingleTypeEqualsVerifierApi<T> usingGetClass() {
return this;
}

/** {@inheritDoc} */
@Override
public SingleTypeEqualsVerifierApi<T> usingBigDecimalCompareTo() {
usingBigDecimalCompareTo = true;
return this;
}

/**
* Signals that all given fields are not relevant for the {@code equals} contract. {@code
* EqualsVerifier} will not fail if one of these fields does not affect the outcome of {@code
Expand Down Expand Up @@ -379,6 +391,7 @@ private Configuration<T> buildConfig() {
hasRedefinedSuperclass,
redefinedSubclass,
usingGetClass,
usingBigDecimalCompareTo,
warningsToSuppress,
factoryCache,
ignoredAnnotationClassNames,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class FieldsChecker<T> implements Checker {
private final SymmetryFieldCheck<T> symmetryFieldCheck;
private final TransientFieldsCheck<T> transientFieldsCheck;
private final TransitivityFieldCheck<T> transitivityFieldCheck;
private final BigDecimalFieldCheck<T> bigDecimalFieldCheck;

public FieldsChecker(Configuration<T> config) {
this.config = config;
Expand All @@ -48,6 +49,8 @@ public FieldsChecker(Configuration<T> config) {
this.symmetryFieldCheck = new SymmetryFieldCheck<>(prefabValues, typeTag);
this.transientFieldsCheck = new TransientFieldsCheck<>(config);
this.transitivityFieldCheck = new TransitivityFieldCheck<>(prefabValues, typeTag);
this.bigDecimalFieldCheck =
new BigDecimalFieldCheck<>(config.getCachedHashCodeInitializer());
}

@Override
Expand Down Expand Up @@ -80,6 +83,10 @@ public void check() {
skippingSignificantFieldCheck
);
}

if (config.isUsingBigDecimalCompareTo()) {
inspector.check(bigDecimalFieldCheck);
}
}

private boolean ignoreMutability(Class<?> type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package nl.jqno.equalsverifier.internal.checkers.fieldchecks;

import static nl.jqno.equalsverifier.internal.util.Assert.assertEquals;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import nl.jqno.equalsverifier.internal.reflection.FieldAccessor;
import nl.jqno.equalsverifier.internal.reflection.ObjectAccessor;
import nl.jqno.equalsverifier.internal.util.CachedHashCodeInitializer;
import nl.jqno.equalsverifier.internal.util.Formatter;

public class BigDecimalFieldCheck<T> implements FieldCheck<T> {

private final CachedHashCodeInitializer<T> cachedHashCodeInitializer;

public BigDecimalFieldCheck(CachedHashCodeInitializer<T> cachedHashCodeInitializer) {
this.cachedHashCodeInitializer = cachedHashCodeInitializer;
}

@Override
public void execute(
ObjectAccessor<T> referenceAccessor,
ObjectAccessor<T> copyAccessor,
FieldAccessor fieldAccessor
) {
if (BigDecimal.class.equals(fieldAccessor.getFieldType())) {
Field field = fieldAccessor.getField();
BigDecimal referenceField = (BigDecimal) referenceAccessor.getField(field);
BigDecimal changedField = referenceField.setScale(
referenceField.scale() + 1,
RoundingMode.UNNECESSARY
);
ObjectAccessor<T> changed = copyAccessor.withFieldSetTo(field, changedField);

T left = referenceAccessor.get();
T right = changed.get();

checkEquals(field, referenceField, changedField, left, right);
checkHashCode(field, referenceField, changedField, left, right);
}
}

private void checkEquals(
Field field,
BigDecimal referenceField,
BigDecimal changedField,
T left,
T right
) {
Formatter f = Formatter.of(
"BigDecimal equality by comparison: object does not equal a copy of itself where BigDecimal field %%" +
" has a value that is equal using compareTo: %% compared to %%" +
"\nIf these values should be considered equal then use compareTo rather than equals for this field." +
"\nIf these values should not be considered equal, then remove usingBigDecimalCompareTo() to disable this check.",
field.getName(),
referenceField,
changedField
);
assertEquals(f, left, right);
}

private void checkHashCode(
Field field,
BigDecimal referenceField,
BigDecimal changedField,
T left,
T right
) {
Formatter f = Formatter.of(
"BigDecimal equality by comparison: hashCode of object does not equal hashCode of a copy of itself" +
" where BigDecimal field %%" +
" has a value that is equal using compareTo: %% compared to %%" +
"\nIf these values should be considered equal then make sure to derive the same constituent hashCode from this field." +
"\nIf these values should not be considered equal, then remove usingBigDecimalCompareTo() to disable this check.",
field.getName(),
referenceField,
changedField
);
int leftHashCode = cachedHashCodeInitializer.getInitializedHashCode(left);
int rightHashCode = cachedHashCodeInitializer.getInitializedHashCode(right);
assertEquals(f, leftHashCode, rightHashCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public final class Configuration<T> {
private final boolean hasRedefinedSuperclass;
private final Class<? extends T> redefinedSubclass;
private final boolean usingGetClass;
private final boolean usingBigDecimalCompareTo;
private final EnumSet<Warning> warningsToSuppress;

private final TypeTag typeTag;
Expand All @@ -47,6 +48,7 @@ private Configuration(
boolean hasRedefinedSuperclass,
Class<? extends T> redefinedSubclass,
boolean usingGetClass,
boolean usingBigDecimalCompareTo,
EnumSet<Warning> warningsToSuppress,
List<T> equalExamples,
List<T> unequalExamples
Expand All @@ -62,6 +64,7 @@ private Configuration(
this.hasRedefinedSuperclass = hasRedefinedSuperclass;
this.redefinedSubclass = redefinedSubclass;
this.usingGetClass = usingGetClass;
this.usingBigDecimalCompareTo = usingBigDecimalCompareTo;
this.warningsToSuppress = warningsToSuppress;
this.equalExamples = equalExamples;
this.unequalExamples = unequalExamples;
Expand All @@ -76,6 +79,7 @@ public static <T> Configuration<T> build(
boolean hasRedefinedSuperclass,
Class<? extends T> redefinedSubclass,
boolean usingGetClass,
boolean usingBigDecimalCompareTo,
EnumSet<Warning> warningsToSuppress,
FactoryCache factoryCache,
Set<String> ignoredAnnotationClassNames,
Expand Down Expand Up @@ -110,6 +114,7 @@ public static <T> Configuration<T> build(
hasRedefinedSuperclass,
redefinedSubclass,
usingGetClass,
usingBigDecimalCompareTo,
warningsToSuppress,
equalExamples,
unequals
Expand Down Expand Up @@ -225,6 +230,10 @@ public boolean isUsingGetClass() {
return usingGetClass;
}

public boolean isUsingBigDecimalCompareTo() {
return usingBigDecimalCompareTo;
}

public EnumSet<Warning> getWarningsToSuppress() {
return EnumSet.copyOf(warningsToSuppress);
}
Expand Down
Loading

0 comments on commit 156b101

Please sign in to comment.