Arbitrary-Precision Decimal
What it is
Computers usually store numbers with a fraction as floating point. Floating point is fast, but it cannot hold 0.1 exactly. That is why 0.1 + 0.2 gives 0.30000000000000004 in most languages. For money this is not good enough. A cent that drifts once drifts again on every payment. A decimal type stores numbers in base ten, so 0.1 is exactly 0.1.
The core idea
Keep two things for each number: a whole number of digits and a scale. The scale is how many of those digits sit after the point. 19.99 is the digits 1999 with scale 2. -0.050 is -50 with scale 3. Adding two numbers means giving both the same scale first, then adding the whole numbers. Multiplying means multiplying the whole numbers and adding the scales. Comparing also needs the same scale first. 1.0 and 1.00 are the same value.
Example
1.10 + 2.205: give 1.10 scale 3, so it becomes 1100. Then 1100 + 2205 = 3305, scale 3, which prints as 3.305. 1.5 * 0.25: 15 * 25 = 375, scale 1 + 2 = 3, which prints as 0.375.
Rounding modes
Rounding keeps some digits and may move the kept part one step.
downmoves toward zero.floormoves toward minus infinity.ceilingmoves toward plus infinity.half_upgoes to the nearest value. A tie goes away from zero, so-2.5becomes-3.half_evengoes to the nearest value. A tie goes to the even neighbour, so2.5becomes2and3.5becomes4.
Banks use half_even because rounding every tie up adds a small bias that grows over many payments. A tie is exact only when every dropped digit after the 5 is zero. 0.1251 is not a tie.
Division
Some quotients never end, like 100 / 3. So division always takes a scale and a mode, and rounds the exact answer once. Rounding twice is a classic bug. 0.125125 rounded to three digits is 0.125, which then rounds to 0.12. Rounded once it is 0.13. Dividing by zero is an error, not zero.
Where you meet this
- Python's
decimalmodule stores numbers in base ten, so0.1 + 0.1 + 0.1 - 0.3is exactly zero, unlike with a float. Its default rounding is round half to even. (Python decimal) - PostgreSQL's
numerictype keeps a very large number of digits exactly, and the manual recommends it for money and other amounts where exactness is required. (PostgreSQL numeric types) - Java's
BigDecimalis an arbitrary-precision decimal number. You must say how to round when a division does not end, andRoundingMode.HALF_EVENis banker's rounding. (Java BigDecimal) - Stripe's API takes amounts as whole numbers in the smallest unit of the currency, for example 1000 means 10.00 USD. That is the same idea: keep money as exact integers, never as floats. (Stripe currencies)
When to choose it: money, prices, tax, interest, or any amount a person will check by hand, where 0.1 + 0.2 must be exactly 0.3 and rounding must follow a stated rule.
When not to: science and graphics, where speed matters more than the last digit and a float's small error is fine.
Common bugs
- Storing numbers as floating point, even only for one step.
- Dropping zeros at the end, so
1.50prints as1.5. - Printing
-0.00for a negative number that rounds to zero. - Comparing the digits without lining up the points first.
- Sending a negative tie toward zero in
half_up. - Deciding a tie from the first dropped digit only.
- Storing the digits in 64 bits, which wraps around after about 19 digits.
- Accepting an exponent of any size, so a short string asks for a huge number.
Keep it fast
Keep numbers in their whole-number form between calls. Build text only in String. Keep powers of ten ready instead of building them one digit at a time.
Learn more
Look up IEEE 754 for floating point and decimal formats. The General Decimal Arithmetic specification explains rounding modes in detail. After this, try the Double-Entry Ledger challenge, where these amounts move between accounts.