11

I recently came up with this code while answering another StackOverflow question. Basically, on blur, this code will properly comma separate by thousands and leave the decimal at two digits (like how USD is written [7,745.56]).

I was wondering if there is more concise way of using regex to , separate and cut off excessive decimal places. I recently updated this post with my most recent attempt. Is there a better way of doing this with regex?

Input -> Target Output

7456 -> 7,456
45345 -> 45,345
25.23523534 -> 25.23
3333.239 -> 3,333.23
234.99 -> 234.99
2300.99 -> 2,300.99
23123123123.22 -> 23,123,123,123.22

Current Regex

var result;
var str = []
reg = new RegExp(/(\d*(\d{2}\.)|\d{1,3})/, "gi");
reversed = "9515321312.2323432".split("").reverse().join("")
while (result = reg.exec(reversed)) {
  str.push(result[2] ? result[2] : result[0])
}
console.log(str.join(",").split("").reverse().join("").replace(",.","."))
user2314737
  • 27,088
  • 20
  • 102
  • 114
Neil
  • 14,063
  • 3
  • 30
  • 51
  • I dont think regex is meant to add characters. So if I have `10000.50` you want `10,000.5000` right? – Rajesh Apr 11 '17 at 08:19
  • @Rajesh No, if I have 1000.562343, then I want 1000.56. But thanks for pointing out that I can't add decimals... didn't think of that. – Neil Apr 11 '17 at 08:20
  • Something like this maybe? http://stackoverflow.com/a/30106316/916000 – Taha Paksu Apr 11 '17 at 09:16
  • @TahaPaksu Interesting solutoin, but I wouldn't consider `console.log(n.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits:2})); ` that much easier. I added the maximum that way the string would cut off digits properly... Still there is some compatibility concerns with this method. I would prefer regex for these reasons. – Neil Apr 11 '17 at 09:20
  • 1
    What compatibility concern is there with `value.toLocaleString('en-US', {maximumFractionDigits:2})`? It works in Firefox, Chrome and IE. – ConnorsFan Apr 15 '17 at 14:56
  • Is the regex a hard requirement? It makes it overcomplicated and performance wise not perfect – Julian Apr 15 '17 at 15:23
  • 1
    The `toLocaleString` is there for this purpose only. Abusing regex for everything is not good. You are terming `toFixed` as messy!? Regex used here is even messier. And @ConnorsFan, why did you delete your answer? That was the only correct answer to [this question which seems to be suffering from an XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – Abhitalks Apr 17 '17 at 05:10
  • @nfnneil Is using `RegExp` a requirement? – guest271314 Apr 17 '17 at 22:30
  • Let's have a look : `var n = 233255.23;` `var regex = /^-?((\d{1,2})(\d{3})*|(\d{3})*)(\.(\d{2}))?$/g;` `var s = ""+n;` `var integer_part = s.replace(regex, "$1");//here it' will be "233255"` `var decimal_part = s.replace(regex, "$5");//here it' will be ".23"` The most difficult part being splitting the decimal_part into 3_digit per 3_digit and I don't think it can be achieved with a short regex :/. – Vivick Apr 17 '17 at 22:32

9 Answers9

11

As an alternative to the Regex, you could use the following approach

Number(num.toFixed(2)).toLocaleString('en-US')

or

num.toLocaleString('en-US', {maximumFractionDigits: 2})

You would still have the toFixed(2), but it's quite clean. toFixed(2) though won't floor the number like you want. Same with {maximumFractionDigits: 2} as the second parameter to toLocaleString as well.

var nums = [7456, 45345, 25.23523534, 3333.239, 234.99, 2300.99, 23123123123.22]

for (var num of nums) 
  console.log(num, '->',  Number(num.toFixed(2)).toLocaleString('en-US') )

Flooring the number like you showed is a bit tricky. Doing something like (num * 100 | 0) / 100 does not work. The calculation loses precision (e.g. .99 will become .98 in certain situations). (also |0 wouldn't work with larger numbers but even Math.floor() has the precision problem).

The solution would be to treat the numbers like strings.

function format(num) {
    var num = num.toLocaleString('en-US')
    var end = num.indexOf('.') < 0 ? num.length : num.indexOf('.') + 3
    return num.substring(0, end)
}

var nums = [7456, 45345, 25.23523534, 3333.239, 234.99, 2300.99, 23123123123.22]

for (var num of nums) console.log(num, '->', format(num))

function format(num) {
  var num = num.toLocaleString('en-US')
  var end = num.indexOf('.') < 0 ? num.length : num.indexOf('.') + 3
  return num.substring(0, end)
}

(when changing to another format than 'en-US' pay attention to the . in numbers as some languages use a , as fractal separator)

For Compatibility, according to CanIUse toLocaleString('en-US') is

supported in effectively all browsers (since IE6+, Firefox 2+, Chrome 1+ etc)

buræquete
  • 14,226
  • 4
  • 44
  • 89
arc
  • 4,553
  • 5
  • 34
  • 43
  • This is the way it needs to be done. The `toLocaleString` is there for this purpose only. Abusing regex for everything is not good. BTW, you can get rid of `toFixed` by passing the object parameter of `{minimumFractionDigits: 2}` to `toLocaleString`. – Abhitalks Apr 17 '17 at 05:09
  • @Abhitalks thanks, I didn't know of this feature. However, it seems `{maximumFractionDigits: 2}` is more fitting. Either solution will round the number though, not simply floor them. – arc Apr 17 '17 at 09:36
8

If you really insist on doing this purely in regex (and truncate instead of round the fractional digits), the only solution I can think of is to use a replacement function as the second argument to .replace():

('' + num).replace(
  /(\d)(?=(?:\d{3})+(?:\.|$))|(\.\d\d?)\d*$/g, 
  function(m, s1, s2){
    return s2 || (s1 + ',');
  }
);

This makes all your test cases pass:

function format(num){
  return ('' + num).replace(
    /(\d)(?=(?:\d{3})+(?:\.|$))|(\.\d\d?)\d*$/g, 
    function(m, s1, s2){
      return s2 || (s1 + ',');
    }
  );
}


test(7456, "7,456");
test(45345, "45,345");
test(25.23523534, "25.23"); //truncated, not rounded
test(3333.239, "3,333.23"); //truncated, not rounded
test(234.99, "234.99");
test(2300.99, "2,300.99");
test(23123123123.22, "23,123,123,123.22");

function test(num, expected){
  var actual = format(num);
  console.log(num + ' -> ' + expected + ' => ' + actual + ': ' + 
    (actual === expected ? 'passed' : 'failed')
   );
}
Tomas Langkaas
  • 4,551
  • 2
  • 19
  • 34
  • what if I don't want to add commas between the decimals? for example, this number 1233489.4545 would become this: 1,233,489.4545 not this: 1,233,489.4,545 – Newsha Nik Jan 12 '21 at 18:25
  • 1
    @NewshaNik, this code dose not add commas between decimals, it only truncates to two decimals. Your input of 1233489.4545 would be output as 1,233,489.45. – Tomas Langkaas Jan 13 '21 at 22:31
4

Try:

var n = 5812090285.2817481974897;
n = n.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
console.log(n);

Outputs:

5,812,090,285.28

Note: .toFixed(2) returns a string. So in order to simplify this further you must add a way to turn n into a string before executing your regex. For example:

n.toString.replace(/(\d)(?=(\d{3})+\.)/g, '$1,');  //ofc with the additional regex

Although you would think it wouldn't matter in javascript, it apparently does in this situation. So I dont know how much 'less' messy it would be to not use.

buræquete
  • 14,226
  • 4
  • 44
  • 89
Jamin
  • 1,362
  • 8
  • 22
  • 1
    Almost, I'm trying to remove the `toFixed` part of it. There should be a way of doing this purely in REGEX. (I know I can't add .00 via regex if the number is round, but I want to slice off the extra decimals via regex). – Neil Apr 11 '17 at 08:24
  • I don't know how much further you can simplify it but I'll see if I can think/find something. – Jamin Apr 11 '17 at 08:29
4

I added another layer where regex that drops the unwanted decimals below hundredths on top of your regex comma adding logic;

val.replace(/(\.\d{2})\d*/, "$1").replace(/(\d)(?=(\d{3})+\b)/g, "$1,")

doIt("7456");
doIt("45345");
doIt("25.23523534");
doIt("3333.239");
doIt("234.99");
doIt("2300.99");
doIt("23123123123.22");
doIt("5812090285.2817481974897");

function doIt(val) {
    console.log(val + " -> " + val.replace(/(\.\d{2})\d*/, "$1").replace(/(\d)(?=(\d{3})+\b)/g, "$1,"));
}

If multiple calls of regex replace is OK, this answer should satisfy you, since it is only has regex replace logic and nothing else.

buræquete
  • 14,226
  • 4
  • 44
  • 89
  • 1
    I'm pretty sure that this is possible in only one regex replace. Thanks for the idea though. – Neil Apr 15 '17 at 17:25
  • @PavelReznikov How? It works as intended `7,456.00`, if there is any decimals given, it keeps it, as OP has requested. – buræquete Apr 16 '17 at 09:29
2

Here is a way to do it without a regular expression:

value.toLocaleString("en-US", { maximumFractionDigits: 2 })

function formatValue() {
    var source = document.getElementById("source");
    var output = document.getElementById("output");
    var value = parseFloat(source.value);
    output.innerText = value.toLocaleString("en-US", { maximumFractionDigits: 2 });
}
<input id="source" type="text" />
<button onclick="formatValue()">Format</button>
<div id="output"></div>
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
2

RegEx to rescue again!

My solution has two parts :

.toFixed : Used to limit the decimal limit

/(\d)(?=(\d\d\d)+(?!\d))/g : It makes use of back reference with three digits at a time

Here's everything put together :

// .toFixed((/\./g.test(num)) ? 2 : 0) it tests if the input number has any decimal places, if so limits it to 2 digits and if not, get's rid of it altogether by setting it to 0
num.toFixed((/\./g.test(num)) ? 2 : 0).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"))

You can see it in action here :

var input = [7456, 45345, 25.23523534, 3333.239, 234.99, 2300.99, 23123123123.22]

input.forEach(function(num) {
  $('div')
    .append(
      $('<p>').text(num + ' => ' +
        num.toFixed( (/\./g.test(num))?2:0 ).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"))
    );
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div> </div>

NOTE: I've only used jQuery to append the results

Community
  • 1
  • 1
Mayank Raj
  • 1,574
  • 11
  • 13
1

You can do like this

(parseFloat(num).toFixed(2)).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,").replace(".00","")

Here just convert number to formatted number with rounded down to 2 decimal places and then remove the .00 if exist.

This can be one approach you can use.

var format = function (num) {
    
return (parseFloat(num).toFixed(2)).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,").replace(".00","")
}
$(function () {
    $("#principalAmtOut").blur(function (e) {
        $(this).val(format($(this).val()));
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="principalAmtOut" type="text" />
Dhiraj
  • 1,430
  • 11
  • 21
1

You can use Intl.NumberFormat with style set to "decimal" and maximumFractionDigits set to 2 at options object passed at second parameter

const nums = [7456, 45345, 25.23523534, 3333.239, 234.99, 2300.99, 23123123123.22];

const formatOptions = {style:"decimal", maximumFractionDigits:2};
           
const formatter = new Intl.NumberFormat("en-US", formatOptions);

const formatNums = num => formatter.format(num);

let formattedNums = nums.map(formatNums);

console.log(formattedNums);
buræquete
  • 14,226
  • 4
  • 44
  • 89
guest271314
  • 1
  • 15
  • 104
  • 177
0

I found a solution based on @Pierre's answer without using of toFixed:

function format(n) {
  n = +n;
  var d = Math.round(n * 100) % 100;
  return (Math.floor(n) + '').replace(/(\d)(?=(\d{3})+$)/g, '$1,') + (d > 9 ? '.' + d : d > 0 ? '.0' + d : '');
}

console.log(format(7456));
console.log(format(7456.0));
console.log(format(7456.1));
console.log(format(7456.01));
console.log(format(7456.001));
console.log(format(45345));
console.log(format(25.23523534));
console.log(format(3333.239));
console.log(format(234.99));
console.log(format(2300.99));
console.log(format(23123123123.22));
console.log(format('23123123123.22'));
Pavel Reznikov
  • 2,968
  • 1
  • 18
  • 17