微信小程序制作科學(xué)計(jì)算器
項(xiàng)目地址:https://github.com/planck-fanqi/wxapp_calculatorWin10
1. 表達(dá)式求值
實(shí)現(xiàn)方法:
-
后綴表達(dá)式
-
簡單編譯器語法樹
本例使用利用簡單編譯器對用戶輸入的表達(dá)式進(jìn)行求值仲吏。相關(guān)文件calculator_compiler.js
測試代碼:
var expression='(1+2)*3'
var tokens=tokenizer(expression);
var ast=parser(tokens);
var est=traverser(ast);
var result=compile(est)
測試結(jié)果:
計(jì)算步驟
1. 詞法解析
function numToken(){//數(shù)值token檢測類岸霹,支持浮點(diǎn)數(shù)檢測
var dotFlag=false;let NUMBERS = /[0-9]/;
this.test=function(char){
if(char=='.' && !dotFlag && (dotFlag=true)) return true;
if(NUMBERS.test(char)) return true;
return false;
}
}
function tokenizer(input, varibles=null) {
let current = 0; let tokens = [];//current定位當(dāng)前字符所在位置
let operators=['+','-','*','/','^','√'];let op_grade={'+':1,'-':1,'*':2,'/':2,'^':3,'√':3};
let LETTERS = /[a-z]/i;
// let NUMBERS = /[0-9]/;
while (current < input.length) {
let char = input[current++];
let NUMBERS=new numToken();
if (char === '(')
tokens.push({ type: 'paren', value: '(' });
else if (char === ')')
tokens.push({ type: 'paren', value: ')' });
else if (NUMBERS.test(char)) {
let value = char;
while (NUMBERS.test(char=input[current]) || char=='.') {//連續(xù)數(shù)值字符串檢測
value += char;
char = input[current++];
}
tokens.push({ type: 'Number', value: parseFloat(value) } );
}
else if (LETTERS.test(char) || char=='π') {
let value = char;
var bugcnt='';
while (LETTERS.test(char=input[current]) && char) {//連續(xù)函數(shù)字符串檢測
value += char;
char = input[current++];
}
if(varibles && varibles[value])//變量字符串賦值连锯,比如Ans者春,e,π
tokens.push({ type:'Number', value:varibles[value]});
else if(value=='e')
tokens.push({ type:'Number', value:Math.E});
else if(value=='π')
tokens.push({ type:'Number', value:Math.PI});
else tokens.push({ type: 'Function', value });
}
else if(operators.indexOf(char)>=0)
tokens.push({ type: 'Operator', value: char, grade:op_grade[char] });
else throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
}
2. 句法解析
function parser(tokens) {
let current = 0;
function walk() {//遞歸函數(shù)遍歷生成語法樹
let token = tokens[current++];
if (token.type === 'Function')
return { type:'Function', value:token.value, expression:walk()}//生成函數(shù)子節(jié)點(diǎn)
if (token.type === 'paren' && token.value === '(' ) {//生成新子語法樹節(jié)點(diǎn)
token = tokens[current];
let node = { type: 'CallExpression', params: [] };
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
return token;
}
let ast = { type: 'CallExpression', params:[]};//新建語法樹根節(jié)點(diǎn)
while (current < tokens.length)
ast.params.push(walk());
return ast;
}
3. 語法樹轉(zhuǎn)換
當(dāng)前獲得的語法樹并不適合計(jì)算,需要將進(jìn)行轉(zhuǎn)換成以計(jì)算符號為父節(jié)點(diǎn)的語法樹。
function traverser(ast) {
function traverseNode(params,start,end) {
if(start+1==end)
if(params[start].type=='CallExpression')//遞歸轉(zhuǎn)換每一個子表達(dá)式
return traverser(params[start]);
else if(params[start].type=='Function')
return { type:'Function', value:params[start].value,
expression:traverser(params[start].expression)}//轉(zhuǎn)換函數(shù)節(jié)點(diǎn)子表達(dá)式
else return params[start];
var mid=end, grade_min=9;
for(var i=start;i<end;i++)
if(params[i].type=='Operator' && params[i].grade<=grade_min) //對于沒有括號但是計(jì)算符號等級不同的計(jì)算式,查找優(yōu)先級最低負(fù)號進(jìn)行分割灾常,比如 1*2+3*4=>(1*2)+(3*4)
grade_min=params[mid=i].grade;
var node={ type:'BinaryOperator', params:[], value:params[mid].value};
if(mid!=start) node.params.push(traverseNode(params,start,mid));
if(mid!=end) node.params.push(traverseNode(params,mid+1,end));
return node;
}
return traverseNode(ast.params,0,ast.params.length);
}
4.編譯計(jì)算
function compile(t_ast,angle=true){
var visitor={//各符號與函數(shù)對應(yīng)計(jì)算式
'+': (a,b)=>{ return a+b; },
'-': (a,b)=>{ if(b==undefined) return -a;//當(dāng) 減號 為單目運(yùn)算符時,將數(shù)字轉(zhuǎn)換成負(fù)數(shù)
return a-b; },
'*': (a,b)=>{ return a*b; },
'/': (a,b)=>{ return a/b; },
'^': (a,b)=>{ return Math.pow(a, b); },
'√': (a,b)=>{ if(b==undefined) return Math.pow(a,.5)//當(dāng) 根號 為單目運(yùn)算符時铃拇,默認(rèn)為開平方根
return Math.pow(b,1/a); },
'sin': (a,angle=false)=>{ return Math.sin( (angle?Math.PI/180:1)*a); },
'cos': (a,angle=false)=>{ return Math.cos( (angle?Math.PI/180:1)*a); },
'tan': (a,angle=false)=>{ return Math.tan( (angle?Math.PI/180:1)*a); },
'cot': (a,angle=false)=>{ return 1/Math.tan( (angle?Math.PI/180:1)*a); },
'asin': (a,angle=false)=>{ return Math.asin(a)/(angle?Math.PI/180:1); },
'acos': (a,angle=false)=>{ return Math.acos(a)/(angle?Math.PI/180:1); },
'atan': (a,angle=false)=>{ return Math.atan(a)/(angle?Math.PI/180:1); },
'acot': (a,angle=false)=>{ return 1/Math.atan(a)/(angle?Math.PI/180:1); },
'log': (a)=>{ return Math.log10(a); },
'ln': (a)=>{ return Math.log(a); },
'sqrt': (a)=>{ return Math.pow(a,0.5); },
'square':(a)=>{ return Math.pow(a,2); },
}
function calculateNode(node){//遞歸方法計(jì)算表達(dá)式根節(jié)點(diǎn)
if(!node) return undefined
if(node.type=='Number') return node.value;
var method=visitor[node.value];
if(node.type=='BinaryOperator')
return method(calculateNode(node.params[0]),calculateNode(node.params[1]));
if(node.type=='Function')
return method(calculateNode(node.expression),angle);
}
return calculateNode(t_ast);
}
2. 用戶輸入合法性檢查
相關(guān)文件BtnCheck.js
钞瀑,為了保證用戶輸入表達(dá)式正確性將對用戶每個輸入進(jìn)行合法性檢查,比如用戶已輸入3.45
后再輸入.
即為無效輸入慷荔。此文件通過用戶輸入生成不同狀態(tài)的狀態(tài)機(jī)以檢查下一個輸入合法性雕什。狀態(tài)機(jī)轉(zhuǎn)換圖如下:
3. 小程序結(jié)構(gòu)
-
界面設(shè)計(jì)
軟件界面采用毛玻璃UI設(shè)計(jì)。相關(guān)文件
index.wxss
,index.wxml
-
邏輯設(shè)計(jì)
相關(guān)文件
index.js
4. 其他
除了基本的計(jì)算功能之外贷岸,本項(xiàng)目添加了一些提高用戶交互的功能壹士,比如,歷史記錄長按輸入偿警。函數(shù)鍵盤滑動查看三角函數(shù)躏救,以及屏幕區(qū)字符定位等功能。不過實(shí)現(xiàn)邏輯比較復(fù)雜螟蒸,沒有詳細(xì)解釋盒使。