13 ms·
How to Handle Monetary Values in JavaScript
- cpburns2009 8y agoI looked into using Dinero.js before but it was overkill for my use case of calculating cart totals in USD only. I went with Big.js because it was better suited for what I wanted: a small library for decimal arithmetic.
- tlb 8y agoCan you illustrate a case where you can't use regular FP arithmetic and round to 2 decimal places at the end?
- maxxxxx 8y agoYou can't model 1.0 correctly but it will be something like 1.000006667. If you add enough of these numbers you will be off.
- recursive 8y agoYour details are off. 1.0 is precisely representable in double precision floating point with no error.
- maxxxxx 8y agoSorry, picked the wrong number. But I think the general idea is correct.
- aaaaaaaaaab 8y agoIntegers can be represented exactly in IEEE754.
- maxxxxx 8y agoHow about 2.1? According to this https://www.h-schmidt.net/FloatConverter/IEEE754.html https://www.h-schmidt.net/FloatConverter/IEEE754.html 2.1 will be represented as 2.099999904632568359375
- noselasd 8y agowell, 2.1 isn't an integer.
- noselasd 8y agoNot all of them, e.g. 9007199254740993 can't be represented in a double precision format (what javascript uses) For single precision floats you start having issues representing integers as low as 16777217
- aaaaaaaaaab 8y agoSure, you can’t represent Graham’s number either... I know you knew what I meant.
- noselasd 8y agoNo not really. You have to define the bounds you were talking about. People believing that a float type can represent all integers that the int type in their language can represent is not exactly uncommon.
- ekke 8y ago(1.005).toFixed(2) // Expected rounding to "1.01" "1.00"
- deleted 8y ago[deleted]
- noselasd 8y agocases like that's everywhere. 2.875.toFixed(2) = "2.88" while 2.675.toFixed(2) = "2.67" 7205759403792794 + 1.1 = 7205759403792795 7205759403792794 + 1.9 = 7205759403792796
- athenot 8y agoBring up a Javascript interpreter and try this: $ node > 0.1 + 0.2 - 0.3 5.551115123125783e-17 Close enough for trivial cases but errors can accumulate. Ruby, Perl5, Python also fail this. Perl6 uses Rat numbers for things like this and doesn't have this problem.
- tlb 8y agoYou'd need to accumulate 10^14 such errors to be off by an entire penny, which doesn't seem relevant to a web shopping cart.
- mikekchar 8y agoAddition and subtraction are usually fine as you point out. Multiplication and division can cause problems depending on the values and the number of times you do it. If you are doing more complicated financial calculations, you could get in trouble more quickly. If you are careful and know exactly what you are doing, then you can certainly get away with normal FP for many applications. My experience has been, though, that many developers have a bit of a laissez-faire attitude towards these kinds of details (time is, of course, another area where precision is incredibly important in corner cases). Rather than expecting all future programmers to be as diligent as they need to be, it's probably easier to simply use tools that make it difficult to make mistakes. Every time I'm working on financial systems I build currency classes (usually I'm doing OOP). Usually people grumble and there will be people who ignore the currency classes. I've never had to wait long before someone trips over an edge cases and everybody understands the benefits.
- dragonwriter 8y ago> Ruby, Perl5, Python also fail this. Ruby and Python have arbitrary precision decimal and rational support in the standard library. While there are arguments tobe had over whether using binary floats by default for simple decimal literals is a good choice, that's a different issue than not having any other kind of numbers available by default.
- recursive 8y agoSure. Divide by two with any kind of predictability. for (let n = 0; n < 1; n += .01) { console.log(`n: ${n}, half: ${(n / 2).toFixed(2)}`); }
- PeterisP 8y agoSure, split an amount in 3 payments - take one dollar, put 0.333333 dollars once, 0.3333333 dollars the second time, and the remainder (1 - 0.33333333 - 0.33333333) as the outstanding balance doesn't match what you should have; you should have had 0.34 but you have 0.33 if you round only at the end. Do the same with a proper decimal datatype, and the same operations give the correct result - pay out 1/3=0.33; pay out 1/3=0.33; the last payment is 1-0.33-0.33 = 0.34 exactly. The same goes with all kinds of other issues e.g. totalling up tax or discount amounts (calculated as a percentage) where you'll get mismatches if rounding happens at the wrong time, etc. E.g. ten separate receipts each selling a 0.10 item with a 9% tax or discount will result in each receipt having 0.01 tax or discount; so the proper total in this case is 1.00 sales with 0.10 tax or discount; different than the case if you sell ten such items in a single transaction. If you do all the calculations in floats, you get this wrong.
- strainer 8y agoI think the advantage behind using something like Big.js is to future proof the code against hyper-inflation. Though a dedicated currency engine would better have the ability to set and conveniently update the currency denomination of stored values to cope with this. With float64's maximum 'safe' integer at almost ten thousand trillion, I believe this should be enough to compute most commercial transactions securely as long as the amounts base denomination is up to date (eg cents or dollars or megadollars). The quantization noise mentioned in this article is possible to round off when appropriate. function round_tail_digits(c){ return Math.round(c*1e15)/1e15 } function round_at_hundreds(c){ return Math.round(c*100)/100 }
- sk5t 8y agoThis approach, at least in languages like C and Java, would effectively smush the effective MAX_INT down to MAX_INT/1e15 or whatever. No bueno.
- strainer 8y agoIt doesn't because basically - the point floats. The max_int doesn't mean the largest whole number that can be represented, it effectively means how much resolution you have on the number line in every one of a very wide range of scales. These languages all give exactly the same standard IEEE754 float math results for basic operations.
- smallnamespace 8y ago> I think the advantage behind using something like Big.js is to future proof the code against hyper-inflation. This is probably over-engineering. Currencies that go through hyperinflation typically end up being officially re-denominated many times, there's no 'zero maintenance' path that a library will give you.
- b2ccb2 8y agoI really like the approach Perl 6 takes with FatRat[1]. You basically have an object which holds a numerator and denominator, so all arithmetic calculations do not loose precision. [1] https://docs.perl6.org/type/FatRat https://docs.perl6.org/type/FatRat
- taeric 8y agoEver seen the numeric tower of lisp? https://en.wikipedia.org/wiki/Numerical_tower https://en.wikipedia.org/wiki/Numerical_tower The link to the scheme documentation that goes over this is quite good.
- b2ccb2 8y agoThat's fantastic! Do you have any more resources on this? I'd like to look at that concept in more depth.
- taeric 8y agoNothing specific. The scheme manual linked is a good one. As are the docs for Common Lisp. https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node16.html https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node16.html Honestly, most all older manuals and documents that were not related to systems programming seem to cover numeric programming decently enough. I get the impression that languages used to worry more about that sort of thing, but then the truly heavy lifting of numeric programming moved into the specialized domains of Matlab and friends, and then programmers slowly unlearned most of what was learned regarding numbers.
- christophilus 8y agoClojure does this out of the box: `(/ 1 3)` is 1/3. Removes a lot of annoying floating point errors.
- doublec 8y agoAdding to the languages that do this, Factor does as well. "1 3 /" produces the rational value "1/3". https://docs.factorcode.org/content/article-rationals.html https://docs.factorcode.org/content/article-rationals.html
- deleted 8y ago[deleted]
- peheje 8y agoNice and simple writeup. Straight to the point without being ceremonious.
- reaperducer 8y agoBanking apps, e-commerce websites, stock exchange platforms, we interact with money daily. We also increasingly rely on technology to handle ours. Yet, there’s no consensus around how to programmatically handle monetary values. I wonder if the average person would find this frightening.
- rpeden 8y agoI don't think it's true that there's no consensus around how to programatically handle monetary values. Maybe there's no consensus around how to do it in JavaScript. In the Java world, there's JSR-354: https://jcp.org/en/jsr/detail?id=354 https://jcp.org/en/jsr/detail?id=354 Cobol has handled money well for decades. .NET has Decimal, which should handle money correctly as long as you're working with values less than 79.2 octillion. In my part of the world, at least, big banks tend to be mostly Java and Cobol on the back end. You'll definitely see Node, Go, and others being used for various tasks throughout the company, but when it comes to keeping track of money and moving it around, it's primarily Java and Cobol doing the heavy lifting. That's what I've heard from other developers, anyway. I haven't worked at a bank myself.
- kickopotomus 8y agoExactly correct. More stuff than most people expect is still handled by COBOL. I haven't worked with a bank in a few years, but back in 2015 the one I was at was attempting to migrate a lot of the COBOL tasks to Java.
- deleted 8y ago[deleted]
- zie 8y agoIBM has had a solution for a long time: http://speleotrove.com/decimal/decarith.html http://speleotrove.com/decimal/decarith.html Python uses it: https://docs.python.org/3.5/library/decimal.html https://docs.python.org/3.5/library/decimal.html Just because JS is stupid doesn't mean other languages are.
- protomyth 8y agoYet, there’s no consensus around how to programmatically handle monetary values. Do what COBOL does and use it to verify your monetary library. Your code will agree with the majority of financial institutions.
- deleted 8y ago[deleted]
- rtkwe 8y ago> Floats: 0.1 + 0.2 // returns 0.30000000000000004 The question is how much of an issue is this really? How often is 4 parts in 10 quadrillion a meaningful error when dealing with money? Especially when most of the time JS is dealing with money it will be presentation.
- jczhang 8y agoYeah seriously. You'll have some front-end lib func converting that to 2 significant digits right of the decimal anyway. Weak argument imo haha.
- deleted 8y ago[deleted]
- john_of_peaches 8y agoBecause value !== 0.3 and that can cause unexpected problems on the edges (which are the worst kind of issues).
- codr4 8y agoI'm far from wizard on the topic, but from my experience it's not that simple since the relative magnitudes of the operands and the operator affect the size of the error in non obvious ways. I spent 13 years maintaining a 2mloc reservation and accounting system that used floats for everything. By the time I got there and understood enough it was simply too late to do anything about it. Just say no, really; that road leads to madness. If you need precision; and many do; consider fixnums, rationals or bignums, in order of increasing complexity and flexibility.
- ouro 8y agoI might suggest reading https://doc.lagout.org/science/0_Computer%20Science/3_Theory/Handbook%20of%20Floating%20Point%20Arithmetic.pdf https://doc.lagout.org/science/0_Computer%20Science/3_Theory... This is in general how floating point math works.
- mikeash 8y agoI ran into this once because a price was slightly too low, and got truncated. A $19.99 item got displayed as $19.98. You can work around this by rounding everywhere but that’s error-prone.
- gvx 8y agoWhile reading, I thought "hang on, what about non-decimal currencies?" but it turns out that is no longer a concern for modern usages: https://en.wikipedia.org/wiki/Non-decimal_currency https://en.wikipedia.org/wiki/Non-decimal_currency
- dpwm 8y agoYou can handle non-decimal currencies by treating money as a fraction. You can implement a correct set of fraction manipulation functions in well under 100 lines of code with a bit of algebra. It seems likely there are several well-tested high-performance fraction libraries for JavaScript. They should be tiny. Some people in the UK apparently voted to leave the EU so that they could have a blue passport [0], perhaps there are some who also hope we return to imperial measurements and pounds, shillings and pence. I'm quite sure that even in that unlikely event, some programmers will still insist on using floating point numbers for currency. [0] Although Malta has a blue passport and is in the EU. And the only reason the passports were blue in the first place was to meet an international standardization.
- mjevans 8y agoEqually important to choosing a valid numeric type to store the data is solid business logic to handle it. In the contrived example of splitting a value of 999.99 among more than one payment, the correct solution is: * Determine the next payment based on the current balance. * Invoice/Apply/Etc that payment * Repeat until there is a balance of 0 (zero).
- lowercased 8y agorecording an incoming payment and applying that payment value to specific accounts is its own thing to be aware of. People want to see that they paid $200, even if it was $150 to one account and $50 to account.
- DataInSolutions 8y agoMy goto for any kind of arbitrary precision math in JavaScript is https://github.com/MikeMcl/big.js https://github.com/MikeMcl/big.js As others have noted, if you're doing any kind of math that involves floating point numbers and you will displaying them to a user, you'll almost certainly want something like this. While it's not specifically a "Money" class it can form an excellent base for one.
- empyrical 8y agoReally excited to see what possibilities for number handling native BigInt in JavaScript will bring https://developers.google.com/web/updates/2018/05/bigint https://developers.google.com/web/updates/2018/05/bigint
- zie 8y agoprobably none. To handle currency correctly you have to handle rounding to the nearest penny correctly. Computers absolutely SUCK at this, in general. So you need a Decimal library, that can handle decimal numbers. Integers ARE NOT DECIMAL. IBM has had a solution for a long time: http://speleotrove.com/decimal/ http://speleotrove.com/decimal/ Python uses it: https://docs.python.org/3.5/library/decimal.html https://docs.python.org/3.5/library/decimal.html Get a proper decimal library, and then you can devote brain cells to actual problems. The IBM link above includes links to libraries that do it right.
- village-idiot 8y agoYou can also just use the smallest unit and work in cents. The Stripe API does this instead of dealing with decimals.
- r32a_ 8y agoMany cryptocurrency libs like Web3 opt to use bignumber js for all money manipulation
- krapp 8y agoDon't. For the love of God, just don't.
- tantalor 8y agoThis advice doesn't have anything to do with JavaScript until the author starts plugging their own library.
- zachrose 8y agoI wish you could add different currencies together: Dinero({amount: 5000, currency: 'USD'}) .add(Dinero({ amount: 1000, currency: 'EUR'})) ...and then convert that sum to a concrete currency later. This feature request inspired by http://blog.ploeh.dk/2017/10/16/money-monoid/ http://blog.ploeh.dk/2017/10/16/money-monoid/
- diNgUrAndI 8y agoHow would you deal with exchange rate, which changes in real time?
- farazzz 8y agoFrom the docs > You must provide your own API to retrieve exchange rates. https://sarahdayan.github.io/dinero.js/module-Dinero.html#~convert https://sarahdayan.github.io/dinero.js/module-Dinero.html#~c...
- zachrose 8y agoIn the lazy version i have suggested, the exchange rates are deferred until you “need to know” and the sum is converted into a concrete currency. If you’re keeping these values around in memory for an extended period of time time and you care about rapid changes to exchange rates, then you have a complex domain and the deferred exchanges may or may not be a good fit, but at that point you need larger system design, not a JavaScript library. Also in my dream money library, division and multiplication would be done with whole number fractions like new Dinero({amount: 1000, currency: ‘usd’}) .multiply({numerator: 1, denominator: 3 }) which can similarly be deferred to prevent rounding errors in partial calculations.
- smallnamespace 8y ago> Using floats to store monetary values is a bad idea This is waay overstating the case, it very much depends on whether getting the exact answer down to the penny matters to you. Worked for a few years at a large investment bank, everything was done in floats because the modeling error of your derivatives pricers would be much larger than the roundoff error, but floats were much more convenient to develop with and faster since you avoided a bunch of casts. Most of the time people think they need things down to the penny, they actually don't. An accountant does not care if you rounded off a few thousand dollars in your $10bn a year income statement. A customer does not really care that you paid them $0.33 three times, rather than $0.33 twice and $0.34 on the third transaction (and fixnums don't even solve this 'extra precision' case without a bunch of extra work).
- jimmy1 8y agoI really want to be generous here: I think your experience is very isolated. I have family that is executive level at a Wall Street bank, they would have you fired over the superfluous loss of a dollar, much less a few thousand dollars because that type of rounding error can end up in the millions over the course of a year if you deal 100+ million dollar transactions 5-6 times a day. Here's how it would go: "Why are we losing a couple of thousands of dollars?" "Well it was easier to just work in floats" "Pack your shit and get the f--- out of here, now."
- endorphone 8y agoIt really depends upon the context. If they're talking about reporting and forecasts, floats are almost certainly fine. The back end actual storage is generally always numerics, however.
- PeterisP 8y agoDepends on the reporting - if it's legally mandated reporting or tax reporting, then it's going to get audited and any rounding mismatches are definitely not okay.
- 8y ago
- tzs 8y agoA while back I did some exhaustive testing of several ways to calculate sales tax on a sale in Python3 and JavaScript, testing the various "obvious" ways against all tax rates in increments of 0.0001 from 0.0000 to 1.0000 and all sale amount from 0.00 to something like 25.00. It was interesting. Here are some simple cases to try. One or more of these break most of the seemingly obvious approaches: 1% of $21.50 3% of $21.50 6% of $21.50 10% of $21.15 Here are some examples of obvious looking but wrong approaches. These all return the tax in pennies, so the desired results are 22, 65, 129, and 212. In puts are floats, such as 0.01 for 1% and 21.50 for $21.50. # 1%, 10% wrong def tax_f1(amt, rate): tax = round(amt * rate,2) return round(tax * 100) # 3%, 10% wrong def tax_f2(amt, rate): return round(amt*rate*100) # 6% wrong def tax_f3(amt, rate): return round(amt*rate*100+.5) Here are two that do work: def tax_f4(amt, rate): amt = round(amt * 100) rate = round(rate * 10000) tax = (amt * rate + 5000)//10000 return tax def tax_f5(amt, rate): amt = int(amt * 100 + .5) rate = int(rate * 10000 + .5) tax = (amt * rate + 5000)//10000 return tax If instead of floating point input, you work with numbers already scaled up to integers (scaling the rates by 10^4 and the money by 10^2), this works: def tax(amt, rate): tax = (amt * rate + 5000)//10000 return tax The last three approaches, (tax, tax_f5, and tax_f4) also work well in PHP, Perl, and JavaScript. In my code, I attach a suffix to rates and moneys that are scaled this way telling the scale factor, so I might have tax4 and cost2, meaning the tax rate is x10^4 and the cost is x10^2. I was disappointed to find that neither Perl nor Python would let my use Unicode subscript numbers in my variable names. :-( I also did a series of exhaustive tests where I'd take every string of the form every string of the form digits dot digits with up to N digits after the dot, parse them with the languages most natural string to float converter, multiple that by 10^N and round to an integer, and verify that this was the "right" integer, and also verified that doing a floating point divide of that integer by 10^N and then using the most natural way in the language to turn that to a string with N digits after the dot gave the right result. The idea here was to convince myself that I would not run into problems at the "convert to integer" or the "convert from integer" part. I did these tests in C, Perl, Python, and JavaScript I think. I'm having trouble finding my code now so I'm not sure if I tested in all of them. Anyway, I concluded that it was safe. It's only when you start computing in float point that you have to worry. Finally, in most of what I was doing at the time, I actually didn't ever need to convert back to a floating point format. All I was going to do with the final price, computed from cost + tax, was just display it to the user...and so if I converted to floating point I'd end up just sprintf'ing it (or equivalent) back to a string. All sprintf would really be being used for was to make sure it had the right padding and leading zeros and stuff like that. So instead of going through floating point, I just did this (JavaScript): function cents_to_dollars(cents) { cents = cents.toString(); while (cents.length < 3) cents = '0' + cents; return cents.substr(0,cents.length-2) + '.' + cents.slice(-2); } (I assume that if I knew JavaScript I could do something much nicer than that while look for the padding).
- projectileboy 8y agoMaybe it’s just that I’ve done a few gigs at financial institutions, but it’s pretty shocking to see the number of people in this thread arguing that it’s a-ok to use floating point for money. I don’t want to get dragged down into specifics, but if you’re doing this, please, please don’t. Money is one of those things like dates and times - everyone who hasn’t done much of it can’t figure out what all the fuss is about. Just... please, at a minimum, if you don’t want to be bothered, find a reputable third-party library with Money and Currency or some such, and use that. It really won’t add much work for you.
- smallnamespace 8y agoI'm curious to hear about your specifics, since I was at a shop where front office used all floats (doubles). Not sure what the back office did, but I doubt it was all done in arbitrary position arithmetic. Note that if an accountant says things need to be accurate down to the penny, that's probably an exaggeration [1]. Financial statements are typically rounded to the thousands, e.g. Intel's are rounded to million [2]. The IRS also lets you round off dollars [3]. I agree though that if you're building a large complex system with many coders, and either 1) nobody has a full view of the calculation and can do the numerical analysis or 2) there's a high chance that at some point in the future you need penny-accurate bookkeeping, then it's safer to use a very wide int or float type. Of course, there's also the case where the accountant or auditor doesn't actually need penny precision, but it's easier to just tell the coder 'just in case'. [1] https://www.accountingtools.com/articles/2017/5/14/the-materiality-principle https://www.accountingtools.com/articles/2017/5/14/the-mater... [2] https://s21.q4cdn.com/600692695/files/doc_financials/2017/annual/Intel_Annual_Report_Final_corrected.pdf https://s21.q4cdn.com/600692695/files/doc_financials/2017/an... [3] https://taxmap.irs.gov/taxmap/pub17/p17-006.htm#TXMP5d034bfb https://taxmap.irs.gov/taxmap/pub17/p17-006.htm#TXMP5d034bfb
- stephengillie 8y agoCollecting the rounding leftovers into another bank account was the plot of Office Space.
- brennankreiman 8y agoThis seems like the basis for stripe handling amounts in cents to maintain integer values.
- bschwindHN 8y agoHandling currency in JavaScript just sounds like a bad idea. It's better to be using a language with a type system that has your back on this kind of thing, and preferably to deal with monetary values as rational numbers. There's a good article on this subject here: https://ren.zone/articles/safe-money https://ren.zone/articles/safe-money
- kisstheblade 8y agoI usually use cents as the stored amount, so no decimals. Is there something obviously wrong with this approach? Ie. if the price is 150.95 I store 15095 in the db and do the calculations with cents, and then divide by 100 when presenting the values. I have never come across fractional cents, maybe that is an issue in some scenarios (would be interested to know what those are... Even tax percentage calculations are rounded to the cent)