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

发布时间:2026/7/5 1:05:50
【OpenHarmony/HarmonyOs 】科学计算器实现细节:本地表达式解析、历史记录与零网络依赖 【OpenHarmony/HarmonyOs 】科学计算器实现细节本地表达式解析、历史记录与零网络依赖项目类型OpenHarmony / HarmonyOS ArkTS 数学学习应用项目名称数学视界对应主题精细化权限管控、隐私保护方案、端侧 AI关键词ArkTS、科学计算器、表达式解析、本地计算、隐私保护、学习数据 一、为什么计算器也值得单独写一篇计算器看起来是一个很常见的功能但在学习类 App 里它不仅仅是“输入数字得到结果”。一个好的数学计算器至少要考虑运算符优先级括号幂运算科学计数法三角函数、对数、平方根角度/弧度切换计算历史错误提示深色模式可读性不依赖网络、不上传表达式。数学视界项目里的Calculator.ets做了一个很重要的选择普通算术表达式没有直接用new Function求值而是手写了解析器。这一点非常适合写成 CSDN 实战文章。二、计算器页面的核心状态计算器的状态主要包括显示值、表达式、角度模式、历史面板、记忆值等Statedisplay: string 0Stateexpression: string StateisRadian: boolean trueStateisSecond: boolean falseStateshowHistory: boolean falseStatememoryValue: number 0StatelastResult: string 0StateawaitingNthRoot: boolean falseStatenthRootY: number 0StateisDarkMode: boolean false这些状态对应不同功能display屏幕主显示expression实际参与计算的表达式isRadian三角函数使用弧度还是角度isSecond科学计算器第二功能键showHistory是否打开历史记录memoryValueM、MR 等记忆功能awaitingNthRoot处理 y 次根号这种两步输入。三、输入处理防止连续运算符普通计算器最容易出现的问题是用户连续输入运算符比如12、3*/4。项目中通过appendOp()做了拦截appendOp(op:string):void{constdisplayLast:stringthis.display.slice(-1)constexprLast:stringthis.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 Errorthis.expression this.showToast(⚠️ 计算错误)return}this.finishCalculation(result) }catch{this.display Errorthis.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:stringthis.formatResult(result)constprevExpr:string historyExpr !undefined? historyExpr :this.expressionthis.lastResult formattedif(AppState.calcHistory.length100) {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 来说这类本地计算能力非常值得打磨。它不花哨但可靠、快速、隐私友好。✨