向上或向下舍入时为 0.5

Rounding up or down when 0.5

提问人:Merc 提问时间:3/11/2019 更新时间:3/14/2019 访问量:1987

问:

我在达到 0.5 时对 Javascript 进行四舍五入的方式有问题。 我正在编写征税计算器,并注意到结果中有 0.1c 的差异。

问题是,他们的结果是我的应用程序转化为 ,而关税说.21480.70521480.7121480.70

这是我在 Javascript 中看到的:

(21480.105).toFixed(2)
"21480.10"
(21480.205).toFixed(2)
"21480.21"
(21480.305).toFixed(2)
"21480.31"
(21480.405).toFixed(2)
"21480.40"
(21480.505).toFixed(2)
"21480.51"
(21480.605).toFixed(2)
"21480.60"
(21480.705).toFixed(2)
"21480.71"
(21480.805).toFixed(2)
"21480.81"
(21480.905).toFixed(2)
"21480.90"

问题:

  • 这种不稳定的路由到底是怎么回事?
  • 获得“四舍五入”结果的最快、最简单的方法是什么(当达到 0.5 时)?
JavaScript 数学 舍入 精度

评论

2赞 Robby Cornelissen 3/11/2019
对问题 1 的答复
1赞 Jonas Wilms 3/11/2019
不过,@robby被正确记录下来。console.log(21480.105)
0赞 Jonas Wilms 3/11/2019
@kaiido我的观点是数字本身是准确的,这是 Fixed 使用的舍入算法中的一个缺陷(或者我完全错了)?
0赞 kumesana 3/11/2019
@JonasWilms完全错了。你让自己被 JavaScript 关于如何将浮点值转换为字符串的策略所欺骗。它将尝试显示比变量中存储的实际数字短的内容。实际数字并不完全是 21480.105,但这就是显示的数字。这应该是你的线索,表明你的推理遗漏了一些东西。
0赞 Joe 3/11/2019
永远不要使用浮点数进行需要精确结果的计算,例如,在使用货币时。

答:

0赞 Jonas Wilms 3/11/2019 #1

您可以四舍五入为整数,然后在显示以下内容时以逗号移动:

function round(n, digits = 2) {
  // rounding to an integer is accurate in more cases, shift left by "digits" to get the number of digits behind the comma
  const str = "" + Math.round(n * 10 ** digits);

  return str
    .padStart(digits + 1, "0") // ensure there are enough digits, 0 -> 000 -> 0.00
    .slice(0, -digits) + "." + str.slice(-digits); // add a comma at "digits" counted from the end
}
1赞 Cat 3/11/2019 #2

