为什么JavaScript中的模运算符会返回小数?

39

为什么在 JavaScript 中,49.90 % 0.10 返回的结果是 0.09999999999999581 ? 我原以为它应该是 0。


3
请参见 Is JavaScript's Math broken? - Matthew Flaschen
查看:Python浮点数取模(不同的语言,但问题——浮点数误差——是一样的)See: Python modulo on floats - user202729
8个回答

61

因为 JavaScript 使用浮点数运算,可能会导致舍入误差。

如果您需要精确的结果保留两位小数,可以在进行运算之前将数字乘以100,然后再在操作后除以100

var result = ( 4990 % 10 ) / 100;

必要时进行四舍五入。


3
在许多金融系统中,这是处理货币内部的方式(乘以100),在呈现时应用货币规则。 - Mic
啊,好棒的技巧,通过先将数字转换为整数来避免浮点数。如此美妙而简单。 - Avatar
这也被称为“定点算术”,因为您始终拥有2(或N)个小数位。 - Aaron Digulla
1
感谢您的见解,但这种方法并不完美。(40.55 * 100 %1000)/100 返回的是0.549999...而不是0.55。 - Hiroki
2
@hiroki,答案中提到“必要时四舍五入”,我建议在输出时加上toFixed(2)。toFixed已经进行了四舍五入。 - Simone-Cu

22

我将在此留下此内容以供将来参考,但是这里有一个方便的函数,可以更精确地处理涉及浮点数的余数(因为JS没有模运算符)。

  function floatSafeRemainder(val, step){
    var valDecCount = (val.toString().split('.')[1] || '').length;
    var stepDecCount = (step.toString().split('.')[1] || '').length;
    var decCount = valDecCount > stepDecCount? valDecCount : stepDecCount;
    var valInt = parseInt(val.toFixed(decCount).replace('.',''));
    var stepInt = parseInt(step.toFixed(decCount).replace('.',''));
    return (valInt % stepInt) / Math.pow(10, decCount);
  }

