酷草 發表於 2022-5-7 14:11:00

JavaScript – Decimal

<h2>前言</h2>
<p>之前就写过一篇 decimal, double, float,但写的有点杂乱,这篇把 JS 的部分独立写成一篇整理版。</p>
<p>&nbsp;</p>
<h2>参考</h2>
<p>JavaScript 浮点数运算的精度问题</p>
<p>关于JavaScript中计算精度丢失的问题</p>
<p>Rounding</p>
<p>C#.Net筑基-深入解密小数内部存储的秘密</p>
<p>big.js设计思路和源码分享</p>
<p>JavaScript数字运算必备库——big.js源码解析</p>
<p>&nbsp;</p>
<h2>The Question: 0.1 + 0.2 = ?</h2>
<p>JS 有一道经典的问题</p>
<div class="cnblogs_code">
<pre>console.log(0.1 + 0.2); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 0.30000000000000004</span></pre>
</div>
<p>第一次接触 JS 的人可能会感到不可思议,但其实上面这道题,并不是 JS 独有的。</p>
<p>C# 也是一样的计算结果</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Program
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Main()
    {
      </span><span style="color: rgba(0, 0, 255, 1)">double</span> x = <span style="color: rgba(128, 0, 128, 1)">0.1</span><span style="color: rgba(0, 0, 0, 1)">;
      </span><span style="color: rgba(0, 0, 255, 1)">double</span> y = <span style="color: rgba(128, 0, 128, 1)">0.2</span><span style="color: rgba(0, 0, 0, 1)">;
      </span><span style="color: rgba(0, 0, 255, 1)">double</span> z = x + y; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 0.30000000000000004</span>
<span style="color: rgba(0, 0, 0, 1)">    }
}</span></pre>
</div>
<h3>Why 0.30...4 ?</h3>
<p>代码上看,写的是十进制,但电脑在存储和计算时,其实用的是二进制 (想要深入理解,可以看这篇,我只懂个大概而已)。</p>
<p>0.1 + 0.2,电脑会先把 0.1 转换成二进制,而这个二进制是个无穷数 0.0001100110011001...(无限),所以只能保留一部分的精度 (IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位)。</p>
<p>最终相加以后再转换成十进制,精度就丢失了,结果就会有偏差。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202411/641294-20241122190328018-114361139.png"></p>
<p>图片来源:关于JavaScript中计算精度丢失的问题(一)</p>
<h3>toFixed 也有问题</h3>
<p>不仅仅是加减乘除,就连 rounding 也是会出错的。</p>
<div class="cnblogs_code">
<pre>console.log(162.125.toFixed(2)); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 162.13 (四舍五入)</span>
console.log(162.295.toFixed(2)); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 162.29 (四舍五入 失败)</span></pre>
</div>
<h3>先转换成整数,再做计算</h3>
<p>可能我们会误以为,只要先把小数转换成整数,然后才做计算,就可以避开精度丢失问题...没有这么简单。</p>
<div class="cnblogs_code">
<pre>console.log(162.315.toFixed(2));            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 错误 162.31</span>
console.log(Math.round(162.315 * 100) / 100); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 正确 162.32</span>
console.log(Math.round(162.295 * 100) / 100); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 依然错误 162.29</span></pre>
</div>
<p>因为 * 100 也是走二进制,所以依然有丢失的可能。</p>
<p>&nbsp;</p>
<h2>The Answer</h2>
<p>上面说了这道题在 C# 也是同样的计算结果,但为什么往往叫的人都是 jser 呢?</p>
<p>因为 C# 有一个 best practice -- 但凡可能会让人计算的数,请使用 decimal。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Program
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Main()
    {
      </span><span style="color: rgba(0, 0, 255, 1)">decimal</span> x = <span style="color: rgba(128, 0, 128, 1)">0.1m</span><span style="color: rgba(0, 0, 0, 1)">;
      </span><span style="color: rgba(0, 0, 255, 1)">decimal</span> y = <span style="color: rgba(128, 0, 128, 1)">0.2m</span><span style="color: rgba(0, 0, 0, 1)">;
      </span><span style="color: rgba(0, 0, 255, 1)">decimal</span> z = x + y; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 0.3</span>
<span style="color: rgba(0, 0, 0, 1)">    }
}</span></pre>
</div>
<p>换成 decimal 计算就正确了。&nbsp;</p>
<h3>decimal vs double</h3>
<p>decimal 的特色就是精度准、计算速度慢(因为 CPU 不支持 decimal 计算)、存储空间大、数目小。</p>
<p>所以如果不 care 精准度的话,大部分情况都会使用 double。(比如做游戏啦、科学啦、这些场景一般上需要计算的快、数目又大,但通常不需要太准)</p>
<p>算钱则一定是用 decimal 的,因为要准嘛。</p>
<h3>How it work?</h3>
<p>为什么 decimal 就 ok 了呢?</p>
<p>因为 decimal 不使用二进制 (这也是它慢的主要原因)。</p>
<p>上面我们提到,精度丢失就是因为十进制转二进制后,变成无穷数,只能存储一部分,而丢失的那部分就不可能还原了,最后就有微差。</p>
<p>decimal 不转二进制,就没有丢失的问题了。</p>
<p>但...不转二进制要怎样计算呢?</p>
<p>C# Decimal 我不清楚。</p>
<p>JS Decimal -- big.js 库的实现方式,就类似于,我们小时候用纸笔做算数那样</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202411/641294-20241122204519088-1045204794.png"></p>
<p>&nbsp;</p>
<div class="cnblogs_code">
<pre>import Big from 'big.js'<span style="color: rgba(0, 0, 0, 1)">;

