Precision Decimal Math in JavaScript with decimal.js

On my current project, we’re doing a lot of math with dollars and cents on a Node.js server. We’re not just adding, but calculating discounts and taxes and the like. Typically, one would do money math in JavaScript by representing the amounts as decimal numbers and using floating-point math.

Unfortunately, floating-point math is not as precise as we’d like it to be, especially when it’s dealing with lots of operations. Sometimes, when you add 1, 0.1, 2, 0.2, 3, 0.3, 4 and 0.4, you get 11.000000000000002—as I did when I was trying to make a test pass the other day.

To solve this issue, we can take one of two paths: We can use Math.round() to create incantations that we hope will toss out those miniscule fractions of pennies at exactly the right time, or we can use a decimal library.

A decimal library’s job is to do math using the same base 10 system that humans use. This makes it especially useful for doing money calculations. A decimal library will never introduce tiny fractions of a penny that you can’t see.

Doing Math with decimal.js

After trying out the minimalist big.js for a bit and finding I needed more functionality, I moved to its more modern and full-featured brother, decimal.js. Plugging it into the above-mentioned test where I’d previously implemented floating-point operations yielded a perfect 11, though because JavaScript does not support operator overloading, I had to use methods to do math instead of using +, -, and friends.

So this:

const taxedAmount = price + tax;

Became this (assuming price and tax are JavaScript Numbers):

const taxedAmount = new Decimal(price).plus(tax);

We also can’t use lodash sum to sum up a list like we did with Numbers:

const totalCharges = _.sum(chargesArray);

But we can sprinkle a little functional programming on the problem with Array.prototype.reduce():

const totalCharges = chargesArray
    .reduce((accum, value) => accum.plus(value), new Decimal(0));

(No, this isn’t pretty. You should extract it into a method you can use everywhere in your project.)

But just to prove that using a decimal library doesn’t make everything more complex, rounding off our final calculations to dollars and cents after calculating percentages or dividing sums goes from this:

const finalAmount = Math.round(amount * 100) / 100;

To this:

const finalAmount = amount.toDecimalPlaces(2);

Returning to Number Land

Once you’re all done doing the math, if you need a native JavaScript Number (e.g. to return to a client), you can just instantiate one using the Decimal:

const returnValue = {
    finalAmount: Number(finalAmount)
};

As it turns out, the library we’re using does this for us when we put a Decimal object into a model slot that expects a Number, so we can just pass our Decimal object in with no further conversion—very handy!

We also use this strategy in our Mocha tests—that way, when they initially fail, we’ll see the actual and expected values in the failure output:

expect(Number(subject.calculate(data))).to.equal(30.00);

If you want to preserve decimal places, you can also render into a string:

// returns e.g. {finalAmount: '100.00'}

const returnValue = {
    finalAmount: finalAmount.toFixed(2)
};

Accuracy Isn’t Always Easy

Sure, it takes a bit more effort to code using a decimal library than it does to use floating-point operations. But it’s worth it to avoid dealing with those phantom fractions of pennies that tend pop up in inconvenient places when you use floating-point math, making your tests awkward and risking real money errors in your application.