近期從新開始學習計算機基礎方面的東西,好比計算機組成原理
、網絡原理
、編譯原理
之類的東西,目前正好在學習編譯原理
,開始對這一塊的東西感興趣,可是理論的學習有點枯燥無味,決定換種方式,那就是先實踐、遇到問題嘗試解決,用實踐推進理論。本來打算寫個中文JS解析的,可是好像有點難,先就找個簡單的來作吧,那就是解析一下四則運算,就有了這個項目,聲明:這是一個很簡單的項目,這是一個很簡單的項目,這是一個很簡單的項目。其中用到的詞法分析、語法分析、自動機都是用簡單的方式實現,畢竟比較菜。html
源碼地址:github前端
實現功能:git
+-*/
正整數運算()
效果演示: github
既然說很簡單,那無論用到的理論和實現的方式都必定要都很簡單,實現這個效果一共須要克服三個問題:算法
*/()
的優先級大於+-
。+
後面好比跟隨數字
或者(
(這裏將-
看成操做,而不是符號)。若是沒有優先級問題,那實現一個計算十分的簡單,好比下面的代碼能夠實現一個簡單的加減或者乘除計算(10之內,超過一位數會遇到問題2,這裏先簡單一點,避過問題2):後端
let calc = (input) => {
let calMap = {
'+': (num1, num2) => num1 + num2,
'-': (num1, num2) => num1 - num2,
'*': (num1, num2) => num1 * num2,
'/': (num1, num2) => num1 / num2,
}
input = [...input].reverse()
while (input.length >= 2) {
let num1 = +input.pop()
let op = input.pop()
let num2 = +input.pop()
input.push(calMap[op](num1, num2))
}
return input[0]
}
expect(calc('1+2+3+4+5-1')).toEqual(14)
expect(calc('1*2*3/3')).toEqual(2)
複製代碼
算法步驟:bash
input = [...input].reverse()
複製代碼
let num1 = +input.pop()
let op = input.pop()
let num2 = +input.pop()
複製代碼
while (input.length >= 2) {
// ......
input.push(calMap[op](num1, num2))
}
複製代碼
動畫演示:網絡
可是如今須要考慮優先級,好比*/
的優先級大於+-
,()
的運算符最高,那如何解決呢,其實都已經有解決方案了,我用的是後綴表達式
,也叫逆波蘭式
函數
1+1
表示成11+
。1+1
表示成+11
,這裏不作深刻逆波蘭式
能夠參考下列文章單元測試
在逆波蘭式子中,1+1*2
能夠轉化爲112*+
代碼演示:
let calc = (input) => {
let calMap = {
'+': (num1, num2) => num1 + num2,
'-': (num1, num2) => num1 - num2,
'*': (num1, num2) => num1 * num2,
'/': (num1, num2) => num1 / num2,
}
input = [...input].reverse()
let resultStack = []
while (input.length) {
let token = input.pop()
if (/[0-9]/.test(token)) {
resultStack.push(token)
continue
}
if (/[+\-*/]/.test(token)) {
let num1 = +resultStack.pop()
let num2 = +resultStack.pop()
resultStack.push(calMap[token](num1, num2))
continue
}
}
return resultStack[0]
}
expect(calc('123*+')).toEqual(7)
複製代碼
轉化以後計算步驟以下:
let resultStack = []
複製代碼
let token = input.pop()
複製代碼
if (/[0-9]/.test(token)) {
resultStack.push(token)
continue
}
複製代碼
if (/[+\-*/]/.test(token)) {
let num1 = +resultStack.pop()
let num2 = +resultStack.pop()
resultStack.push(calMap[token](num1, num2))
continue
}
複製代碼
while (input.length) {
// ...
}
return resultStack[0]
複製代碼
動畫演示:
轉化成逆波蘭式以後有兩個優勢:
(1+2)*(3+4)
,能夠轉化爲12+34+*
,按照逆波蘭式運算方法便可完成運算這是問題1的最後一個小問題了,這個問題的實現過程以下:
let parse = (input) => {
input = [...input].reverse()
let resultStack = [], opStack = []
while (input.length) {
let token = input.pop()
if (/[0-9]/.test(token)) {
resultStack.push(token)
continue
}
if (/[+\-*/]/.test(token)) {
opStack.push(token)
continue
}
}
return [...resultStack, ...opStack.reverse()].join('')
}
expect(parse(`1+2-3+4-5`)).toEqual('12+3-4+5-')
複製代碼
準備兩個棧,一個棧存放結果,一個棧存放操做符,最後將兩個棧拼接起來上面的實現能夠將1+2-3+4-5
轉化爲12+3-4+5-
,可是若是涉及到優先級,就無能爲力了,例如
expect(parse(`1+2*3`)).toEqual('123*+')
複製代碼
1+2*3
的轉化結果應該是123*+
,但其實轉化的結果倒是123+*
,*/
的優先級高於+
,因此,應該作以下修改
let parse = (input) => {
input = [...input].reverse()
let resultStack = [], opStack = []
while (input.length) {
let token = input.pop()
if (/[0-9]/.test(token)) {
resultStack.push(token)
continue
}
// if (/[+\-*/]/.test(token)) {
// opStack.push(token)
// continue
// }
if (/[*/]/.test(token)) {
while (opStack.length) {
let preOp = opStack.pop()
if (/[+\-]/.test(preOp)) {
opStack.push(preOp)
opStack.push(token)
token = null
break
} else {
resultStack.push(preOp)
continue
}
}
token && opStack.push(token)
continue
}
if (/[+\-]/.test(token)) {
while (opStack.length) {
resultStack.push(opStack.pop())
}
opStack.push(token)
continue
}
}
return [...resultStack, ...opStack.reverse()].join('')
}
expect(parse(`1+2`)).toEqual('12+')
expect(parse(`1+2*3`)).toEqual('123*+')
複製代碼
*/
的時候,取出棧頂元素,判斷棧中的元素的優先級低是否低於*/
,若是是就直接將操做符推入opStack
,而後退出,不然一直將棧中取出的元素推入resultStack
。if (/[+\-]/.test(preOp)) {
opStack.push(preOp)// 這裏用了棧來作判斷,因此判斷完還得還回去...
opStack.push(token)
token = null
break
}else {
resultStack.push(preOp)
continue
}
複製代碼
token && opStack.push(token)
continue
複製代碼
+-
的時候,由於已是最低的優先級了,因此直接將全部的操做符出棧就好了if (/[+\-]/.test(token)) {
while (opStack.length) {
resultStack.push(opStack.pop())
}
opStack.push(token)
continue
}
複製代碼
到這裏已經解決了+-*/
的優先級問題,只剩下()
的優先級問題了,他的優先級是最高的,因此這裏作以下修改便可:
if (/[+\-]/.test(token)) {
while (opStack.length) {
let op=opStack.pop()
if (/\(/.test(op)){
opStack.push(op)
break
}
resultStack.push(op)
}
opStack.push(token)
continue
}
if (/\(/.test(token)) {
opStack.push(token)
continue
}
if (/\)/.test(token)) {
let preOp = opStack.pop()
while (preOp !== '('&&opStack.length) {
resultStack.push(preOp)
preOp = opStack.pop()
}
continue
}
複製代碼
+-
的時候,再也不無腦彈出,若是是(
就不彈出了while (opStack.length) {
let op=opStack.pop()
if (/\(/.test(op)){
opStack.push(op)
break
}
resultStack.push(op)
}
opStack.push(token)
複製代碼
(
的時候,就推入opStack
if (/\(/.test(token)) {
opStack.push(token)
continue
}
複製代碼
)
的時候,就持續彈出opStack
到resultStack
,直到遇到(
,(
不推入resultStack
if (/\)/.test(token)) {
let preOp = opStack.pop()
while (preOp !== '('&&opStack.length) {
resultStack.push(preOp)
preOp = opStack.pop()
}
continue
}
複製代碼
動畫示例:
如此,就完成了中綴轉後綴了,那麼整個問題1就已經被解決了,經過calc(parse(input))
就能完成
中綴=>後綴=>計算
的整個流程了。
雖然上面已經解決了中綴=>後綴=>計算
的大問題,可是最基礎的問題還沒解決,那就是輸入問題,在上面問題1的解決過程當中,輸入不過是簡單的切割,並且還侷限在10之內。而接下來,要解決的就是這個輸入的問題,如何分割輸入,達到要求?
demo
仍是能夠的,可是對於以後的語法檢測之類的東西不太有利,因此不太好,我放棄了這種方法(1+22)*(333+4444)`.match(/([0-9]+)|([+\-*/])|(\()|(\))/g)
// 輸出
// (11)["(", "1", "+", "22", ")", "*", "(", "333", "+", "4444", ")"]
複製代碼
while(input.length){
let token = input.pop()
if(/[0-9]/.test(token)) // 進入數字分析
if(/[+\-*/\(\)]/.test(token))// 進入符號分析
}
複製代碼
接下來試用解決方案2來解決這個問題:
當咱們分割的時候,並不單純保存值,而是將每一個節點保存成一個類似的結構,這個結構可使用對象表示:
{
type:'',
value:''
}
複製代碼
其中,type
是節點類型,能夠將四則運算中全部可能出現的類型概括出來,個人概括以下:
TYPE_NUMBER: 'TYPE_NUMBER', // 數字
TYPE_LEFT_BRACKET: 'TYPE_LEFT_BRACKET', // (
TYPE_RIGHT_BRACKET: 'TYPE_RIGHT_BRACKET', // )
TYPE_OPERATION_ADD: 'TYPE_OPERATION_ADD', // +
TYPE_OPERATION_SUB: 'TYPE_OPERATION_SUB', // -
TYPE_OPERATION_MUL: 'TYPE_OPERATION_MUL', // *
TYPE_OPERATION_DIV: 'TYPE_OPERATION_DIV', // /
複製代碼
value
則是對應的真實值,好比123
、+
、-
、*
、/
。
若是是數字,則繼續往下讀,直到不是數字爲止,將這過程中全部的讀取結果放到value
中,最後入隊。
if (token.match(/[0-9]/)) {
let next = tokens.pop()
while (next !== undefined) {
if (!next.match(/[0-9]/)) break
token += next
next = tokens.pop()
}
result.push({
type: type.TYPE_NUMBER,
value: +token
})
token = next
}
複製代碼
先定義一個符號和類型對照表,若是不在表中,說明是異常輸入,拋出異常,若是取到了,說明是正常輸入,入隊便可。
const opMap = {
'(': type.TYPE_LEFT_BRACKET,
')': type.TYPE_RIGHT_BRACKET,
'+': type.TYPE_OPERATION_ADD,
'-': type.TYPE_OPERATION_SUB,
'*': type.TYPE_OPERATION_MUL,
'/': type.TYPE_OPERATION_DIV
}
let type = opMap[token]
if (!type) throw `error input: ${token}`
result.push({
type,
value: token,
})
複製代碼
這樣就完成了輸入的處理,這時候,其餘的函數也須要處理一下,應爲輸入已經從字符串變成了tokenize
以後的序列了,修改完成以後就是能夠calc(parse(tokenize()))
完成一整套騷操做了。
語法檢測要解決的問題其實就是判斷輸入的正確性,是否知足四則運算的規則,這裏用了相似狀機的思想,不過簡單到爆炸,而且只能作單步斷定~~ 定義一個語法表,該表定義了一個節點後面能夠出現的節點類型,好比,+
後面只能出現數字
或者(
之類。
let syntax = {
[type.TYPE_NUMBER]: [
type.TYPE_OPERATION_ADD,
type.TYPE_OPERATION_SUB,
type.TYPE_OPERATION_MUL,
type.TYPE_OPERATION_DIV,
type.TYPE_RIGHT_BRACKET
],
[type.TYPE_OPERATION_ADD]: [
type.TYPE_NUMBER,
type.TYPE_LEFT_BRACKET
],
//...
}
複製代碼
這樣咱們就能夠簡單的使用下面的語法斷定方法了:
while (tokens.length) {
// ...
let next = tokens.pop()
if (!syntax[token.type].includes(next.type)) throw `syntax error: ${token.value} -> ${next.value}`
// ...
}
複製代碼
對於()
,這裏使用的是引用計數,若是是(
,則計數+1
,若是是)
,則計數-1
,檢測到最後的時候斷定一下計數就行了:
// ...
if (token.type === type.TYPE_LEFT_BRACKET) {
bracketCount++
}
// ...
if (next.type === type.TYPE_RIGHT_BRACKET) {
bracketCount--
}
// ...
if (bracketCount < 0) {
throw `syntax error: toooooo much ) -> )`
}
// ...
複製代碼
()
的處理或許可使用遞歸的方式,進入()
以後從新開始一個新的表達式解析總之:文章到此爲止,有不少不夠詳細的地方還請見諒,多多交流,共同成長。