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.
If we do more research into rounding, we'll come across the Math.round documentation.
Toward the bottom, there's a section about a "PHP-Like rounding Method".
This looks very similar to the code we have in AccountingJS's toFixed method. It's pretty much the same thing. It takes a number and a precision, then you try to get a multiplier (which they call a factor).
Then you take the original number and multiply it by the factor, round it, then divide it by the factor to get back to the original number of decimal points.
The only difference is that they return a number rather than a string (as they do in AccountingJS).
But we should still be skeptical that this works every time.
Remember this example:
.615 * 100 // 6.15
We know that .615 isn't exactly .615 in Javascript, but we're hoping it's close enough that when we multiply it by 100, we'll get 61.5.
But what if that isn't always true? What if when we multiply by 100 we don't get the number we'd expect? Like this example:
1.005 * 100 // 100.49999999999999
If you were to pass this into Math.round, it would round down instead of up, since you're passing in a 4 instead of a 5.
Math.round(100.49999999999999) // 100
Yikes. You'd expect 1.005 to round up to 1.01, but 1.005 * 100 is actually 100.49999999999999, and then if you Math.round that you get 100, and if you divide that back down by 100, you get 1 when you really wanted 1.01.
This is discouraging, but let's look at what's happening at the foundation of all this.
If you multiply 1.005 by 100, you expect to get 100.5.
1.005 * 100 ==> 100.5
Then you round that with Math.round and expect to get 101:
100.5 ==> 101
Then you divide by 100 and expect to get 1.01:
101 ==> 1.01
But we keep running into problems with the first step. When we multiply 1.005 * 100, we realize it's not really 100.5.
Our technique is still sound, but the way Javascript represents the numbers is causing us problems and not allowing us to go from the first step to the second step with consistent accuracy.
One thing to think about is that when we go from 1.005 to 100.5, it helps to think of the numbers as strings instead of numbers:
'1.005' ==> '100.5'
When we look at it this way, it becomes clear that the difference between the left side and the right side is just that we've moved the decimal over 2 places.
If we can do this reliably, then we can guarantee that this first step works.
Once we have a string, we can turn that back into a number by using the Number constructor:
Number('100.5'); // 100.5
Now that we have 100.5 we can use Math.round to get 101, then we can divide by 100 and get 1.01, which is the number we expected to get when rounding 1.005.
So this strategy works, but you have to do a bunch of string manipulation. Luckily there's a better way: scientific notation.
In Javascript we can represent numbers by scientific notation.
// as a decimal 100.5 // as scientific notation 1.005e2 // 1.005 * 10^2
1.005e2 is a better way of writing 1.005 * 100 because 1.005e2 actually gives you 100.5 in Javascript, which is what we want (not 100.499999999999).
It's also convenient to look at because while the "e2" is saying, "multiply this number by 10^2, we can also just think of it as "take 1.005 and move the decimal by 2 places to the right". If it were "e-2" we'd move it 2 places to the left.
So 1.005e2 gives us 100.5. Then we can Math.round that to get 101:
Math.round(1.005e2) // 101
From there we can divide by 100 and get 1.01 (the value we expected when rounding 1.005):
Math.round(1.005e2)/100 // 1.01
This is nice but dividing by 100 is inconsistent. So we can stay consistent by also using scientific notation when moving the decimal back to it's original position:
Math.round(1.005e2) // 101 101 + 'e-2' // '101e-2' Number('101e-2') // 1.01
Let's use this to write a better toFixed method in AccountingJS.
function betterToFixed(value, precision) { // Take our original number and create a string // Example: value = 1.005, precision = 2, creates a string '1.005e2' // Then wrap it in a Number constructor to get a number again. var exponentialForm = Number(value + 'e' + precision); // Round this exponentialForm var rounded = Math.round(exponentialForm); // Reverse the decimal move from the first step to get our final result // Wrap resulting string in Number constructor to get a number var finalResult = Number(rounded + 'e-' + precision); return finalResult; }
And this works as we'd hope:
betterToFixed(1.005, 2); // 1.01 betterToFixed(.615, 2); // 0.62 betterToFixed(10.235, 2); // 10.24 // Bad behavior from the built in toFixed method: 10.235.toFixed(2); // "10.23"
One last tweak we can me to our betterToFixed method is to return a string rather than a number (since that what AccountingJS expects), so we can make that change by actually using the native toFixed method to simply turn our value into a string:
function betterToFixed(value, precision) { var exponentialForm = Number(value + 'e' + precision); var rounded = Math.round(exponentialForm); var finalResult = Number(rounded + 'e-' + precision); // turn our value into a string with .toFixed return finalResult.toFixed(precision); }
Summary
- We can round numbers more consistently if we use scientific notation.
- First we move the decimal with something like '1.005e2'
- Then we round this exponential form with Math.round.
- Finally, we move the decimal back to it's original position with 'e-2".