Mastering Javascript Fundamentals: AccountingJS, formatMoney
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.
Today we're looking at the formatMoney method in AccountingJS.
This is how we use it:
It's pretty simple. We pass in a number, and it formats it as currency with the default settings provided, and spits it out as a string. Our default currency is a dollar sign, our default separator is a comma, our default decimal is a period, and our default precision is 2 decimal places.
Here's another example where customize some of these settings:
The next example shows that we don't need to customize everything, we can just customize the currency and precision. Here we've left out the separator and decimal symbol:
And finally, we can also customize the format of the string in terms of where the currency symbol appears in relation to the value, using the %s and %v symbols. It also shows that instead of passing in individual arguments for each of the settings, we can pass them in an object:
So these two statements are the same:
Let's take a look at the source code:
In the first line we're assigning the function to two different variables: formatMoney and lib.formatMoney. formatMoney is just a convenience so we can refer to it easily throughout the library. By assigning the function to lib.formatMoney, we're assigning it as a property on the library object that we provide for users. If you don't do this step, then users won't be able to use this function.
The next section talks about recursively formatting arrays. What this code allows you to do is format not only specific values, but also arrays of values:
We can also format arrays of arrays.
This can go any number of arrays deep. Sweet. This is a hidden functionality made possible by recursion.
The next section is labeled "Clean up number":
This strips out extra characters in case you provide a string for a value, and it extracts just a number value. It uses the unformat function to do this. Let's look at a few examples of this in action:
It just looks for a number value in there and then adds whatever your currency setting is (default: $).
The next section uses the defaults function (that we looked at in another post) to either use the default settings: lib.settings.currency
But you can also override or customize specific features. Say, for example, that you want to use a specific symbol. You can either pass in a symbol as one of the arguments in formatMoney like this:
Or you can pass in an object with those properties specified.
The comment explains that it builds an options object from the second parameter (if the second parameter is an object), or it builds it by combining all of the individual parameters into an object.
The way it works is a little convoluted. Here we have the function call to defaults:
And we pass in two arguments:
The second argument is all the way down on line 309.
On lines 302-308 we're trying to figure out what the first argument should be (using a ternary operator).
We do a check to see if symbol is an object. If it is, it passes symbol directly into the defaults function. Otherwise it will try to build up an options object using the parameters given to formatMoney, grabbing the values from each of those arguments.
Then it has this lib.setting.currency object as the default object in case you don't want to override anything.
In the next section, we take the options format property from the object we just built, and we run checkCurrencyFormat on it to get an object that has formats for positive, negative, and zero numbers:
This is important so we can customize the format of negative numbers in particular, since accounting software might display negatives in parentheses, for example.
Now that we have this formats object, we can choose which format to use for the value (number) being passed into formatMoney.
The logic here is confusing: if the number is greater than 0, then return formats.pos (positive). If "number is greater than 0" is false, then it will go to the next section:
In this second (false case) section, it checks to see if number is less than zero. If that's true, it will return formats.neg, if it's false that means number is not less than zero and it's not more than zero, so it must be zero, so it returns formats.zero.
It's a very convoluted example of chaining together ternary operators.
We could write longer but much more readable code by using if/else statements like this:
The next line suffers from similar problems as the previous lines. It's really long and complicated.
First it takes our useFormat and replaces '%s' with a symbol from the options object ('$', for example):
Similarly, it replaces '%v' with the formatted number:
Getting a formatted number is not as straightforward as we might think, because the formatted number has to take into account the precision, the thousand separator, and the decimal. There is a function called formatNumber defined elsewhere in AccountingJS that we use to do this, and we use it inside of .replace to get what we want.
We can rewrite this into more readable code by breaking things out into separate variables, and breaking the lines up so they're readable without scrolling:
Another note about the API design of AccountingJS: rarely is it a good idea to have a really long list of arguments to a function. You end up with code that's out of control. These long lines that go off the page and you need to scroll to follow the logic.
A second downside is that when you're using formatNumber, how do you know what order the arguments go in? There's no logic to it.
By doing this, it forces people to either memorize the code, or spend time in the docs. It's not a great experience. You also add complexity in your function. In this case it would have been better for them to accept only a single object with the settings, rather than allowing the user to either use a long list of parameters or an object.
Summary
- formatMoney takes a value and returns that value formatted nicely as money, as a string.
- The user can specify formatting settings like currency symbol, separator, etc.
- They can do this via a long list of parameters, or by passing in an options object.
- Because of this, the code is long, convoluted, and difficult to read.
- It would be better to only allow the user to specify formatting by passing in a format object.