Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 328
Milosz Avatar asked Mar 02 '12 19:03

Milosz


People also ask

Which JavaScript method allows one to specify the number of decimal digits for a number?

JavaScript Number toFixed() The toFixed() method rounds the string to a specified number of decimals.

How do you check if a number is a decimal number in JavaScript?

JavaScript Code:function number_test(n) { var result = (n - Math. floor(n)) !== 0; if (result) return 'Number has a decimal place. '; else return 'It is a whole number.

How do I get 2 decimal places in JavaScript?

Use the toFixed() method to format a number to 2 decimal places, e.g. num. toFixed(2) . The toFixed method takes a parameter, representing how many digits should appear after the decimal and returns the result.


1 Answers

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 (!)
like image 192
Mike Samuel Avatar answered Sep 18 '22 20:09

Mike Samuel