10

What is the fastest way to count the number of significant digits of a number?

I have the following function, which works, but is quite slow due to string operations.

/**
 * Count the number of significant digits of a number.
 *
 * For example:
 *   2.34 returns 3
 *   0.0034 returns 2
 *   120.5e+3 returns 4
 *
 * @param {Number} value
 * @return {Number} The number of significant digits
 */
function digits (value) {
  return value
      .toExponential()
      .replace(/e[\+\-0-9]*$/, '')  // remove exponential notation
      .replace( /^0\.?0*|\./, '')    // remove decimal point and leading zeros
      .length
};

Is there a faster way?

Update: here a list of assertions to test correct functioning:

assert.equal(digits(0), 0);
assert.equal(digits(2), 1);
assert.equal(digits(1234), 4);
assert.equal(digits(2.34), 3);
assert.equal(digits(3000), 1);
assert.equal(digits(0.0034), 2);
assert.equal(digits(120.5e50), 4);
assert.equal(digits(1120.5e+50), 5);
assert.equal(digits(120.52e-50), 5);
assert.equal(digits(Math.PI), 16);

My own method failed for digits(0), I fixed that by adding a ? to the second regexp.

Jos de Jong
  • 6,602
  • 3
  • 38
  • 58
  • 2
    What you're trying to do is something that is fundamentally challenged by the fact that floating-point numbers are represented as *binary* floating point. – Pointy Apr 05 '14 at 18:15
  • Perhaps look at http://ostermiller.org/calc/SignificantFigures.js – bdrx Apr 05 '14 at 18:21
  • 1
    No answer will ever be sufficient unless some bench testing is being used. How do you define *fastest*? Any one can bring any arbitrary solution and we all can argue all night that it's faster/slower than yours. – Mohammed Joraid Apr 05 '14 at 18:26
  • How did you measure "quite slow"? Was it "too slow" for some requirement of yours - how do you use it? – Bergi Apr 05 '14 at 18:35
  • I need to know the number of significant digits to determine whether a number has the right conditions to be converted to a BigNumber: that is when the number has max 15 digits, else you already have a number suffering from round-off errors. The current method is not "too slow", but I know that string operations are slow compared to numeric operations. It would be great if someone knows some brilliant solution using bit wise operations or something like that. – Jos de Jong Apr 05 '14 at 18:43
  • In that case, why not just check the exponent? Again, you can't trust the decimal representation. The safest thing to do would be to explore a binary solution via [typed arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays). – Pointy Apr 05 '14 at 19:21
  • @Pointy The exponent doesn't tell anything about the number of significant digits? How to use typed arrays in this regard? – Jos de Jong Apr 05 '14 at 19:23
  • @JosdeJong I'll add an answer. The advantage is that you can do it without performing any math on the value. The disadvantage is that typed arrays are not supported in older browsers. – Pointy Apr 05 '14 at 19:38
  • Answer added. (It's not a complete solution, but it's a start; I don't really fully understand what it is you want to check.) – Pointy Apr 05 '14 at 19:51
  • Is it possible to get a list of input/expected output sample aside from the ones mentioned in the question, cuz they seems to pass the testing easily, but other error prone values are harder to spot. Like (1000.5e50) what should be the expected output? 6 or 7 or .. ? – Mohammed Joraid Apr 05 '14 at 19:57
  • Just saw ur update. It seems i had the wrong idea bout what significant digits are :\ – Mohammed Joraid Apr 05 '14 at 20:42
  • 1
    Are you working from some sort of paper that describes the technique you're trying to implement? The more I think about it, the more I think that what you're trying to do is inherently extremely difficult due to the combination of the nature of floating point math in general, and the difficulties encountered when interpreting the behavior of binary floating point math after decimal conversion. Even if you do the binary stuff I suggested, things are going to be weird: the value `0.3` is a repeating fraction in binary for example. – Pointy Apr 05 '14 at 23:23
  • @Pointy no this is not about a paper but just a search for a fast, pragmatic solution. However, your ideas around accessing low level bits of a Number are very interesting and much broader, and may be worth a blog, paper, or a library on its own. – Jos de Jong Apr 06 '14 at 10:42
  • 1
    [Here is a simple jsbin that explores the issue.](http://jsbin.com/huqod/1) Try some simple values in the input box; 18 is interesting. Note that even after some simple operations, things get weird. Sometimes the decimal representation will be "clean", but the binary is a repeating fraction. Sometimes the decimal representation is imprecise. Floating point math is just bizarre and really hard to handle analytically. – Pointy Apr 06 '14 at 16:31
  • wow, thanks! This is really interesting. It's fun to see that values like 0.3 indeed result in an unfortunate, repeating sequence. This gives quite some insight. – Jos de Jong Apr 06 '14 at 19:40

6 Answers6

9

Here's a more mathematical way of doing the same operation (which appears to be significantly faster)

JSPerf comparing the three implementations

Accurate for integer n < +-(2^53) per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5
Floats are converted to a string and then coerced to an int (by removing the decimal so similar rules apply)

var log10 = Math.log(10);
function getSignificantDigitCount(n) {
    n = Math.abs(String(n).replace(".", "")); //remove decimal and make positive
    if (n == 0) return 0;
    while (n != 0 && n % 10 == 0) n /= 10; //kill the 0s at the end of n

    return Math.floor(Math.log(n) / log10) + 1; //get number of digits
}
megawac
  • 10,953
  • 5
  • 40
  • 61
2

Slight improvement of regular expression

function digits (value) {
  return value
      .toExponential()
      .replace(/^([0-9]+)\.?([0-9]+)?e[\+\-0-9]*$/g, "$1$2")
      .length
};
Omer Arshad
  • 520
  • 7
  • 16
  • How would this be faster? Simply reducing the number of calls to `.replace()` may not always be faster - in most cases it just comes off as trying to show off your regex-wrangling skillz. – BoltClock Apr 05 '14 at 18:24
  • 1
    Edit: sorry, in first instance I thought this method was faster but it seems like this improvement does not make a lot of difference :( – Jos de Jong Apr 05 '14 at 19:11
2

And yet another approach, that uses string operations and handles some special cases for better performance:

function digits(value) {
    if (value === 0) {
        return 0;
    }
    //create absolute value and
    var t1 = ("" + Math.abs(value));
    //remove decimal point
    var t2 = t1.replace(".","");

    //if number is represented by scientific notation,
    //the places before "e" (minus "-" and ".") are the
    //significant digits. So here we can just return the index
    //"-234.3e+50" -> "2343e+50" -> indexOf("e") === 4
    var i = t2.indexOf("e");
    if (i > -1) {
        return i;
    } 

    //if the original number had a decimal point,
    //trailing zeros are already removed, since irrelevant
    //0.001230000.toString() -> "0.00123" -> "000123"
    if (t2.length < t1.length) {
        // -> remove only leading zeros
        return t2.replace(/^0+/,'').length;
    }

    //if number did not contain decimal point,
    //leading zeros are already removed
    //000123000.toString() -> "123000"
    // -> remove only trailing zeros
    return t2.replace(/0+$/,'').length;
}
basilikum
  • 10,378
  • 5
  • 45
  • 58
1

Regular string checking. A slight of improvement though.

function digits(value) {
  value = "" + value;
  var res = 0;
  for (var i = 0, len = value.length; i < len; i++){
    if (value[i]==="e")break;
    if (+value[i]>=0)
      res++;
}
  return res;
};

jsperf Benchmark testing result as compared to the OP's and other answers code.


Update

function digits(value) {

  console.log(value);
  value = "" + (+value);

  var res = 0;
  for (var i = 0, len = value.length; i < len; i++) {
  
  if (value[i] === "e") 
    {
    break;
    }
    
    if (+value[i] >= 0)
    {
     res++;
    }     
  }
  console.log(value);
  return res;
}

function check(val1, val2) {

  console.log( val1+"==="+val2 +" = "+ (val1 === val2));
  return val1 === val2;
}


check(digits(0), 1);
check(digits(2), 1);
check(digits(1234), 4);
check(digits("0012003400"), 8);
check(digits("0022.002200"), 6);
check(digits(2.34), 3);
check(digits(3000), 4);
check(digits(0.0034), 2);
check(digits(12003), 5);
check(digits(1.23e+50), 3);
check(digits("1.23e+50"), 3);
check(digits(120.5e51), 4);
check(digits(1120.5e+52), 5);
check(digits(120.52e-53), 5);
check(digits(Math.PI), 16);
Mohammed Joraid
  • 6,202
  • 2
  • 27
  • 38
  • 1
    Nice and simple. This doesn't work though for large/small values: the string representation then contains an exponent (like "1.23e+50"). It would work if we would count the the numbers until the exponential part though. – Jos de Jong Apr 05 '14 at 19:56
  • 1
    Even more, it doesn't work correctly for a value having zero's halfway, such as "12003" -> has 5 significant digits but the method returns 3. – Jos de Jong Apr 05 '14 at 19:57
1

You can directly examine the bytes of a floating-point value by using typed arrays. The advantages of doing this are that it's fast, and it doesn't require any math to be done. You can look directly at the bits of the mantissa.

You can start with this:

var n = yourFloatingPointValue; 

var f64 = new Float64Array(1);
var dv = new DataView(f64.buffer);

dv.setFloat64(0, n, false); // false -> big-endian

var bytes = [];
for (var i = 0; i < 8; i++)
  bytes.push(dv.getUint8(i));

Now the bytes array contains integers representing the 8-bit values of the floating point value as it looks in memory. The first byte contains the sign bit in the top bit position, and the first 7 bits of the exponent in the rest. The second byte contains the 5 least-significant bits of the exponent and the first three bits of the mantissa. The rest of the bytes are all mantissa.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 1
    would you please wrap it into a function so maybe we can add it to out little benchmark testing that we are running. Just curious, what happens to var n later on? – Mohammed Joraid Apr 05 '14 at 19:54
  • Ah I get your point. Really cool to have access to the internal bits via a TypedArray! Would indeed be interesting to see how this performs. If seriously faster, it's easy to create a function which selects this method only when supported by the browser, and else falls back on a different solution. – Jos de Jong Apr 05 '14 at 20:03
  • I will be happy to wrap this in a function. It's going to be hard to compare the results, because it's working strictly in binary. I have a real-world obligation coming up shortly but I'll be able to add more in a few hours. – Pointy Apr 05 '14 at 20:10
  • I'm looking forward to see how you estimate the number of significant base10-digits from the binary representation… – Bergi Apr 05 '14 at 20:59
  • @Bergi well given what appears to be the motivation in the OP, I don't think that's really necessary. If you're running out of precision, you're doing so whether you look at the bits or the decimal representation. To me the problem with looking at the decimal expansion is that it doesn't tell you the actual situation. A number with plenty of bits left might expand quite differently. – Pointy Apr 05 '14 at 22:38
  • @Bergi actually the more I think about it, the more I think this is a wild goose chase. What about `0.3`? That'll pepper the mantissa with lots of ones. You get similar numbers that are ill-behaved when you convert from binary to decimal after some math. I think the effort is misguided, in short; floating point is just complicated, and if its semantics are undesirable some "Big Decimal" fixed-point solution should be pursued instead. – Pointy Apr 05 '14 at 23:03
-1

There is a faster and indirect way to do it, which is converting it to a string and finding the length of it.

a = 2.303
sig_fig = len(str(a))-len(str(int(a)))-1

The extra -1 is for the "."