3

I am trying to understand some code where a number is converted to a currency format. Thus, if you have 16.9 it converts to $16.90. The problem with the code is if you have an amount over $1,000, it just returns $1, an amount over $2,000 returns $2, etc. Amounts in the hundreds show up fine.

Here is the function:

var _formatCurrency = function(amount) {
    return "$" + parseFloat(amount).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
};

(The reason the semicolon is after the bracket is because this function is in itself a statement in another function. That function is not relevant to this discussion.)

I found out that the person who originally put the code in there found it somewhere but didn't fully understand it and didn't test this particular scenario. I myself have not dealt much with regular expressions. I am not only trying to fix it, but to understand how it is working as it is now.

Here's what I've found out. The code between the backslash after the open parenthesis and the backslash before the g is the pattern. The g means global search. The \d means digit, and the (?=\d{3})+\. appears to mean find 3 digits plus a decimal point. I'm not sure I have that right, though, because if that was correct shouldn't it ignore numbers like 5.4? That works fine. Also, I'm not sure what the '$1,' is for. It looks to me like it is supposed to be placed where the digits are, but wouldn't that change all the numbers to $1? Also, why is there a comma after the 1?

Racil Hilan
  • 24,690
  • 13
  • 50
  • 55
apex2022
  • 777
  • 2
  • 8
  • 28
  • 1
    Side note; there are a few good regular expression validators on the web that you can search for to test your expressions. One of them I just found is https://regexr.com/ which will perform an explain on the pattern so you can learn. There are also some handy cheatsheets out there. Such as https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/ – Taplar Mar 13 '18 at 16:24
  • Are you just trying to do it specifically for USD? – th3n3wguy Mar 13 '18 at 16:37
  • maybe it's a typo but you got 1 parenthesis too many in your pattern: */(\d)(?=\d{3})+\.* **)** */g* – Scaramouche Mar 13 '18 at 16:50
  • I actually forgot an open parenthesis after ?=. – apex2022 Mar 13 '18 at 17:02
  • is your `amount` argument ALWAYS going to be a string of digits with/with out a dot somewhere (no commas or anything) when you feed it to the function? – Scaramouche Mar 13 '18 at 17:30

4 Answers4

3

Regarding your comment

I was hoping to just edit the regex so it would work properly.

The regex you are currently using is obviously not working for you so I think you should consider alternatives even if they are not too similar, and

Trying to keep the code change as small as possible

Understandable but sometimes it is better to use a code that is a little bit bigger and MORE READABLE than to go with compact and hieroglyphical.

Back to business:

I'm assuming you are getting a string as an argument and this string is composed only of digits and may or may not have a dot before the last 1 or 2 digts. Something like

//input     //intended output
1           $1.00
20          $20.00
34.2        $34.20
23.1        $23.10
62516.16    $62,516.16
15.26       $15.26
4654656     $4,654,656.00
0.3         $0.30

I will let you do a pre-check of (assumed) non-valids like 1. | 2.2. | .6 | 4.8.1 | 4.856 | etc.

Proposed solution:

var _formatCurrency = function(amount) {
    amount = "$" + amount.replace(/(\d)(?=(\d{3})+(\.(\d){0,2})*$)/g, '$1,');

    if(amount.indexOf('.') === -1)
        return amount + '.00';

    var decimals = amount.split('.')[1];

    return decimals.length < 2 ? amount + '0' : amount;
};

Regex break down:

  • (\d): Matches one digit. Parentheses group things for referencing when needed.

  • (?=(\d{3})+(\.(\d){0,2})*$). Now this guy. From end to beginning:

    • $: Matches the end of the string. This is what allows you to match from the end instead of the beginning which is very handy for adding the commas.
    • (\.(\d){0,2})*: This part processes the dot and decimals. The \. matches the dot. (\d){0,2} matches 0, 1 or 2 digits (the decimals). The * implies that this whole group can be empty.
    • ?=(\d{3})+: \d{3} matches 3 digits exactly. + means at least one occurrence. Finally ?= matches a group after the main expression without including it in the result. In this case it takes three digits at a time (from the end remember?) and leaves them out of the result for when replacing.
    • g: Match and replace globally, the whole string.
  • Replacing with $1,: This is how captured groups are referenced for replacing, in this case the wanted group is number 1. Since the pattern will match every digit in the position 3n+1 (starting from the end or the dot) and catch it in the group number 1 ((\d)), then replacing that catch with $1, will effectively add a comma after each capture.

Try it and please feedback.

Also if you haven't already you should (and SO has not provided me with a format to stress this enough) really really look into this site as suggested by Taplar

Scaramouche
  • 3,188
  • 2
  • 20
  • 46
1

