I've just spent an unhealthy amount of time reviewing this, so I'm sharing my findings.
There is no single optimal solution to this. It depends on what the range of values you need to support. You can merge multiple solutions to balance performance and correctness, though.
The main caveats that make this complicated are:
- Trying to parse it with pure math doesn't go well, try
0.1 + 0.2 in your console and see the result. It's very challenging to get meaningful results using primitive math operations if you want to support floating-point numbers.
- JavaScript doesn't display the full length of numbers that have many digits, so even the simple solutions like
String.length aren't great.
I'll include benchmarks with all implementations, but note that some handle particular types of input better. You should evaluate them based on the input you expect to receive.
Here is how my benchmark looks. Note that each implementation has two benchmarks noted under it. One for all values, including the ones that aren't supported, and with only 32-bit integers as they're supported by all implementations.
const benchmark = require('benchmark');
const suite = new benchmark.Suite;
const data = [ // Digits
Number.MIN_VALUE, // 325
-123143, // 6
-0.12, // 3
0, // 1
0.000123, // 7
0.12, // 3
1, // 1
123.123, // 6
234524, // 6
1000000, // 7
Number.MAX_VALUE // 309
];
function countDigits(num) {
// …
}
suite
.add('Benchmark', () => data.map(d => countDigits(d)))
.on('cycle', (event) => console.log(String(event.target)))
.run({ 'async': true });
Correctness
First, looking purely at handling a wide range of values correctly. This is the fastest implementation I could come up with that returns the correct result for as many values as possible.
It merges Caltrop's and Mwr247's answer, and handles cases that were missed by both.
function countDigits(num) {
if (num === 0) {
return 1;
}
if (Number.isInteger(num)) {
return Math.floor(Math.log10(Math.abs(num))) + 1;
}
num = Math.abs(num);
const str = "" + num;
const e = str.indexOf('e', 1);
if (e === -1) {
return str.length - 1;
}
const s1 = str.slice(0, e);
const s2 = str.slice(e + 1);
if (num >= 1) {
return parseInt(s2) + 1;
}
return s1[1] !== '.'
? Math.abs(parseInt(s2)) + 1
: Math.abs(parseInt(s2)) + s1.length - 1;
}
// All x 5,296,360 ops/sec ±0.85% (96 runs sampled)
// 32-bit integers x 15,941,480 ops/sec ±1.19% (97 runs sampled)
Compromising
Chances are you expect specific input and so don't have to worry about the extreme numbers, so a more performant solution can be worthwhile.
Caltrop's answer handles positives, negatives, and floating points. The downsides are that it doesn't work for large numbers. The string representation of integers over 20 digits and large floating points are standard notation (i.e. 5e-324) so they'll get miscounted.
Here is a derived version with one change, which runs faster for me:
function countDigits(num) {
num = Math.abs(num);
return Number.isInteger(num) ? ("" + num).length : ("" + num).length - 1;
}
// All x 3,986,610 ops/sec ±1.51% (88 runs sampled)
// 32-bit integers x 49,595,986 ops/sec ±1.55% (92 runs sampled)
//
// Incorrect result for Number.MIN_VALUE and Number.MAX_VALUE
- Uses
Number.isInteger instead of (num << 0) - num.
- More accurate with input like
Number.MAX_VALUE as this returns 23 instead of 22… though both are wrong regardless.
Mwr247 shared two great implementations, but both only work with integers. However, there is a difference between them. The solution that bit shifts will not work with values that are too large either, as bit operations are limited to 32-bit numbers.
function countDigits(x) {
return Math.max(Math.floor(Math.log10(Math.abs(x))), 0) + 1;
}
// All x 5,704,235 ops/sec ±1.70% (92 runs sampled)
// 32-bit integers x 16,256,824 ops/sec ±1.28% (93 runs sampled)
//
// Incorrect result for Number.MIN_VALUE, -0.12, 0.000123, 0.12, 123.123
function countDigits(x) {
return (Math.log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
}
// All x 8,428,740 ops/sec ±1.51% (94 runs sampled)
// 32-bit integers x 16,327,264 ops/sec ±3.23% (86 runs sampled)
//
// Incorrect result for Number.MIN_VALUE, -0.12, 0.000123, 0.12, 123.123, Number.MAX_VALUE
Other solution I've found or have tried to come up with either have performed slower, or don't provide any benefit over those listed above.
Notes
- Regular expressions can't keep up with the proposed solutions provided here, and suffer from the same caveats as other string parsing solutions.
- Solutions that use Math operations will impact performance before you're able to address precision issues if you want to support floating points.
(number+'').lengthwill convert the content automatically to string, then you can use.length.