JavaScript中有一种可靠的方法来获取任意数字的小数位数吗?

时间:2012-03-02 19:53:13

标签: javascript floating-point decimal precision fractions

重要的是要注意我不是在寻找舍入函数。我正在寻找一个函数,它返回任意数字的简化十进制表示中的小数位数。也就是说,我们有以下内容:

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

我的第一直觉是使用字符串表示:在'.'上拆分,然后在'e-'上,然后进行数学运算(例如详细):

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;
  }
}

对于上面的示例,此功能有效。但是,在我测试了所有可能的值之前,我并不满意,因此我将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

这一开始看起来很合理,但接下来我意识到5e-324 * 10应该是5e-323!然后它击中了我:我正在处理非常小数量的量化效应。数字不仅在存储之前被量化;另外,存储在二进制中的一些数字具有不合理的长十进制表示,因此它们的十进制表示被截断。这对我来说很不幸,因为这意味着我无法使用字符串表示法获得真正的小数精度。

所以我来找你,StackOverflow社区。你们中间是否有人知道一种可靠的方法来获得一个真正的小数点后精度?

如果有人问的话,这个函数的目的是用于另一个将float转换为简化分数的函数(也就是说,它返回相对互质的整数分子和非零自然分母)。这个外部函数中唯一缺少的部分是一个可靠的方法来确定浮点数中的小数位数,所以我可以乘以10的适当幂。希望我过度思考它。

9 个答案:

答案 0 :(得分:17)

历史记录:下面的评论帖子可以指第一和第二种实施方式。我在2017年9月交换了订单,因为导致错误的实施引起了混乱。

如果您想要将"0.1e-100"映射到101的内容,那么您可以尝试类似

的内容
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
}

根据规范,任何基于内置数字 - >字符串转换的解决方案只能精确到指数以外的21个位置。

9.8.1 ToString Applied to the Number Type

  
      
  1. 否则,令n,k和s为整数,使得k≥1,10k-1≤s<1。 10k,s×10n-k的数值是m,k尽可能小。请注意,k是s的十进制表示中的位数,s不能被10整除,并且s的最低有效位不一定由这些条件唯一确定。
  2.   
  3. 如果k≤n≤21,则返回由s的十进制表示的k个数字组成的字符串(按顺序,没有前导零),然后是n-k出现的字符“0”。
  4.   
  5. 如果0 < n≤21,返回由s的十进制表示的最高n位数字组成的字符串,后跟小数点'。',后跟s的十进制表示的其余k-n位。
  6.   
  7. 如果-6&lt; n≤0,返回由字符'0'组成的字符串,后跟小数点'。',后跟-n出现的字符'0',后跟s的十进制表示的k位数。
  8.   

历史记录:下面的实施是有问题的。我把它留在这里作为评论主题的上下文。

根据Number.prototype.toFixed的定义,似乎以下内容应该有效,但由于IEEE-754表示双值,某些数字会产生错误结果。例如,decimalPlaces(0.123)将返回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 (!)

答案 1 :(得分:12)

好吧,我使用的解决方案基于以下事实:如果将浮点数乘以10的右幂,则得到一个整数。

例如,如果乘以3.14 * 10 ^ 2,则得到314(整数)。然后,exponent表示浮点数具有的小数位数。

所以,我认为如果我通过增加10的幂来逐渐乘以浮点,那么你最终会得到解决方案。

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));

答案 2 :(得分:3)

适用于小于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;
}

答案 3 :(得分:2)

2017年更新

这是基于Edwin答案的简化版本。它有一个测试套件,并返回正确的小数位数,包括NaN,Infinity,指数表示法,以及带有连续分数的有问题表示的数字,例如0.0029或0.0408。这涵盖绝大多数财务应用程序,其中0.0408有4位小数(不是6)比3.14e-21有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));

权衡是该方法限制为最多10个保证小数。它可能正确返回更多小数,但不依赖于此。小于1e-10的数字可以被认为是零,并且函数将返回0.选择该特定值以正确解决11.6894转角情况,其中乘以10的幂的简单方法失败(它返回5而不是4 )。

然而,在{0.00},0.0408,0.1584和4.3573之后,我发现这是the 5th个角落。在每次之后,我不得不将精度降低一个小数。我不知道是否有其他小数小于10的数字,此函数可能返回不正确的小数位数。为安全起见,请寻找arbitrary precision library

请注意,转换为字符串并按.拆分只是最多7位小数的​​解决方案。 String(0.0000007) === "7e-7"。或者甚至更少?浮点表示不直观。

答案 4 :(得分:1)

不仅数字在存储之前被量化;另外,存储在二进制中的一些数字具有不合理的长十进制表示,因此它们的十进制表示被截断。

JavaScript使用IEEE-754双精度(64位)格式表示数字。据我所知,这给你53位精度,或十五到十六位十进制数。

因此对于任何具有更多数字的数字,您只需得到近似值。有一些库可以更精确地处理大数字,包括this thread中提到的那些。

答案 5 :(得分:1)

2021 更新

处理科学和非科学表示的 Mike Samuel 的优化版本。

    id   color wave  like  dislike
0   01  yellow    1     0        7
1   01  yellow    2     0        7
2   01     red    1     1        1
3   01     red    2     1        1
4   01    blue    1     0        2
5   01    blue    2     0        3
6   02  yellow    1     0        2
7   02  yellow    2     1        1
8   02     red    1     1        1
9   02     red    2     0        2
10  02    blue    1     0        7
11  02    blue    2     0        7

答案 6 :(得分:0)

这对我有用

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

此方法期望该值为标准数字。

答案 7 :(得分:0)

昵称答案的优化版本。

该函数要求 n 是一个字符串。即使全为 0,此函数也会获取小数,例如 1.00 -> 2 位小数。

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

答案 8 :(得分:-1)

根据gion_13的回答,我想出了这个:

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));

当没有小数位时,它修复了返回1。据我所知,这可以毫无错误地工作。

相关问题