2021-02-28

微信小程序制作科学计算器

项目地址:https://github.com/planck-fanqi/wxapp_calculatorWin10

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.wxssindex.wxml

  • 逻辑设计

    相关文件index.js

4. 其他

除了基本的计算功能之外,本项目添加了一些提高用户交互的功能,比如,历史记录长按输入。函数键盘滑动查看三角函数,以及屏幕区字符定位等功能。不过实现逻辑比较复杂,没有详细解释。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容