36

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

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

我的第一直觉是使用字符串表示: split on '.',然后 on '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 社区。你们当中有没有人知道一种可靠的方法来获得一个数字的真正的小数点后精度?

如果有人问,此函数的目的是用于将浮点数转换为简化分数的另一个函数(即,它返回相对互质的整数分子和非零自然分母)。这个外部函数中唯一缺少的部分是确定浮点数中小数位数的可靠方法,因此我可以将其乘以 10 的适当幂。希望我想多了。

4

10 回答 10

23

历史注释:下面的评论线程可能涉及第一个和第二个实现。我在 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 应用于 Number 类型

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

历史注释:下面的实现是有问题的。我把它留在这里作为评论线程的上下文。

根据 的定义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 (!)

于 2012-03-02T20:13:38.223 回答
11

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

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

所以,我想如果我逐渐将浮点数乘以 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));

于 2013-12-02T17:56:46.150 回答
4

2017 更新

这是基于 Edwin 的回答的简化版本。它有一个测试套件并返回正确的小数位数,包括 NaN、无穷大、指数符号和连续分数的有问题表示的数字,例如 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 )。

然而,这是我发现的第 5 个极端情况,在 0.0029、0.0408、0.1584 和 4.3573 之后。每次之后,我都必须将精度降低一位小数。我不知道是否还有其他小于 10 位小数的数字,此函数可能会返回错误的小数位数。为了安全起见,请查找任意精度库

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

于 2017-06-29T03:14:49.370 回答
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;
}
于 2012-03-02T20:08:03.870 回答
2

这对我有用

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

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

于 2019-07-19T01:54:35.183 回答
1

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

JavaScript 使用IEEE-754双精度(64 位)格式表示数字。据我了解,这为您提供了 53 位精度,或 15 到 16 个十进制数字。

因此,对于任何具有更多位数的数字,您只会得到一个近似值。周围有一些库可以更精确地处理大量数字,包括这个线程中提到的那些。

于 2012-03-02T20:29:44.990 回答
1

2021 更新

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

// 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

于 2021-05-23T00:28:07.853 回答
1

简单的“单线”

如果您正在做的事情需要超过16 位的精度,那么这适合您。

这种“单线”在另一种情况下可以正常工作99.99999999999999%(是的,即使是那个数字。)

function numDec(n){return n%1==0?0:(""+n).length-(""+n).lastIndexOf(".")-1}

片段中的演示:

function numDec(n){return n%1==0?0:(""+n).length-(""+n).lastIndexOf(".")-1}


setInterval(function(){
  n=Math.random()*10000000000;
  document.body.innerHTML=n+' ← '+numDec(n)+' decimal places';
},777);
body{font-size:123.4567890%; font-family:'fira code';}


更多信息:

于 2021-09-28T04:10:34.870 回答
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
于 2021-07-07T11:55:03.587 回答
-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。据我所知,这可以正常工作。

于 2013-12-26T15:45:15.220 回答