The pattern is invalid, and your understanding of the function is incorrect. This function formats a number in a standard US currency, and here is how it works:

  1. The parseFloat() function converts a string value to a decimal number.
  2. The toFixed(2) function rounds the decimal number to 2 digits after the decimal point.
  3. The replace() function is used here to add the thousands spearators (i.e. a comma after every 3 digits). The pattern is incorrect, so here is a suggested fix /(\d)(?=(\d{3})+\.)/g and this is how it works:
    • The (\d) captures a digit.
    • The (?=(\d{3})+\.) is called a look-ahead and it ensures that the captured digit above has one set of 3 digits (\d{3}) or more + followed by the decimal point \. after it followed by a decimal point.
    • The g flag/modifier is to apply the pattern globally, that is on the entire amount.
    • The replacement $1, replaces the pattern with the first captured group $1, which is in our case the digit (\d) (so technically replacing the digit with itself to make sure we don't lose the digit in the replacement) followed by a comma ,. So like I said, this is just to add the thousands separator.

Here are some tests with the suggested fix. Note that it works fine with numbers and strings:

var _formatCurrency = function(amount) {
  return "$" + parseFloat(amount).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
};

console.log(_formatCurrency('1'));
console.log(_formatCurrency('100'));
console.log(_formatCurrency('1000'));
console.log(_formatCurrency('1000000.559'));
console.log(_formatCurrency('10000000000.559'));
console.log(_formatCurrency(1));
console.log(_formatCurrency(100));
console.log(_formatCurrency(1000));
console.log(_formatCurrency(1000000.559));
console.log(_formatCurrency(10000000000.559));
Racil Hilan
  • 24,690
  • 13
  • 50
  • 55
  • Well, as I commented above, when I typed my question I accidentally left out the parenthesis. What you put above is exactly the string I have in my code. So I wonder why it's working for you but not for me. Hmm... – apex2022 Mar 13 '18 at 17:41
  • No, you commented on the semicolon after the curly bracket, which is pretty obvious. By the way, the function is missing a semicolon at the end of the `return` line, which I added in my answer. That doesn't change anything, but good for consistency. As to why it doesn't work for you, you'll need to provide us with more info. Post the exact values that you are using. Remember that the function converts the strings to numbers first, so if you pass it something like `123abc` or `123,123`, it will only take the first part up to the non-digit and format it like `$123`. – Racil Hilan Mar 13 '18 at 17:54
  • My answer gives you what you asked for, which is an explanation of how the function works, what the `$1` means and why it is followed by a comma. – Racil Hilan Mar 13 '18 at 17:58
  • Super Working Fine :) – Thulasiram Jan 05 '21 at 14:37
0

Okay, I want to apologize to everyone who answered. I did some further tracing and found out the JSON call which was bringing in the amount did in fact have a comma in it, so it is just parsing that first digit. I was looking in the wrong place in the code when I thought there was no comma in there already. I do appreciate everyone's input and hope you won't think too bad of me for not catching that before this whole exercise. If nothing else, at least I now know how that regex operates so I can make use of it in the future. Now I just have to go about removing that comma.

Have a great day!

apex2022
  • 777
  • 2
  • 8
  • 28
-1

Assuming that you are working with USD only, then this should work for you as an alternative to Regular Expressions. I have also included a few tests to verify that it is working properly.

var test1 = '16.9';
var test2 = '2000.5';
var test3 = '300000.23';
var test4 = '3000000.23';

function stringToUSD(inputString) {
  const splitValues = inputString.split('.');
    
  const wholeNumber = splitValues[0].split('')
    .map(val => parseInt(val))
    .reverse()
    .map((val, idx, arr) => idx !== 0 && (idx + 1) % 3 === 0 && arr[idx + 1] !== undefined ? `,${val}` : val)
    .reverse()
    .join('');
    
  return parseFloat(`${wholeNumber}.${splitValues[1]}`).toFixed(2);
}

console.log(stringToUSD(test1));
console.log(stringToUSD(test2));
console.log(stringToUSD(test3));
console.log(stringToUSD(test4));
th3n3wguy
  • 3,649
  • 2
  • 23
  • 30
  • Thanks, but I was hoping to just edit the regex so it would work properly. Trying to keep the code change as small as possible. Also, when I click on Run Code Snippet, it shows only 1 decimal place for the first two numbers. – apex2022 Mar 13 '18 at 17:06
  • @DavidTarvin => I updated my answer to fix the issue where it wasn't showing 2 decimal places. I am not big on Regular Expressions, so I couldn't help there. I just wanted to show an alternative for anyone who lands on this page in the future. An upvote (don't have to accept as the answer) would be helpful. :) – th3n3wguy Mar 13 '18 at 17:09