Precise Financial Calculation in JavaScript. What Are the Gotchas?
Asked Answered
C

9

167

In the interest of creating cross-platform code, I'd like to develop a simple financial application in JavaScript. The calculations required involve compound interest and relatively long decimal numbers. I'd like to know what mistakes to avoid when using JavaScript to do this type of math—if it is possible at all!

Creeps answered 20/5, 2010 at 18:4 Comment(0)
U
147

You should probably scale your decimal values by 100, and represent all the monetary values in whole cents. This is to avoid problems with floating-point logic and arithmetic. There is no decimal data type in JavaScript - the only numeric data type is floating-point. Therefore it is generally recommended to handle money as 2550 cents instead of 25.50 dollars.

Consider that in JavaScript:

var result = 1.0 + 2.0;     // (result === 3.0) returns true

But:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

The expression 0.1 + 0.2 === 0.3 returns false, but fortunately integer arithmetic in floating-point is exact, so decimal representation errors can be avoided by scaling1.

Note that while the set of real numbers is infinite, only a finite number of them (18,437,736,874,454,810,627 to be exact) can be represented exactly by the JavaScript floating-point format. Therefore the representation of the other numbers will be an approximation of the actual number2.


1 Douglas Crockford: JavaScript: The Good Parts: Appendix A - Awful Parts (page 105).
2 David Flanagan: JavaScript: The Definitive Guide, Fourth Edition: 3.1.3 Floating-Point Literals (page 31).

Uphill answered 20/5, 2010 at 18:14 Comment(15)
And as a reminder, always round calculations to the cent, and do so in the least beneficial way to the consumer, I.E. If you're calculating tax, round up. If you're calculating interest earned, truncate.Judicative
Does this extend to all cases? If want to get 5.999999999% or 3,000.57, it's suggested I turn the former to 5,999,999,999 and the latter to 300,057 and keep track of the original value of each in order to perform the correct calculation?Creeps
@Cirrostratus: You may want to check stackoverflow.com/questions/744099. If you go ahead with the scaling method, in general you would want to scale your value by the number of decimal digits you wish to retain precision. If you need 2 decimal places, scale by 100, if you need 4, scale by 10000.Uphill
... Regarding the 3000.57 value, yes, if you store that value in JavaScript variables, and you intend to do arithmetic on it, you might want to store it scaled to 300057 (number of cents). Because 3000.57 + 0.11 === 3000.68 returns false.Uphill
Counting pennies instead of dollars will not help. When counting pennies, you loose the ability to add 1 to an integer at about 10^16. When counting dollars you lose the ability to add .01 to a number at 10^14. It's the same either way.Lenticularis
At the threshold points when using the scaling method you could start to employ multiple numeric variables to represent one large number. Some funky bit manipulation is needed because apparently there are caveats there too. It does seem to get onerous: might want to package up such an endeavour as a reusable library.Curative
@Lenticularis a penny is a coin; the unit of currency is the cent. In some countries. In the UK, a penny is a unit of currency, but the plural is "pence". Other currencies are divided into kopeks, fen, paisas, etc.Dragline
I just created a npm and Bower module that should hopefully help with this task!Enteron
@Lenticularis Decimals are not entirely useless. With (binary) floats, you cannot even precisely represent an amount. Decimals also give you error-free additions and subtractions (as long as there's no overflow). Things start go wrong only when you start more advanced calculations such as multiplication or division. In a word, decimals are useful when dealing with finance but cannot completely eliminate the problem. No radix can, actually.Overawe
@JohnK I'm wondering whether those accumulated small benefits will drive a financial company to bankruptcy. Even if the error in a single calculation is very small, the number of calculations can be huge. Shouldn't they have some ways of dealing with these errors?Overawe
@slashingweapon, anyone feel free to correct me if I'm wrong but 10^16 cents is a hundred trillion dollars. For MOST circumstances this isn't going to be an issue. 0.1 + 0.2 will crop up WAY before you are dealing with more than a hundred trillion dollars.Josefajosefina
True, but you're focusing just on sums. Think about what happens when you compute (A * B / C). Depending on the size of the values and your order of operations, you can have an overflow even though the final answer would be well within the size limit. eg: ((A*B)/C) produces a large intermediate value, whereas (A*(B/C)) does not.Lenticularis
I think the smallest legal denomination of US currency is actually the mill, or 1/1000 dollars. That's why gas stations can set their prices as $3.599Swept
@Lenticularis I realize I'm 11 years overdue, but regarding your comment about it not mattering when adding 1 or 0.01, it very much DOES matter, because a binary float is not able to store 0.01. It has a never ending string of binary digits after the point, much like the number 1/3 in decimal. It can, however, store any integer up to 9e15 exactly. If integers are good enough for your financial calculations, then so are floating point numbers where you keep all relevant digits (say, cents) in front of the point.Jobber
@Jobber Your point is well taken. The overall lesson here is that if you're serious about financial computation you should use a proper decimal library. Floating point computation is fine most of the time, including casual financial calculations of reasonable size, but there are weird limits and estimations lurking in floating point numbers. (Not weird if you understand them, but weird from a normal person's "let's do some simple arithmetic with biggish numbers" point of view.)Lenticularis
R
34

