The short answer is: because binary!
The longer answer is that computers store data as binary and while it’s ok for integers, it gets a little complicated for fractions.
You probably know this, but in 8 bits you would have:
64 | 32 | 16 | 8 | 4 | 2 | 1 | Number base 10 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 7 |
0 | 0 | 1 | 0 | 1 | 0 | 1 | 21 |
But with fractions...
2 | 1 | . | 1/2 | 1/4 | 1/8 | 1/16 | ... |
---|
The floating point you don’t actually “store”, you tell the computer how many bits are for the integer part (in this case 2) and how many are for the fraction part (in this case 4).
But since you have smaller and smaller parts, some numbers aren’t direct representations and you need an infinite (or close to that) number of bits that, when added, will give the number you want.
So, the reason is that when you finish up with all the roundings and depending on how many bits you’ve used, you’ll get a different result than you might expect.
It will be really close, but not perfect.
Why not use base 10 then?
Because binary!
Even if you can store it in base 10 (say, with strings), you then have to do the operations as you would in base 10, but with base 2 as an intermediary... and that will chew on the performance.
Then again, there must have other ways to do it that I have no idea about.
Little exercise
How would you sum two numbers in base 10? (or how do you do it with pen and paper?)
- check the signs (a plus added with a minus is a subtraction, but two minuses are an addition)
- check for fractions
- “align” the numbers
- sum each number
- if one number overflows (that is, more than 10), then you carry for the next
- go back and be sure if you’re also adding the carried numbers
- print the result!
All that just for sum... that is the easiest one!
Enter @noriller/manual-calculator
I wanted a “simple” project, just to get my mind off things... and wouldn’t you know... it’s a lot more complicated than I’ve anticipated.
https://www.npmjs.com/package/@noriller/manual-calculator
Sum, subtraction, multiplication, and division. All that is there.
The really tricky one is division, even now, in an infinite division, the more digits you want it to have, the growth is exponential (50k digits took almost 18 minutes while 1000 digits take “only” around 400 ms).
Check a few runs I made here: https://github.com/Noriller/manual-calculation/tree/master/performance/runs
And/or try yourself with these tests: https://github.com/Noriller/manual-calculation/tree/master/performance/tests (follow the README on how to!)
Bugs and unexpected things aside, at least you can trust those digits should be the same you would get if you calculated it by hand.
So, this was my shameless promotion...
Check it out and probably forget it... until you find yourself really wanting that 0.1 + 0.2 === 0.3
or you want to use numbers that won't wrap and round to weird numbers like 987654321987654319876
becoming 987654321987654300000
or even 987654321987654254592n
(with the BigInt wrapper), and of course, if you need a division with thousands of digits (albeit with some time cost).
Cover Photo by Michal Matlon on Unsplash