14
\$\begingroup\$

Like Twitter and Instagram and others I wanted to display numbers like 1.2K and 3.8M etc. My function works well enough but I would appreciate any input you may have.

function abbrNum (num) {
    if(typeof num !== 'number') {
        throw new TypeError('Expected a number');
    }

    var shortNumber;
    var exponent;
    var suffixes = ['K', 'M', 'B', 'T'];
    var size = (num + '').length;

    exponent = size % 3 === 0 ? size - 3 : size - (size % 3);

    if(num < 1000) {
        return num;
    } else {
        shortNumber = Math.round(10 * (num / Math.pow(10, exponent))) / 10;
    }

    if(exponent < 6) {
        shortNumber += suffixes[0];
    } else if(exponent < 9) {
        shortNumber += suffixes[1];
    } else if(exponent < 12) {
        shortNumber += suffixes[2];
    } else if(exponent < 16) {
        shortNumber += suffixes[3];
    }

    return shortNumber;
}

Update: This code (updated according to feedback) is available on npm.

\$\endgroup\$
  • \$\begingroup\$ Be careful of long- vs. short-scale billions... \$\endgroup\$ – Schism Aug 11 '15 at 22:18
  • \$\begingroup\$ I probably could not help rewriting size % 3 === 0 ? size - 3 : size - (size % 3) as (size - 1) - (size - 1) % 3. \$\endgroup\$ – Carsten S Aug 12 '15 at 8:30
  • \$\begingroup\$ @CarstenS I would prefer Math.floor((size-1)/3) \$\endgroup\$ – Taemyr Aug 12 '15 at 12:35
18
\$\begingroup\$

converting to string just to get the length won't work if there is a decimal or it's large enough that the string conversion uses scientific notation.

var size = floor(log(num)/log(10))+1;

This will give the place of the highest significant digit (1 to 9 results in 1, 10 to 99 results in 2, 100 to 999 results in 3, etc...)

\$\endgroup\$
  • \$\begingroup\$ Can I ask why you're dividing log(num) by log(10)? Isn't log(10) always 1? In which case you could just do floor(log(num))+1 \$\endgroup\$ – Andy F Aug 11 '15 at 14:18
  • 3
    \$\begingroup\$ @AndyF I wasn't sure if the base of log() was 10 and the division will always make sure that I get the base I want. \$\endgroup\$ – ratchet freak Aug 11 '15 at 14:20
  • 10
    \$\begingroup\$ Math.log() gives you a natural logarithm, so log(10) is roughly 2.3. @ratchetfreak is doing the right thing, since log(a)/log(b) = logarithm base b of a. \$\endgroup\$ – Plutor Aug 11 '15 at 14:35
  • 1
    \$\begingroup\$ Beware the troubles of floating point math, or why it is generally a good idea to write unit tests: Math.floor(Math.log(1000)/Math.log(10))+1) already gives me 3 instead of 4 with Chrome. While there is no built-in method to compute the integer logarithm (that I know of) it's an easy to write method and will actually work correctly. \$\endgroup\$ – Voo Aug 12 '15 at 19:15
11
\$\begingroup\$

Your function does not entirely work as you would expect for larger numbers.

I did some testing of your code in my browser's console and I came across this:

> abbrNum(1234567890123)
"1.2T"
> abbrNum(1234567890123456)
"1.2T"

You should add some support for when a number is too big. Perhaps you could throw an exception that the number is out of bounds that your function will work in.


This little construct:

    if(exponent < 6) {
        shortNumber += suffixes[0];
    } else if(exponent < 9) {
        shortNumber += suffixes[1];
    } else if(exponent < 12) {
        shortNumber += suffixes[2];
    } else if(exponent < 16) {
        shortNumber += suffixes[3];
    }

    return shortNumber;
}

Is not very flexible. What if you decide to add more suffixes? As you add more and more if elses, this construct just gets more and more ugly.

I recommend creating an object where the values are the number to check the exponent against, and the keys are the suffixes to add.

That would look like this:

var suffixes = {
    "K": 6,
    "M": 9,
    ...
}

Then, to check which suffix to add, you simply loop through this construct:

for(var suffix in suffixes) {
    if(exponent < suffixes[suffix]) {
        shortNumber += suffix;
        break;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Can you clarify your solution for adding the suffix? As it's written it will just add every suffix for numbers less than one million for example. \$\endgroup\$ – cfj Aug 11 '15 at 14:10
  • 1
    \$\begingroup\$ @cfj Ah, you are right. I have changed the code; it should work now. \$\endgroup\$ – SirPython Aug 11 '15 at 14:11
  • \$\begingroup\$ Yup, it works now. Just make sure you delete the old array. \$\endgroup\$ – SirPython Aug 11 '15 at 14:15
7
\$\begingroup\$

It does currently not work for negative numbers – also a user of the API may just assume this – which could easily be solved by using the absolute value of the input number:

if(Math.abs(num) < 1000) {
\$\endgroup\$
0
\$\begingroup\$

Looks pretty clear for me, A few things:

1 - If is less than 1k, avoid the other operations, so after the type-check, add that validation.

2 - For more expressibility, considerate using a dictionary or avoid array of suffix.

  function abbreviationNumber (num) {
        if(typeof num !== 'number') {
            throw new TypeError('Expected a number');
        }

        if(num < 1000) {
            return num;
        }

        var shortNumber;
        var exponent;
        var size = (num + '').length;

        exponent = size % 3 === 0 ? size - 3 : size - (size % 3);    
        shortNumber = Math.round(10 * (num / Math.pow(10, exponent))) / 10;

        if (exponent < 6) {
            shortNumber += 'K';
        } else if(exponent < 9) {
            shortNumber += 'M';
        } else if(exponent < 12) {
            shortNumber += 'B';
        } else if(exponent < 16) {
            shortNumber += 'T';
        }

        return shortNumber;
    }
\$\endgroup\$
-1
\$\begingroup\$

None of the given answers - or the original code for that - is actually correct.

Here is a minimal test harness to test the heart of the whole problem: Figuring out the right exponent for a given number:

function test(input, expected, func) {
    var result = func(input);
    if (result !== expected) {
        console.log("Invalid result for " + input + " expected: " + expected + " actual: " + result);
    }
    return result === expected;
}

function testHarness(funcName, func) {
    console.log("Testing " + funcName);
    var successful = true;
    for (var i = 1, number = 1; i <= 50; i++, number *= 10) {
        successful &= test(number, i, func);
        if (!successful) {
            break;
        }
    }
    if (successful) {
        console.log(funcName + " passed test correctly.")
    } else {
        console.log(funcName + " did NOT pass test!")
    }
    return successful;
}

The original answer fails at 1e21 because that is when Chrome decides to use scientific notation for floats, while ratchet freak's solution already gives the wrong result for 1000.

For reference here is the obvious implementation of an ilog10 algorithm that is as correct as possible (to the best of my knowledge) considering the inherent imprecision of floating point (you just cannot represent 1e55 or so as a IEEE-754 double so that's where this becomes problematic):

function ilog10(val) {
    var i = 1;
    var number = 10;
    while (val >= number) {
        i += 1;
        number *= 10;
    }
    return i;
}

What also seems to work - but I'd definitely want to write more test cases to be sure is to use Math.floor(Math.log10(val)) + 1. It passes the basic unit test in any case.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.