In the fast-paced world of finance, precision is everything. It's always better to avoid a million$ loss because of a rounding / precision error.
When it Comes to Money, Think About Datatype
The starting point of this article is the realization that money is not your average basic number that you can use to count the apples in your basket. What do you get when you try to multiply 10€ with 10$? Tough huh… Have you ever found a miraculous $1.546 in the pocket of an old jacket? Yeah I know, this is not really possible either. These silly examples are here to illustrate the fact that money has its own particular rules and can't only be modeled by a simple number. I reassure you, I am not the first one who realized that (and maybe you did realize it way before me). In 2002, the programmer Martin Fowler proposed in the Patterns of Enterprise Application Architecture a way to represent money, with specific attributes and operand rules. For him, the two minimal viable attributes needed for a money datatype were:
amount
currency
This really basic representation will be our starting point to construct a simple but robust monetary model.
Money Amounts, How to Represent Them
A money amount is definitely a particular number: it has a fixed precision (again, you can't have 4.376$ in your pocket). You need to choose a way to represent it that helps you to respect this constraint.
A Naïve Approach, the Native number JavaScript Datatype
Spoiler alert, this is definitely not a good idea if you don't want to see a few cents (if it's not dollars) disappear in the dark world of floating-points number representation.
The Costly Precision Error
If you have some experience with coding in JavaScript, you know that even the most simple calculation can result in a precision error that you would not expect at first. The most obvious and well-known example to highlight this phenomenon is:
0.1 + 0.2 !== 0.3 // true
0.1 + 0.2 // 0.30000000000000004
If this example doesn't fully convince you, I advice you to have a look at this article which deeps dive a bit further into all the problematic calculation results that you can encounter working with JavaScript native number
type…
This slight delta in the results may appear harmless to you (with a magnitude of about ~ 10^-16), however in a critical financial application, such error can cascade quickly. Consider transferring funds between thousands of accounts, where each transaction involves similar calculations. The slight inaccuracies add up, and before you know it, your financial statements are off by thousands of dollars. And honestly, we can all agree that when it comes to money, error is not allowed: both legally and to build a trusting relationship with your customers.
Why Such an Error?
The first question I asked myself when encountering the issue in one of my projects is why ? I discovered that the source of the problem is not JavaScript and these imprecisions also affect other modern programming languages (Java, C, Python, …).
// In C
#include <stdio.h>
int main() {
double a = 0.1;
double b = 0.2;
double sum = a + b;
if (sum == 0.3) {
printf("Equal\n");
} else {
printf("Not Equal\n"); // This block is executed
}
return 0;
}
// > Not equal
// In Java
public class doublePrecision {
public static void main(String[] args) {
double total = 0;
total += 5.6;
total += 5.8;
System.out.println(total);
}
}
// > 11.399999999999
In fact, the root cause lies in the standard used by these languages to represent floating-point numbers : the double(or single)-precision floating-point format, specified by the IEEE 754 standard.
IEEE 754 Standard: a Story of Bit Representation
In Javascript, the native type number corresponds to double-precision floating-point numbers, which means that a number is encoded with 64 bits and divided into three parts:
- 1 bit for the sign
- 11 bits for the exponent
- 52 bits for the mantissa (or fraction) which ranges from 0 to 1
Then, you need to use the following formula to convert your bit representation into a decimal value:
An example of double-precision floating-point number representation, an approximation of 1/3 :
0 01111111101 0101010101010101010101010101010101010101010101010101
= (-1)^0 x (1 + 2^-2 + 2^-4 + 2^-6 + ... + 2^-50 + 2^-52) x 2^(1021-1023)
= 0.333333333333333314829616256247390992939472198486328125
~ 1/3
This format allows us to represent a vast range of values, but it can't represent every possible number with absolute precision (just between 0 and 1 you can find an infinity of numbers…). Many numbers cannot be represented exactly in binary form. To loop on the first example, that's the issue with 0.1 and 0.2. Double-point floating representation gives us an approximation of these value, so when you add these two imprecise representations, the result is also not exact.
A possible solution: Arbitrary Decimal Arithmetic
Now that you are fully convinced that handling money amounts with native JavaScript number type is a bad idea (at least I hope you begin to have doubts about it), the 1Billion$ question is how should you proceed ? A solution could be to make use of some of the powerful fixed-precision arithmetic packages available in JavaScript. For example Decimal.js (which is used by the popular ORM Prisma to represent its Decimal datatype) or Big.js.
These packages provide you with special datatypes that allow you to perform calculations with getting rid of precision errors we explained above.
// Example using Decimal.js
const Decimal = require('decimal.js');
const a = new Decimal('0.1');
const b = new Decimal('0.2');
const result = a.plus(b);
console.log(result.toString()); // Output: '0.3'
This approach provides you with another advantage, it drastically extends the maximum value that can be represented, which can become pretty handy when you are dealing with cryptocurrencies for example.
Even if it is really robust, that it not the one I prefer to choose to implement in my web applications. I find it easier and clearer to apply Stripe strategy to only deal with integer values.
Learn from Masters : Stripe, a No-Floating Points Strategy
We, at Theodo Fintech, value pragmatism! We love to take inspiration from the most succeeding companies in the industry. Stripe, the well-known billions$ company specialized in payment services, made the choice to handle money amounts without floating numbers but with integers. To do that, they use the smallest unit of the currency to represent a monetary amount.
// 10 USD are represented by
{
"amount": 1000,
"currency": "USD"
}
Currency Minimal Units… Are Not Consistent!
I guess that many of you already know this: all currencies don't have the smallest unit of the same magnitude. Most of them are "two-decimal" currencies (EUR, USD, GBP) which means that their smallest unit is 1/100th of the currency. However, some are "three-decimal" currencies (KWD) or even "zero-decimal" currencies (JPY). (You can find more information about it by following the ISO4217 standard). To handle these disparities, you should integrate to your money data representation the multiplicative factor to convert an amount represented in the smallest unit into the corresponding currency.
I Chose a Way to Represent Money Amount… OK, But Now, How Do I Round Them?
I guess you already figured it out, you can either work with native number , third party arbitrary-precision packages or integers, calculations can (and will) lead you to floating-point results that you will need to round to your monetary finite precision. As a quick example is never to much, let's say that you handle integer values and contracted a loan of 16k$ with a really precise interest rate of 8.5413% (ouch…). You then need to refund 16k$ plus an additional amount of
1600000 * 0.085413 // amount in cents
//Output in cents: 136660.8
The crux is to properly handle the rounding process of money amounts after calculations. Most of the time, you end up having to choose between three different kinds of rounding:
- Classic rounding: Round to the closest value and if halfway between two values, round up
- Banker rounding: Round to the closest value and if halfway between two values, round down if the number is even, and round up if the number is odd (this gives you numerical stability when you have to perform lots of rounding)
- Custom rounding: based on particular legislation for the currency you use and the use case you are handling
When it comes to roundings, there is not really any "magic sauce": you need to arbitrate depending on your situation. I recommend you always check legislation when you deal with a new currency and a new rounding use case (conversion, money split, interest rates for credits, …). You better follow regulations right away to avoid further troubles. For example, when it comes to conversion rates, most currencies have determined rules about needed precision and rounding rules (you can have a look at the EUR conversion rate rules here).
Conclusion
This article is not exhaustive about all the existing possibilities to handle money amounts in JavaScript and is also not intended to give you a complete/perfect money data model. I tried to give you enough hints and guidelines to implement a representation that proved to be consistent, resilient and which was chosen by big actors of the fintech industry. I hope that you will be able to perform money amount calculations in your future projects without forgetting any cents in your pocket (otherwise don't forget to have a look in your old jacket)!
Top comments (2)
Great article; I didn't know that about Banker rounding! I guess if we use integers, then BigInt is a good idea, too.
Yes
BigInt
can definitely be a good workaround also!