Scaling every value by 100 is the solution. Doing it by hand is probably useless, since you can find libraries that do that for you. I recommend moneysafe, which offers a functional API well suited for ES6 applications:

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

Works both in Node.js and the browser.

Rhatany answered 7/8, 2017 at 16:10 Comment(4)
Upvoted. The "scale by 100" point is already covered in the accepted answer, however it's good that you added a software package option with modern JavaScript syntax. FWIW the in$, $ value names are ambiguous to someone who's not used the package before. I know it was Eric's choice to name things that way, but I still feel it's enough of a mistake that I'd probably rename them in the import/destructured require statement.Creeps
Scaling by 100 only helps until you start wanting to do something like calculate percentages (perform division, essentially).Climate
I wish I could upvote a comment multiple times. Scaling by 100 just isn't sufficient. The only numeric datatype in JavaScript is still a floating point data type, and you're still going to end up with significant rounding errors.Sarson
And another heads up from the readme: Money$afe has not yet been tested in production at scale.. Just pointing that out so anyone can then consider if that's appropriate for their use caseHomorganic
W
15

Unfortunately all of the answers so far ignore the fact that not all currencies have 100 sub-units (e.g., the cent is the sub-unit of the US dollar (USD)). Currencies like the Iraqi Dinar (IQD) have 1000 sub-units: an Iraqi Dinar has 1000 fils. The Japanese Yen (JPY) has no sub-units. So "multiply by 100 to do integer arithmetic" isn't always the correct answer.

Additionally for monetary calculations you also need to keep track of the currency. You can't add a US Dollar (USD) to an Indian Rupee (INR) (without first converting one to the other).

There are also limitations on the maximum amount that can be represented by JavaScript's integer data type.

In monetary calculations you also have to keep in mind that money has finite precision (typically 0-3 decimal points) & rounding needs to be done in particular ways (e.g., "normal" rounding vs. banker's rounding). The type of rounding to be performed might also vary by jurisdiction/currency.

How to handle money in javascript has a very good discussion of the relevant points.

In my searches I found the dinero.js library that addresses many of the issues wrt monetary calculations. Haven't used it yet in a production system so can't give an informed opinion on it.

Wendiewendin answered 17/3, 2020 at 9:49 Comment(0)
P
13

There's no such thing as "precise" financial calculation because of just two decimal fraction digits but that's a more general problem.

In JavaScript, you can scale every value by 100 and use Math.round() everytime a fraction can occur.

You could use an object to store the numbers and include the rounding in its prototypes valueOf() method. Like this:

sys = require('sys');

var Money = function(amount) {
    this.amount = amount;
}

Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}
    
var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

That way, everytime you use a Money-object, it will be represented as rounded to two decimals. The unrounded value is still accessible via m.amount.

You can build in your own rounding algorithm into Money.prototype.valueOf(), if you like.

