起因
昨天被人問到了一個問題:
因為 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 了,加上計算屬性,咔咔好用。
相關資源
- 前端 BUG 錄 - 科學計數法是什麼? 這裡有一些精度相關的資料,可以點進去看看
- 測試地址:http://jsrun.net/9f9Kp/edit
回覆評論區老鐵的方案哈 ,方案可行。
再補個精度相關的內容。
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。