简体   繁体   中英

Is there a reliable way in JavaScript to obtain the number of decimal places of an arbitrary number?

It's important to note that I'm not looking for a rounding function. I am looking for a function that returns the number of decimal places in an arbitrary number's simplified decimal representation. That is, we have the following:

decimalPlaces(5555.0);     //=> 0
decimalPlaces(5555);       //=> 0
decimalPlaces(555.5);      //=> 1
decimalPlaces(555.50);     //=> 1
decimalPlaces(0.0000005);  //=> 7
decimalPlaces(5e-7);       //=> 7
decimalPlaces(0.00000055); //=> 8
decimalPlaces(5.5e-7);     //=> 8

My first instinct was to use the string representations: split on '.' , then on 'e-' , and do the math, like so (the example is verbose):

function decimalPlaces(number) {
  var parts = number.toString().split('.', 2),
    integerPart = parts[0],
    decimalPart = parts[1],
    exponentPart;

  if (integerPart.charAt(0) === '-') {
    integerPart = integerPart.substring(1);
  }

  if (decimalPart !== undefined) {
    parts = decimalPart.split('e-', 2);
    decimalPart = parts[0];
  }
  else {
    parts = integerPart.split('e-', 2);
    integerPart = parts[0];
  }
  exponentPart = parts[1];

  if (exponentPart !== undefined) {
    return integerPart.length +
      (decimalPart !== undefined ? decimalPart.length : 0) - 1 +
      parseInt(exponentPart);
  }
  else {
    return decimalPart !== undefined ? decimalPart.length : 0;
  }
}

For my examples above, this function works. However, I'm not satisfied until I've tested every possible value, so I busted out Number.MIN_VALUE .

Number.MIN_VALUE;                      //=> 5e-324
decimalPlaces(Number.MIN_VALUE);       //=> 324

Number.MIN_VALUE * 100;                //=> 4.94e-322
decimalPlaces(Number.MIN_VALUE * 100); //=> 324

This looked reasonable at first, but then on a double take I realized that 5e-324 * 10 should be 5e-323 ! And then it hit me: I'm dealing with the effects of quantization of very small numbers. Not only are numbers being quantized before storage; additionally, some numbers stored in binary have unreasonably long decimal representations, so their decimal representations are being truncated. This is unfortunate for me, because it means that I can't get at their true decimal precision using their string representations.

So I come to you, StackOverflow community. Does anyone among you know a reliable way to get at a number's true post-decimal-point precision?

The purpose of this function, should anyone ask, is for use in another function that converts a float into a simplified fraction (that is, it returns the relatively coprime integer numerator and nonzero natural denominator). The only missing piece in this outer function is a reliable way to determine the number of decimal places in the float so I can multiply it by the appropriate power of 10. Hopefully I'm overthinking it.

Historical note: the comment thread below may refer to first and second implementations. I swapped the order in September 2017 since leading with a buggy implementation caused confusion.

If you want something that maps "0.1e-100" to 101, then you can try something like

function decimalPlaces(n) {
  // Make sure it is a number and use the builtin number -> string.
  var s = "" + (+n);
  // Pull out the fraction and the exponent.
  var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s);
  // NaN or Infinity or integer.
  // We arbitrarily decide that Infinity is integral.
  if (!match) { return 0; }
  // Count the number of digits in the fraction and subtract the
  // exponent to simulate moving the decimal point left by exponent places.
  // 1.234e+2 has 1 fraction digit and '234'.length -  2 == 1
  // 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
  return Math.max(
      0,  // lower limit.
      (match[1] == '0' ? 0 : (match[1] || '').length)  // fraction length
      - (match[2] || 0));  // exponent
}

According to the spec, any solution based on the builtin number->string conversion can only be accurate to 21 places beyond the exponent.

9.8.1 ToString Applied to the Number Type

  1. Otherwise, let n, k, and s be integers such that k ≥ 1, 10k−1 ≤ s < 10k, the Number value for s × 10n−k is m, and k is as small as possible. Note that k is the number of digits in the decimal representation of s, that s is not divisible by 10, and that the least significant digit of s is not necessarily uniquely determined by these criteria.
  2. If k ≤ n ≤ 21, return the String consisting of the k digits of the decimal representation of s (in order, with no leading zeroes), followed by n−k occurrences of the character '0'.
  3. If 0 < n ≤ 21, return the String consisting of the most significant n digits of the decimal representation of s, followed by a decimal point '.', followed by the remaining k−n digits of the decimal representation of s.
  4. If −6 < n ≤ 0, return the String consisting of the character '0', followed by a decimal point '.', followed by −n occurrences of the character '0', followed by the k digits of the decimal representation of s.

