From 118b4c34ec42d8adbeeacef8f47095565a021f19 Mon Sep 17 00:00:00 2001 From: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:19:38 +0700 Subject: [PATCH] Fix Quantity equals/hashCode contract for quantities of different kinds Quantity.equals() delegated to compareTo(), which throws IllegalArgumentException when the two quantities have different KindOfQuantity, so comparing a Mass to a Volume threw instead of returning false. Quantity also overrode equals() without overriding hashCode(), so equal quantities expressed in different units hashed differently and did not match in a HashSet or HashMap. --- api/src/org/labkey/api/ontology/Quantity.java | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/ontology/Quantity.java b/api/src/org/labkey/api/ontology/Quantity.java index a3a3c197b10..9f5db10f3e5 100644 --- a/api/src/org/labkey/api/ontology/Quantity.java +++ b/api/src/org/labkey/api/ontology/Quantity.java @@ -30,6 +30,9 @@ import java.math.BigDecimal; import java.text.Format; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; /* CONSIDER: it's tempting to store BigDecimal in memory after parse for math/conversion purposed, even if we store as double in the database */ @@ -220,7 +223,14 @@ public String toString() @Override public boolean equals(Object obj) { - return obj instanceof Quantity other && 0 == compareTo(other); + return obj instanceof Quantity other && this.kind == other.kind && 0 == compareTo(other); + } + + /* compareTo() treats equal values as equal regardless of BigDecimal scale or Double/BigDecimal representation, so hash on the common double value */ + @Override + public int hashCode() + { + return Objects.hash(kind, value.doubleValue()); } @Override @@ -577,6 +587,53 @@ public void testConversion() assertEquals(new Quantity(KindOfQuantity.Mass, 1234000d), q); } + @Test + public void testEqualsAcrossKinds() + { + Quantity mass = Quantity.of(1, Unit.g); + Quantity volume = Quantity.of(1, Unit.mL); + Quantity count = Quantity.of(1, Unit.unit); + + assertNotEquals(mass, volume); + assertNotEquals(volume, mass); + assertNotEquals(mass, count); + assertNotEquals(count, volume); + + assertNotEquals(mass, null); + assertNotEquals(mass, 1.0); + } + + @Test + public void testHashCode() + { + Quantity oneGram = Quantity.of(1, Unit.g); + Quantity oneThousandMilligrams = Quantity.of(1000, Unit.mg); + Quantity oneThousandthKilogram = Quantity.of(new BigDecimal("0.001"), Unit.kg); + Quantity oneGramAsBigDecimal = Quantity.of(new BigDecimal("1.000"), Unit.g); + + assertEquals(oneGram, oneThousandMilligrams); + assertEquals(oneGram.hashCode(), oneThousandMilligrams.hashCode()); + assertEquals(oneGram, oneThousandthKilogram); + assertEquals(oneGram.hashCode(), oneThousandthKilogram.hashCode()); + assertEquals(oneGram, oneGramAsBigDecimal); + assertEquals(oneGram.hashCode(), oneGramAsBigDecimal.hashCode()); + + Set quantities = new HashSet<>(); + quantities.add(oneGram); + assertTrue(quantities.contains(oneThousandMilligrams)); + assertTrue(quantities.contains(oneThousandthKilogram)); + assertTrue(quantities.contains(oneGramAsBigDecimal)); + + quantities.add(oneThousandMilligrams); + quantities.add(oneThousandthKilogram); + quantities.add(oneGramAsBigDecimal); + assertEquals(1, quantities.size()); + + quantities.add(Quantity.of(2, Unit.g)); + quantities.add(Quantity.of(1, Unit.mL)); + assertEquals(3, quantities.size()); + } + @Test public void testDoubleValue() {