Another js function to insert commas in numbers

By Luke Smith on December 14, 2008 9:52 PM

This evening's academic exercise was to write your basic 1234 => 1,234 function as terse as possible, and without a loop. At first I thought I could do it with just a clever regex, and maybe I could have, but I abandoned that approach.

This is what I ended up with:

function formatNumber(n) {
    if (!isFinite(n)) {
        return n;
    }

    var s = ""+n, abs = Math.abs(n), _, i;

    if (abs >= 1000) {
        _  = (""+abs).split(/\./);
        i  = _[0].length % 3 || 3;

        _[0] = s.slice(0,i + (n < 0)) +
               _[0].slice(i).replace(/(\d{3})/g,',$1');

        s = _.join('.');
    }

    return s;
}

I could have omitted the storage of Math.abs(n) and the short circuit for numbers not needing comma injection, but eh. No need to sacrifice runtime performance for a few characters.

The basic principle is to use string replace to insert commas on only the chunk of the number that needs them. intAsString.length % 3 || 3; will return the index of the first character that needs a comma before it (for positive values, hence the abs), so everything beyond that point needs to be divided into three digit chunks, each preceded by a comma.

Update - 1/16/2009

I happened across a similar function, based on PHP's number_format function. It prompted me to adapt in the extra params to support precision and specified characters for decimal and thousands divider.

I put the result in a gist. I put together this page to confirm they were behaving comparably.

Update - 3/18/2009

Updated to account for input 1000. > should have been >= Oops! Thanks to Simon for helping me notice that :)

I also put the barebones function in a gist

12 Comments

  1. Gravatar

    Thanks for sharing.

    let's see if those gravatars work ;)

    S

  2. Gravatar

    How about :
    set n to the no. to be translated:

    var s = function(n) {
    return (n<1000)? n : arguments.callee(Math.floor(n/1000))+','+n%1000;
    }(n);

  3. Gravatar

    The classic problem of formatting numbers in JavaScript is that division in js is untrustworthy.

    For example, using your method, 1234.03 becomes "1,234.02999999999997"

    Also, you aren't addressing negative numbers.

  4. Gravatar

    I was only processing +ve integers - so here's a variant for -ve's and using strings not maths:


    function commaize(n) {
    n=n+'';
    return (Math.abs(n) <= 999) ? n : commaize(n.slice(0,-3))+','+n.slice(-3);
    }

    Interestingly the slice doesn't seem to work with no.s (which js should cast to string?) ! hence the n=n+''

  5. Gravatar

    Yep, that does account for positive and negative ints and certainly has a small byte count.

    I guess I considered it a requirement that the function handle both a) garbage input and b) floats. And of course I stated I wanted to avoid loops, not recursion, but really I wanted to avoid that, too. There's overhead associated with each function call that I wanted to avoid, but I wonder how each version would benchmark against increasingly large values.

    So if you are looking to specifically handle ints this should do it. Though you might try adding

    n = (n|0)+'';

    to force float/garbage input to ints. That will make any garbage input 0 and drop the decimal (not equiv to Math.floor for negatives).

    And literal numbers, strings, and booleans coerce to their Object representation (for that transaction only) when used with dot notation.

    E.g.
    4000.slice(0,-3) is executed as (new Number(4000)).slice(0,-3) // BOOM

  6. Gravatar

    Oh, and thanks for helping me spot that value 1000 bug! :D

  7. Gravatar

    You mentioned the idea of handling this with a regex. That will definitely improve the terseness. Here's my take on that approach: http://blog.stevenlevithan.com/archives/commafy-numbers

  8. Gravatar

    Thanks Steve. I must have subscribed to your blog after you wrote that :)

    The method you used in the reverse lookup example had occurred to me, but the repeated split, reverse, join seemed excessive for the sake of a simpler regex.

    I wonder why in your Number.prototype.commafy implementation you use the function form of String rather than the constructor form, since the function form will output a string literal which would then need to be coerced to an object instance to support the method call. Perhaps you chose that route for the byte savings? All splitting hairs anyway :)

    Since I'm not a fan of modifying the native prototypes, I'd declare it like

    function commafy(n) {
    return (n+'').replace(/(^|[^\w.])(\d{4,})/g, function(_, $1, $2) {
    return $1 + $2.replace(/\d(?=(?:\d\d\d)+(?!\d))/g, "$&,");
    });
    }

    Thanks for the link!

  9. Gravatar

    My String.prototype.commafy returns a string literal, so the Number.prototype version does the same for consistency. I generally prefer working with string literals, unless for some reason I want to store properties on a string. It saves hassle compared to the flip side of consistently working with String objects. Like you said though, this stuff isn't a big deal, but of course it does affect things like typeof/instanceof.

  10. Gravatar

    Upon rereading your comment, I guess I missed the point -- that you were merely talking about avoiding the coercion to an object in the background -- rather than caring whether a string literal or object was returned (since running the commafy method on the string results in a string literal being returned anyway). Well, like I said, I like the consistency of dealing with string literals (so that when String objects are involved, I know there's a reason for it), and I doubt there's any measurable (much less, noticeable) difference unless you run the code in a tight loop.

    (Since my last comment hasn't shown up yet and I can't look back at what I said, hopefully this isn't too redundant. :-) )

  11. Gravatar

    I also prefer working with literals, but yeah, the question was because it was an internal operation that relied on the String object method.

    Sorry about the comment system. Recently moved blogging engines, so I'm still learning how to make it suck less :)

  12. Gravatar

    I think using regex to insert commas in numbers is too slow.
    Here is my own one.

    function formatCurrency(n) {
        var q = Math.abs(n).toString().split('.');
        var s = q[0].split('');
        var i = s.length;
        while(i-=3 > 0) s.splice(i,0,',');
        return (n<0?'-':'')+s.join('')+((q[1])?'.'+q[1]:'');
    }

Your comment?

HTML is ok

No TrackBacks (http://lucassmith.name/mt/mt-tb.cgi/117)

ls.n

LucasSmith.name

Luke and Liam

I'm Luke. I am a front end engineer at Yahoo! on the YUI team.

Mostly I write about code stuff, but occassionally I'll mix in some real life. You've been warned.

Archives

Tags

Feeds

Subscribe to feed Recent entries

Content licensed under Creative Commons

Code licensed under BSD license

©2005 - 2010 Lucas Smith

Powered by Movable Type