Psychologize answered 20/5, 2010 at 18:40 Comment(6)
I like this object-oriented approach, the fact that the Money object holds both values is very useful. It's the exact type of functionality I like to create in my custom Objective-C classes.Creeps
It's not accurate enough to round.Loudspeaker
Shouldn't sys.puts(m+n); //80.76 actually read sys.puts(m+n); //80.77? I believe you forgot to round the .5 up.Castellany
This kind of approach has a number of subtle issues that can crop up. For instance, you haven't implemented safe methods of addition, subtraction, multiplication and so on, so you are likely to run into rounding errors when combining money amountsPrehensile
The problem here is that e.g. Money(0.1)means that the JavaScript lexer reads the string "0.1" from the source and then converts it to a binary floating point and then you already did an unintended rounding. The problem is about representation (binary vs decimal) not about precision.Backstop
I've downvoted this answer because I find it multiply misleading. 1. Precise financial calculations are those carried out in accordance with your country's accounting norms. If you're in the US that'd be GAAP. 2) To suggest rolling your own is a very bad idea indeed, given the fact that someone's money is at steak and a mistake may be a legal exposure for the author. To get a hint of what it takes to do currency computations right, check out e.g. Java's BigDecimal class.Infirmity
P
9

use decimaljs ... It a very good library that solves a harsh part of the problem ...

just use it in all your operation.

https://github.com/MikeMcl/decimal.js/

Preeminence answered 25/1, 2019 at 20:38 Comment(0)
L
5

Your problem stems from inaccuracy in floating point calculations. If you're just using rounding to solve this you'll have greater error when you're multiplying and dividing.

The solution is below, an explanation follows:

You'll need to think about mathematics behind this to understand it. Real numbers like 1/3 cannot be represented in math with decimal values since they're endless (e.g. - .333333333333333 ...). Some numbers in decimal cannot be represented in binary correctly. For example, 0.1 cannot be represented in binary correctly with a limited number of digits.

For more detailed description look here: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Take a look at the solution implementation: http://floating-point-gui.de/languages/javascript/

Loudspeaker answered 10/6, 2013 at 17:10 Comment(0)
P
4

Due to the binary nature of their encoding, some decimal numbers cannot be represented with perfect accuracy. For example

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

If you need to use pure javascript then you have need to think about solution for every calculation. For above code we can convert decimals to whole integers.

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

Avoiding Problems with Decimal Math in JavaScript

There is a dedicated library for financial calculations with great documentation. Finance.js

Phalanstery answered 24/12, 2019 at 16:21 Comment(1)
I like that Finance.js has example apps as wellCreeps
D
1

Use this code for currency calculation and round numbers in two digits.

<!DOCTYPE html>
<html>
<body>

<h1>JavaScript Variables</h1>

<p id="test1"></p>
<p id="test2"></p>
<p id="test3"></p>

<script>

function setDecimalPoint(num) {
    if (isNaN(parseFloat(num)))
        return 0;
    else {
        var Number = parseFloat(num);
        var multiplicator = Math.pow(10, 2);
        Number = parseFloat((Number * multiplicator).toFixed(2));
        return (Math.round(Number) / multiplicator);
    }
}

document.getElementById("test1").innerHTML = "Without our method O/P is: " + (655.93 * 9)/100;
document.getElementById("test2").innerHTML = "Calculator O/P: 59.0337, Our value is: " + setDecimalPoint((655.93 * 9)/100);
document.getElementById("test3").innerHTML = "Calculator O/P: 32.888.175, Our value is: " + setDecimalPoint(756.05 * 43.5);
</script>

</body>
</html>
Dumortierite answered 22/3, 2022 at 10:32 Comment(0)
B
0

Here we are using JavaScript and Node.js to trade real money in financial markets. We came up with a custom Decimal class representing immutable decimal numbers and providing logic for calculations.

import { decimal, } from "@reiryoku/mida";

0.1 + 0.2; //= 0.30000000000000004
decimal(0.1).add(0.2); //= 0.3
decimal("0.1").add("0.2"); //= 0.3

If you are curious you can see the definition here https://github.com/Reiryoku-Technologies/Mida/blob/master/src/core/decimals/MidaDecimal.ts

The package is available for anyone on npm.

Blindage answered 21/5, 2023 at 0:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.