我想用 JS 實現 0.1 + 0.2 輸出 0.3

linong發表於2021-12-29

起因

昨天被人問到了一個問題:

因為 JS 精度問題 0.1 + 0.2 == 0.30000000000000004 ,可以不可以得出一個正確的值。0.1 + 0.2 == 0.3

這簡單,變成整數,然後再除回去。或者取整,保留小數。

  • ((0.1 + 0.2) * 10).toFixed()/10
  • (0.1 * 10 + 0.2 * 10)/10

然後又被問了,如果我是兩位小數呢?三位呢? 讓我說你就沒悟性啊!同樣道理你加就完事了。然後他急了,他說三十位呢?

這樣問題就變了,變成了 JS 是否能存下這樣一個數字

我一想,這不就是大數計算嗎,我刷過。為了簡單我找了個庫 bignumber 來實現這部分邏輯
new BigNumber(0.1).plus(0.2) 簡單的一批。

但是這裡我自己發現了問題:需要按照特定的寫法,這不合理。我又想到,我之前刷過計算器,裡面給你輸入一個 '1+2+3*4' 然後讓你輸出計算結果(雖然有偷懶的 eval 方案,但是架不住有題解呀)

說整就整,一查 :?鬱悶

  • 題一,只支援加減
  • 題二,支援括號,不支援乘除
  • 題三,逐漸變態。根本就不是數學。是一套自己的邏輯。
  • 題四,加密,需要開會員。

沒辦法,白嫖失敗(tongxin),自己寫又太費時間,看看題解 get 了一個關鍵詞逆波蘭式,可以處理計算優先順序。

表示式 轉換為 逆波蘭式

這就不得不吐槽,網上的垃圾資源了

  • 居然不支援小數
  • 居然只支援個位
  • 輸出結果也是有問題的
  • 居然搜尋結果在第一屏
  • 居然前一頁都是同一篇文章

好在整數計算是沒問題的,自己動手豐衣足食。我們把獲取到數字哪裡在做一個 while 以保證完整。

    function calculator(str) {
        let n = 0, charStack = [], numStack = [], reducerStr =  [], leftIndex = -1
        const op = {
            '+' : 1,
            '-' : 1,
            '*' : 2,
            '/' : 2,
            '(' : 3,
            ')' : 3
        }
        while(n < str.length) {
            const byte = str[n]
            // 數字
            // if(/\d/.test(byte)) {
            if(/[\d\.]+/.test(byte)) {
                // reducerStr.push(byte)
                let result = byte;
                let _str = str[n+1]
                while(/[\d\.]+/.test(_str)){
                    result+=_str;
                    n++;
                    _str = str[n+1]
                }
                reducerStr.push(result)
            } else if(/\(|\)/.test(byte)) {
                // 左括號入棧
                if(byte === '(') {
                    charStack.push(byte)
                    leftIndex = n
                    // console.log('左括號', byte)
                // 右括號出棧
                } else {
                    let nowChar = charStack.pop()
                    while(nowChar && nowChar !== '(') {
                    reducerStr.push(nowChar)
                    nowChar = charStack.pop()
                    }
                }
                // 符號
            } else {
                // 字元棧頂元素
                let nowChar = charStack[charStack.length - 1]
                while(nowChar && op[byte] < op[nowChar] && nowChar !== '(') {
                    charStack.pop()
                    reducerStr.push(nowChar)
                    nowChar = charStack[charStack.length - 1]
                }
                charStack.push(byte)
            }
            n++
        }
        while(charStack.length) {
            reducerStr.push(charStack.pop())
        }
        return reducerStr
    }

解析逆波蘭式計算結果

這個也要吐槽,應該是個題解,他把小數取整了 凸(艹皿艹 )

把取整幹掉。然後把計算位置換成我們的庫。

    var evalRPN = function(tokens) {
        const stack = [];
        const n = tokens.length;
        for (let i = 0; i < n; i++) {
            const token = tokens[i];
            if (isNumber(token)) {
                stack.push((token));
                // stack.push(parseInt(token));
            } else {
                const num2 = stack.pop();
                const num1 = stack.pop();
                if (token === '+') {
                    stack.push(new BigNumber(num1).plus(num2));
                } else if (token === '-') {
                    stack.push(new BigNumber(num1).minus(num2));
                } else if (token === '*') {
                    stack.push(new BigNumber(num1).times(num2));
                } else if (token === '/') {
                    stack.push(new BigNumber(num1).dividedBy(num2));
                    // stack.push(num1 / num2 > 0 ? Math.floor(num1 / num2) : Math.ceil(num1 / num2));
                }
            }
        }
        return stack.pop();
    };

    const isNumber = (token) => {
        return !('+' === token || '-' === token || '*' === token || '/' === token );
    }

便捷測試

這裡當然是上 vue 了,加上計算屬性,咔咔好用。

image.png

相關資源

  1. 前端 BUG 錄 - 科學計數法是什麼? 這裡有一些精度相關的資料,可以點進去看看
  2. 測試地址:http://jsrun.net/9f9Kp/edit

回覆評論區老鐵的方案哈 ,方案可行。

image.png

再補個精度相關的內容。

Math.abs(0.1+0.2 - 0.3) < Number.EPSILON
Math.abs(0.1+0.2 - 0.3000000000000001) < Number.EPSILON
Math.abs(0.1+0.2 - 0.300000000000001) < Number.EPSILON
EPSILON 屬性的值接近於 2.2204460492503130808472633361816E-16,或者 2-52。

相關文章