This is a complex topic, mainly for two reasons: 1. it works on two layers (storage and code) 2. there is a context to take care of.
[Modern] programming languages have decimal/rational data types, which (within limits) are exact. Where this is not possible, and/or it's undesirable for any reason, just use an int and scale it manually (e.g. 1.05 dollars = int 105).
However, point 2 is very problematic and important to consider. How do account 3 items that cost 1/3$ each (e.g. if in a bundle)? What if they're sold separately? This really depends on the requirements.
My 20 cents: if you start a project, start storing currency in an exact form. Once a project grows, correcting the FP error problem is a big PITA (assuming it's realistically possible).
>[Modern] programming languages have decimal/rational data types
This caveat is kind of funny, in light of COBOL having support for decimal / fixed precision data types baked directly into the language.
It's not a problem with "non-modern" languages, it's a problem with C and many of its successors. That's precisely why many "non-modern" languages have stuck around so long.
Additionally, mainframes are so strongly optimized for hardware-accelerated fixed point decimal computing that for a lot of financial calculations it can be legitimately difficult to match their performance with standard commercial hardware.
> It's not a problem with "non-modern" languages, it's a problem with C and many of its successors.
Not really. Any semi-decent modern language allows the creation of custom types which support the desired behavior and often some syntactic sugar (like operator overloading) to make their usage more natural. Take C++, for example, the archetypal "C successor": It's almost trivial to define a class which stores a fixed-precision number and overload the +, -, *, etc. operators to make it as convenient as a built-in type, and put it in library. In my book, this is vastly superior to making such a type a built-in, because you can never satisfy everyone's requirements.
It is also trivial to keep doing C mistakes with a C++ compiler, hence no matter how many ISO revisions it will still have, lack of safety due to C copy-paste compatibility will never be fixed.
> [...] no matter how many ISO revisions it will still have, lack of safety due to C copy-paste compatibility will never be fixed.
Okay, no idea how that's relevant to "built-in decimal types" vs "library-defined decimal types", but if it makes you feel better, you can do the same in Rust or Python, two languages which are "modern" compared to COBOL, don't inherit C's flaws, and which enable defining custom number types/classes/whatever together with convenient operator overloading.
> Python not really as the language doesn't provide any way to keep invariants
Again, how is that relevant? If there's no way to enforce an invariant in custom data types, then there's also no way to enforce invariants in code using built-in data types.
What I meant [1] was: In Python, invariants are enforced by conventions, not by the compiler. If that's not suitable for a given use case, then Python is entirely unsuited for that use case, regardless whether it provides built-in decimal types or user-defined decimal types. That's why I said that your objection regarding invariant enforcement is irrelevant to this discussion.
> How do account 3 items that cost 1/3$ each (e.g. if in a bundle)?
You never account for fractional discrete items, it makes no sense. A bundle is one product, and a split bundle is another. For products sold by weight or volume, it's usually handled with a unit price, and a fractional quantity. That way the continuous values can be rounded but money that is accounted for needs not be.
My last job they wanted me to invoice them hours worked, which was some number like 7.6.
This number plays badly when you run it through GST and other things - you get repeaters.
So I looked up common practice here, even tried asking finance who just said "be exact", and eventually settled on that below 1 cent fractions I would round up to the nearest cent in my favour for each line item.
First invoice I hand them, they manually tally up all the line items and hours, and complain it's over by 55 cents.
So I change it to give rounded line items but straight multiplied to the total - and they complain it doesn't match.
Finally I just print decimal exact numbers (which are occasionally huge) and they stop complaining - because excel is now happy the sums match when they keep second guessing my invoices.
All of this of course was irrelevant - I still had to put hours into their payroll system as well (which they checked against) and my contract specifically stated what my day rate was to be in lieu of notice.
So how should you do currency? Probably in whatever form that matches how finance are using excel, which does it wrong.
I wish this was untrue, but I have spent years hearing the words "why dont my reports match?" - no amount of logic, diagrams, explaining, the next quarter or instance - "why dont my reports match?"
The “exact” version they wanted was full of approximations too. They just didn’t have enough numerical literacy to understand how to say how much approximation they are ok with.
I guarantee nothing in anyone’s time accounting system is measured to double-precision accuracy. Or at least, I’ve never quite figured out the knack myself for stopping work within a particular 6 picosecond window.
Sure, but at the end of the day someone had to pay me an integer amount of cents. They wanted a total which was a normal dollar figure. But when you sum up 7.6 times whatever a whole lot, you might get a nice round number or you might get an irrational repeater.
What's notable is clearly no one had actually thought this through at a policy level - the answer was "excel goes brrrr" depending on how they want to add up and subtotal things.
Generally what is done is that “int 1 != $0.01” rather it’s “int 100 = $0.01”, as in the base of the integer is 1/100th a cent. That doesn’t perfectly solve your example case perfectly though admittedly.
[Modern] programming languages have decimal/rational data types, which (within limits) are exact. Where this is not possible, and/or it's undesirable for any reason, just use an int and scale it manually (e.g. 1.05 dollars = int 105).
However, point 2 is very problematic and important to consider. How do account 3 items that cost 1/3$ each (e.g. if in a bundle)? What if they're sold separately? This really depends on the requirements.
My 20 cents: if you start a project, start storing currency in an exact form. Once a project grows, correcting the FP error problem is a big PITA (assuming it's realistically possible).