摘要:也就是說(shuō)不僅是會(huì)產(chǎn)生這種問(wèn)題,只要是采用的浮點(diǎn)數(shù)編碼方式來(lái)表示浮點(diǎn)數(shù)時(shí),則會(huì)產(chǎn)生這類(lèi)問(wèn)題。到這里我們都理解只要采取的浮點(diǎn)數(shù)編碼的語(yǔ)言均會(huì)出現(xiàn)上述問(wèn)題,只是它們的標(biāo)準(zhǔn)類(lèi)庫(kù)已經(jīng)為我們提供了解決方案而已。
Brief
一天有個(gè)朋友問(wèn)我“JS中計(jì)算0.7 * 180怎么會(huì)等于125.99999999998,坑也太多了吧!”那時(shí)我猜測(cè)是二進(jìn)制表示數(shù)值時(shí)發(fā)生round-off error所導(dǎo)致,但并不清楚具體是如何導(dǎo)致,并且有什么方法去規(guī)避。于是用了3周時(shí)間靜下心把這個(gè)問(wèn)題搞懂,在學(xué)習(xí)的過(guò)程中還發(fā)現(xiàn)不僅0.7 * 180==125.99999999998,還有以下的坑
著名的 0.1 + 0.2 === 0.30000000000000004
1000000000000000128 === 1000000000000000129
IEEE 754 Floating-point眾所周知JS僅有Number這個(gè)數(shù)值類(lèi)型,而Number采用的時(shí)IEEE 754 64位雙精度浮點(diǎn)數(shù)編碼。而浮點(diǎn)數(shù)表示方式具有以下特點(diǎn):
浮點(diǎn)數(shù)可表示的值范圍比同等位數(shù)的整數(shù)表示方式的值范圍要大得多;
浮點(diǎn)數(shù)無(wú)法精確表示其值范圍內(nèi)的所有數(shù)值,而有符號(hào)和無(wú)符號(hào)整數(shù)則是精確表示其值范圍內(nèi)的每個(gè)數(shù)值;
浮點(diǎn)數(shù)只能精確表示m*2e的數(shù)值;
當(dāng)biased-exponent為2e-1-1時(shí),浮點(diǎn)數(shù)能精確表示該范圍內(nèi)的各整數(shù)值;
當(dāng)biased-exponent不為2e-1-1時(shí),浮點(diǎn)數(shù)不能精確表示該范圍內(nèi)的各整數(shù)值。
由于部分?jǐn)?shù)值無(wú)法精確表示(存儲(chǔ)),于是在運(yùn)算統(tǒng)計(jì)后偏差會(huì)愈見(jiàn)明顯。
想了解更多浮點(diǎn)數(shù)的知識(shí)可參考以下文章:
《基礎(chǔ)野:細(xì)說(shuō)原碼、反碼和補(bǔ)碼》
《基礎(chǔ)野:細(xì)說(shuō)無(wú)符號(hào)整數(shù)》
《基礎(chǔ)野:細(xì)說(shuō)有符號(hào)整數(shù)》
《基礎(chǔ)野:細(xì)說(shuō)浮點(diǎn)數(shù)》
Why 0.1 + 0.2 === 0.30000000000000004?在浮點(diǎn)數(shù)運(yùn)算中產(chǎn)生誤差值的示例中,最出名應(yīng)該是0.1 + 0.2 === 0.30000000000000004了,到底有多有名?看看這個(gè)網(wǎng)站就知道了http://0.30000000000000004.com/。也就是說(shuō)不僅是JavaScript會(huì)產(chǎn)生這種問(wèn)題,只要是采用IEEE 754 Floating-point的浮點(diǎn)數(shù)編碼方式來(lái)表示浮點(diǎn)數(shù)時(shí),則會(huì)產(chǎn)生這類(lèi)問(wèn)題。下面我們來(lái)分析整個(gè)運(yùn)算過(guò)程。
0.1 的二進(jìn)制表示為 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-4;
當(dāng)64bit的存儲(chǔ)空間無(wú)法存儲(chǔ)完整的無(wú)限循環(huán)小數(shù),而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.1實(shí)際存儲(chǔ)時(shí)的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;
0.2 的二進(jìn)制表示為 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-3;
當(dāng)64bit的存儲(chǔ)空間無(wú)法存儲(chǔ)完整的無(wú)限循環(huán)小數(shù),而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.2實(shí)際存儲(chǔ)時(shí)的位模式是0-01111111100-1001100110011001100110011001100110011001100110011010;
實(shí)際存儲(chǔ)的位模式作為操作數(shù)進(jìn)行浮點(diǎn)數(shù)加法,得到 0-01111111101-0011001100110011001100110011001100110011001100110100。轉(zhuǎn)換為十進(jìn)制即為0.30000000000000004。
Why 0.7 * 180===125.99999999998?0.7實(shí)際存儲(chǔ)時(shí)的位模式是0-01111111110-0110011001100110011001100110011001100110011001100110;
180實(shí)際存儲(chǔ)時(shí)的位模式是0-10000000110-0110100000000000000000000000000000000000000000000000;
實(shí)際存儲(chǔ)的位模式作為操作數(shù)進(jìn)行浮點(diǎn)數(shù)乘法,得到0-10000000101-1111011111111111111111111111111111111111101010000001。轉(zhuǎn)換為十進(jìn)制即為125.99999999998。
Why 1000000000000000128 === 1000000000000000129?1000000000000000128實(shí)際存儲(chǔ)時(shí)的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
1000000000000000129實(shí)際存儲(chǔ)時(shí)的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
因此1000000000000000128和1000000000000000129的實(shí)際存儲(chǔ)的位模式是一樣的。
Solution到這里我們都理解只要采取IEEE 754 FP的浮點(diǎn)數(shù)編碼的語(yǔ)言均會(huì)出現(xiàn)上述問(wèn)題,只是它們的標(biāo)準(zhǔn)類(lèi)庫(kù)已經(jīng)為我們提供了解決方案而已。而JS呢?顯然沒(méi)有。壞處自然是掉坑了,而好處恰恰也是掉坑了:)
針對(duì)不同的應(yīng)用需求,我們有不同的實(shí)現(xiàn)方式。
Solution 0x00 - Simple implementation對(duì)于小數(shù)和小整數(shù)的簡(jiǎn)單運(yùn)算可用如下方式
function numAdd(num1/*:String*/, num2/*:String*/) { var baseNum, baseNum1, baseNum2; try { baseNum1 = num1.split(".")[1].length; } catch (e) { baseNum1 = 0; } try { baseNum2 = num2.split(".")[1].length; } catch (e) { baseNum2 = 0; } baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); return (num1 * baseNum + num2 * baseNum) / baseNum; };Solution 0x01 - math.js
若需要復(fù)雜且全面的運(yùn)算功能那必須上math.js,其內(nèi)部引用了decimal.js和fraction.js。功能異常強(qiáng)大,用于生產(chǎn)環(huán)境上妥妥的!
Solution 0x02 - D.jsD.js算是我的練手項(xiàng)目吧,截止本文發(fā)表時(shí)D.js版本為V0.2.0,僅實(shí)現(xiàn)了加、減、乘和整除運(yùn)算而已,bug是一堆堆的,但至少解決了0.1+0.2的問(wèn)題了。
var sum = D.add(0.1, 0.2) console.log(sum + "") // 0.3 var product = D.mul("1e-2", "2e-4") console.log(product + "") // 0.000002 var quotient = D.div(-3, 2) console.log(quotient + "") // -(1+1/2)
解題思路:
由于僅位于Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER間的整數(shù)才能被精準(zhǔn)地表示,也就是只要保證運(yùn)算過(guò)程的操作數(shù)和結(jié)果均落在這個(gè)閥值內(nèi),那么運(yùn)算結(jié)果就是精準(zhǔn)無(wú)誤的;
問(wèn)題的關(guān)鍵落在如何將小數(shù)和極大數(shù)轉(zhuǎn)換或拆分為Number.MIN_SAFE_INTEGER至Number.MAX_SAFE_INTEGER閥值間的數(shù)了;
小數(shù)轉(zhuǎn)換為整數(shù),自然就是通過(guò)科學(xué)計(jì)數(shù)法表示,并通過(guò)右移小數(shù)點(diǎn),減小冪的方式處理;(如0.000123 等價(jià)于 123 * 10-6)
而極大數(shù)則需要拆分,拆分的規(guī)則是多樣的。
按因式拆分:假設(shè)對(duì)12345進(jìn)行拆分得到 5 * 2469;
按位拆分:假設(shè)以3個(gè)數(shù)值為一組對(duì)12345進(jìn)行拆分得到345和12,而實(shí)際值為12*1000 + 345。
就我而言,1 的拆分規(guī)則結(jié)構(gòu)不穩(wěn)定,而且不直觀;而 2 的規(guī)則直觀,且拆分和恢復(fù)的公式固定。
余數(shù)由符號(hào)位、分子和分母組成,而符號(hào)與整數(shù)部分一致,因此只需考慮如何表示分子和分母即可。
無(wú)限循環(huán)數(shù)則僅需考慮如何表示循環(huán)數(shù)段即可。(如10.2343434則分成10.23 和循環(huán)數(shù)34和34的權(quán)重即可)
得到編碼規(guī)則后,那就剩下基于指定編碼如何實(shí)現(xiàn)各種運(yùn)算的問(wèn)題了。
基于上述的數(shù)值編碼規(guī)則如何實(shí)現(xiàn)加、減運(yùn)算呢?
基于上述的數(shù)值編碼規(guī)則如何實(shí)現(xiàn)乘、除運(yùn)算呢?(其實(shí)只要加、減運(yùn)算解決了,乘除必然可解,就是效率問(wèn)題而已)
基于上述的數(shù)值編碼規(guī)則如何實(shí)現(xiàn)其它如sin、tan、%等數(shù)學(xué)運(yùn)算呢?
另外由于涉及數(shù)學(xué)運(yùn)算,那么將作為add、sub、mul和div等入?yún)⒌淖兞勘3秩缤瑪?shù)學(xué)公式運(yùn)算數(shù)般純凈(Persistent/Immutable Data Structure)是必須的,那是否還要引入immutable.js呢?(D.js現(xiàn)在采用按需生成副本的方式,可預(yù)見(jiàn)隨著代碼量的增加,這種方式會(huì)導(dǎo)致整體代碼無(wú)法維護(hù))
Conclusion依照我的尿性,D.js將采取不定期持續(xù)更新的策略(待我理解Persistent/Immutable Data Structure后吧:))。歡迎各位指教!
尊重原創(chuàng),轉(zhuǎn)載請(qǐng)注明來(lái)自:http://www.cnblogs.com/fsjohnhuang/p/5115672.html ^_^肥子John
Thankshttp://es5.github.io
https://github.com/MikeMcl/decimal.js/
http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html
http://demon.tw/copy-paste/javascript-precision.html
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/79328.html
摘要:標(biāo)準(zhǔn)是浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn)的標(biāo)準(zhǔn)編號(hào),等同于國(guó)際標(biāo)準(zhǔn)。標(biāo)準(zhǔn)規(guī)定了計(jì)算機(jī)程序設(shè)計(jì)環(huán)境中的二進(jìn)制和十進(jìn)制的浮點(diǎn)數(shù)之間的交換算術(shù)格式以及方法。 初學(xué)JavaScript,在進(jìn)行小數(shù)(浮點(diǎn)數(shù))運(yùn)算時(shí),經(jīng)常會(huì)碰到這樣的情況:0.1 + 0.2=0.30000000000000004,記得當(dāng)時(shí),教程告訴我們說(shuō),0.1 + 0.2在JavaScript運(yùn)算中,它的值是不固定的,可以在后面學(xué)習(xí)和試驗(yàn)中,漸漸...
摘要:方法使用定點(diǎn)表示法來(lái)格式化一個(gè)數(shù),會(huì)對(duì)結(jié)果進(jìn)行四舍五入。該數(shù)值在必要時(shí)進(jìn)行四舍五入,另外在必要時(shí)會(huì)用來(lái)填充小數(shù)部分,以便小數(shù)部分有指定的位數(shù)。如果數(shù)值大于,該方法會(huì)簡(jiǎn)單調(diào)用并返回一個(gè)指數(shù)記數(shù)法格式的字符串。在環(huán)境中,只能是之間,測(cè)試版本為。 showImg(https://segmentfault.com/img/remote/1460000011913134?w=768&h=521)...
摘要:吐槽一句,大二的專(zhuān)業(yè)課數(shù)字邏輯電路終于用在工作上了。,整數(shù)位為,且精度只到十分位,因此是。如果是不限精度的話,轉(zhuǎn)換后的二進(jìn)制數(shù)應(yīng)該是無(wú)限循環(huán)。再看一下百科給出的標(biāo)準(zhǔn)因此,的類(lèi)型,最高的位是符號(hào)位,接著的位是指數(shù),剩下的位為有效數(shù)字。 showImg(https://segmentfault.com/img/remote/1460000011902479?w=600&h=600); 用一...
摘要:與的映射關(guān)系為。與根對(duì)應(yīng)的對(duì)應(yīng)的層疊上下文,是其他的祖先,的范圍覆蓋整條。注意的默認(rèn)值為,自動(dòng)賦值為。對(duì)于,它會(huì)將賦予給對(duì)應(yīng)的,而則不會(huì)。 一、前言 ?假如只是開(kāi)發(fā)簡(jiǎn)單的彈窗效果,懂得通過(guò)z-index來(lái)調(diào)整元素間的層疊關(guān)系就夠了。但要將多個(gè)彈窗間層疊關(guān)系給處理好,那么充分理解z-index背后的原理及兼容性問(wèn)題就是必要的知識(shí)...
摘要:推導(dǎo)為何等于在中所有數(shù)值都以標(biāo)準(zhǔn)的雙精度浮點(diǎn)數(shù)進(jìn)行存儲(chǔ)的。先來(lái)了解下標(biāo)準(zhǔn)下的雙精度浮點(diǎn)數(shù)。精度位總共是,因?yàn)橛每茖W(xué)計(jì)數(shù)法表示,所以首位固定的就沒(méi)有占用空間。驗(yàn)證完成的最大安全數(shù)是如何來(lái)的根據(jù)雙精度浮點(diǎn)數(shù)的構(gòu)成,精度位數(shù)是。 閱讀完本文可以了解到 0.1 + 0.2 為什么等于 0.30000000000000004 以及 JavaScript 中最大安全數(shù)是如何來(lái)的。 十進(jìn)制小數(shù)轉(zhuǎn)為二...
閱讀 3684·2021-08-10 09:42
閱讀 584·2019-08-30 15:55
閱讀 880·2019-08-30 15:54
閱讀 3104·2019-08-30 13:45
閱讀 549·2019-08-29 16:23
閱讀 1986·2019-08-29 16:23
閱讀 976·2019-08-29 15:18
閱讀 2255·2019-08-29 12:57