bigint-money: an NPM package for doing currency math
Not long ago, I was confused about doing math with money and floating point issues. After a bunch of research and help from friends I figured out what many already did before me: Using floating points is indeed bad. You can get rounding issues and really want to use ‘precise math’.
After that, I realized I wanted to use the new Ecmascript bigint type as a basis for representing money. This is supported by recent versions of Node.js and Typescript, which is where I need it.
There’s a problem though, the
bigint type only holds ‘whole numbers’ and
doesn’t do fixed-point decimal-math. I looked around on NPM for something that
would abstract this for me, but couldn’t find a library.
So I spent a couple of days writing a simple npm library called
that wraps the
bigint type. It’s written in Typescript and open-source so
it’s here for anyone to use: https://www.npmjs.com/package/bigint-money.
- Uses the the Ecmascript bigint type.
- Written in Typescript.
- Loosely follows Martin Fowler’s Money Type from “Patterns of Enterprise Application Architecture”.
- Faster than Money packages that use non-native bigdecimal libraries.
- All rounding is done via the “Bankers Rounding” (a.k.a. “round half to even”).
- Uses 12 decimals for any calculations.
Most ‘money’ libraries on NPM only use 2 digits for precision, or use Javacript’s “number” and will quickly overflow.
The only comparible library I found was big-money. It’s probably
My simple benchmark calculates a ledger with 1 million entries.
bigint-money | big-money ledger 816 ms | 43.201 ms % 100 % | 5294 %
The benchmark script can be found in the repository.
I want to stress though that I can’t take credit for this (pretty good) result.
The library is fast because
bigint is fast.
I did need to implement a few functions myself, and I actually feel that somebody who’s better at writing fast code could probably do a much better job than I did.
I imagine that specifically the
functions could probably be optimized by someone much smarter than me.
This is how it generally works:
// Creating a money object. import Money from 'bigint-money'; const foo = new Money('5', 'USD');
It’s possible to create a new money object with a Number as well
const foo = new Money(5, 'USD');
However, if you pass it a number that’s ‘unsafe’ such as a float, an error will be thrown:
const foo = new Money(.5, 'USD'); // UnsafeIntegerException
Once you have a Money object, you can use
toFixed() to output a string.
const foo = new Money('5', 'USD'); console.log(foo.toFixed(2)); // 5.00
You can use
.subtract() on it:
const foo = new Money('5', 'USD'); const bar = foo.add('10');
All Money objects are immutable. Calling those functions does not change the original value:
console.log(foo.toFixed(2), bar.toFixed(2)); // 5.00 1.00
You can also pass Money objects to subtract and add:
const startBalance = new Money(1000, 'USD'); const salary = new Money(2000, 'USD'); const newBalance = startBalance.add(salary);
If you try to add money from different currencies, an error will be thrown:
new Money(1000, 'USD').add( new Money( 50000, 'YEN' )); // IncompatibleCurencyError
Division and multiplication:
// Division const result = new Money(10).divide(3); // Multiplication const result = new Money('2000').multiply('1.25');
Right now the Money object has a single function for comparison.
const money1 = new Money('1.00', 'EUR'); money.compare(2); // Returns -1 money.compare(1); // Returns 0 money.compare(0); // Returns 1 money.compare('0.01'); // returns -1 money.compare(new Money('1.000005', 'EUR')); // returns 1 money.compare(new Money('1', 'CAD')); // throws IncompatibleCurrencyError
The idea is that if the object is smaller than the passed one, -1 returned. 0 is returned if they’re equal and 1 is returned if the passed value is higher.
This makes it easy to sort:
const values = [ new Money('1', 'USD'), new Money('2', 'USD') ]; values.sort( (a, b) => a.compare(b) );
When splitting money equal parts, it might be possible to lose a penny. For example, when dividing $1 between 3 people, each person gets $ 0.33 but there’s a spare $ 0.01.
The allocate function splits a Money value in even parts, but the remainder is distributed over the parts round-robin.
const earnings = new Money(100, 'USD'); console.log( earnings.allocate(3, 2); ); // Results in 3 Money objects: // 33.34 // 33.33 // 33.33
Splitting debts (negative values) also works as expected.
The second argument of the allocate function is the precision. Basically the number of digits you are interested in.
For USD and most currencies this is 2. It’s required to pass this argument because the Money object can’t guess the desired precision.
npm i bigint-money