Get the fundamentals down and the level of everything you do will rise. - Michael Jordan
As stated in my original post, I do 1 hour of video lessons from Watch and Code every day. If you're interested in learning Javascript in a way that goes beyond basic tutorials and gives you a foundational, practical knowledge without relying on frameworks - I'd highly recommend it. If you're reading these posts, please keep in mind that these are just my notes, and I'm not an expert (yet!). If your goal is also to master the fundamentals of Javascript, please head over to Watch and Code and start your journey there!
All screenshots were annotated using Shotty.
toFixed and Rounding Issues
Now we're going to look at the actual API methods of AccountingJS.
Let's start with the toFixed method. This fixes rounding issues with the native toFixed method that will sometimes round down where you'd expect it to round up.
/** * Implementation of toFixed() that treats floats more like decimals * * Fixes binary rounding issues (eg. (0.615).toFixed(2) === "0.61") that present * problems for accounting- and finance-related software. */ var toFixed = lib.toFixed = function(value, precision) { precision = checkPrecision(precision, lib.settings.number.precision); var power = Math.pow(10, precision); // Multiply up by precision, round accurately, then divide and use native toFixed(): return (Math.round(lib.unformat(value) * power) / power).toFixed(precision); };
This has to do with how computers deal with numbers:
Demo visualization: http://bartaz.github.io/ieee754-visualization/
Numbers are the sum of powers of 2.
The number 1 is just 2 to the 0th power:
The number 2 is 2 to the 1st power:
3 is 2 to the 1st power plus 2 to the 0th power:
Every positive integer up to a certain point can be represented this way. We say "up to a certain point" because JS and other languages limit the amount of data you can use to represent numbers. So it trips up with really big numbers, but most reasonable numbers are ok.
Let's look at fractions:
All these numbers can be easily represented using powers of 2 (using negative exponents).
But not all decimals work this nicely. Let's look at the example that AccountingJS gives: 0.615
0.615 can't be perfectly represented by powers of 2.
Because of this, Javascript will try to approximate that and get as close as it can. But it never quite gets there. Depending on the number, sometimes it will be higher and sometimes it will be lower.
toFixed is going to round based on the approximate value that Javascript gets. So it's really rounding 0.51499999999 or something like that.
So now it's clear that the browser's toFixed won't work to round decimals in accounting software.
Let's look at a solution.
We can multiply by 100 to get the number out of the deep decimals, use Math.round, then divide by 100 to get back to our desired number:
Let's look at AccountingJS. First we'll notice something weird: both var toFixed and lib.toFixed are give then value of this function...
toFixed is used elsewhere in the application, so this makes it shorter to type, so you don't have to type lib.toFixed every time. An additional benefit is that if you changed the name of lib, then every time you used lib.toFixed you'd have to change it, but with this approach that wouldn't be necessary.
AccountingJS's toFixed takes two arguments: value and precision.
Precision is the number of decimal places. The default for currencies will be 2, and for numbers will be 0.
Here it's going to set precision to the provided precision argument, and if none was provided it will use the default precision value:
The next line is where we choose what to multiply by:
Remember this was our approach (100 is the same as 10^2):
.615 * (10^2) ==> 61.5 ==> 62 ==> .62
The thing to note is that the exponent is the number of decimal places we want. We don't necessarily always want 2. You can vary the output by changing the exponent.
Math.pow is how we do exponents in Javascript.
So 10^2 is represented as Math.pow(10, 2).
The rest of the logic is stuff we've seen already.
You take the value:
Then, in this case we have this extra layer of processing that removes any formatting (for example, if you had a currency symbol with the value, lib.unformat would strip that off because we need just the number):
Once we have that value, we multiply it by power (100 for example):
Then we're going to round this result:
Once we have that rounded result, we divide by power to get the original number of decimal points that we want:
Then finally, this gives us a number:
But we want to transform this into a string. An easy way to do that (which is kind of funny) is that we're using the native toFixed to do that, although we're using it in a different way.
We're using it strictly to convert our number to a string. Here's an example of toFixed being used to simply return a string:
This seems to fix our original problem of numbers not rounding the way they should in accounting software, but let's not celebrate too early. More to come...
Summary
- Javascript doesn't round the way we want in accounting software.
- This is to do with some decimal numbers being not completely precise because computers have to think in terms of powers of 2.
- AccountingJS creates its own toFixed method that essentially does this:
- Multiply to get the number to only one decimal place
- Use Math.round to round using more predictable behavior
- Then divide the number back down to get it to the number of decimal places you wanted.