这在大多数情况下都有效。(请参阅下面的注释。

舍入问题可以通过使用 指数表示法:

function round(value, decimals) {
  return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
}

console.log(round(21480.105, 2).toFixed(2));

http://www.jacklmoore.com/notes/rounding-in-javascript/ 找到

注意:正如 Mark Dickinson 所指出的,这不是一个通用的解决方案,因为它在某些情况下会返回 NaN,例如 和 具有较大的输入。 欢迎进行编辑以使其更加健壮。round(0.0000001, 2)

评论

0赞 Merc 3/12/2019
我需要四舍五入到 ,而不是(因为这是端口计算器似乎正在做的事情)。啊21480.70521480.7021480.71
0赞 Mark Dickinson 3/13/2019
当您尝试此解决方案时会发生什么?value = 0.000001
0赞 Cat 3/14/2019
@MarkDickinson我进入了片段。你得到不同的结果吗?-- 或者,如果关注的是精确性,你会得到更多的调用(和)与不同的参数(0.00roundtoFixedround(0.123456, 5).toFixed(5)
0赞 Cat 3/14/2019
@Merc 要想出一个与端口计算器的输出相匹配的“正确”脚本,你需要知道它遵循的规则。按照 的思路添加一个异常很容易(尽管这个确切的逻辑与你的原始代码的原因不同,但你明白了)——但除非你能预见到所有这些异常,否则两种算法之间会出现不匹配。if(val%1==0.705){ return roundDownForNoGoodReason(val); }
0赞 Mark Dickinson 3/14/2019
@Cat:对不起,我错过了一个零。尝试,你会得到 ,这不会很好地舍入:我得到的结果。大输入也有同样的问题。因此,这不是一个通用的解决方案:它有一些局限性,可能值得记录。0.0000001 + 'e' + 2"1e-7e2"NaNMath.round(0.0000001 + 'e' + 2)
1赞 kumesana 3/11/2019 #3

原因 -

您可能听说过,在某些语言(例如 JavaScript)中,带有小数部分的数字调用浮点数,而浮点数则用于处理数值运算的近似值。不是精确的计算,近似值。因为您期望如何计算和存储 1/3 或 2 的平方根,并进行精确的计算?

如果你没有,那么现在你已经听说过了。

这意味着,当您键入数字文字 21480.105 时,最终存储在计算机内存中的实际值实际上不是 21480.105,而是它的近似值。最接近 21480.105 的值,可以表示为浮点数。

由于这个值不完全是 21480.105,这意味着它要么略高于该值,要么略低于该值。正如预期的那样,更多的将被四舍五入,更少的将被四舍五入。

解决方案 -

你的问题来自近似值,似乎你负担不起。解决方案是使用精确的数字,而不是近似值。

使用整数。这些都是准确的。将数字转换为字符串时添加小数点。

0赞 HumbleOne 3/11/2019 #4

这种不稳定的路由到底是怎么回事?

请参考警告性的 Mozilla 文档,其中确定了这些差异的原因。“浮点数不能以二进制精确表示所有小数,这可能会导致意想不到的结果......”

另外,请参考浮点数学坏了吗? (感谢 Robby Cornelissen 提供参考)

获得“四舍五入”结果的最快、最简单的方法是什么(当达到 0.5 时)?

使用像会计 .js 这样的 JS 库来舍入、格式化和呈现货币。

例如。。。

function roundToNearestCent(rawValue) {
  return accounting.toFixed(rawValue, 2);
}

const roundedValue = roundToNearestCent(21480.105);
console.log(roundedValue);
<script src="https://combinatronics.com/openexchangerates/accounting.js/master/accounting.js"></script>

另外,请考虑查看 JavaScript 中的 BigDecimal

希望对您有所帮助!

评论

0赞 Wiimm 3/11/2019
(Math.round(21480.105*100)/100).toPrecision(18)由于四舍五入而产生。这并不意味着这适用于此类型的所有计算。"21480.1100000000006"
0赞 HumbleOne 3/12/2019
toPrecision() 导致精度损失,而不是本答案中提出的方法。请参阅答案中提供的前 2 个链接,以更好地理解为什么浮点可能无法正确存储所有十进制值。
0赞 Wiimm 3/12/2019
并产生.如果显示 2 个小数数字,则使用它再次显示,因为 1.005*100 是 100.4999 并四舍五入。所以它无济于事!Math.round(1.005*100)/1001(Math.round(1.005*100)/100).toFixed(2)1.00
0赞 HumbleOne 3/12/2019
观点很好。我已经修改了我的答案来解释这一点。
0赞 Wiimm 3/12/2019
然后想一想:浮点数是根据 2 的幂存储的。因此,不是 n/p 且 p 倍数为 2 的小数总是不准确,而是四舍五入的。每个数学运算(加法、多重、div,..)都会使结果更加不准确。
3赞 visibleman 3/11/2019 #5

因此,正如其他一些人已经解释的那样,“不稳定”舍入的原因是浮点精度问题。您可以使用 JavaScript 数字的方法对此进行调查。toExponential()

(21480.905).toExponential(20)
#>"2.14809049999999988358e+4"
(21480.805).toExponential(20)
#>"2.14808050000000002910e+4"

正如你在这里看到的,得到一个略小于 的双精度表示,而得到一个略大于原始值的双精度表示。由于该方法适用于双重表示形式,并且不知道您的原始预期值,因此它会尽其所能并且应该使用它所拥有的信息来执行所有操作。21480.90521480.90521480.805toFixed()

解决此问题的一种方法是将小数点移动到乘法所需的小数位数,然后使用标准,然后再次将小数点移回,通过除法或乘以倒数。最后,我们调用方法以确保输出值正确填充零。Math.round()toFixed()

var x1 = 21480.905;
var x2 = -21480.705;

function round_up(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); // Or you can just use 1/rup
  return (Math.round(x*rup)*rdwn).toFixed(nd)
}
function round_down(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); 
  return (Math.round(x*-rup)*-rdwn).toFixed(nd)
}

function round_tozero(x,nd)
{
   return x>0?round_down(x,nd):round_up(x,nd) 
}



console.log(x1,'up',round_up(x1,2));
console.log(x1,'down',round_down(x1,2));
console.log(x1,'to0',round_tozero(x1,2));

console.log(x2,'up',round_up(x2,2));
console.log(x2,'down',round_down(x2,2));
console.log(x2,'to0',round_tozero(x2,2));

最后: 遇到这样的问题通常是坐下来好好想想你是否真的对你的问题使用了正确的数据类型。由于浮点错误会随着迭代计算而累积,而且由于人们有时对货币神奇地消失/出现在 CPU 中非常敏感,也许您最好将货币计数器保持在整数“美分”(或其他一些经过深思熟虑的结构)而不是浮点“美元”。

评论

0赞 Merc 3/12/2019
我需要四舍五入到 ,而不是(因为这是端口计算器似乎正在做的事情)。啊21480.70521480.7021480.71
0赞 visibleman 3/12/2019
使用负 rup,rdwn 将向下舍入 0.5 而不是向上,但请注意,对于负数(而不是向零),它也会向下舍入。如果您总是想将 0.5 个案例四舍五入为零,您可以为负数添加一个 if 大小写
1赞 Merc 3/12/2019
你能把它变成一个函数吗?如果你这样做了,我认为这应该是公认的答案,因为它是最灵活的,也是最不黑客的
0赞 visibleman 3/12/2019
@Merc,我已经将它的答案编辑为函数。函数名称有些欺骗性,但它们描述了如何处理 0.5 大小写,而不是一般的大小写舍入功能。