微信小程序制作科学计算器
1. 表达式求值
实现方法:
本例使用利用简单编译器对用户输入的表达式进行求值。相关文件calculator_compiler.js
测试代码:
var expression='(1+2)*3'
var tokens=tokenizer(expression);
var ast=parser(tokens);
var est=traverser(ast);
var result=compile(est)
测试结果:
计算步骤
1. 词法解析
function numToken(){//数值token检测类,支持浮点数检测
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定位当前字符所在位置
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=='.') {//连续数值字符串检测
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) {//连续函数字符串检测
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() {//递归函数遍历生成语法树
let token = tokens[current++];
if (token.type === 'Function')
return { type:'Function', value:token.value, expression:walk()}//生成函数子节点
if (token.type === 'paren' && token.value === '(' ) {//生成新子语法树节点
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:[]};//新建语法树根节点
while (current < tokens.length)
ast.params.push(walk());
return ast;
}
3. 语法树转换
当前获得的语法树并不适合计算,需要将进行转换成以计算符号为父节点的语法树。
function traverser(ast) {
function traverseNode(params,start,end) {
if(start+1==end)
if(params[start].type=='CallExpression')//递归转换每一个子表达式
return traverser(params[start]);
else if(params[start].type=='Function')
return { type:'Function', value:params[start].value,
expression:traverser(params[start].expression)}//转换函数节点子表达式
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) //对于没有括号但是计算符号等级不同的计算式,查找优先级最低负号进行分割,比如 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.编译计算
function compile(t_ast,angle=true){
var visitor={//各符号与函数对应计算式
'+': (a,b)=>{ return a+b; },
'-': (a,b)=>{ if(b==undefined) return -a;//当 减号 为单目运算符时,将数字转换成负数
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)//当 根号 为单目运算符时,默认为开平方根
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){//递归方法计算表达式根节点
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. 用户输入合法性检查
相关文件BtnCheck.js
,为了保证用户输入表达式正确性将对用户每个输入进行合法性检查,比如用户已输入3.45
后再输入.
即为无效输入。此文件通过用户输入生成不同状态的状态机以检查下一个输入合法性。状态机转换图如下:
3. 小程序结构
-
界面设计
软件界面采用毛玻璃UI设计。相关文件
index.wxss
,index.wxml
-
逻辑设计
相关文件
index.js
4. 其他
除了基本的计算功能之外,本项目添加了一些提高用户交互的功能,比如,历史记录长按输入。函数键盘滑动查看三角函数,以及屏幕区字符定位等功能。不过实现逻辑比较复杂,没有详细解释。