Reviewing the Money Math That Used a Float
The total was computed in binary floating point, so the sum was a cent off on some carts, and the difference only appeared when the decimals did not fit the…
I started at the incident, because that is where the symptom landed, and walked it backwards link by link until the off stopped looking innocent.
Strip the money computed in binary floating point down and one property makes it intermittent: most decimal amounts round to a representable double and back without visible error, so the bug hides until the amount happens to land on a value the mantissa cannot hold exactly.
I have since met the same mechanics in plenty of language whose default numeric type is the IEEE 754 double.
Why the decimal is inexact
The double is the binary, and the binary is the powers of two, and the powers of two are the exact for the halves and the quarters, and the exact is the not for the tenths, and the tenths is the decimal's fraction, and the fraction is the repeating in the binary, and the repeating is the truncated, and the truncated is the error, and the error is the small, and the small is the accumulated.
The accumulation is the sum, and the sum is the cart's items, and the items are the many, and the many is the error's growth, and the growth is the visible, and the visible is the cent, and the cent is the reconciliation's difference, and the difference is the audit, and the audit is the incident, and the incident is the reason the money should never be the float.
The visible is the not always, because the amount is the sometimes exact, and the exact is the no error, and the no error is the test's pass, and the pass is the confidence, and the confidence is the deploy, and the deploy is the production's amount, and the amount is the other, and the other is the error, and the error is the intermittent, and the intermittent is the hard to catch.
What the review saw
The review saw the arithmetic, and the arithmetic was the sum, and the sum was the reduce, and the reduce was the idiomatic, and the idiomatic was the pass, because the code was the correct JavaScript, and the correct was the review's criterion, and the criterion was the met, and the met was the miss, because the type was the wrong and the type is not the syntax.
The review did not ask what the number represents, and the represents is the money, and the money is the decimal, and the decimal is the exact's requirement, and the requirement is the type, and the type is the integer cents or the decimal, and the two are the fix, and the fix is the representation, and the representation is the review's question that nobody asked.
This is the same representation error as the AI-suggested rounding that broke the money, and the shared property is the one worth fixing.
The questions that catch it
What does this number represent? The answer is the money, and the money is the exact decimal, and the exact is the type's requirement, and the requirement is the fix, and the question is the review's first, because the float is the measurement's type and not the money's.
Where is the rounding applied? The answer should be the once at the end, and the once is the deterministic, and the deterministic is the fix, and the fix is the explicit rounding, and the explicit is the discipline, and the question is the review's second, because the rounding scattered through the calculation compounds.
Is the sum compared or displayed? The displayed is the format, and the format is the two decimals, and the two is the rounding's place, and the compared is the equality, and the equality is the float's trap, and the trap is the fix, and the question is the review's third.
What happens on a hundred items? The answer is the accumulation, and the accumulation is the visible error, and the visible is the test's case, and the case is the fix, and the question is the review's fourth, because the small cart hides the drift.
The fix
Stored the money as integer cents. The amount is the integer, and the integer is the exact, and the exact is the arithmetic's safety, and the safety is the fix, and the fix is the representation, and the representation is the one decision, and the decision is the discipline, because the integer arithmetic is exact and the float arithmetic is not.
Converted at the boundary only. The API sends the decimal string, and the string is the parsed to the cents, and the cents is the internal, and the internal is the arithmetic, and the arithmetic is the converted back at the response, and the back is the string, and the string is the fix, and the fix is the boundary's discipline.
Rounded once, explicitly, at the end. The rounding is the single place, and the single is the deterministic, and the deterministic is the fix, and the fix is the function, and the function is the tested, and the tested is the discipline, because the scattered rounding is the compounding error.
Asserted the totals in the tests with the awkward amounts. The test uses the amounts that do not fit the binary, and the amounts are the error's exposure, and the exposure is the catch, and the catch is the guard, and the guard is the prevention, and the prevention is the test.
The rule
Binary floating point cannot represent most decimal fractions exactly, so money computed as a float drifts by amounts that appear only on some values. Store money as integer minor units or a decimal type, convert at the boundary, round once at the end, and test with amounts that do not divide evenly.
The cart total was a cent off on some orders and exact on others, because the prices were doubles and the sum accumulated the binary representation error until it crossed a rounding boundary. Name that one property in review and this class of bug gets hard to ship.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud