常常會碰到一個問題,"爲何
0.1 + 0.2 !== 0.3
? ",我找了不少資料,儘量全面地分析緣由和解決辦法。javascript
文章可能有點枯燥,囧。html
這裏先給出判斷方法java
Math.abs(0.1+0.2-0.3) <= Number.EPSILON
複製代碼
IEEE 754git
IEEE 754 規定了四種表示浮點數值的方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,不多使用)與延伸雙精確度(79比特以上,一般以80位實現)。github
該標準的全稱爲IEEE二進制浮點數算術標準(ANSI/IEEE Std 754-1985),又稱IEC 60559:1989,微處理器系統的二進制浮點數算術(原本的編號是IEC 559:1989)。算法
單精度浮點數格式是一種數據類型,在計算機存儲器中佔用 4 個位元(32 bits),利用「浮點」(浮動小數點)的方法,能夠表示一個範圍很大的數值。安全
在 IEEE 754-2008 的定義中,32-bit base 2 格式被正式稱爲 binary32 格式。這種格式在 IEEE 754-1985 被定義爲 single,即單精度。須要注意的是,在更早的一些計算機系統中,也存在着其餘 4 字節的浮點數格式。bash
定義ide
第 1 位表示正負,中間 8 位表示指數,後 23 位儲存有效數位(有效數位是 24 位)。post
中間八位共可表示 28=256 個數,指數能夠是二補碼;或 0 到 255,0 到 126 表明-127 到-1,127 表明零,128-255 表明 1-128。
有效數位最左手邊的 1 並不會儲存,由於它必定存在(二進制的第一個有效數字一定是 1)。換言之,有效數位是 24 位,實際儲存 23 位。
雙精度浮點數(double)是計算機使用的一種數據類型。比起單精度浮點數,雙精度浮點數(double)使用 64 位(8 字節) 來存儲一個浮點數。 它能夠表示十進位制的 15 或 16 位有效數字,其能夠表示的數字的絕對值範圍大約是 [2.23e-308,1.79e308]
定義
和單精度相似,第 1 位表示正負,後 11 位爲指數位,最後 52 位表示精確度(有效位數是 53 位)。
Number.EPSILON
Number.EPSILON
=== 2.220446049250313e-16
,表示 1 與 Number 可表示的大於 1 的最小的浮點數之間的差值。其接近於 2**-52
用 Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
能夠判斷 0.1 + 0.2
與 0.3
的大小。
Number.MAX_SAFE_INTEGER
Number.MAX_SAFE_INTEGER
常量表示在 JavaScript 中最大的安全整數(maxinum safe integer)(2**53 - 1
、9007199254740991
)。
由於 Javascript 的數字存儲使用了 IEEE 754 中規定的雙精度浮點數數據類型,而這一數據類型可以安全存儲 -(2**53 - 1)
到 2**53 - 1
之間的數值(包含邊界值)。
這裏安全存儲的意思是指可以準確區分兩個不相同的值,例如 Number.MAX_SAFE_INTEGER + 1
=== Number.MAX_SAFE_INTEGER + 2
將獲得 true 的結果
Number.MAX_VALUE
Number.MAX_VALUE
屬性表示在 JavaScript 裏所能表示的最大數值。
MAX_VALUE
屬性值接近於 1.79e308
,也就是雙精度浮點型能表示的最大數字。大於 MAX_VALUE
的值表明 Infinity
。
看個例子
Number.MIN_SAFE_INTEGER
表明在 JavaScript 中最小的安全的 integer 型數字 -(2**53 - 1)
、9007199254740991
.
Number.MIN_VALUE
Number.MIN_VALUE
屬性表示在 JavaScript 中所能表示的最小的正值。
MIN_VALUE
屬性是 JavaScript 裏最接近 0 的正值,而不是最小的負值。
MIN_VALUE 的值約爲 5e-324
。小於 MIN_VALUE
("underflow values") 的值將會轉換爲 0。
注意下,用 Math.abs(0.1 + 0.2 - 0.3) < Number.MIN_VALUE
將會返回 false
Number.isSafeInteger()
Number.isSafeInteger()
方法用來判斷傳入的參數值是不是一個「安全整數」(safe integer)。
好比,2**53 - 1
是一個安全整數,它能被精確表示,在任何 IEEE-754 舍入模式(rounding mode)下,沒有其餘整數舍入結果爲該整數。做爲對比,2**53
就不是一個安全整數,它可以使用 IEEE-754 表示,可是 2**53 + 1
不能使用 IEEE-754 直接表示,在就近舍入(round-to-nearest)和向零舍入中,會被舍入爲 2**53
。
這個地方比較複雜,涉及到二進制小數沒法表示時自動截斷,在 JS 中測試時發現,截斷的精度有時是 52 位,有時是 53 位。在 0.1 + 0.2
中截斷精度是 52 位,在 0.1 + 0.5
中截斷精度是 53 位。
(0.1).toString(2)
=== "0.000 110011001100110011001100110011001100110011001100110 1「
(0.2).toString(2)
=== "0.00 1100110011001100110011001100110011001100110011001101"
(0.30000000000000004).toString(2)
=== "0.0100110011001100110011001100110011001100110011001101"
(0.3).toString(2)
=== "0.010011001100110011001100110011001100110011001100110011"
咱們看看 0.1 是如何被表示成這麼一大串數字的。
0.1 * 2 = 0.2 -> 0
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0 ... 一直循環,沒法達到 1
複製代碼
因此最終 0.1 用二進制表示是 0.0001 1001 1001 1001 ...
,可是咱們看上面 (0.1).toString()
最後的六位 001101
,正常循環應該是 001100
,因此截斷以後,0.1 二進制表示的值變大了!!!。0.2 轉換爲二進制表示截斷以後也變大了。
經過對比 0.一、0.2 及它們的和的二進制表示,能夠發現字符串的長度變化了,可是精確度卻沒有變化,也就是從 1 開始到最後的字符串長度都是 52。
0.1 + 0.2
原本應該是長度在爲 57,可是因爲沒法表示這樣一個數,從新從 1 開始的數字開始計數,會截斷最後的三個數字 (最後精確度爲 52 或者 53 )。
咱們再來看一個例子, 0.1 + 0.5 === 0.6
爲 true,實際不能這麼比較,極其容易出錯。
(0.1).toString(2)
=== "0.0001100110011001100110011001100110011001100110011001101「
,字符串長度爲 57,精度爲 52。
(0.5).toString(2)
=== "0.1"
(0.6).toString(2)
=== "0.10011001100110011001100110011001100110011001100110011"
,這個字符串長度爲 55,精度爲 53.
0.1 + 0.5
的本來結果爲 0.1001100110011001100110011001100110011001100110011001101
,這個數字沒法用二進制表示,由於從第一個 1 開始日後的總長度爲 55,大於 53,因此截斷以後變成了 0.10011001100110011001100110011001100110011001100110011
,這個結果和 0.6 的二進制表示正好相等!!!因此有 0.1 + 0.5 === 0.6
。
在 0.一、0.二、0.3 分別是怎麼表示的 這一節中,咱們看到 0.1+0.2
結果的精確度是 52 位,而 0.1+0.5
的精確度是 53 位的,結合以前講的雙精度浮點數的表示方法,難免有個疑惑,精確度不該該都是用 53 位的嗎?
咱們進一步看看,0.1~0.9 這幾個小數的二進制表示
0.1 -> "0.0001100110011001100110011001100110011001100110011001101" 精度 52 位
0.2 -> "0.001100110011001100110011001100110011001100110011001101" 精度 52 位
0.3 -> "0.010011001100110011001100110011001100110011001100110011" 精度 53 位
0.4 -> "0.01100110011001100110011001100110011001100110011001101" 精度 52 位
0.5 -> "0.1"
0.6 -> "0.10011001100110011001100110011001100110011001100110011" 精度 53 位
0.7 -> "0.1011001100110011001100110011001100110011001100110011" 精度 52 位
0.8 -> "0.1100110011001100110011001100110011001100110011001101" 精度 52 位
0.9 -> "0.11100110011001100110011001100110011001100110011001101" 精度 53 位
複製代碼
說實話,沒有從這幾個數字中得到什麼規律!!!0.七、0.9 的精確度位數和預想的不同。。
歡迎各位留言討論這一部分~
小數運算不許是由於要計算的數字小數部分沒法用二進制精確表示所致使,咱們能夠把小數轉化成整數運算以後再變回小數來解決!
如下解決辦法來自 number-precision
/** * 精確加法 */
function plus(num1: number, num2: number, ...others: number[]): number {
if (others.length > 0) {
// 遞歸
return plus(plus(num1, num2), others[0], ...others.slice(1));
}
// digitLength 是獲取小數的點後面的字符個數
// 下面是計算讓 num一、num2 都爲整數時的最小倍數
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
// 讓 num一、num2 都變成整數,而後運算,而後再變回小數
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}
複製代碼
計算過程相似於
0.11 + 0.345
0.11 -> digitLength(0.11) -> 2
0.345 -> digitLength(0.345) -> 3
故 baseNum = 3
0.11 * 10**3 = 110
0.345 * 10**3 = 345
110 + 345 = 455
455 / baseNum= 0.455
複製代碼
計算的關鍵就在於把小數轉換成可準確表示的整數,下面的代碼只是大概的功能實現,若是要直接使用,能夠用 github.com/nefe/number…
更新: 因爲小數 * 整數也可能致使計算不許確,因此以前的代碼存在偏差。修復事後不容許直接對小數參數作運算,必須轉換成整數運算而後再轉換回來
digitLength
獲取到數字的小數部分的位數,這是變爲整數的關鍵。
// 兼容多種類型的表示
// 1.11 或者 1.11e-30 或者 1e-30
export function digitLength(num: number): number {
// 1.11 -> eSplit: ['1.11']
// 1.11e-30 -> eSplit: ["1.11", "-30"]
const eSplit = num.toString().split(/[eE]/)
// 右邊的 `|| ''` 爲了防止 1e-30 -> eSplit: ["1", "-30"] 這種
// 左邊 1.11 有兩個小數,右邊 e 後面有 -30,因此是 2 - (-30) 爲 32
const len = (eSplit[0].split('.')[1] || '').length - Number(eSplit[1] || 0)
return len > 0 ? len : 0
}
複製代碼
baseNum
計算出讓 num1
、num2
都爲整數的最小 10 的倍數
export function baseNum(num1: number, num2: number): number {
return Math.pow(10, Math.max(digitLength(num1), digitLength(num2)))
}
複製代碼
strip
對錯誤的數進行修正
/**
* 把錯誤的數據轉正
* strip(0.09999999999999998)=0.1
*/
+export function strip(num: number, precision = 12): number {
+ // (0.09999999999999998).toPrecision(12) => "0.100000000000"
+ // parseFloat("0.100000000000") => 0.1
+ return +parseFloat(num.toPrecision(precision))
+}
複製代碼
float2Fixed
把傳入的數變爲整數
+export function float2Fixed(num: number) {
+ // 1.23456 => 123456
+ if (num.toString().indexOf('e') === -1) {
+ return Number(num.toString().replace('.', ''))
+ }
+ // 1.1e-30
+ const dLen = digitLength(num)
+ // 這個地方須要輔助矯正,num * Math.pow(10, dLen) 小數和整數相乘仍然可能會出現不許的狀況
+ return dLen > 0 ? strip(num * Math.pow(10, dLen)) : num
+}
複製代碼
乘法計算
export function times(num1: number, num2: number): number {
const bn = digitLength(num1) + digitLength(num2)
- const intNum1 = num1 * Math.pow(10, digitLength(num1))
- const intNum2 = num2 * Math.pow(10, digitLength(num2))
+ const intNum1 = float2Fixed(num1)
+ const intNum2 = float2Fixed(num2)
return (intNum1 * intNum2) / Math.pow(10, bn)
}
複製代碼
加法計算
export function plus(num1: number, num2: number): number {
const bn = baseNum(num1, num2)
- return (num1 * bn + num2 * bn) / bn
+ // fix:不能使用 num1 * bn,小數與整數相乘可能不許確,須要精確乘 times
+ return (times(num1, bn) + times(num2, bn)) / bn
}
複製代碼
減法計算
export function minus(num1: number, num2: number): number {
const bn = baseNum(num1, num2)
- return (num1 * bn - num2 * bn) / bn
+ return (times(num1, bn) - times(num2, bn)) / bn
}
複製代碼
除法計算
export function divide(num1: number, num2: number): number {
const bn = baseNum(num1, num2)
- const intNum1 = num1 * bn
- const intNum2 = num2 * bn
+ const intNum1 = times(num1, bn)
+ const intNum2 = times(num2, bn)
// 要檢查擴大後的數字是否超過了安全邊界
return intNum1 / intNum2
+ // 避免把數字擴的太大的寫法
+ // const num1Changed = float2Fixed(num1)
+ // const num2Changed = float2Fixed(num2)
+ // return times(num1Changed / num2Changed, strip(Math.pow(10, digitLength(num2) - digitLength(num1))))
}
複製代碼
這四種運算的原理都是先放大數字,使之可以精確表示,計算以後再縮小數字,獲得實際值。
測試結果
import { plus, minus, divide, times } from './index'
test('javascript/number-precision-operation', () => {
expect(plus(0.1, 0.2)).toBe(0.3) // 0.30000000000000004
expect(plus(0.1, 0.7)).toBe(0.8) // 0.7999999999999999
expect(minus(1, 0.9)).toBe(0.1) // 0.09999999999999998
expect(divide(0.1, 0.3)).toBe(0.3333333333333333) // 0.33333333333333337
expect(times(0.1, 0.1)).toBe(0.01) // 0.010000000000000002
})
複製代碼
參考
歡迎在本文下面評論或者在 GitHub issue 中參與討論 github.com/lxfriday/gi…
歡迎你們關注個人掘金和公衆號,算法、TypeScript、React 及其生態源碼按期講解
歡迎進羣討論~~
|