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.
checkCurrencyFormat
A good way to structure comments before a function:
- Explanatory paragraph that provides context.
- Parameters: What does this function expect?
- Returns: What will it return?
In the case of AccountingJS, we can distill this wordy comment:
/** * Parses a format string or object and returns format obj for use in rendering * * `format` is either a string with the default (positive) format, or object * containing `pos` (required), `neg` and `zero` values (or a function returning * either a string or object) * * Either string or format.pos must contain "%v" (value) to be valid * */
Into this more concise and clear comment:
/** * Parses a format string or object and returns format obj for use in rendering * * Parameters: * string has "default positive format", must contain "%v" * object has 'pos' (required, must contain "%v"), 'neg', 'zero' properties * function returns a string or object like above * * Returns: * object * */
Now let's move into the body of the function:
The first line makes a variable defaults that is equal to lib.settings.currency.format:
Which, earlier in the program, is set to "%s%v":
These "%s" and "%v" symbols allow us to control the format of our currency: whether the symbol should come before the value, or vice versa. It also allows us to put a space in between if we want.
The next line is pretty simple:
It checks to see whether the format parameter is a function (using typeof), and if it is, it sets format to the return value of the format() function. The return value should be a string or an object.
The next line checks to see if format is a string. Then it uses the match method that strings have to check to see if the string contains "%v" inside of it.
.match simply checks to see if a given value is in a string. For example:
'jacob'.match('j') // ['g'] 'jacob'.match('z') // null
And since the corresponding boolean value for any array will be true, this will give us a true or false result to the right of the && in the if statement. If both conditions are true: format is a string AND it contains "%v", then we go on to the code inside the if case:
Remember from our comments that the function checkCurrencyFormat must return an object. So this is simply returning our object with the formats for the positive, negative, and zero cases.
In the negative case, it's doing some extra work to remove any negative sign that might have been in the string beforehand, and then adding it back in directly in front of the value. The idea here is to give a consistent format for negative numbers. However, this behavior is never really described, and could be surprising to a user who tried to format their negatives as "-- -$v", for example.
Ok so that handles the case where our format argument is a string. Well what if it's not a string and doesn't contain "%v"?
Question:
What if you wanted the format of your negative numbers to be in parentheses, without a negative sign? Wouldn't this code put an unwanted negative sign in there?
For example:
'(%s%v)'.replace("-", "").replace("%v", "-%v"); // returns: "(%s-%v)"
Continuing on: Then we go to the else if section:
The point of this section is that if we had passed in an invalid string for format then it will create a default object and return it.
It will also return a default object if you passed in an object originally, but it was missing something like format.pos.
This part is a little confusing, and the ternary operator makes things hard to read:
If defaults is not a string, then it will simply return defaults. And remember that defaults is the value from lib.settings.currency.format from the very top of the file.
If it's not a string, then it's already on object, so we can just return it. That's what's happening here:
If defaults is a string, then it will return lib.settings.currency.format, which is going to be set to this object:
The point of this is this scenario:
If you run checkCurrencyFormat one time, and say we pass in an invalid string, we then have to return the defaults object right here:
Then we run into the block of code pictured above, and lib.settings.currency.format will initially be a string. But then we're going to change it to an object. And that's not going to affect the first time we run it, but the second time we run it, if we have to return the defaults object again, this part is going to be true...
...so we can just return the object immediately:
Instead of having to create a new object:
And that's why the comment says that it "casts it to an object for faster checking next time".
But really the performance difference is going to be marginal. You probably won't be able to tell the difference. But they've chosen to do it this way, so it's important to be able to read it to understand the code. It makes the code quite a bit more confusing.
To recap, what's going to happen here is, if defaults is NOT a string, that means it's an object, which means we've already gone through this logic once and lib.settings.currency.format is already an object. If defaults is still a string, it's going to go to this part of the ternary operator:
It's going to set lib.settings.currency.format to this new object -- which, now that we've seen the logic here:
It's pretty much the same thing.
So the positive is going to be the defaults, which at this point is a string, zero is going to be the same, and the negative, we're going to take the value and just throw a negative sign on the front.
Finally, if the format argument to checkCurrencyFormat is not a string, and there's nothing wrong with the object, then you'll arrive at this case:
Those are all the different possibilities going step by step through the code.
Code like this can be very hard to understand. Each specific step isn't complicated, but having gone through this entire thing, you might be uncomfortable because you find it difficult to summarize what happens in all the different cases. It's hard to keep track of in your head.
One thing you can do to fix this problem is to simply write out the different scenarios in the comments:
// Scenarios: // A: // B: // C: // D: // E: // F:
First, we know the parameter, format, can be a string -- but more than just a string, it can be a valid string or an invalid string:
// Scenarios: // A: Valid string // B: Invalid string // C: // D: // E: // F:
The other thing we know is that it can be a valid object or an invalid object.
// Scenarios: // A: Valid string // B: Invalid string // C: Valid object // D: Invalid object // E: // F:
We also saw that format can also be a function:
// Scenarios: // A: Valid string // B: Invalid string // C: Valid object // D: Invalid object // E: Function // F:
You could also run checkCurrencyFormat and not pass in anything. So "nothing" is another scenario:
// Scenarios: // A: Valid string // B: Invalid string // C: Valid object // D: Invalid object // E: Function // F: Nothing
Now let's look at what happens in each of these cases.
In the first one, it will convert the string to a format object:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string // C: Valid object // D: Invalid object // E: Function // F: Nothing
In the second case where we have an invalid string, we'll try to create a default object -- we saw this logic here in the else if section:
To summarize that, we could say "use default and turn it to an obj if it's not already:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string ==> use default and turn it to an obj if it's not already // C: Valid object // D: Invalid object // E: Function // F: Nothing
So the very first time you run checkCurrencyFormat, format is probably going to be a string, but that first run-through will change the default all the way at the top:
It will change this format property from a string to an object. That's why we can say "use default and turn it to an obj if it's not already."
The third case, and this is a good case, is if you pass in an object for format and there's nothing wrong with it -- in that case you can just (at the very bottom) assume everything is fine and simply return the format object. You don't have to do anything.
You can just return format immediately.
So in this case we can leave the object alone:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string ==> use default and turn it to an obj if it's not already // C: Valid object ==> leave the object alone // D: Invalid object // E: Function // F: Nothing
If we have an invalid object, this is very similar to having an invalid string. We use default and turn it into an obj if it's not already:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string ==> use default and turn it to an obj if it's not already // C: Valid object ==> leave the object alone // D: Invalid object ==> use default and turn it to an obj if it's not already // E: Function // F: Nothing
If we have a function, it falls into any of the other cases. It depends on what the function returns:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string ==> use default and turn it to an obj if it's not already // C: Valid object ==> leave the object alone // D: Invalid object ==> use default and turn it to an obj if it's not already // E: Function ==> depends on what the function returns // F: Nothing
The final case -- if you don't pass in anything into checkCurrencyFormat, then it will try to use default and turn it to an object it's not already:
// Scenarios: // A: Valid string ==> convert string to a format object // B: Invalid string ==> use default and turn it to an obj if it's not already // C: Valid object ==> leave the object alone // D: Invalid object ==> use default and turn it to an obj if it's not already // E: Function ==> depends on what the function returns // F: Nothing ==> use default and turn it to an obj if it's not already
An exercise like this is interesting to run through the scenarios and note things like: there are six possible things that you can pass in to the function, but if you look at the actual outcomes, there are only four unique outcomes. Six possible inputs, but only four unique outputs.
Summary:
- checkCurrencyFormat takes an argument, format that can be a string, object, or function.
- It returns a format object
- So if the format parameter is a string, it changes it to an object and returns it.
- If format is a valid object, it simply returns it back, untouched.
- If format is an invalid object, it uses the default (lib.settings.currency.format) and changes it to an object if it isn't already.