64-bit bank balances ‘ought to be enough for anybody’?
tigerbeetle.com
tigerbeetle.com
For accounting you should only ever use arbitrary precision math library with ability to specify rounding rules. If your programming environment/language does not have one, it is unsuitable to be used for accounting, billing, invoicing, payments, etc.
Having the underlying library is, of course, not enough. You also need to be able to properly store (data structures, databases), transfer (wire formats, data structures), process (order of operations, rounding rules), present (UI toolkit) and so on.
I have never in my life joined a software project for any organisation that was able to do basic arithmetic on money correctly (and I worked for companies ranging from small startups just needing invoices and very simple billing to risk management departments for largest financial institutions on Earth processing trillions of dollars on a daily basis).
round in favor of the bank / financial institution you are working for
You issue an invoice for 1.44 USD (aka, amount due), then the 1.44 USD is used as a basis for accounting and is all consistent.
Then, if you are a nice company and the situation applies in your case, you may issue a credit in favor of the customer for 1.44 USD - 1.433 USD that will be used as a discount on a future invoice
The best part is that the moment where you decide to issue the credit invoice or not, is the perfect moment to track the rounding errors and even keep a very detailed journal of the entries (e.g. for auditors).
A good accountant so will sooner or later investigate those rounding errors, as they will show up somewhere ultimately. And a general policy of rounding in one direction is the last thing you want an auditor to find.
Eg: fuel is usually priced 3 decimal, so 4 gallons x 25.5444 usd/gallon gives = $102.1776 to 4 dp, but will be billed as $102.18
I assure you, I would have had a million bugs filed on that before it even hit production.
[1] https://globalmarkets.cib.bnpparibas/app/uploads/sites/4/202...
Where issues could come in is when these things are multiplied with a bunch of other numbers (each number with defined rounding but not after each operation) and then have some defined rounding at the end. There different computer numerics could in give slightly different results, but those can easily be resolved on settlement (for small stuff that stays well within the back offices - at least that is how I remember it).
Also, not totally unusual for one or both participants to forget about some rounding they might have agreed bilaterally if it was some one off etc.
And correcting the invoices meant playing with numbers so that at least the PDF and accounting software agreed on the total value and tax.
She also said they had at least 2 different employees and an external company look at it and not able to fix it. She also told me not to bother because she does not believe the problem can be fixed (that's what she was told).
I looked at the software, it had two separate copies of the invoice calculation (separate for on screen and for printing to PDF). And of course it would send the invoice to the accounting software which calculated the invoice in a different way still.
I ran couple of experiments to reverse engineer how the accounting software did the calculations -- the exact order of them and the exact rounding rules. Then I built a small module that captured those calculations. Then I changed all doubles to arbitrary precision.
It took two days and the problem was fixed but it took couple more days before accounting department actually believed it.
Your setup made it sound like something crazy and inane, like "the PDF printer used on those machines changed floating point rounding modes" or what have you.
In this particular case we had two separate pieces of code, one running on the client (for on screen presentation) and one on the backend (to create the PDF on a shared location and produce a download URL).
That's actually kinda normal, in some industries.
For example, an amazon marketplace seller's warehouse management system might not be tightly integrated with Amazon's basket/checkout display logic.
In some situations the results of recalculating are supposed to be different. For example, if there's a "5% off when you buy 3 widgets" offer and you check out with 2 widgets in your basket, the offer doesn't apply. But if you checked out 3 widgets, thus getting the offer, then the seller found they were low on stock and could only send you 2, you should get the 5% discount on those 2.
Those 3 people that now didn't had a job probably weren't all that happy xD
My career advice is to work in a field / company / job where you can be somewhere in the top 10-20% of all employees. Just don't overdo it, if you are top 1% you are probably aiming too low and could be working for better paying, more rewarding field / company / job.
For a lot of my career I was working for financial institutions like banks. A lot of really badly managed projects with definitely not top level developers. Easy to be a top performer. I really like helping people and projects and it was working well for me especially when it was easy for me to provide valuable help.
I got hired once for a really good company with really top performers and suddenly I lost the status that I was so used to. I was keeping up with my work, sure, but I was no longer a shiny star. I got back to working for banks.
AGI is 40,001 to 40,025 then your tax is X dollars.
Being accurate to the penny isn't worth the trouble.
I run a Commercial Real Estate Servicing platform, where we are accruing interest on large balances daily. Our method is to not do the rounding daily, but add up all the numbers for a given period, say a month, and then round to the penny and create a single adjustment rounding transaction along with it. Accounting departments love us for it.
If we rounded daily before storing the amount, the adjustment for accounting is usually a few pennies at least every month they have to make. Our method, it's roughly $0.01 per year with monthly periods, adjusted usually at the very end. Which on a $20MM loan, is very well within the bounds of acceptable.
Or it is because rounding errors happen and accounting is a bitch. The first option makes for a better movie plot so.
I brought up specific math problems that floats couldn't handle and they weren't phased
"Formally introduced in IEEE 754-2008, it is intended for applications where it is necessary to emulate decimal rounding exactly, such as financial and tax computations."
https://en.wikipedia.org/wiki/Decimal128_floating-point_form...
Did you know different countries and different currencies have different rounding rules, for example for tax-related calculations? Does "IEEE decimal128" support this? Unless you can get all countries on our planet to agree on a single standard, any solution that does not allow specifying rounding rules is pretty much useless (unless you want to implement rounding yourself which tends to be very tricky -- I know because I attempted this couple of times).
Yes, of course IEEE decimal supports setting the rounding mode. The authors of the spec aren't ignorant of what's needed for financial and tax computations.
Use fe_dec_setround from ISO/IEC TR 24732, "Extension for the programming language C to support decimal floating-point arithmetic".
The modes listed at https://www.ibm.com/docs/en/zos/2.5.0?topic=functions-fe-dec... are:
FE_DEC_DOWNWARD
rounds towards minus infinity
FE_DEC_TONEAREST
rounds to nearest
FE_DEC_TOWARDZERO
rounds toward zero
FE_DEC_UPWARD
rounds toward plus infinity
FE_DEC_TONEARESTFROMZERO
rounds to nearest, ties away from zero
_FE_DEC_AWAYFROMZERO
rounds away from zero
_FE_DEC_TONEARESTTOWARDZERO
rounds to nearest, ties toward zero
_FE_DEC_PREPAREFORSHORTER
rounds to prepare for shorter precisionhttps://en.wikipedia.org/wiki/IEEE_754#Roundings_to_nearest
EDIT: apparently IEEE does not specify a "round-to-odd" for ties despite this having been used for banking in the UK :/
A classic example in invoicing is an item that is advertised for 60.00 (to the final user) VAT 10% included.
If you try making an invoice for that sum in a few programs you will find three or four way it is implemented.
Some will have 54.54+5.45=59.99, some will have 54.54+5.46=60.00, some will have 54.55+5.46=60.01 (and possibly a "discount" of 0.01), some will have 54.545+5.45=60.00, some will have 54.54545454+5.45=60,00.
My point is it's probably better to use existing, well-tested provisions than to build your own from scaled integers, to get one of those three results.
As a bonus, you might get hardware support in the future.
At the end of the day what I want (and I presume any other customer wants) is a correct invoice with the correct net, tax and total, and this will only happen when (if) the programmer understands the base issues and uses the correct library/algorithm/whatever.
Many duties are calculated based on net weight, and often the net weight per goods line is the result of a calculation, for example you're importing N items with a per-item weight of X. If you have a large number of goods items above 1kg but less than 10kg that has weight-based duties, the rounding mode can matter a lot.
None of the rounding modes mentioned captures this below/above 1kg split, so you have to do this in code anyway. Might as well do the rounding there too, to be sure some injected code doesn't mess up the expected rounding mode or similar[1].
[1]: https://irrlicht.sourceforge.io/forum/viewtopic.php?t=8773
As I understand it, one of the new things in IEEE 754 is the idea of a "context", which stores this information. This can be global, but does not need to be. With Python's decimal module it is a thread-local variable.
If you are concerned about, say, mixing thread-local and async, you can also use context methods directly, like:
>>> import decimal
>>> x = decimal.Decimal("54.1234")
>>> y = decimal.Decimal("987.340")
>>> x+y
Decimal('1041.4634')
>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero,
Overflow])
>>>
>>> c1 = decimal.Context(prec=4)
>>> c1.add(x, y)
Decimal('1041')
>>> c2 = decimal.Context(prec=5)
>>> c2.add(x, y)
Decimal('1041.5')
>>> c3 = decimal.Context(prec=5, rounding=decimal.ROUND_DOWN)
>>> c3.add(x, y)
Decimal('1041.4')
I don't know what the C or C++ API proposals are.IEEE does specify multiple rounding modes. Does it make more sense to use an existing spec, or roll your own numeric library with rounding modes?
What has changed? Or how different were your internalised rules?
Also, provided you have a data structure that can represent money, you should presumably be able to serialize that data structure and store or send it just like any other data, right? Why do you need special database or wire format support? The trivial example is to marshal it to JSON put it on disk or write it to the network using the protocol suite of your choice, right?
Problems will arise when you'll do multiplicative operations on money, for instance when working out taxes. There are precise rounding rules to apply, and the solution is to tackle these issue one abstraction layer above, on the operations rather than the values, because some time you'll want to carry out rounding between each tax operation, sometimes at the end, on one line or on a whole batch of transactions.
Other problems you'll bump into is the asynchronous nature of money flows. You won't realize it with credit cards (well you'll figure it out soon enough when you'll stumble upon "race conditions"), but this becomes explicit when dealing with mandates, direct bank transfers or checks. You need to move the money out of sight of the user in a ledger specific to that person that holds transactions being processed (can takes days or weeks in some case) and move it back to the original ledger if the transaction fail. Otherwise you'll bump into issues of double spending. This is something that is out of your control sometimes (had the unfortunate experience to issue withdrawals multiple times on a big bank's payment processor API and theses dunces sent the money multiples times). Use idempotency keys profusely as well as (distributed) locks. The hardest part is not getting your part right, it's handling external actors bad implementations. Also fuck HTTP w/ hooks. Some actors do not even make sure you received the webhook. The non binary aspect of HTTP is a bullshit argument and I'd gladly trade it for a binary protocol that has strong quality of service mike MQTT, since anyway I'll have to implement some kind of smart broker when issuing orders to a shitty HTTP api anyway.
But yes, I can see how databases not having a money type with corresponding routines is going to make life harder.
What I've used before is "Express the value in pennies. Divide by X (number of parties). Take the whole number part and give it to each person evenly. Take the modulus and distribute it one by one to each remaining party, until there is nothing left."
Example: allocate $10.01 among 3 parties:
1. You have 1001 pennies
2. Divide by 3 - give each person 333 pennies
3. Take the modulo 1001 % 3 - you have 2 pennies remaining. Time to distribute them round-robin!
4. Give one penny to person A. You have 1 penny remaining.
5. Give one penny to person B. You have no pennies remaining.
If you do this many times, randomly establish the order of the parties each time you round-robin them. In that case, no one will be systematically under-allocated, just because their name starts with the letter Z.
This is not a universal truth, and each situation is different. It may not apply to other kinds of money dividing situations.
Just as you should use proper DATE or DATETIME data types for time and not roll your own with strings, integers, seconds-from-epoch, or any other schemes at least if you want to keep your sanity.
But hopefully only after understanding how it treats these, and whether that's compatible with your requirements.
> Just as you should use proper DATE or DATETIME data types for time and not roll your own
I'm always happy to use the database's date/time format – if it's actually implemented in a sane way and is compatible with my data.
For example, I work with data provided by external partners that specifies dates:
Sometimes they're only specifying the year as a single-digit integer (and you have to guess which one they mean, based on the current date and the hope that the files they send you are not older than 5-10 years). Sometimes there is no year at all. Sometimes the timestamps have an implied timezone, sometimes they're UTC, and sometimes they're supposed to be UTC, but really are in some unspecified local timezone.
In these cases, it can indeed be better to store these as strings and deferring interpretation until you actually process them.
The article is talking about serialized representations, i.e. how you store amounts in a database. The comment is talking about how to arrive at amounts as part of arithmetic calculations, e.g. determining interest, percentage fees etc.
I can certainly sympathise with this stance. There is about a billion things you can do better but you have limited time to do anything so you have to prioritise. And if one invoice in ten thousand is incorrect by one cent, and only one client in ten thousand who received the wrong invoice will actually find it out, then it is hard to argue you should be spending time on fixing this one problem.
Just don't say you can do accounting correctly on floats and we will remain friends.
Example in js: Number(9999999.999999999).toString() // => 9999999.999999998
And make sure you're not rounding using Math.round
Math.round(-1.5) // => -1
or toFixed
(2090.5 * 8.61).toFixed(2) // => 17999.20 should have been 17999.21 8.165.toFixed(2) // => 8.16 should be 8.17
The better solution is to use arbitrary precision decimals, and transport them as strings. Store them as arbitrary precision decimals in the database when possible.
The way I solve this problem isn't by constantly hopping projects. I try to find projects that actually require extreme reliability so that I can be doing what I want in an environment where there is a business case for it.
Decimals are still the way to go, you just have to pick a level of precision acceptable for your application.
My management definitely does not want me spending my time chasing errors over fractions of a pennies. The only time those errors are discovered is when I compare the output of new code against old code.
[0] https://blog.plover.com//prog/Moonpig.html#fp-sucks
[1] https://observablehq.com/@rreusser/half-precision-floating-p...
Things get out of hand when you need to round multiple different things that have to sum up at the end.
For example:
- Items in an invoice are rounded and summed. (eg. $1.1234 * 5.678kg)
- Payments of an invoice can be paid in multiple installments, with interests that are also rounded (eg. 1.77% per month).
- The value paid of interest *per item* must match the total value paid of interest in all installments of all invoices in the same period.
This observation should tell you that's it actually quite viable to be off as long as the errors are small enough.
But if I imagine myself as a business owner I would be annoyed with my accounting firm if they spent billable hours chasing down a discrepancy of a few pennies.
As long as things are consistent, no one cares if you are correct. If you lose a penny in the backend calculation, and the frontend shows the amount without the penny, and the email contains the amount without the penny, and the PDF download contains the amount without the penny, no one will care that there should be a penny there.
It becomes problematic if some places are wrong and some are right, and they are not consistent. You won't get credit for being right in only some places.
As long as the error is small enough to be inconsequential, being consistent is more important than being correct.
The size of the errors almost always is a large factor in their decision, but ultimately the software we write exists to serve the needs of the business, and if the business decides that larger errors are okay for some reason, then so be it.
Sometimes there's also value in doing something objectively poorly, but in a predictable and well-understood way.
Unilaterally starting to "do numbers better" sounds like a recipe for, let's say, interesting times in the finance/accounting world.
Besides that, using BigDecimal with two decimal places is sufficient in the java world imho. Depends on your use case. I'm entirely sceptical of people claiming general things. Depends on the requirements I'd say.
If a large taxpayer starts rounding down as part of intermediate calculations of their tax liability, I think they'd get some questions.
But yes, rounding does happen a lot – what's important is that everybody uses the same, transparent rules for that, or it becomes impossible to double-check somebody's books, tax declaration, invoice etc.
> But yes, rounding does happen a lot – what's important is that everybody uses the same, transparent rules for that, or it becomes impossible to double-check somebody's books, tax declaration, invoice etc.
They don't, that's why it doesn't matter that much ;)
You and me? Probably a few cents.
A bank or a large corporation selling things billed in sub-cent amounts? Single-digit percentages of their gross revenue, i.e. many millions.
Just as a very simple example: I'm your bank/phone provider/..., and I'm charging you a flat fee of 1.9 cents per transaction/call/... You're my customer and make a billion of such transactions per year.
Option 1: 1000000000 * 0.019 = 19000, you owe me $19000000.
Option 2: There are no fractional cents, so let's just round up each individual billing event. 1000000000 * 0.02 = 20000000, you owe me $20000000. Cool, a free extra million for the company!
This is why these things are precisely regulated when it comes to sales tax/VAT, for example.
> There's a thing called "Kaufmännisches Runden", which is kind of cheating as well.
Always depends on which side you're on. If you're getting a refund, it can work in your favor! Importantly, it's a precisely defined rule so that it's not possible to cheat in the implementation.
And as a meta-point:
> I have been fighting over this with countless people, teams and companies. [...] I have never in my life joined a software project for any organisation that was able to do basic arithmetic on money correctly [...]
Are you absolutely sure that you are the only person that understands how to do accounting arithmetics on computers correctly?
My guess would be that the status quo is a combination of a lot of legacy code and procedures, but more importantly of differing priorities.
Maybe you value arithmetic correctness over simplicity of procedures (sometimes these need to be published in regulatory texts or even laws) or compatibility with other entities and their procedures much more than the industry average?
I had our CFO stand behind me while I talked him through every step of our VAT calculations once, because he was legally responsible if I made us round it wrong, to the wrong number of digits. And had we e.g. done something grossly incompetent like used floats for those calculations it most certainly would have been wrong, but so would it if I used fewer than five digits past the decimal point or failed to round in the right direction after that.
It's usually not hard, but it requires being aware that you need to look up the right rules. And know better than using floats.
That would have been my default assumption
This is not to say that using floats and rounding correctly necessarily does yield different results, by the way (although most likely it will) – but if they do differ, you're going to have a bad time using floats.
1: https://en.wikipedia.org/wiki/Decimal64_floating-point_forma... 2: https://dev.mysql.com/doc/refman/8.0/en/precision-math-decim...
The standard in ad-tech (not sure about banking) is to use int64s representing either microdollars or microcents, so a max capacity of 9.3*10^13 or 10^11 dollars
So you need to run a massive physics simulation really fast? Yes, floats are great.
You need to calculate taxes on a massive corporation's fiscal year? Bad idea.
Some libraries advertise "arbitrary precision", many computer systems have a "decimal" type intended for currency, etc. and then they won't make all the same mistakes, but as the OP said you still need to control rounding rules and make sure they match the law.
That depends on whether the hundred-billion-dollar corporation cares about being off by a dollar.
And by "off" I mean "different from how humans round", not necessarily further away from an infinite-precision calculation. In fact at "massive corporation" level I would guess that binary floating point is more accurate than a typical fractional penny system.
How many hundred billion dollar corporations are private? Public companies would care a great deal about accounting accuracy.
Every single publicly listed company, every single one of them, is off when it comes to calculating their taxes by way more than just a dollar. And I don't mean clever accounting tricks or tax avoidance schemes, I just mean in terms of actual mistakes being made.
FP numbers have their use but they re better reserved for scientists doing actually scientific stuff and not just to represent what are actually tiny numbers (in the grand scheme of things) and which can be represented perfectly by other means.
-2.7755575615628914e-17
And now you overdrafted
There is no floating point value equal to 0.3.
You can represent 0.3 as 0.300000…0004, which rounds to 0.3 again in the end.
But you need to reason about the number and nature of intermediate operations, which is tricky, since errors usually accumulate and don’t always cancel out.
No, it really is the original sin here.
> since errors usually accumulate and don’t always cancel out.
The problem is that from the system's perspective, these aren't "errors". 0.3000000....4 is a perfectly valid value. It's just not the value that you want. But the computer doesn't know what you want.
When I say "error" here I mean the mathematical term, i.e. numerical error, from error analysis, not "error" as in "an erroneous result".
There is a formalism for measuring this type of error and making sure it does not exceed your desired precision.
> It's just not the value that you want.
My point is exactly that if you're looking at 0.300000...4, you aren't done with your calculation yet. If you stop there and show that value to a user somewhere (or are blindly casting it to a decimal or arbitrary precision type), you are using IEEE 754 wrong.
You know that your input values have a precision of only one or two sub-decimal digits, in this example, so considering more than ten digits of precision of your output is wrong. You have to round!
It's the same type of error that newspapers sometimes make when they say "the damage is estimated to be on the order of $100 million (€93.819 million)".
Yes, this is often more complicated and error-prone (the human kind this time) than just using decimals or integers, and sometimes it will outright not work (since it's not precise enough – which your error analysis should tell you!)! But that doesn't mean that IEEE 754 is somehow inherently not suitable for this type of task.
As a practical example, Bitcoin was (according to at least one source) designed with floating point precision and error analysis in mind, i.e. by limiting the domain of possible values so that it fits into double-length IEEE 754 floating point values losslessly – not because it's necessarily a good idea to do Bitcoin arithmetics using floating point numbers, but to put bounds on the resulting errors if somebody does it anyway: That's applied error analysis :)
Often these things are not a big problem on their own, but then later gets compounded because someone does something stupid like passing these imprecise values around to be distorted further all over the place.
And sometimes the reason it doesn't become a legal problem turns out to be because your finance department quietly works their way around it by expending expensive manpower accounting for discrepancies that shouldn't be there in the first place, and so increases the cost to the business by many magnitudes over the loss the developers might have assumed to be the worst case (if they're aware of the discrepancy at all).
This is one of those things you can get away with many times, many places, with no ill effects. But when it finally bites you it can get expensive and/or really bad to deal with, and it's fixed by simply never doing money calculations on datatypes with imprecise arithmetic, and having a five minute conversation with your finance team about what your local rules for rounding tax amounts are.
Where in financial accounting do people multiply an amount of money by a multiplicand larger than order-of-unity?
Yeah, that's one example. I wasn't imaginative enough; thanks!
Also if the local tax code states using 5 decimal places for intermediate values when you will introduce “errors” using formats that give greater precision as well as those that give less precision. Having worked on mortgage and pension calculations I can state that the (very) small errors seen at individual steps because of this can balloon significantly through repeated calculations.
Furthermore, the name floating point gives away the other issue. Floating point numbers are accurate to a given number of significant figures not decimal places. For large numbers any decimal places you have in the result are at best an estimate, and as above any rounding errors at each stage can compound into a much larger error by the end of a calculation.
And binary has trouble representing fractions that are common in prices:
$ bc
obase=2
scale=20
1/5
1/5 in binary is a repeating binary fraction: 0.0011001100110011...Just as you can't express 1/3 or 1/7 precisely as a non-repeating decimal fraction, you can't express 1/5 and 1/10 as a non-repeating binary fraction. As a result, most prices involving cents in currency cannot be expressed precisely as binary floating point numbers.
edit: fixed formatting
It gets even worse when you start doing calculations on floats/doubles.
These inaccuracies are ok for a lot of things. graphics often uses floats and the errors are small enough they don't matter.
But currency absolutely needs to be accurate, and for that reason, floats/doubles are in appropriate.
I would imagine almost no-one knows this (-: What's a shocking number?
Almost every developer I've worked with who haven't implemented invoicing or billing at least once and had their finance team yell at them for producing wrong numbers...
(and I'll edit this to add the limitation "who implement billing related software" - it's still true, and closer to my intended point)
if the error is less than their hourly salary rate, it don't even worth mentioning.
if it worth a day or two of salary, its nice to fix but never a priority
There's a reason that in 28 years of working in software, the only thing the financial teams I've worked with have obsessed over have been whether or not we get the VAT calculations right, and the "sticker price" of the discrepancy has never been what they worry about. For calculations that does not involve getting tax amounts wrong, they often couldn't care less about much bigger discrepancies, but get tax wrong in the wrong jurisdiction and it's a lot of pain.
If your incoming VAT are not matched with outgoing VAT from your supplier, you will be charged.
If your supplier declared VAT, but failed to pay it, you have choice: either you have to pay it or you will be inspected to proof that it was not a fake.
Every sale to physical customer in Russia should be uploaded to tax service cloud. You (as a customer) could check your receipt online or using app, and get a reward for reporting tax evasion.
This system boosted VAT revenue x1.5 in a few years.
What is your jurisdiction? In Canada, I can't for the life of me imagine the CRA would remotely care about decimal-point accuracy. In fact, most of their online forms explicitly remove the decimals.
For aggregate totals of your VAT liability across your total set of invoices, you'd be fine with rounding up to the nearest pound, to the Inland Revenue's benefit. For individual invoices however, you were required to stick to very specific rounding rules.
On personal tax forms you have to round in the taxpayer favour. If your income is 12345.67 you round it to 12345. If your expense (say giftaid) is 12345.67 you round it to 12346.
Surprised it's the other way with VAT, but then I do very little with tax other than click a few buttons and confirm "yes, you have to tax me as I have children".
The key, though, is that with taxes, if at all unsure you've got the rules right, the safest option is to round in the tax offices favour.
It's in general a lot less painful to explain an overpayment than underpayment if something is broken.
Of course, better yet, get it right.
I bet they care about you not throwing away decimals in intermediate calculations for VAT or sales tax.
The payments gateway we worked with calculated their fees to a thousandth of a cent, but of course we could only bill whole cents. So basically every billing period there would be a < $0.01 balance due that carried over to the next bill, and every so often the carryover would add up to a full cent that needed to be charged. When we implemented our MVP we (engineering) explained this to the product and business teams, and it blew their minds. Our suggestion was to have a tooltip on the 1 cent charge with a link to a help article explaining how the accounting worked, but they were strongly against it and had us list the 1 cent as something like "other fees" with no explanation. They seemed convinced it was a thing that would happen only rarely, even as we were telling them otherwise. Anyway, that 1 cent charge just infuriated clients for some reason, and every month or two we'd get bug reports about it or requests to explain why it kept showing up. Fun times...
If the month's charges were 406.783228 and I get a bill for 406.79 then that seems perfectly good.
If I get a bill that says 406.78, plus a separate 0.01, that's weird.
Because the bill included a detailed breakdown of all the fees by type and transaction. Typically our clients were charged a fixed monthly fee, a fixed authorization fee charged every time a payment was attempted (even if it was declined), and a fee applied to successful payments that was a percentage of the payment amount. There were other fees for things like processing chargebacks, but IIRC they were the same for everyone.
> If the month's charges were 406.783228 and I get a bill for 406.79 then that seems perfectly good.
Yeah, we definitely weren't allowed to round up and keep the change. I can't claim to know all the details involved but I suspect that doing so would've at least violated our contract with the payments gateway. Might've actually been illegal.
> If I get a bill that says 406.78, plus a separate 0.01, that's weird.
That's not how it worked. For the sake of simplicity let's assume that your activity is always the same, therefore you have new charges totaling exactly $406.783228 every month.
* Month 1: You owe $406.783228, your bill is $406.78, a balance of $0.003228 rolls over.
* Month 2: You owe $406.786456, your bill is $406.78, a balance of $0.006456 rolls over.
* Month 3: You owe $406.789684, your bill is $406.78, a balance of $0.009684 rolls over.
* Month 4: You owe $406.792912, your bill is $406.79, there's a $0.01 "other fee" line item on the bill, and a balance of $0.002912 rolls over.
> Because the bill included a detailed breakdown of all the fees by type and transaction.
Was it all rounded [down] to the nearest penny?
That type of bill can already fail to add up to the total very easily, like x.xx4 + x.xx4 + x.xx4. So I'm still not sure why there was a need to have a line item to explain this single penny.
Was there only a single charge on each bill that used fractional pennies? So that this was the only time that things wouldn't add up perfectly?
Yet, life goes on; nobody goes to jail.
You could probably treat the extra microcents as being on the next pay period. Though that's annoying as if I close an account I'd expect it to be paid in full, not a few microcents remaining.
1: https://stackoverflow.com/questions/45223778/is-bankers-roun...
There is an obvious alternative here, which is to just absorb that fraction of cent, so the customer doesn't see it. Handling bug reports and infuriated clients costs more.
You could even just round up to the penny and ding the customer; if there is a 0.3 cent fraction coming from the payments gateway, turn it into a customer-facing penny, thereby collecting an extra 0.7 cents.
That way, too, there would almost certainly be zero complaints and bug reports.
If the customer is supposed to pay $103.45395 every month, you could turn it into $103.46, pocketing an extra $0.00605, or into $103.45, where you're out $0.00395.
Think about it; when you eat at a restaurant, often the prices for a meal are round like $11.50, even though the restaurants expenses are down to the penny. Why is that? Because they arbitrarily set the price. They don't say, oh, our ground beef supplier charges to the penny penny, so lunch will have to be $11.57.
Oh, the business teams found the approach puzzling---but what do they know, right? If they were smart, they would be software engineers.
Well in this case the business teams selected the 3rd party payments gateway that the company would work with, negotiated the contracts with them, and worked with the 3rd party to set up how the customers would be charged. They and/or the product team determined that we'd use the 3rd party system to handle the billing because building it in house wouldn't generate any new revenue. They weren't stupid, but they choose an approach to payments processing that was pretty low level (because it generated more revenue, of course), without anyone at the company having a good understanding of low level payment processing details. The engineers learned because we kind of had to, but three years into the project (when I left) business/product would still routinely struggle with the details.
So anyway, WRT to billing, the task handed to engineering was: pull the billing detail from the 3rd party's API and assemble it into a statement that can be handed to a client. We had zero control over how the charges were generated or applying any rounding to the total.
Amount owing: $123.45678
Please pay one of: $123.46 (a credit of $0.00322 will be applied)
$123.45 (a balance of $0.00678 will carry forward)
On the next statement, if they paid $123.46: Previous balance: ( $0.00322) [credit]
New charges: 123.45678 { here we have a detailed breakdown }
Amount owing: 123.45356
Please pay one of: $123.46 (a credit of $0.00644 will be applied)
$123.45 (a balance of $0.00356 will carry forward)
etc.That's literally "put the billing detail from the 3rd party API and assemble it into a statement". Since the billing detail from the 3rd party API is in thousandths of a cent, then that implies the statement must have thousandths of a cent.
If the two payment options were determined to be too confusing, one of the two could be dropped.
Again, that was not something we could control.
So, in another life I worked on reporting software for a foreign branch of a US bank. You've heard of the bank. You would probably recognize the CEO's name, in fact.
We had been fucking this up for years. I fixed it. We had some customers who yelled at us because our reports were "wrong" i.e. they were double checking our work and apparently making the same mistake. They could not be reasoned with. Bear in mind, we're talking about differences of pennies, or a few dollars on very large transactions. Some of our customers insisted we were calculating the values incorrectly and demanded we "fix" it.
What do you think happened next? You have one guess.
A shocking number of people who create tax codes have no idea how many decimal places they are using.
It's probably better now, but I recall having to reverse engineer the tax tables to figure out how many decimal points of accuracy were used, and what rounding rules were used, so we could match their numbers.
These numbers would change from year to year, with no change in the underlying tax codes.
They're saying that only arbitrary precision arithmetics are acceptable, and additionally claiming that everybody else in the world gets money arithmetics wrong.
I doubt both of these statements, and especially the assertion that there's exactly one "correct" way of doing arithmetics with money.
> Maybe you value arithmetic correctness over simplicity of procedures
Lots of places(not typically the USA) has this codified in law. For example, do a web search for: EU money rounding rules. You will find several different rounding and precision rules, depending on the context of what you are doing with the money, all from places like the Central Bank and the EU Commission.
It's mostly US developers that are clueless here, because US laws are fuzzy at best, and the general rule is, you do whatever your bank/regulatory authority does, and if they don't happen to know (and I've met several that don't), then you have to figure it out yourself.
In the USA, we use decimal.ROUND_HALF_UP, because we have seen in practice this is what our USA based banks & govt tend to do in the wild. It should be noted IEEE 754 rounding recommends using decimal.ROUND_HALF_EVEN. https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
In other places, we do whatever their laws require, or treat them like the USA and do whatever our local bank/govt authority tends to do in practice.
Using a "money" class that stores things as integer pennies gets you a long way there. "Division" and its close friend "multiply by noninteger number" are the only real problems, so you need to be careful not to provide a generic method and instead methods like divideWithLocaleTaxRounding(). You also need to check whether you're supposed to apply tax per-item or you can do it to the whole bill.
I think we had a "apply tax to whole bill but then re-distribute it to line items" method, which guaranteed that the total would be correct.
There are reasonable arguments for "integer decimal fraction of penny" as the correct unit. Digikey price some parts in 0.1 of a penny or cent, for example.
Attempting to convert things between binary "fractions" (floating point) and decimal fractions will result in misery.
I think we also had a "rational number" class for storing things like "1/3".
If I ever have someone paying me more than 2^64 dollars, I'll rewrite Tarsnap's accounting system.
So, not as much margin as one might think.
EMV uses integers encoded as BCD for money. If I remember well, in most cases it is 6 bytes or 12 digits. That is more than 2^32 (most of POS machines were 32 bit ARM until relatively recently).
The terminal I worked on had 32 bit ARM. Rather than convert 12 digit numbers to 32 bit integers I decided to write my own arithmetic library that did operations directly on BCD strings of arbitrary length (but, in practice, EMV only allows 6 bytes for the amount anyway).
I got you:
Over a full year it needs to keep track of all the rounding it does when it pays you interest and when that rounding reaches a penny it's supposed to pay you that penny.
I'm a bit surprised by this advice. I thought the common wisdom was to use a decimal type like BigDecimal in Java.
Do these rounding rules need to vary by jurisdiction?
this is now a fintech anecdotes thread, but my first ever fintech job was part of a two man special priviledges team directly under a director of X at one of the sifis. we were supposed to cut across multiple departments and through red tape with the main goal if eliminating significant overhead in certain processes (we brought some multi-day multi-step calculations down to 8 minutes in one instance, 2 minutes in another, kept us on a vendor list for a very very very long time). i didn't know how things are done, so i used bigdecimal throughout, with a lot of precision and explicit rounding rules. i did back of napkin numerical analysis for at least inner loop code. we duplicated work done by other departments (!!!), things like instrument pricing, because we couldn't wait for their batch jobs to complete. at the end we were getting from slightly to wildly different results. it took a lot of conversations with business to realize that 1) our calculations were correct 2) for them to realize what the sources of errors were and where they were coming from 3) for everyone to just kind of go eeeehh not a big deal.
i wrote a translator for a subset of j/k to java bytecode using java asm, i was pretty proud of that system, because it allowed to express pricing rules in a what i thought was much more readable way, dynamic reload without restart, but man i am very very sorry for the developers who inherited that system.
Fixed point decimal if you're lucky (or unlucky, since fixed point sucks)
Arbitrary precision decimal floating point essentially never used
1) If it happens, it happens rapidly, and you don't want to implement this in a hurry
2) If it happens, the global economy could well be melting down, and your financial institution will have other priorities to attend to
3) Retaining existing staff, and hiring new engineers, will be challenging at best
You really don't want to be implementing this change in those circumstances.
Besides, if you think a billion billions might be reached (64 bits) then why wouldn't a billion billion billion billions also be reached (128 bits)? 128 bits seems just as arbitrary as 64 bits in this context.
I lived through hyperinflation (in Brazil). What happened was that, at regular intervals, the old hyper-inflated currency was replaced with a new currency that's 10^3 times less than the original one. Cutting decimal zeros is better for humans, and three is convenient because it matches the usual three-digit grouping (that is, 123.450,00 becomes 123,45). This had to be done because, otherwise, calculators would become useless (most common pocket calculators had only eight digits).
Edit: as another hypothetical, what if the $0.001 coin is released to support micropayment use cases?
> https://www.law.cornell.edu/uscode/text/31/5101 says "United States money is expressed in dollars, dimes or tenths, cents or hundreths,[1] and mills or thousandths. A dime is a tenth of a dollar, a cent is a hundredth of a dollar, and a mill is a thousandth of a dollar."
> [1] So in original. Probably should be “hundredths,”.
About the only time you see values given in mills is with gas prices, like $4.999/gal, though often denoted as tenths of a cent. It's also indirectly used in property taxes.
Anybody who truly thinks Bitcoin could hit $100K value certainly doesn't want to spend them.
https://cs.wikipedia.org/wiki/%C4%8Ceskoslovensk%C3%A1_m%C4%...
So, you'll need subpenny fractions (e.g. 8 decimal points), or BigDecimal, or decimal-normalized floats.
(And some of our back-end systems then did ludicrous broken wrong-headed rounding to turn them into fictional currency values... Ho hum.)
But if you are not an expert, you better stick to BigDecimal and absorb the performance costs.
> final double residual = df - ldf + Math.ulp(d) * (factor * 0.983);
That was maybe 15 years ago - hopefully they've fired that programmer and fixed it in the meantime. We don't know, because we don't use QuickBooks any more.
So it's useful for applications where you're mostly doing human input/output <-> stored value.
But as soon as you do any non-trivial math on those values, using (fixed point?) integers wins. At the cost of a simple stored value <-> human readable conversion.
I'd think most financial applications fall into the category "do math, so integers win over BCD".
Why is a BCD decimal 128 worse at math then a fixed point integer? You are saying it is more CPU efficient? Are you saying some operations with fixed point integer math operations are more accurate then dec128?
I've seen this asserted several times, both in the post and in comments, but I've never seen a single concrete example of it being better. Can someone provide an example?
What I was asking for is, when they said it was just "better", are they specifically saying "CPU computations is a bottleneck, thus BCD is not as good as fixed point integers"? Which is fine if it is, I just would like that to be stated clearly. In my line of work, BCD CPU is NEVER the bottleneck, it never will be, and it is likely that the time it takes for a CPU to compute the BCD operation it will still be stalling on pref etching the next instruction from main memory anyway.
But maybe, for their specific ledger specific database, it is better. If so, show the benchmark, and how it impacted their specific code. But don't expect me to just take fewer CPU instructions for math operations to directly translate to more desirable.
What you really want is a rational (fractional value: numerator and denominator) of some form.
As I understand it, the regulations related to money can require specific rounding modes and a specific number of digits for intermediate representations. These are much easier to manage with, eg, Python's decimal module than doing everything as integers.
For example, at https://news.ycombinator.com/item?id=36687627 I pointed to US law at https://www.law.cornell.edu/cfr/text/7/1005.83 with:
(3) Divide the result in paragraph (a)(2) of this section by 5.5, and round
down to three decimal places to compute the fuel cost adjustment factor;
(4) Add the result in paragraph (a)(3) of this section to $1.91;
(5) Divide the result in paragraph (a)(4) of this section by 480;
(6) Round the result in paragraph (a)(5) of this section down to five decimal
places to compute the mileage rate.To give a concrete example, Ethereum has a lot of precision (1 ether = 10^18 wei), and there are like 120M ether, so that's more than 64 bits just to represent the supply.
Idk though, is this a real concern with traditional finance? Zimbabwe had their huge bills, but I assume the small ones were unused. Just like you round US cents, you could probably round their currency at some cutoff. If you were processing Ether outside the blockchain like just some other currency, you'd probably round somewhere too. If you look at Stripe's API for example, they do everything with fixed-size (JSON's 53-bit) integers defined as cents or whatever.
Yeah... I started reading this article and was waiting for the crypto angle, because in normal finance I don't see how it could plausibly matter (disclaimer: not my domain).
The only reason I could imagine needing to track this level of precision on fractional units of currency was the sort of financial trickery that is enabled by cryptocurrencies.
What tipped the scales, was realizing that this applied also in normal finance, especially exchanges (not crypto) that operate with high precision: https://news.ycombinator.com/item?id=37573939
CME have contracts with 1/128ths of a dollar
they also have a tenancy to add another power of 1/2th every 10 years or so
this causes problem for fixed point, but amusingly this works perfectly well with floating point
>>> 0.1 + 0.2
0.30000000000000004
which is still gonna round to 0.3, but ugh. Also, during aggregations, it's not nice how you get different answers depending on the order of operations when they shouldn't matter.so floating point works fine as floating point is binary fractions
TigerBeetle may have made the right choice for some market, but I predict that there are vanishingly few sales calls where this becomes an important selling point, unless it's potential customers wondering why they are wasting all those bits and checks for a whole lot of freakin' zeros.
> less than 10^15 cents
There are systems that don't work in terms of cents (cf. the examples of issues in many of the comments here), or even in thousandths of a cent, but with significantly more precision.
Literally, in other words:
Where you run into problems then, with 10 ^ n integer scaling, is when n is large. When n is large, you aren't left with sufficient room in the remaining bytes to represent the whole-number part. In trading systems, for instance, you can easily hit 10 ^ 10 to represent fractional traded prices.
Concretely, if you need to scale by 10 ^ 10, then your whole-number part is 2 ^ 64 / 10 ^ 10 = 1,844,674,407, which isn't terribly large.
At the same time, you can understand that 10^10 scaling is at least significantly more precise. And I can imagine these things are viral too, who you trade with, also determines your minimum resolution. You can always downsample in presentation, but once downsampled in storage, it's impossible to upsample.
It also wasn't the only use case. But it tipped the scales.
You can maintain exact calculations through an entire data pipeline this way, as long as your base numbers are all integers and ratios, and optionally have one rounding step at the end if you want a decimal value. Most math languages and lisps do this.
There must be libraries for other languages that can do it too, but it’s much nicer to work with when it’s built-in.
I'm sure you've done your homework, and I'm long out of the finance business, but even so, I think the applications where this matters are, as financial applications go, very unusual.
> [for every account] we keep two separate strictly positive integer amounts: one for debits and another for credits
It is not sufficient that their integer type is able to handle individual transactions. Their integer type must be able to handle the sum of the absolute value of all transactions that have occurred on an account. And I think it's easy to come up with realistic situations where you hit that.
So say you take the NYSE, which trades about ~$18 billion per day [0]. This is ~$1.8 trillion cents, or about 2^51 microcents. After 2^12 business days (~=16 years) you'll already be hitting the limit. (This is just a toy example ofc.)
[0] https://www.nyse.com/trading-data#:~:text=The%20New%20York%2....
> When you need to take the net balance, the two balances can be subtracted accordingly and the net displayed as a single positive or negative number.
This means you have to reconcile credits and debits using a different type than each of the columns is in. Not a huge deal by itself, but now that secondary type has to be a weird 129 bit integer (or larger) to ensure it can represent every intermediate sum of credits and debits.
If they just would have sacrificed 1 bit they could have used a single signed 128 bit data type for all storage and computation.
I suspect they have to enforce the “all debits and credits are positive” invariant in a number of places anyway (or risk coercing a negative signed 128 input into an unsigned 128 but ledger entry), so why not sacrifice a bit that will probably never be used otherwise anyway and have a uniform structure for numeric types?
The motivation is to preserve information, and I think this becomes less confusing once you understand that accounting is in fact a type system.
I hope this comment makes this more clear: https://news.ycombinator.com/item?id=37571942
Now I'm trying to remember if I watched Office Space before or after...
I personally don't see the advantage this has over a decimal 128 numeric value. In either situation, if you have 10 / 3, you will get 3.33 and need to round. Ultimately, the math concepts in finance are different then in abstract math. If for some reason you need to divide $10 by 3, it should result in three numbers: 3.33, 3.33, and 3.34.
But fundamentally, I don't see how their fixed integer math fixes something as fundamental as that, over a standard decimal 128 representation.
Exactly! We don't try to "fix this math problem"; instead, we just want to reduce the surface area by using plain integers instead of decimal floating points.
Then again, 128 bits is plenty for microcents or whatever the minimim unit is.
I’m guessing back in the day when things were 32 bit or less, there were entire financial database implementations that handled this abstraction?
My experiences in various bits of banking include where juniors ignore advice NOT to store currency in floating point values and then come whining that arithmetic is broken, and tech dudes in a lab deciding that every single FX flow in an investment bank should have 2MB of (unshared) calendar hidden inside its abstraction which made some individual trades too big to load even for powerful machines...
Fixed point calcs in integers are good.
I feel like any dev team should pick up a copy of this for on-boarding: https://jvns.ca/blog/2023/06/23/new-zine--how-integers-and-f...
Floating point is inherently an approximation - your bank balance should not be an approximation.
I think this is a perfect example of bad floating point teaching. Floating point is not an approximation in any sense. If the numerical result of your calculation is representable in floating point you will get an exact answer always. And for results that aren't representable you decide exactly what should be done about that. It's like saying integers are an approximation because 5/2 == 2.
A `float` on the other hand, approximates the real numbers. It can perfectly represent exactly 0% of them.
Having control over the rounding behavior is meaningless - floats cannot correctly represent any non-contrived calculation. Exact representation is important in financial systems.
That said, it is bizarre to claim that if the result can be represented you get the exact result, when the core problem is that the result cannot be represented because the representation is an approximation.
I know, I am being a little facetious. A double has a 52 bit mantissa, so it can exactly represent integers that need 52 or less bits.
Still, as a percent, a float can represent 0% of the reals. There are an infinite amount of numbers it cannot represent, even if we give it lower and upper bounds. Whereas an int can represent 100% of the integers within a lower and upper bound.
Like adjusting all your financial calculations to use microcents and partitoning instead of division to keep the result representable by integers? Neither can represent 1/3 even shifted. When you want to do exact calculations with floats, and you can, you just have to set yourself up so that the result is exactly representable, it's not as intractable as you make it seem.
> A `float` on the other hand, approximates the real numbers
Okay so that's not at all what they do, they represent subsets of the reals, just like how integers represent a subset of the reals. Even arbitrary precision libraries can only represent a subset of the rationals.
Can you expand on what you mean by that? Whenever I have dealt with calculation errors (either in fixed or floating point) "set yourself up so that the result is exactly representable" has been the key problem to prevent errors accumulating.
I obviously disagree on some other points, but circular discussions go nowhere!
Edit to add: Essentially I'm fishing for tactics. A big one in graphics development is detailed here: https://developer.nvidia.com/content/depth-precision-visuali... - but in fixed point I got very used to working out how to premultiply variables depending on their expected ranges.
Sure. My point is that this subset is useless. Because trying to add, subtract, multiply, or divide members of this set will result in a number outside the set.
> When you want to do exact calculations with floats, and you can, you just have to set yourself up so that the result is exactly representable, it's not as intractable as you make it seem.
In the general case, you absolutely cannot. Lets look at some examples.
In forex trading, you need 9 digits after the decimal place in the price. So right off the bat, a valid price like 1000000.000000001 cannot be represented by a float. If the exchange sends your system that price, your system is guaranteed to be wrong.
Lets say you start at a representable price, like 1000000.0 and want to tick it up or down by the tick size, say 0.025 . The result of that addition / subtraction is not representable, so you cannot calculate and round prices correctly.
If you don't have control of your inputs, and you need precision, floats will never work.
Nearly all mathematical calculations cause the result to be outside the range of integers. You can't do much else other than subtract without accounting for edge cases. No matter what tool you use you must work with your chosen representation and around its limitations and make sure your domain can be modeled exactly. For example Python's base random function chooses a floating point uniformly in the range [0, 1) but it achieves this by requiring that the result be a multiple of 2^-52 which is exactly representable so rounding doesn't introduce bias.
> If you don't have control of your inputs
Well you clearly do to some degree because you're sure you can model anything you might might receive with fixed sized fixed precision integers. I'm not saying this means you can just switch to floats but that you're doing the same thing, mapping the real life problem domain exactly to a subset of the reals that is closed under the operations you want to perform.
This issue is what COBOL was designed to solve. Fixed decimal point arithmetic in base 10
This still doesn't do that, its just integer math. It doesn't matter how your CPU implements it, as long as `x = a + b` gives the correct value for x.
You don't have to immediately reach for an abstraction layer when you hear the word "bits" ;)
It's funny I've always thought of two-column bookkeeping as a kludge that was invented because the author was unaware of negative numbers. But here there's actually a justifiable technical reason why they're superior (in this specific context)! History is a silly thing
(I know this only because I somehow happened to major in Accounting back in university), but when you represent a general ledger of accounts, there are always two positive columns for amounts in any given account, one for debits, the other for credits.
The golden rule is that you always add to either column. You always preserve information.
To see why two columns (or two integer balances) preserves more information, take this example:
1. An account A with a debit balance of $1m and a credit balance of $1m, and
2. An account B with a debit balance of $0 and a credit balance of $0.
Account A contains volume information in that you can immediately see not only that account A was transacted against, but with significant amounts. Conversely, account B shows no volume. But it's also clear even that there were no transactions between A or B.Whereas, if you take the net of the two amounts, and reduce that to a single amount in storage (as opposed to only taking the net in presentation), and if you use negative numbers, you lose this information. It's a subtle thing.
The other angle here, is to consider why some engineers shortcut to negative numbers in the first place. I find that it's usually because they haven't fully grokked that accounting is itself a type system. You get assets, liabilities, equity, income and expenses (as account types), and the debit/credit balances, when considering the type of an account, tell you further information. For example, did a bank account suddenly transition into overdraft? Different account types, increase on different sides.
I think this is also the reason that you find accountants typically wrapping amounts in parentheses, and then specifying the DR or CR side (or type of account), rather than using a negative sign. It's a tradition of preserving information.
Using two positive numbers preserves some information, but still destroys plenty. Just less than a single signed number would. Consider two accounts:
1. An account A with a debit balance of $1m and a credit balance of $0, and
2. An account B with a debit balance of $1m and a credit balance of $0.
One of those was opened by a lottery winner last week who did a single transaction dropping in their winnings. The other was opened by a retiree 60 years ago who has been dripping small deposits in for their entire working history. Which is which?That distinction is still lost by summing debits and summit credits.
To really preserve information, you'd need the full list of all transactions. But, obviously, that comes at a significant performance cost. So the way to look at storing sums for debits and credits is that it's a trade-off which gives you a little more information than just a single balance but is still a relatively small fixed-size amount of data.
This is the principle.
Not to suggest that the sum of debits and credits alone is sufficient to that end, but rather to explain why it's at least necessary.
> To really preserve information, you'd need the full list of all transactions.
Exactly. And so this is, of course, what TigerBeetle also does (with full durability).
And, at the same time, TigerBeetle doesn't decompose account balances to a single integer, for the reasons given.
Both of these are important.
To be clear, the context for this thread is the latter: Why not decompose account balances to a single positive/negative integer?
* https://martin.kleppmann.com/2011/03/07/accounting-for-compu...
Used to use this CLI tool "ledger" to keep the books for a small nonprofit I was treasurer-ing for. Awesome tool, and (in conjunction with this read and a few others) it taught me a ton about all that good GAAP stuff
When you sync back up, do a big tally at the end, and that's your final number.
(This constitutes a CRDT, and is known as the Positive/Negative Counter).
Is there an example where it makes a noticeable difference (at least one cent in the final result) that does not involve unrealistic amounts or numbers of items?
I'm not arguing for Doubles, just collecting arguments to convince.
And for everyday individual transactions it's hard to see a problem. Maybe the problem is more when you're summing up every single financial transaction for the year? But even in that case, if the smallest financial resolution is a cent, all the floating point noise seems like it would be occurring many decimal points beyond. Even if you're dealing with billions of dollars.
To be clear, I wouldn't do it myself -- I'm too risk-averse, too afraid of unknown unknowns. But it is hard to see what actual real-life negative consequences there would be for 99.9% of businesses, unless I'm missing something? Like the parent commenter, I'm looking for where I'm wrong here.
It can also be noticeable if you're just trying to calculate something like "is the invoice paid off". Maybe your view layer is showing $0.00 balance to the end user, but the backend hasn't correctly rounded off those extra bits from a floating point calculation, so your backend logic is now saying the invoice is not actually fully paid off, even if the end user has no idea what they could possibly still owe.
A clean solution would be to use rational numbers of the form y/x stored as a pair of 64-bit numbers. It takes the same 128-bits space as in TigerBeetle proposal but has advantage of exact arithmetic with no rounding errors.
EDIT: typos
64-bit integers are barely enough anyways. If the world-wide patrimony is somewhere near $100tn or $200tn, that's around a bit more than 10^16 (so let's round the exponent up to 17) cents, and that's just two digits short of the maximum that can be expressed with 64-bit signed integers!
If you want to sum things like inter-bank debt, or if we have a lot of growth or hyperinflation we might need to express between 1¢ and $1e18 or $1e19, but the max range with signed 64-bit integers is [-$9.2 x 10^18, $9.2 x 10^18] if we still need to represent cents. So 64 bits is starting to be a tad too small for financial purposes.
Even unsigned 64-bit integers are not that comfortable, but signed integers are needed if you're dealing with assets and liabilities.
128-bit is definitely too big, but it's a lot easier to deal with than variable-length integers. One might still use variable length encodings to save space on the wire or on disk, but in-memory 128-bit integers is the way to go.
All the best,
-HG
[1] https://web.archive.org/web/20211014094900/https://ren.zone/...
This is definitely useful because you can have the type system tell you if you've implemented e.g. exchange rate conversion incorrectly. But it's also a hassle because you need to reify currency values discovered at runtime as types, which isn't pretty [1].
[1] https://github.com/runeksvendsen/order-graph/blob/eef0006cba...
Thus, TigerBeetle doesn't preclude the use of money types at a higher layer (you would have seen that we tried to emphasize this also in the post).
Another way to think of this, is that we focus on the storage of accounting information by providing high-performance accounting primitives (e.g. execute 8k double entry transactions all within 5ms in a single network roundtrip DB query).
But the accounting policy (rounding etc.) remains the responsibility of the application/organization, since this may differ according to requirements/jurisdiction.
who / when / where / why / what / how / how much
https://docs.tigerbeetle.com/reference/transfersIs this supposed to be some kind of joke?
Unsigned underflow wraps around to the highest number. Meanwhile signed gives you a negative number, which tends to be properly handled by code that assumes positive numbers.
- Anonymous
LOL. Is that real? OMG, that's incredible. Image if this trend kept going: what sign of humanity that new SI units need to be minted first because inflation drives currencies to insane new denominations, rather than cat videos, IoT and porn bulge yottabytes out of bounds?
https://en.wikipedia.org/wiki/Hyperinflation_in_Zimbabwe
They eventually stopped printing currency.
Either the programmers have to do the right thing or tell their boss that they can never support Bitcoin.
Both options give me a warm, fuzzy feeling inside when I watch them in real time.
Of all the problems to solve, the size of the registers is the most trivial.
(TLDR: It's the 10^10 precision in trading systems that gets you.)
How do you know you convert your internal string to a float/int correctly? You have to deal with the numeric types eventually
Obviously the raw integer (64 bit or 128 bit) will go faster since it would have CPU-level assistance, but I'm curious to what that actually ends up being in practice, especially at the scale of trading. Is it 10% faster? 10X faster? Somewhere in between?
Seemed rather clear, what's difference with what you say ?
The rounding-for-settlement issue that you describe is, I think, separate.
As far as I know fixed point numbers have a fixed fractional part in bits, but this is different from using integers with a multiplicative factor, like 100, to represent correctly a fractional part as 1/100.
I'd imagine if I have to protect against bit-level fuckery something like this would be a better representation:
struct Dough {
u64 amount;
u64 amount_again;
u64 amount_just_to_be_really_sure;
}
that and hardware-level protection of course... "bit level issues"? you eventually are going to need to use those bits to add, multiply, subtract or divide the numbers.
Using character strings to represent numbers is certainly one way to do arbitrary precision/ BigInteger (like the article describes). But you might want to postpone transcoding it to characters until you decide to export your numbers across a boundary. Otherwise all of the arithmetic operations you do have to suffer this round trip each time.
In a similar vein, the only "tricky" solution I saw once was a financial system that stored rational numbers as fractions, and used fractions for all computations too. A decimal point was only used for the final results (end-user UI, APIs, etc.). I still think it was a an overkill.
https://en.wikipedia.org/wiki/Hungarian_pengő (caption: "100 million trillion (100 quintillion) pengő (1946)")
At its nadir (US$ 1.0 = 4.6*10^29 P), a reasonable-sized transaction denominated in, for example, micro-pengős, would have overflowed a uint128_t.
Conclusion: just use floating point, it's inflation-proof.
If hyperinflation brings that date closer, the dollar will probably also stop being a global reserve. Or possibly: will be caused by it ceasing to be a global reserve.
Wait, what? 800 years is absolute peanuts for the fate of orbital bodies. Wikipedia's Timeline of the Far Future estimates the demise of the moon in 7.59 billion years; still in orbit around the Earth (albeit a much higher one), it gets destabilized by the expanding sun and eventually swallowed by it, along with the Earth. If they aren't swallowed by the sun, they estimate 65 billion years before we lose the moon in one way or another.
Even the unit of replication is a factory under human control with no novel AI, and which can only replicate with significant human oversight of already existing mining, processing, and manufacturing equipment, it doesn't take insane reproduction rates to disassemble the moon in 800 years.
However, the main point of the example is "800 years is too far ahead to plan for how much the US dollar might inflate" by way of demonstrating how extreme things can change. As far as I know, no fiat currency has existed that long, and only three country-like entities[0].
Mount Rushmore isn't likely to be targeted by nukes, but I strongly suspect that it is defended against vandals (politically motivated or otherwise) with dynamite (or similar categories of explosives) — that said, if you've always wanted to go and have not yet done so, you should, as I could've said much the same thing about the World Trade Centre 22 years and a fortnight ago.
(I wonder if a single unfriendly nuke on US soil would cause an economic shock? Normally the assumption would be what else might come with it).
[0] https://www.brainscape.com/academy/longest-lasting-empires-w...