Historical note: The implementation below is problematic. I leave it here as context for the comment thread.

Based on the definition of Number.prototype.toFixed , it seems like the following should work but due to the IEEE-754 representation of double values, certain numbers will produce false results. For example, decimalPlaces(0.123) will return 20 .

 function decimalPlaces(number) { // toFixed produces a fixed representation accurate to 20 decimal places // without an exponent. // The ^-?\\d*\\. strips off any sign, integer portion, and decimal point // leaving only the decimal fraction. // The 0+$ strips off any trailing zeroes. return ((+number).toFixed(20)).replace(/^-?\\d*\\.?|0+$/g, '').length; } // The OP's examples: console.log(decimalPlaces(5555.0)); // 0 console.log(decimalPlaces(5555)); // 0 console.log(decimalPlaces(555.5)); // 1 console.log(decimalPlaces(555.50)); // 1 console.log(decimalPlaces(0.0000005)); // 7 console.log(decimalPlaces(5e-7)); // 7 console.log(decimalPlaces(0.00000055)); // 8 console.log(decimalPlaces(5e-8)); // 8 console.log(decimalPlaces(0.123)); // 20 (!)

Well, I use a solution based on the fact that if you multiply a floating-point number by the right power of 10, you get an integer.

For instance, if you multiply 3.14 * 10 ^ 2, you get 314 (an integer). The exponent represents then the number of decimals the floating-point number has.

So, I thought that if I gradually multiply a floating-point by increasing powers of 10, you eventually arrive to the solution.

 let decimalPlaces = function () { function isInt(n) { return typeof n === 'number' && parseFloat(n) == parseInt(n, 10) && !isNaN(n); } return function (n) { const a = Math.abs(n); let c = a, count = 1; while (!isInt(c) && isFinite(c)) { c = a * Math.pow(10, count++); } return count - 1; }; }(); for (const x of [ 0.0028, 0.0029, 0.0408, 0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, 2.e-14, -3.14e-21, 5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 0.000006, 0.0000007, 0.123, 0.121, 0.1215 ]) console.log(x, '->', decimalPlaces(x));

2017 Update

Here's a simplified version based on Edwin's answer. It has a test suite and returns the correct number of decimals for corner cases including NaN, Infinity, exponent notations, and numbers with problematic representations of their successive fractions, such as 0.0029 or 0.0408. This covers the vast majority of financial applications, where 0.0408 having 4 decimals (not 6) is more important than 3.14e-21 having 23.

 function decimalPlaces(n) { function hasFraction(n) { return Math.abs(Math.round(n) - n) > 1e-10; } let count = 0; // multiply by increasing powers of 10 until the fractional part is ~ 0 while (hasFraction(n * (10 ** count)) && isFinite(10 ** count)) count++; return count; } for (const x of [ 0.0028, 0.0029, 0.0408, 0.1584, 4.3573, // corner cases against Edwin's answer 11.6894, 0, 1.0, 1.00, 0.123, 1e-3, -1e2, -1e-2, -0.1, NaN, 1E500, Infinity, Math.PI, 1/3, 3.14, 2.e-3, 2.e-14, 1e-9, // 9 1e-10, // should be 10, but is below the precision limit -3.14e-13, // 15 3.e-13, // 13 3.e-14, // should be 14, but is below the precision limit 123.12345678901234567890, // 14, the precision limit 5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 0.000006, 0.0000007, 0.123, 0.121, 0.1215 ]) console.log(x, '->', decimalPlaces(x));

The tradeoff is that the method is limited to maximum 10 guaranteed decimals. It may return more decimals correctly, but don't rely on that. Numbers smaller than 1e-10 may be considered zero, and the function will return 0. That particular value was chosen to solve correctly the 11.6894 corner case, for which the simple method of multiplying by powers of 10 fails (it returns 5 instead of 4).

However, this is the 5th corner case I've discovered, after 0.0029, 0.0408, 0.1584 and 4.3573. After each, I had to reduce the precision by one decimal. I don't know if there are other numbers with less than 10 decimals for which this function may return an incorrect number of decimals. To be on the safe side, look for an arbitrary precision library .

Note that converting to string and splitting by . is only a solution for up to 7 decimals. String(0.0000007) === "7e-7" . Or maybe even less? Floating point representation isn't intuitive.

this works for numbers smaller than e-17 :

function decimalPlaces(n){
    var a;
    return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'$1').length)>=1?a:0;
}

