Fix Quantity equals/hashCode contract for quantities of different kinds - #7945
Open
eastagiletracker wants to merge 1 commit into
Open
Fix Quantity equals/hashCode contract for quantities of different kinds#7945eastagiletracker wants to merge 1 commit into
eastagiletracker wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR proposes a fix for the
equals/hashCodecontract onorg.labkey.api.ontology.Quantity, so that comparing a mass to a volume returnsfalseinstead of throwing, and equal quantities expressed in different units hash alike. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/389. You can sign in with your GitHub ID to claim ownership of the project.What is wrong
Quantity.equals(Object)delegates tocompareTo(Quantity), which throwsIllegalArgumentExceptionwhen the two operands have a differentKindOfQuantity.equalsis therefore not total:Quantity.of(1, Unit.g).equals(Quantity.of(1, Unit.mL))throws rather than answeringfalse.Quantityalso overridesequalswithout overridinghashCode, so it inherits identity hashing while comparing by value —1gand1000mgareequalbut land in different buckets.That matters because
Quantityinstances travel as plainObject/Numberthrough code that has no idea they are special:Parameter.getValueToBindunwraps them on the way to JDBC,ColumnRenderProperties.getDefaultFormatFn,DataColumn.getStringValueandDisplayColumnrender them, andAbstractQueryUpdateService.coerceTypesValueputs them into the provided-values map on insert and update. AnyObjects.equals,List.contains,HashSetorHashMapreached from there is either silently wrong (the hash) or throws (a cross-kind compare).Reproduction on
developate307b80Compiled
:server:modules:platform:apifrom a clean checkout and ran a three-line driver against the module's own runtime classpath:The change
equalsnow short-circuits onthis.kind == other.kindbefore delegating, so a cross-kind comparison answersfalseinstead of throwing, andhashCodeis implemented asObjects.hash(kind, value.doubleValue()). Hashing thedoubleview is what keeps it consistent withequals:compareTotreats equal values as equal acrossBigDecimalscale and across theDouble/BigDecimalsplit that the constructors can produce, and two values that compare equal always share the samedoubleValue(). Five lines of production code;compareTois deliberately left throwing, since sorting a list that mixes masses and volumes should still fail loudly. Nothing that previously returnedtruechanges, so same-kind behaviour is untouched, andQuantityis not used as a key in any collection in the tree today.Verification
Two tests were added to the existing
Quantity.TestCase, whichExperimentModule.getUnitTests()already registers. Reverting only the two behavioural lines while keeping the tests puts both new tests red on the unpatched tree —testEqualsAcrossKindswith theIllegalArgumentExceptionabove,testHashCodewithexpected:<380812044> but was:<846918683>— while the nine pre-existing tests stay green. With the fix applied,Quantity$TestCaseandUnit$TestCaserun 18 tests green.For a wider check, all 138 embedded
*$TestCaseclasses under:server:modules:platform:apiwere run throughJUnitCorebefore and after the change. The result set is byte-identical: 73 pass, 65 fail, and those 65 are the ones that need a running server rather than anything this change touches.:server:modules:platform:api:compileJavaand:server:modules:platform:experiment:compileJavaare both green.How this was managed
We imported your issues and pull requests onto a board and used it to run this work — the story for this fix is at https://eastagiletracker.com/projects/389/stories/281041 and the board itself at https://eastagiletracker.com/projects/389 (7,929 stories imported from this repository's issues and pull requests, plus milestones as epics).
If you'd rather not receive contributions like this, reply
no-more-prson this pull request and we won't open any further ones on your repositories.Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com