背景:
近期項目須要自己完畢Excel的公式解析和求值,在Java中能夠使用POI解析Excel公式然后求值。可是項目須要JS端和Java后端均須要支持公式解析,所以就須要自己寫一套了。事實上公式解析器整體上並不復雜。原理使用逆波蘭表達式就可了。
難點:
1. 針對復雜的用戶輸入環境解析公式,須要注意公式書寫不規范、大寫和小寫、空格等問題,甚至公式出錯的推斷。
2. 須要解決函數擴展、函數運行等問題。
3. 須要解決地址、地址范圍取數,求值問題。
4. 處理括號帶來的優先級提升。
5. 解決公式嵌套求知問題。
6. 財務小數精度,解決採用IEEE 754標准出現的0.3 –0.2 != 0.1問題。
7. 解決循環引用問題,也就是公式鏈成環問題。
理論和原理:
1. 基於逆波蘭表達式。將用戶輸入解析為后綴表達式。
2. 抽象操作數和操作符,操作數(operand)操作符(operator)分別放入兩個stack。
3. 將函數抽象為operator。地址、地址范圍抽象為operand。
關於逆波蘭表達式,在數據結構和編譯原理中都已經講爛了。簡介下:
逆波蘭表達式是波蘭邏輯學家在1929年提出的一種表達式表達方式。
我們傳統的表達式操作符一般都是在兩個數之間的(先僅限於二元操作符)。而逆波蘭表達式的操作符在數字的后面。看以下一個簡單樣例就知道了:
轉換前:1 + 2 – 3 * 4 轉換后:1 , 2 , + , 3 , 4 , * , -
長處:逆波蘭表達式很適合機器運行。能屏蔽掉括號對運算符的優先級的提升。
一般算法:
逆波蘭表達式的一般解析算法是建立在簡單算術表達式上的,它是我們進行公式解析和運行的基礎:
1. 構建兩個棧Operand(操作數棧)和Operator(操作符棧)。
2.掃描給定的字符串,假設得到一個數字,則提取(掃描是一位一位的,一定要提取一個完整的數字)數字(下面用Operand取代),然后把Operand壓入Operand棧中。
3. 假設獲得一個運算符(比方+或者*,以下用B取代),則須要和Operator棧棧頂元素(用A替代)比較:
1) 假設A不存在,則把B壓入Operator棧中。
2)假設B是一個左括號,則忽略A和B的優先級比較,把B壓入Operator棧。
3)假設B是一個右括號。則把Operator棧順序出棧,然后把彈出的元素順序壓入Operand棧中,直到棧頂彈出的是左括號,括號不入Operand棧中。
4)假設A是左括號。則把B直接壓入Operator棧。
5)假設B優先級比較A高。則把B直接壓入Operator棧。
6)假設B優先級低於或等於A的優先級。則把A出棧然后壓入Operand棧,重復進行此步驟直到棧頂優先級高於B的優先級或者棧頂是一個括號。
4.掃描完成后。把Operator棧的元素依次出棧,然后依次壓入Operand棧中。
算法特點:
1.使用兩個stack,用來構建后綴表達式,Operator棧忽略括號的情況下,始終是高優先級的Operator在棧頂。
2.括號的優先級最低,優先級優自定義。
3.到最后我們僅僅剩下一個Operator棧,從棧低依次運算就可以。
4.優先級比較是關鍵,優先級關系到出入棧順序和終於結果。
公式解析的復雜性:
相比傳統的算術表達式的解析,Excel類公式的解析更加復雜:
1. 須要支持的操作數和操作符眾多:不不過數字操作數和數學操作符,還會包括比較運算符、函數、邏輯操作數。字符串操作數,地址(如Excel的A1,A2)等。
2. 公式的組成比較復雜。掃描困難:掃描時須要提取數字(有可能是科學計數法)、單詞(比方函數、地址、地址范圍),還可能出現一元操作符(比方取非!。預防需求的變化,需求是最坑程序猿的)。
3. 同一操作符代表的含義不同:操作符重載情況比較到。比方“-”。就可以能是“減號”,也可能是“符號”;再比方運行函數功能時。函數須要多少個操作數。地址范圍(A1:A10)怎樣運行求值。
4. 計算精度,這是最麻煩的(Java和JS的那個坑爹的0.1問題),既要保證效率,又要保證精度(這個世界不公平啊)。
總結:
使用逆波蘭表達式解析公式,我們須要對整改算法做小小的修改。將操作數和操作符的范圍提升,不只局限在算術表達式的范圍內。
這就要求我們解析的時候書寫正則表達式(或詞法分析的辦法)來提取完整的操作數、操作符,甚至依據上下文環境對操作符進行重載(比方-究竟是減法還是符號)。今天到這兒,明天說下改進算法和代碼。
PS:轉載請注明出處。