This works for me

const decimalPlaces = value.substring(value.indexOf('.') + 1).length;

This method expects the value to be a standard number.

2021 Update

An optimized version of Mike Samuel handling scientific and non-scientific representation.

 // Helper function to extract the number of decimal assuming the // input is a number (either as a number of a stringified number) // Note: if a stringified number has an exponent, it will always be // '<x>e+123' or '<x>e-123' or '<x.dd...d>e+123' or '<x.dd...d>e-123'. // No need to check for '<x>e123', '<x>E+123', '<x>E-123' etc. const _numDecimals = v => { const [i, p, d, e, n] = v.toString().split(/(\\.|e[\\-+])/g); const f = e === 'e-'; return ((p === '.' && (!e || f) && d.length) + (f && parseInt(n))) || (p === 'e-' && parseInt(d)) || 0; } // But if you want to be extra safe...you can replace _numDecimals // with this: const _numSafeDecimals = v => { let [i, p, d, e, n] = v.toString().split(/(\\.|[eE][\\-+])/g); e = e.toLowerCase(); const f = e === 'e-'; return ((p === '.' && (!e || f) && d.length) + (f && parseInt(n))) || (p.toLowerCase() === 'e-' && parseInt(d)) || 0; } // Augmenting Number proto. Number.prototype.numDecimals = function () { return (this % 1 !== 0 && _numDecimals(this)) || 0; } // Independent function. const numDecimals = num => ( (!isNaN(num) && num % 1 !== 0 && _numDecimals(num)) || 0 ); // Tests: const test = n => ( console.log('Number of decimals of', n, '=', n.numDecimals()) ); test(1.234e+2); // --> 1 test(0.123); // ---> 3 test(123.123); // ---> 3 test(0.000123); // ---> 6 test(1e-20); // --> 20 test(1.2e-20); // --> 21 test(1.23E-20); // --> 22 test(1.23456789E-20); // --> 28 test(10); // --> 0 test(1.2e20); // --> 0 test(1.2e+20); // --> 0 test(1.2E100); // --> 0 test(Infinity); // --> 0 test(-1.234e+2); // --> 1 test(-0.123); // ---> 3 test(-123.123); // ---> 3 test(-0.000123); // ---> 6 test(-1e-20); // --> 20 test(-1.2e-20); // --> 21 test(-1.23E-20); // --> 22 test(-1.23456789E-20); // --> 28 test(-10); // --> 0 test(-1.2e20); // --> 0 test(-1.2e+20); // --> 0 test(-1.2E100); // --> 0 test(-Infinity); // --> 0

Not only are numbers being quantized before storage; additionally, some numbers stored in binary have unreasonably long decimal representations, so their decimal representations are being truncated.

JavaScript represents numbers using IEEE-754 double-precision (64 bit) format. As I understand it this gives you 53 bits precision, or fifteen to sixteen decimal digits.

So for any number with more digits you just get an approximation. There are some libraries around to handle large numbers with more precision, including those mentioned in this thread .

An optimized version of nick answer.

The function requires that n is a string. This function gets the decimal even if there all 0, like 1.00 -> 2 decimals.

function getDecimalPlaces(n) {
    var i = n.indexOf($DecimalSeparator)
    return i > 0 ? n.length - i - 1 : 0
}

console.log(getDecimalPlaces("5555.0"));  // 1
console.log(getDecimalPlaces("5555"));  // 0
console.log(getDecimalPlaces("555.5"));  // 1
console.log(getDecimalPlaces("555.50"));  // 2
console.log(getDecimalPlaces("0.0000005"));  // 7
console.log(getDecimalPlaces("0.00000055"));  // 8
console.log(getDecimalPlaces("0.00005500"));  // 8

Based on gion_13 answer I came up with this:

 function decimalPlaces(n){ let result= /^-?[0-9]+\\.([0-9]+)$/.exec(n); return result === null ? 0 : result[1].length; } for (const x of [ 0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, -3.14e-21, 5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 0.000006, 0.0000007, 0.123, 0.121, 0.1215 ]) console.log(x, '->', decimalPlaces(x));

It fixes the returning 1 when there are no decimal places. As far as I can tell this works without errors.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM