如何在JavaScript中避免使用科学计数法表示大数字?

303

当JavaScript在字符串上下文中使用时,它将大于21位数的整数转换为科学计数法。我正在将整数作为URL的一部分打印出来。如何防止发生此转换?


6
您希望将1E21显示为“1000000000000000000000”吗?您关心的是数字的显示方式还是存储方式? - outis
7
我关心它的显示方式:我有一个document.write(myvariable)命令。 - chris
3
我需要数字作为URL的一部分。 - chris
9
人类用户并不是唯一想要读取数字的群体。当遇到包含科学计数法坐标的"translate"转换时,D3似乎会抛出异常。 - O. R. Mapper
5
当你处理一定范围内的数字时,仍然有使用价值。毕竟,对于不了解科学计数法的用户来说,科学计数法将很难阅读。 - hippietrail
显示剩余2条评论
28个回答

189

有一个Number.toFixed方法,但如果数字大于等于1e21,则使用科学计数法,并且最大精度为20。除此之外,你可以自己编写,但会比较混乱。

function toFixed(x) {
  if (Math.abs(x) < 1.0) {
    var e = parseInt(x.toString().split('e-')[1]);
    if (e) {
        x *= Math.pow(10,e-1);
        x = '0.' + (new Array(e)).join('0') + x.toString().substring(2);
    }
  } else {
    var e = parseInt(x.toString().split('+')[1]);
    if (e > 20) {
        e -= 20;
        x /= Math.pow(10,e);
        x += (new Array(e+1)).join('0');
    }
  }
  return x;
}

上面使用了简单的字符串重复((new Array(n+1)).join(str))来实现。你可以使用俄罗斯农民乘法定义String.prototype.repeat并使用它代替。
这个答案只适用于问题的背景:在不使用科学计数法的情况下显示大数字。对于其他任何事情,你应该使用BigInt库,例如BigNumber、Leemon的BigIntBigInteger。未来,新的本地BigInt(注意:不是Leemon的)应该会可用;Chromium和基于它的浏览器(Chrome、新的Edge [v79+]、Brave)以及Firefox都支持;Safari的支持正在进行中。
以下是如何使用BigInt: BigInt(n).toString()

例子:

const n = 13523563246234613317632;
console.log("toFixed (wrong): " + n.toFixed());
console.log("BigInt (right):  " + BigInt(n).toString());

注意,如果你输出一个超过15-16位数字(具体来说,大于Number.MAX_SAFE_INTEGER + 1 [9,007,199,254,740,992])的JavaScript数字(不是BigInt),可能会被四舍五入,因为JavaScript的数字类型(IEEE-754双精度浮点数)无法精确地保存那些超过这个范围的所有整数。从Number.MAX_SAFE_INTEGER + 1开始,它按2的倍数工作,所以它不能再保存奇数(同样,在18,014,398,509,481,984处,它开始按照4、8、16等倍数工作)。

因此,如果你可以依赖于BigInt支持,请将数字输出为你传递给BigInt函数的字符串:

const n = BigInt("YourNumberHere");

例子:

const n1 = BigInt(18014398509481985); // WRONG, will round to 18014398509481984
                                      // before `BigInt` sees it
console.log(n1.toString() + " <== WRONG");
const n2 = BigInt("18014398509481985"); // RIGHT, BigInt handles it
console.log(n2.toString() + " <== Right");


2
你的解决方案给我2^1000的结果与wolframalpha非常不同。有什么建议吗? - Shane Reustle
5
@Shane:这个问答是关于将浮点数显示为十进制整数的,不涉及无法用浮点格式表示的数字(在转换为十进制时会出现此类情况)。你需要一个JS bigint库,正如最后一行提到的那样。 - outis
1
@PeterOlson:看起来我忘记加上一个 Math.abs了。谢谢你提醒我。 - outis
3
实际上,这段代码在处理非常小的负数时无法正常工作:toFixed( -1E-20 ) -> "0.0000000000000000000.09999999999999999"。 - davidthings
1
此外,任何大于 Number.MAX_SAFE_INTEGER 的数字可能(但很可能不会)被准确表示。 - Pac0
显示剩余7条评论

130

我知道这是一个较旧的问题,但最近有活动。 MDN toLocaleString

const myNumb = 1000000000000000000000;
console.log( myNumb ); // 1e+21
console.log( myNumb.toLocaleString() ); // "1,000,000,000,000,000,000,000"
console.log( myNumb.toLocaleString('fullwide', {useGrouping:false}) ); // "1000000000000000000000"

您可以使用选项来格式化输出。

注意:

Number.toLocaleString() 保留小数点后16位,因此会四舍五入,以便...

const myNumb = 586084736227728377283728272309128120398;
console.log( myNumb.toLocaleString('fullwide', { useGrouping: false }) );

...返回...

586084736227728400000000000000000000000

如果精度对于预期结果很重要,那么这可能是不可取的。


5
这似乎不适用于非常小的小数: var myNumb = 0.0000000001; console.log( myNumb.toLocaleString('fullwide', { useGrouping: false }) ); - eh1160
6
可以使用maximumSignificantDigits选项(最大21)来格式化非常小的十进制数。例如:console.log( myNumb.toLocaleString('fullwide', { useGrouping: true, maximumSignificantDigits:6}) ); - Sir. MaNiAl
maximumFractionDigits defaults to 3, so I think the actual sol'n is value.toLocaleString('fullwide',{useGrouping:false,maximumFractionDigits:20}) - mpen
4
16 位小数舍入不是 toLocaleString() 的限制,而是 Number 类型的限制。Number.MAX_SAFE_INTEGER = 9007199254740991 - forresthopkinsa
4
使用 fullwide 样式可能导致结果不稳定,这取决于用户的语言环境。不同的语言环境可以使用句点或逗号作为分隔符。使用版本 toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 }) 是否更好呢?请注意,翻译时不能改变原文的含义,并且不提供任何额外的解释。 - Dartess
显示剩余3条评论

38

对于小数位较少且已知精度要求的数字,可以使用 toFixed 方法,并使用正则表达式去除尾部多余的零。

Number(1e-7).toFixed(8).replace(/\.?0+$/,"") //0.000

3
请注意避免将0作为toFixed调用的参数 - 这将导致删除重要的尾随零:(1000).toFixed(0).replace(/\.?0+$/,"") // 1, 而不是 1000 - Egor Nepomnyaschih
一个更强大的正则表达式是 Number(1e-7).toFixed(8).replace(/(?<=\.\d*[1-9])0+$|\.0*$/,"")(它只会删除小数点右侧最不重要的零位数字)。 - MT0

20

该文章的问题是如何避免科学计数法,并将数字作为普通数字。

因此,如果只需将e(科学)计数法数字转换为普通数字(包括分数情况下),而不失精度,则必须避免使用Math对象和其他Javascript数字方法,以避免在处理大数字和大分数时发生四舍五入(这总是由于二进制格式的内部存储而发生)。

以下函数将e(科学)计数法数字转换为普通数字(包括分数),处理大数字和大分数而不损失精度,因为它不使用内置的math和number函数来处理或操作数字。

该函数还处理正常数字,因此可以将预计成为'e'符号的数字传递给该函数进行修复。

该函数应与不同的本地十进位点配合使用。

提供了94个测试用例。

对于大型e-notation数字,请将数字作为字符串传递。

示例:

eToNumber("123456789123456789.111122223333444455556666777788889999e+50");
// output:
"12345678912345678911112222333344445555666677778888999900000000000000"

eToNumber("123.456123456789123456895e-80");
// output:
"0.00000000000000000000000000000000000000000000000000000000000000000000000000000123456123456789123456895"

eToNumber("123456789123456789.111122223333444455556666777788889999e-50");
// output:
"0.00000000000000000000000000000000123456789123456789111122223333444455556666777788889999"

Javascript中有效的科学计数法数字包括以下内容:

123e1   ==> 1230
123E1   ==> 1230
123e+1  ==> 1230
123.e+1 ==> 1230
123e-1  ==> 12.3
0.1e-1  ==> 0.01
.1e-1   ==> 0.01
-123e1  ==> -1230

/******************************************************************
 * Converts e-Notation Numbers to Plain Numbers
 ******************************************************************
 * @function eToNumber(number)
 * @version  1.00
 * @param   {e nottation Number} valid Number in exponent format.
 *          pass number as a string for very large 'e' numbers or with large fractions
 *          (none 'e' number returned as is).
 * @return  {string}  a decimal number string.
 * @author  Mohsen Alyafei
 * @date    17 Jan 2020
 * Note: No check is made for NaN or undefined input numbers.
 *
 *****************************************************************/
function eToNumber(num) {
  let sign = "";
  (num += "").charAt(0) == "-" && (num = num.substring(1), sign = "-");
  let arr = num.split(/[e]/ig);
  if (arr.length < 2) return sign + num;
  let dot = (.1).toLocaleString().substr(1, 1), n = arr[0], exp = +arr[1],
      w = (n = n.replace(/^0+/, '')).replace(dot, ''),
    pos = n.split(dot)[1] ? n.indexOf(dot) + exp : w.length + exp,
    L   = pos - w.length, s = "" + BigInt(w);
    w   = exp >= 0 ? (L >= 0 ? s + "0".repeat(L) : r()) : (pos <= 0 ? "0" + dot + "0".repeat(Math.abs(pos)) + s : r());
  L= w.split(dot); if (L[0]==0 && L[1]==0 || (+w==0 && +s==0) ) w = 0; //** added 9/10/2021
  return sign + w;
  function r() {return w.replace(new RegExp(`^(.{${pos}})(.)`), `$1${dot}$2`)}
}
//*****************************************************************

//================================================
//             Test Cases
//================================================
let r = 0; // test tracker
r |= test(1, "123456789123456789.111122223333444455556666777788889999e+50", "12345678912345678911112222333344445555666677778888999900000000000000");
r |= test(2, "123456789123456789.111122223333444455556666777788889999e-50", "0.00000000000000000000000000000000123456789123456789111122223333444455556666777788889999");
r |= test(3, "123456789e3", "123456789000");
r |= test(4, "123456789e1", "1234567890");
r |= test(5, "1.123e3", "1123");
r |= test(6, "12.123e3", "12123");
r |= test(7, "1.1234e1", "11.234");
r |= test(8, "1.1234e4", "11234");
r |= test(9, "1.1234e5", "112340");
r |= test(10, "123e+0", "123");
r |= test(11, "123E0", "123");
// //============================
r |= test(12, "123e-1", "12.3");
r |= test(13, "123e-2", "1.23");
r |= test(14, "123e-3", "0.123");
r |= test(15, "123e-4", "0.0123");
r |= test(16, "123e-2", "1.23");
r |= test(17, "12345.678e-1", "1234.5678");
r |= test(18, "12345.678e-5", "0.12345678");
r |= test(19, "12345.678e-6", "0.012345678");
r |= test(20, "123.4e-2", "1.234");
r |= test(21, "123.4e-3", "0.1234");
r |= test(22, "123.4e-4", "0.01234");
r |= test(23, "-123e+0", "-123");
r |= test(24, "123e1", "1230");
r |= test(25, "123e3", "123000");
r |= test(26, -1e33, "-1000000000000000000000000000000000");
r |= test(27, "123e+3", "123000");
r |= test(28, "123E+7", "1230000000");
r |= test(29, "-123.456e+1", "-1234.56");
r |= test(30, "-1.0e+1", "-10");
r |= test(31, "-1.e+1", "-10");
r |= test(32, "-1e+1", "-10");
r |= test(34, "-0", "-0");
r |= test(37, "0e0", "0");
r |= test(38, "123.456e+4", "1234560");
r |= test(39, "123E-0", "123");
r |= test(40, "123.456e+50", "12345600000000000000000000000000000000000000000000000");

r |= test(41, "123e-0", "123");
r |= test(42, "123e-1", "12.3");
r |= test(43, "123e-3", "0.123");
r |= test(44, "123.456E-1", "12.3456");
r |= test(45, "123.456123456789123456895e-80", "0.00000000000000000000000000000000000000000000000000000000000000000000000000000123456123456789123456895");
r |= test(46, "-123.456e-50", "-0.00000000000000000000000000000000000000000000000123456");

r |= test(47, "-0e+1", "-0");
r |= test(48, "0e+1", "0");
r |= test(49, "0.1e+1", "1");
r |= test(50, "-0.01e+1", "-0.1");
r |= test(51, "0.01e+1", "0.1");
r |= test(52, "-123e-7", "-0.0000123");
r |= test(53, "123.456e-4", "0.0123456");

r |= test(54, "1.e-5", "0.00001"); // handle missing base fractional part
r |= test(55, ".123e3", "123"); // handle missing base whole part

// The Electron's Mass:
r |= test(56, "9.10938356e-31", "0.000000000000000000000000000000910938356");
// The Earth's Mass:
r |= test(57, "5.9724e+24", "5972400000000000000000000");
// Planck constant:
r |= test(58, "6.62607015e-34", "0.000000000000000000000000000000000662607015");

r |= test(59, "0.000e3", "0");
r |= test(60, "0.000000000000000e3", "0");
r |= test(61, "-0.0001e+9", "-100000");
r |= test(62, "-0.0e1", "-0");
r |= test(63, "-0.0000e1", "-0");


r |= test(64, "1.2000e0", "1.2000");
r |= test(65, "1.2000e-0", "1.2000");
r |= test(66, "1.2000e+0", "1.2000");
r |= test(67, "1.2000e+10", "12000000000");
r |= test(68, "1.12356789445566771234e2", "112.356789445566771234");

// ------------- testing for Non e-Notation Numbers -------------
r |= test(69, "12345.7898", "12345.7898") // no exponent
r |= test(70, 12345.7898, "12345.7898") // no exponent
r |= test(71, 0.00000000000001, "0.00000000000001") // from 1e-14
r |= test(72, 0.0000000000001, "0.0000000000001") // from 1e-13
r |= test(73, 0.000000000001, "0.000000000001") // from 1e-12
r |= test(74, 0.00000000001, "0.00000000001") // from 1e-11
r |= test(75, 0.0000000001, "0.0000000001") // from 1e-10
r |= test(76, 0.000000001, "0.000000001") // from 1e-9
r |= test(77, 0.00000001, "0.00000001") // from 1e-8
r |= test(78, 0.0000001, "0.0000001") // from 1e-7
r |= test(79, 1e-7, "0.0000001") // from 1e-7
r |= test(80, -0.0000001, "-0.0000001") // from 1e-7
r |= test(81, 0.0000005, "0.0000005") // from 1e-7
r |= test(82, 0.1000005, "0.1000005") // from 1e-7
r |= test(83, 1e-6, "0.000001") // from 1e-6

r |= test(84, 0.000001, "0.000001"); // from 1e-6
r |= test(85, 0.00001, "0.00001"); // from 1e-5
r |= test(86, 0.0001, "0.0001"); // from 1e-4
r |= test(87, 0.001, "0.001"); // from 1e-3
r |= test(88, 0.01, "0.01"); // from 1e-2
r |= test(89, 0.1, "0.1") // from 1e-1
r |= test(90, -0.0000000000000345, "-0.0000000000000345"); // from -3.45e-14
r |= test(91, -0, "0");
r |= test(92, "-0", "-0");
r |= test(93,2e64,"20000000000000000000000000000000000000000000000000000000000000000");
r |= test(94,"2830869077153280552556547081187254342445169156730","2830869077153280552556547081187254342445169156730");

if (r == 0) console.log("All 94 tests passed.");

//================================================
//             Test function
//================================================
function test(testNumber, n1, should) {
  let result = eToNumber(n1);
  if (result !== should) {
    console.log(`Test ${testNumber} Failed. Output: ${result}\n             Should be: ${should}`);
    return 1;
  }
}


我们确定这在每个国家都能正常工作吗?我有点担心toLocaleString(),但我无法测试它。可能没有人会测试,所以我们需要依靠对该函数的理解,并确保其稳定性。 - FlorianB
1
@FlorianB 我已经使用DevTools进行了广泛的测试,并在小数点为“.”或“,”时正常工作。发现一个非常奇怪的情况,即当数字为0,00(意思是0.00)时进行了修复。 - Mohsen Alyafei
1
你在这段代码上做得非常好。问题是:你是否也编写了一个将其转换回相反方向的工具?如果有的话,我也想看看。 - Lonnie Best
@LonnieBest 我认为我需要改进和优化它,并在时间允许时做相反的事情。谢谢。 - Mohsen Alyafei
我不知道是否有什么遗漏,但是在我的情况下,加入console.log(eToNumber(9999999999999999999999999999999999999999999))确实会四舍五入为"10000000000000000000000000000000000000000000"。 - RoboKy
@RoboKy 这是因为你将一个非常大的数字传递给了函数。在你的情况下,Javascript将这样的数字转换为1e+43。尝试使用console.log(9999999999999999999999999999999999999999999);打印这个数字并查看结果。为了避免这种情况,请将大数字作为字符串传递给函数,例如console.log(eToNumber("9999999999999999999999999999999999999999999")),这将得到正确的输出。 - undefined

16
使用正则表达式解决问题。这种方法没有精度问题,而且代码量不多。

function toPlainString(num) {
  return (''+ +num).replace(/(-?)(\d*)\.?(\d*)e([+-]\d+)/,
    function(a,b,c,d,e) {
      return e < 0
        ? b + '0.' + Array(1-e-c.length).join(0) + c + d
        : b + c + d + Array(e-d.length+1).join(0);
    });
}

console.log(toPlainString(12345e+12));
console.log(toPlainString(12345e+24));
console.log(toPlainString(-12345e+24));
console.log(toPlainString(12345e-12));
console.log(toPlainString(123e-12));
console.log(toPlainString(-123e-12));
console.log(toPlainString(-123.45e-56));
console.log(toPlainString('1e-8'));
console.log(toPlainString('1.0e-8'));


3
这适用于带引号的普通数字、不带引号的普通数字、带引号的科学计数法和不带引号的科学计数法。 - a_a
1
仍然存在问题。尝试: console.log(toPlainString("1e-5")); console.log(toPlainString("1e-6")); console.log(toPlainString("1e-7")); 虽然1、2、3、4、5、6都正常工作,但1e-7突然跳到了8个小数位。 - otterslide
@AdamLeggett 目前仍无法将以下数字转换为正确结果:1e-7 应该是 0.0000001,但实际上输出的是 0.00000001,即多了一个零。同样的问题也出现在 1e-8、1e-9 以及更小的数值中。 - Mohsen Alyafei
2
@MohsenAlyafei (-0).toString() 返回 "0",我并不想在这里进一步扩展科学计数法。这需要明确测试 -0 或使用 toLocaleString/Intl.NumberFormat。 - Adam Leggett
1
@MohsenAlyafei 0 === -0 - dsasmblr
显示剩余3条评论

15

另一个可能的解决方案:

function toFix(i){
 var str='';
 do{
   let a = i%10;
   i=Math.trunc(i/10);
   str = a+str;
 }while(i>0)
 return str;
}

7
这并未保留原始值...例如31415926535897932384626433832795变成了31415926535897938480804068462624。 - zero_cool
不确定,但可能与JavaScript如何处理大数字有关。 - zero_cool
1
@domsson,基本上是因为IEEE浮点数算术运算的应用。在Javascript中,数字实际上被表示为浮点数以及“十进制数”,没有本地的“整数精确类型”。您可以在此处阅读更多信息:https://medium.com/dailyjs/javascripts-number-type-8d59199db1b6 - Pac0
1
@zero_cool 任何大于Number.MAX_SAFE_INTEGER的数字可能会遇到这个问题。 - Pac0

13

这是一个适用于任意数字的Number.prototype.toFixed 方法的简短变体:

Number.prototype.toFixedSpecial = function(n) {
  var str = this.toFixed(n);
  if (str.indexOf('e+') === -1)
    return str;

  // if number is in scientific notation, pick (b)ase and (p)ower
  str = str.replace('.', '').split('e+').reduce(function(b, p) {
    return b + Array(p - b.length + 2).join(0);
  });
  
  if (n > 0)
    str += '.' + Array(n + 1).join(0);
  
  return str;
};

console.log( 1e21.toFixedSpecial(2) );       // "1000000000000000000000.00"
console.log( 2.1e24.toFixedSpecial(0) );     // "2100000000000000000000000"
console.log( 1234567..toFixedSpecial(1) );   // "1234567.0"
console.log( 1234567.89.toFixedSpecial(3) ); // "1234567.890"


5
当然它们不相等,因为第一个是格式化的字符串,而第二个是数字。如果你把第一个转换成数字,你会发现它们是完全相等的:https://jsfiddle.net/qd6hpnyx/1/。你可以撤回你的踩赞了`:P`。 - VisioN
最好也将末尾的“.0”替换为空字符串。 - vsync
当我尝试 console.log(2.2e307.toFixedSpecial(10)) 时,我得到了一个有些奇怪的值。我的意思是...我得到了几个尾随零。无论如何,我还是点赞了,因为这似乎是我需要的最接近的答案。 - peter.petrov
@peter.petrov 是的,你得到了 .0000000000 是因为你指定了参数 10。如果你想要去掉它们,使用 0 - VisioN
@VisioN 哦,好的,谢谢。我以为这个参数是用来指定数字的基数的。 - peter.petrov
显示剩余3条评论

7
以下解决方案可以避免非常大和非常小的数字自动使用指数格式。这是outis的解决方案(链接),其中包含一个错误修复:它不能用于非常小的负数。

function numberToString(num)
{
    let numStr = String(num);

    if (Math.abs(num) < 1.0)
    {
        let e = parseInt(num.toString().split('e-')[1]);
        if (e)
        {
            let negative = num < 0;
            if (negative) num *= -1
            num *= Math.pow(10, e - 1);
            numStr = '0.' + (new Array(e)).join('0') + num.toString().substring(2);
            if (negative) numStr = "-" + numStr;
        }
    }
    else
    {
        let e = parseInt(num.toString().split('+')[1]);
        if (e > 20)
        {
            e -= 20;
            num /= Math.pow(10, e);
            numStr = num.toString() + (new Array(e + 1)).join('0');
        }
    }

    return numStr;
}

// testing ...
console.log(numberToString(+0.0000000000000000001));
console.log(numberToString(-0.0000000000000000001));
console.log(numberToString(+314564649798762418795));
console.log(numberToString(-314564649798762418795));


1
numberToString(0.0000002) "0.00000019999999999999998" - sigmaxf
1
@raphadko 这是臭名昭著的Javascript 浮点数精度问题,当在JS中使用数字时,这是另一个完全不同的痛点...例如,请参见https://dev59.com/tXM_5IYBdhLWcg3wQQpd - user56reinstatemonica8

4
你可以使用 from-exponential 模块。它轻量且经过充分测试。
import fromExponential from 'from-exponential';

fromExponential(1.123e-10); // => '0.0000000001123'

4
其他人的回答并不能给出准确的数字!
此功能可以精确计算所需数字,并将其返回为字符串,以防止被javascript更改!
如果您需要数值结果,只需将函数结果乘以1即可!
function toNonExponential(value) {
    // if value is not a number try to convert it to number
    if (typeof value !== "number") {
        value = parseFloat(value);

        // after convert, if value is not a number return empty string
        if (isNaN(value)) {
            return "";
        }
    }

    var sign;
    var e;

    // if value is negative, save "-" in sign variable and calculate the absolute value
    if (value < 0) {
        sign = "-";
        value = Math.abs(value);
    }
    else {
        sign = "";
    }

    // if value is between 0 and 1
    if (value < 1.0) {
        // get e value
        e = parseInt(value.toString().split('e-')[1]);

        // if value is exponential convert it to non exponential
        if (e) {
            value *= Math.pow(10, e - 1);
            value = '0.' + (new Array(e)).join('0') + value.toString().substring(2);
        }
    }
    else {
        // get e value
        e = parseInt(value.toString().split('e+')[1]);

        // if value is exponential convert it to non exponential
        if (e) {
            value /= Math.pow(10, e);
            value += (new Array(e + 1)).join('0');
        }
    }

    // if value has negative sign, add to it
    return sign + value;
}

关于这段代码,一些额外的信息可能会很有用。 - Pyves
函数中添加的注释。 - user1297556

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接