【OpenHarmony/HarmonyOs 】科学计算器实现细节:本地表达式解析、历史记录与零网络依赖
【OpenHarmony/HarmonyOs 】科学计算器实现细节:本地表达式解析、历史记录与零网络依赖
项目类型:OpenHarmony / HarmonyOS ArkTS 数学学习应用
项目名称:数学视界
对应主题:精细化权限管控、隐私保护方案、端侧 AI
关键词:ArkTS、科学计算器、表达式解析、本地计算、隐私保护、学习数据 🧮
一、为什么计算器也值得单独写一篇?
计算器看起来是一个很常见的功能,但在学习类 App 里,它不仅仅是“输入数字,得到结果”。一个好的数学计算器至少要考虑:
- 运算符优先级;
- 括号;
- 幂运算;
- 科学计数法;
- 三角函数、对数、平方根;
- 角度/弧度切换;
- 计算历史;
- 错误提示;
- 深色模式可读性;
- 不依赖网络、不上传表达式。
数学视界项目里的Calculator.ets做了一个很重要的选择:普通算术表达式没有直接用new Function求值,而是手写了解析器。这一点非常适合写成 CSDN 实战文章。
二、计算器页面的核心状态
计算器的状态主要包括显示值、表达式、角度模式、历史面板、记忆值等:
@Statedisplay: string ='0'@Stateexpression: string =''@StateisRadian: boolean = true@StateisSecond: boolean = false@StateshowHistory: boolean = false@StatememoryValue: number =0@StatelastResult: string ='0'@StateawaitingNthRoot: boolean = false@StatenthRootY: number =0@StateisDarkMode: boolean = false这些状态对应不同功能:
display:屏幕主显示;expression:实际参与计算的表达式;isRadian:三角函数使用弧度还是角度;isSecond:科学计算器第二功能键;showHistory:是否打开历史记录;memoryValue:M+、MR 等记忆功能;awaitingNthRoot:处理 y 次根号这种两步输入。
三、输入处理:防止连续运算符
普通计算器最容易出现的问题是用户连续输入运算符,比如1++2、3*/4。项目中通过appendOp()做了拦截:
appendOp(op:string):void{constdisplayLast:string=this.display.slice(-1)constexprLast:string=this.expression.slice(-1)constopChars:string='+-*/%^'if(opChars.indexOf(displayLast) >=0|| opChars.indexOf(exprLast) >=0) {return}this.display += opthis.expression += op }这个判断很简单,但用户体验会明显提升。错误表达式越早拦截,后面的解析器压力越小。
四、表达式解析器:不用 new Function 的本地求值
项目中定义了一个解析游标:
interfaceExprEvalState{ s:stringi: number }它表示当前正在解析的字符串和位置。解析器按优先级拆成几层:
parseNumberSt():解析数字和科学计数法;parsePrimarySt():解析数字或括号;parseUnarySt():解析正负号;parsePowerSt():解析幂运算;parseTermSt():解析乘除取余;parseExprSt():解析加减。
入口函数如下:
private evaluateArithmeticExpression(expr: string): number {conststate: ExprEvalState = { s: expr, i:0}constv: number = this.parseExprSt(state) this.skipWsSt(state) if (state.i< state.s.length) { throw new Error('extra') } return v }这就是一个典型的递归下降解析器。它的好处是可控、安全、可扩展。
五、运算符优先级:从加减到幂运算
加减优先级最低:
private parseExprSt(state: ExprEvalState): number { let v: number = this.parseTermSt(state) while (true) { this.skipWsSt(state)constop: string =state.s[state.i] if (op === '+') {state.i++ v += this.parseTermSt(state) } else if (op === '-') {state.i++ v -= this.parseTermSt(state) } else { break } } return v }乘除取余优先级更高:
private parseTermSt(state: ExprEvalState): number { let v: number = this.parsePowerSt(state) while (true) { this.skipWsSt(state)constop: string =state.s[state.i] if (op === '*') {state.i++ v *= this.parsePowerSt(state) } else if (op === '/') {state.i++ v /= this.parsePowerSt(state) } else if (op === '%') {state.i++ v %= this.parsePowerSt(state) } else { break } } return v }幂运算使用右结合:
private parsePowerSt(state: ExprEvalState): number {constleft: number = this.parseUnarySt(state) this.skipWsSt(state) if (state.i +1< state.s.length && state.s[state.i] === '*' && state.s[state.i + 1] === '*') { state.i += 2 const right: number = this.parsePowerSt(state) return Math.pow(left, right) } return left }右结合意味着2^3^2会按2^(3^2)理解,这更符合数学幂运算习惯。
六、计算流程:预处理、求值、记录历史
点击等号后会进入calculate():
calculate(): void {try{ let expr: string =this.expression.replace(/,/g,'')if(expr ===''|| expr.trim() ==='') {return} expr = expr.replace(/\^/g,'**') expr =this.preprocessExpression(expr)constresult: number =this.evaluateArithmeticExpression(expr)if(!isFinite(result)) {this.display ='Error'this.expression =''this.showToast('⚠️ 计算错误')return}this.finishCalculation(result) }catch{this.display ='Error'this.expression =''this.showToast('⚠️ 表达式错误') } }这里有几个关键点:
- 先去掉千分位逗号;
- 把
^转换成**; - 做函数名和常量预处理;
- 调用本地解析器求值;
- 捕获异常并显示错误提示。
七、科学函数预处理
项目支持一些常见科学函数:
preprocessExpression(expr:string):string{letresult:string= expr result = result.replace(/sin\(/g,'Math.sin(') result = result.replace(/cos\(/g,'Math.cos(') result = result.replace(/tan\(/g,'Math.tan(') result = result.replace(/sqrt\(/g,'Math.sqrt(') result = result.replace(/log\(/g,'Math.log10(') result = result.replace(/ln\(/g,'Math.log(') result = result.replace(/abs\(/g,'Math.abs(') result = result.replace(/exp\(/g,'Math.exp(') result = result.replace(/pi/g,Math.PI.toString()) result =this.replaceStandaloneE(result)returnresult }这部分目前将函数名映射到了Math。后续如果要让解析器完全接管科学函数,可以把parsePrimarySt()扩展成支持函数调用,例如识别sin(...)、sqrt(...),再在白名单中执行对应数学函数。
八、历史记录:计算也是学习行为
计算成功后,项目会写入历史记录:
finishCalculation(result:number, historyExpr?:string):void{constformatted:string=this.formatResult(result)constprevExpr:string= historyExpr !==undefined? historyExpr :this.expressionthis.lastResult= formattedif(AppState.calcHistory.length>=100) {AppState.calcHistory.pop() }AppState.calcHistory.unshift({id:Date.now().toString(),expression: prevExpr,result: formatted,time:newDate().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit'}),favorite:false, })this.display= formattedthis.expression= formattedAppState.recordCalculation() }这里有两个设计点:
- 历史最多保留 100 条,避免无限增长;
- 每次计算会调用
AppState.recordCalculation(),同步到学习统计。
也就是说,计算器不只是工具页,它也参与了整个数学学习闭环。
九、隐私角度:零网络依赖更适合学习工具
科学计算器完全可以接云端 AI,让它解释每一步。但对当前项目来说,本地计算更合适:
- 🔐 表达式不上传;
- ⚡ 计算即时完成;
- 📶 没网也能用;
- 🧒 更适合学生独立练习;
- 🧩 可与本地历史、成就、今日目标联动。
这也是“精细化权限管控”的一种体现:不是所有智能都要接云端,不是所有计算都要请求权限。
十、总结
这篇文章和另一个项目里的“公式计算器”不同,重点在数学视界科学计算器的本地表达式解析。
核心实现包括:
- 🧮 用
display和expression分离展示与计算; - ✋ 用
appendOp()拦截连续运算符; - 🧠 用递归下降解析器处理优先级;
- 🔢 支持加减乘除、取余、幂运算、括号和科学计数法;
- 🧾 用
calcHistory保存最近 100 条计算记录; - 🎯 用
recordCalculation()接入学习统计; - 🔐 全程本地计算,零网络依赖。
对 OpenHarmony 数学学习 App 来说,这类本地计算能力非常值得打磨。它不花哨,但可靠、快速、隐私友好。✨