const n1 </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Big(8881.12<span style="color: rgba(0, 0, 0, 1)">);
const n2 </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Big(165.26<span style="color: rgba(0, 0, 0, 1)">);
console.log(n1.plus(n2).toNumber()); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 正确 9046.38</span>
console.log(8881.12 + 165.26);       <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 错误 9046.380000000001</span></pre>
</div>
<p>首先它把 number 转成 string,接着 split,然后按位置保存</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202411/641294-20241122204736489-1487439816.png"></p>
<p>最后按位置做加法计算,进位。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202411/641294-20241122204519088-1045204794.png"></p>
<p>2 + 6 = 8,</p>
<p>1 + 2 = 3,</p>
<p>1 + 5 = 6,</p>
<p>8 + 6 = 14,保留 4,进位 1</p>
<p>8 + 1 + 1 = 10,保留 0,进位 1&nbsp;</p>
<p>8 + 1 = 9</p>
<p>答案倒着看:9046.38</p>
<p>注:这个例子刚巧两个数目的小数点位置是相同的,假如不相同,那在计算之前先对齐就可以了。</p>
<p>由于是挨个挨个位置做加法计算,计算时虽然也会转成二进制,但它每个位置都是整数 (整数转二进制是不会无穷的),不是小数,所以就彻底避开了精度丢失的问题。</p>
<p>当然,这个加法计算,肯定是慢的,要 for loop,要自己处理进位等等一系列繁琐的操作,但视乎也没其它的办法了。</p>
<p>&nbsp;</p>
<h2>JS Decimal&nbsp;の big.js 库</h2>
<p>JS 没有原生的 decimal 类型 (tc39 proposal),但是有 library 可以实现 decimal 的效果。</p>
<p>big.js,&nbsp;bignumber.js,&nbsp;decimal.js&nbsp;这 3 个库都是同一个作者。</p>
<p>big.js 最轻,也是我目前用着的,三者之间的区别可以看这篇:&nbsp;What is the difference between big.js, bignumber.js and decimal.js?&nbsp;或它的翻译篇</p>
安装
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">yarn add big.js
yarn add </span>@types/big.js --dev</pre>
</div>
<p>使用</p>
<div class="cnblogs_code">
<pre>import Big from 'big.js'<span style="color: rgba(0, 0, 0, 1)">;
console.log(Big(</span>0.1).plus(0.2).toNumber()); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 0.3</span></pre>
</div>
<p>第一步是把 number 变成 Big 对象.</p>
<p>Big(0.1) 或者 new Big(0.1) 都可以, new 是 optional 的.</p>
<p>接着就是调用各做 operator 方法. 比如&nbsp;plus, minus, mul / times, div (加减乘除, 注: mul 和 times 都是乘, alias 而已)</p>
<p>最后通过 toNumber 把 Big 对象转换成 JS 的 number 类型.</p>
<p>除了加减乘除, big js 也提供了许多对比方法, ===, &gt;, &gt;=, &lt;, &lt;= 等等. 这样写起来就比较方便了.</p>
<p><img src="https://img2022.cnblogs.com/blog/641294/202205/641294-20220507152234276-1329609823.png"></p>
<p>big.js 没有提供 min、max、sum 这些功能,需要的话得用 reduce 自己累加实现。</p>
<p>toPrecision() 类似 JS 的 toFixed 返回 string</p>
<p>round() 类似 Math.round 但它支持 round to n decimal point,而且有不同的 rounding mode,默认是四舍五入。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202308/641294-20230827170600481-2131018399.png"></p>
<p>&nbsp;</p>
<h2>JS Workaround (Number.EPSILON)</h2>
<p>如果不想大费周章搞 decimal,也可以用一些小技巧解决。</p>
<div class="cnblogs_code">
<pre>const value = 0.1 + 0.2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (value === 0.3<span style="color: rgba(0, 0, 0, 1)">) {
console.log(</span>'yes'<span style="color: rgba(0, 0, 0, 1)">);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'no'); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> will be no, because it is 0.30000000000000004</span>
}</pre>
</div>
<p>把 if expression 换成</p>
<div class="cnblogs_code">
<pre>const value = 0.1 + 0.2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (0.3 - value &lt; Number.EPSILON) { <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> EPSILON is a very very small number 2.220446049250313e-16</span>
console.log('yes'); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> will be yes</span>
} <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'no'<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>Number.EPSILON 是 es6 的新特性。</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/keatkeat/p/16242309.html
頁: [1]
查看完整版本: JavaScript – Decimal