$(function() {
  
  
  function floatSafeModulus(val, step) {
    var valDecCount = (val.toString().split('.')[1] || '').length;
    var stepDecCount = (step.toString().split('.')[1] || '').length;
    var decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
    var valInt = parseInt(val.toFixed(decCount).replace('.', ''));
    var stepInt = parseInt(step.toFixed(decCount).replace('.', ''));
    return (valInt % stepInt) / Math.pow(10, decCount);
  }
  
  
  $("#form").submit(function(e) {
    e.preventDefault();
    var safe = 'Invalid';
    var normal = 'Invalid';
    var var1 = parseFloat($('#var1').val());
    var var2 = parseFloat($('#var2').val());
    if (!isNaN(var1) && !isNaN(var2)) {
      safe = floatSafeModulus(var1, var2);
      normal = var1 % var2
    }
    $('#safeResult').text(safe);
    $('#normalResult').text(normal);
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<form id="form" novalidate>
  <div>
    <input type="number" id="var1">%
    <input type="number" id="var2">
  </div>
  <div>safe: <span id="safeResult"></span><div>
  <div>normal (%): <span id="normalResult"></span></div>
  <input type="submit" value="try it out">
</form>


2
这个答案被低估了。虽然有点啰嗦,但脚本更加健壮。 - Domino
可能看起来有点吓人,但请花时间仔细阅读。它非常有效。我唯一想补充的是:如果您关心负数,JavaScript 取模运算不会像您期望的那样处理它们。您可以将返回值更改为:return (((valInt % stepInt) + stepInt) % stepInt) / Math.pow(10, decCount) - James

20

Javascript的Number类型使用“IEEE双精度”来存储值。它们不能精确地存储所有十进制数。由于将十进制数转换为二进制时存在舍入误差,因此结果并非零。

49.90 = 49.89999999999999857891452848...
 0.10 =  0.10000000000000000555111512...

因此,floor(49.90/0.10)仅为498,余数将为0.09999 ...


看起来您正在使用数字来存储美元金额。 不要这样做,因为浮点运算会传播和放大舍入误差。相反,应该将金额存储为美分的数量。整数可以被精确表示,4990%10将返回0。


3

原因

浮点数无法精确存储所有小数值。因此,使用浮点格式时,输入值始终会存在舍入误差。 输入的误差当然会导致输出的误差。对于离散函数或运算符,其在离散点周围的输出可能存在很大差异。模运算是一个离散操作,您的情况显然就是这个问题的一个例子。

浮点值的输入和输出

因此,在使用浮点变量时,您应该始终意识到这一点。任何使用浮点数进行计算的输出都应该在显示之前进行格式化/调整,以充分考虑这一点。
当仅使用连续函数和运算符时,将其四舍五入到所需精度通常就足够了(不要截断)。用于将浮点数转换为字符串的标准格式功能通常会自动完成此操作。
为了根据期望的输入精度和输出精度得出正确的输出,您还应该:

  • 将输入值舍入到期望的精度或确保没有可以输入更高精度的值。
  • 在舍入/格式化输出之前添加一个小值,该值小于或等于所需精度的1/4,并且大于舍入误差和计算期间产生的最大预期误差。如果不可能完成这一步骤,则所使用数据类型的精度组合不足以提供所需的输出精度。

这两件事情通常被忽略,对于大多数用户来说,由于没有执行这些操作而导致的差异太小,不会对其产生重要影响,但我曾经遇到过一个项目,如果没有进行这些更正,用户将不接受输出结果。

离散函数或运算符(例如模运算)

当涉及离散运算符或函数时,可能需要额外的更正才能确保输出如预期。舍入并在四舍五入之前添加小的更正无法解决问题。
可能需要在应用离散函数或运算符后立即对中间计算结果进行特殊检查/更正。

此问题的具体情况

在此情况下,您期望输入具有一定的精度,因此可以通过纠正舍入误差的输出来解决问题,这种误差比所需的精度小得多。

如果我们说你的数据类型的精度是e。
则您输入的值将不会存储为您输入的a和b的值,而是作为a*(1+/-e)和b*(1+/-e)。
将a*(1+/-e)除以b*(1+/-e)的结果将导致(a/b)(1+/-2e)。
模数函数必须截断结果并再次相乘。 因此,结果将为(a/b
b)(1+/-3e)=a(1+/-3e),导致a*3e的误差。
这个模加上了a*3e的可能误差,因为减去具有可能误差为a*3e和a*e的2个值。
因此,您应该检查总可能误差a*4e是否小于所需精度,如果满足该条件并且结果与b的差异不超过其最大可能误差,则可以安全地将其替换为0。

最好避免出现问题

通常更有效的方法是通过使用整数或固定点格式等可以存储预期输入而不会产生舍入误差的数据类型来进行此类计算。一个例子就是您永远不应该在财务计算中使用浮点值。

1

看一下浮点数及其缺点 - 像0.1这样的数字无法正确保存为浮点数,因此总会出现这样的问题。将您的数字乘以*10或*100,并使用整数进行计算。


0
希望这对某人有所帮助:
let asBuffer = new ArrayBuffer(8);
let asInt = new Uint32Array(asBuffer );
let asFloat = new Float32Array(asBuffer );
asFloat.set([49.9,.1]);
asInt.set([asFloat[0]/asFloat[1], asFloat[0]%asFloat[1]]);

Result

asInt Uint32Array(2) [499, 0, buffer: ArrayBuffer(8), 
byteLength: 8, byteOffset: 0, length: 2, 
Symbol(Symbol.toStringTag): 'Uint32Array']
//In general I'm moving more and more towards 
//Using typed arrays to store values in JS.

解释会非常方便。 - madfcat
解释会非常方便。 - undefined

0

-2

这不是一个完美的答案,但它可以工作。

function format_float_bug(num)
{
   return parseFloat( num.toFixed(15) ); 
} 

你可以按照以下方式使用:

format_float_bug(4990 % 10);

因为下面的数字(49.89999999999999857891452848)的前15位小数像是9999999


这根本不起作用,而且你的示例调用甚至没有使用浮点数! - Domino
使用 "toFixed" 和 "parseFloat" 并不是那么糟糕。您可以在 "toFixed" 函数中定义小数位数。您应该在函数中设置 2 个参数,并从第二个参数计算小数位数,这是模数。 - FragBis
const fmod = (x, y) => { let expY = y.toString().split('.'); let nbDec = expY.length > 1 ? expY[1].length : 0; return parseFloat( (x % y).toFixed(nbDec) ); }; - FragBis

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