【OpenHarmony/HarmonyOs 】科学计算器实现细节:本地表达式解析、历史记录与零网络依赖

【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++23*/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() }

这里有两个设计点:

  1. 历史最多保留 100 条,避免无限增长;
  2. 每次计算会调用AppState.recordCalculation(),同步到学习统计。

也就是说,计算器不只是工具页,它也参与了整个数学学习闭环。

九、隐私角度:零网络依赖更适合学习工具

科学计算器完全可以接云端 AI,让它解释每一步。但对当前项目来说,本地计算更合适:

  • 🔐 表达式不上传;
  • ⚡ 计算即时完成;
  • 📶 没网也能用;
  • 🧒 更适合学生独立练习;
  • 🧩 可与本地历史、成就、今日目标联动。

这也是“精细化权限管控”的一种体现:不是所有智能都要接云端,不是所有计算都要请求权限。

十、总结

这篇文章和另一个项目里的“公式计算器”不同,重点在数学视界科学计算器的本地表达式解析。

核心实现包括:

  • 🧮 用displayexpression分离展示与计算;
  • ✋ 用appendOp()拦截连续运算符;
  • 🧠 用递归下降解析器处理优先级;
  • 🔢 支持加减乘除、取余、幂运算、括号和科学计数法;
  • 🧾 用calcHistory保存最近 100 条计算记录;
  • 🎯 用recordCalculation()接入学习统计;
  • 🔐 全程本地计算,零网络依赖。

对 OpenHarmony 数学学习 App 来说,这类本地计算能力非常值得打磨。它不花哨,但可靠、快速、隐私友好。✨