【最新鸿蒙开发api12、DevEco5.0版本 | 逆波兰表达式】- 计算器的实现!!!

你是否曾经想过,当你在计算器上按下那些看似简单的按钮时,背后究竟发生了什么魔法?今天,我们就要揭开这个神秘的面纱,一起来探索如何在HarmonyOS上实现一个简单而强大的计算器!准备好你的咖啡,因为我们即将踏上一段既有趣又富有挑战性的编码之旅。

一、二话不说,先看效果图

image.pngimage.pngimage.png

二、基本实现思路

在我们开始编码之前,让我们先来理清思路。实现这个计算器主要分为三个步骤:

  1. 中缀表达式转为后缀表达式
  2. 计算后缀表达式
  3. UI布局

听起来很简单,对吧?但是,正如古人所说:”台上一分钟,台下十年功”。让我们一步步来解析这个看似简单的过程。其实也不难,哈哈哈

1. 中缀表达式转为后缀表达式

首先,什么是中缀表达式?简单来说,就是我们平常使用的表达式,例如 3 + 4 * 2。但是,计算机并不像我们人类那样聪明,它需要一种更明确的表达方式 – 这就是后缀表达式(逆波兰表达式)的用武之地了。

将中缀表达式转换为后缀表达式的过程,就像是在玩一场复杂的纸牌游戏。我们需要仔细考虑每个运算符的优先级,就像在决定哪张牌应该先出一样。这个过程可以通过使用栈来实现,就像我们在整理扑克牌时会用到的那样。

转换步骤

  1. 初始化两个栈:一个用于存放操作符(operations),另一个用于存放输出结果(output)。

  2. 遍历中缀表达式的每个token

    • 如果是数字,则直接放入output栈。

    • 如果是左括号,则放入operations栈。

    • 如果是右括号,则从operations栈弹出操作符直到遇到左括号为止。

    • 如果是操作符,则根据操作符优先级处理:

      • 若当前操作符优先级高于栈顶操作符,则将当前操作符放入operations栈。
      • 否则,将operations栈顶的操作符弹出并放入output栈,直到当前操作符优先级高于栈顶操作符。

2. 计算后缀表达式

一旦我们得到了后缀表达式,计算就变得相对简单了。我们只需要从左到右扫描表达式,遇到数字就入栈,遇到运算符就取出栈顶的两个数进行运算,然后将结果压回栈中。这个过程就像是在玩一个反向的俄罗斯方块游戏,我们不断地堆积数字,然后用运算符消除它们。

计算步骤

  1. 初始化一个栈用于存放数字。

  2. 遍历后缀表达式的每个token

    • 如果是数字,则压入栈中。
    • 如果是操作符,则从栈中弹出两个数字进行运算,并将结果压入栈中。

3. UI布局

最后,我们需要为我们的计算器穿上一件漂亮的外衣。使用HarmonyOS的UI组件,我们可以创建一个既美观又实用的界面。我们将使用Grid布局来组织按钮,使用Column和Row来排列表达式和结果显示区域。

三、核心代码实现

现在,让我们深入代码的海洋,看看这些想法是如何变成现实的。

1. 中缀表达式转为后缀表达式

getCalculate(expr:string): number {
    if('+-✖➗%^'.includes((expr[expr.length - 1])))
      return NaN
    
    let tokens = expr.match(/(d+(.d+)?|+|-|✖|➗|%|(|)|!|^)/g) || [];
    let output: string[] = []
    let operations: string[] = []

    const getPrecedence = (op:string):number => {
      switch(op){
        case '!':
          return 6
        case '^':
          return 4
        case '%':
          return 3
        case '✖':
        case '➗':
          return 2
        case '+':
        case '-':
          return 1
        default:
          return 0
      }
    }

    
    let prevToken:string = 'NaN';
    for (let token of tokens) {
      
      if (!isNaN(Number(token))) {
        output.push(token)
      } else if (token == '(') {
        operations.push(token)
      } else if (token == '!') {
        if (isNaN(Number(prevToken)))
          return NaN
        else
          operations.push(token)
      } else if (token == '-' && (prevToken == 'NaN' || prevToken == '(' || '+-✖➗%^'.includes(prevToken))) {
        
        output.push('-1')
        operations.push('✖')
      } else {
        
        while (operations.length > 0 && getPrecedence(operations[operations.length - 1]) >= getPrecedence(token)) {
          let op = operations.pop()
          if (op === '(')
            break
          if (op) {
            output.push(op)
          }
        }
        if (token != ')')
          operations.push(token)
      }
      prevToken = token;
    }

    
    while (operations.length > 0) {
      const op = operations.pop()
      if (op !== undefined)
        output.push(op)
    }

这段代码就像是一个精密的排序机器。它将表达式拆分成一个个”令牌”,然后根据每个运算符的优先级,将它们重新排列成后缀表达式。注意我们如何处理负数和特殊运算符(如阶乘和幂运算),这就像是在处理一些特殊的扑克牌。

2. 计算后缀表达式


    let nums: number[] = []

    for (let value of output) {
      if (!isNaN(Number(value))) {
        nums.push(Number(value))
      } else if (value == '!') {
        let num1 = nums.pop()
        if (num1 !== undefined)
          nums.push(this.factorial(num1))
      } else {
        
        let num1 = nums.pop() ?? 0
        let num2 = nums.pop() ?? 0
        switch (value) {
          case '+':
            nums.push(num2 + num1)
            break
          case '-':
            nums.push(num2 - num1)
            break
          case '➗':
            if (num1 !== 0)
              nums.push(num2 / num1)
            else
              return NaN  
            break
          case '✖':
            nums.push(num2 * num1)
            break
          case '%':
            nums.push(num2 % num1)
            break
          case '^':
            nums.push(Math.pow(num2, num1))
            break
        }
      }
    }

    return nums[0] ?? NaN;
  }

这部分代码就像是一个高效的装配线。它从左到右读取后缀表达式,遇到数字就放入”仓库”(栈),遇到运算符就从”仓库”中取出数字进行运算,然后将结果放回”仓库”。最后,仓库中剩下的唯一一个数字就是我们的计算结果。

3. UI布局

@Component
struct KeyGrid{
  @State rowsTemp:string = '1fr 1fr 1fr 1fr 1fr'
  @State rows:number = 5
  @Link@Watch('aboutToAppear') isShowMore: boolean
  @Link expressionFontSize:number
  @Link expressionColor:string
  @Link resultFontSize:number
  @Link resultColor:string
  onKeyPress?: (key:string) => void
  deleteLast?: () => void
  deleteAll?: () => void
  calculateExp?: () => void

  @State pressedItems: Map = new Map();

  aboutToAppear(): void {
    this.JudgeIsMore(this.isShowMore)
  }

  JudgeIsMore(judge: boolean){
    if( judge == true){
      this.rowsTemp = '1fr 1fr 1fr 1fr 1fr 1fr'
      this.rows = 6
    }else{
      this.rowsTemp = '1fr 1fr 1fr 1fr 1fr'
      this.rows = 5
    }
  }

  build(){
    Grid(){
      
      if( this.isShowMore){
        GridItem(){
          Text('(')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('(')
        })
        GridItem(){
          Text(')')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress(')')
        })
        GridItem(){
          Text('!')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('!')
        })
        GridItem(){
          Text('^')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('^')
        })
      }

      
      GridItem(){
        Text('AC')
      }.KeyBoxStyleOther()
      .onClick( () => {
        if( this.deleteAll)
        this.deleteAll()
      })
      .selected(true)

我们使用Grid容器和Column和Row来创建一个层次分明的布局,以及如何使用动画来增加用户体验。

四、完整代码(希望大家点个免费的赞或关注,完整代码直接给大家了,有问题请评论)

import { Header } from '../common/components/commonComponents'
import Decimal from '@arkts.math.Decimal';
const nums:string[] = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '00', '0', '.']


@Entry
@Component
struct Calculator {
  @State
  @Watch('calculateExp')
  expression:string = ''
  @State result:Decimal = new Decimal(0)
  @State theResult: string = ''
  @State isShowMore:boolean = false

  @State expressionFontSize: number = 40
  @State expressionColor: string = '#000000'
  @State resultFontSize: number = 30
  @State resultColor: string = '#000000'
  @State isHaveDecimal: boolean = false
  @State currentNumber: string = ''  


  build() {
    Column(){
      
      Row(){
        Image($r('app.media.cal_more'))
          .width(30)
          .height(30)
          .margin(20)
          .onClick( () => {
            this.isShowMore = !this.isShowMore
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.End)
      .alignItems(VerticalAlign.Top)
      Column(){
        
        Row(){
          TextArea({text:this.expression})
            .fontSize(this.expressionFontSize)
            .fontColor(this.expressionColor)
            .fontWeight(700)
            .textAlign(TextAlign.End)
              
            .backgroundColor(Color.White)
            .padding({bottom:20})
            .animation( {
              duration:500
            })
        }
        .padding({right:20})
        .justifyContent(FlexAlign.End)
        .width('100%')
        Divider().width('94%').opacity(1)
        
        Row(){
          Text(this.theResult)
            .textAlign(TextAlign.End)
            .backgroundColor(Color.White)
            .fontColor(this.resultColor)
            .fontSize(this.resultFontSize)
            .fontWeight(700)
            .maxLines(1)
            .textOverflow({ overflow:TextOverflow.MARQUEE })
            .animation( {
              duration:500
            })

        }
        .padding({right:20})
        .justifyContent(FlexAlign.End)
        .width('100%')
        .height(60)

        KeyGrid({isShowMore: this.isShowMore, expressionFontSize: this.expressionFontSize, expressionColor: this.expressionColor,
          resultFontSize: this.resultFontSize, resultColor: this.resultColor,
          onKeyPress: (key):void => this.handleKeyEvent(key), deleteLast: ():void => this.deleteLast(),
          deleteAll: ():void => this.deleteAll(), calculateExp: ():void => this.calculateExp()
        })
          .width('100%')
          .height('50%')
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.End)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .padding({bottom:50})
  }

  

  handleKeyEvent(key: string) {
    const operators = '+-✖➗%^';

    if (operators.includes(key)) {
      
      this.currentNumber = '';
      this.isHaveDecimal = false;
      if (this.expression === '' ) {
        return; 
      }else if(operators.includes(this.expression[this.expression.length - 1])){
        this.expression = this.expression.slice(0,-1)
        this.expression += key;
        return;
      }
    } else if (key === '.') {
      if (this.currentNumber.includes('.')) {
        return; 
      }
      if (this.currentNumber === '' || operators.includes(this.expression[this.expression.length - 1])) {
        this.currentNumber = '0';
        this.expression += '0';
      }
    } else if (key.match(/d/)) {
      if (this.currentNumber === '0' && key !== '0') {
        this.currentNumber = '';
        this.expression = this.expression.slice(0, -1);
      }
    }

    this.currentNumber += key;
    this.expression += key;
  }

  

  deleteLast() {
    if( this.expression.length === 0){
      this.result = new Decimal(0);
    }
    if (this.expression.endsWith('.')) {
      this.currentNumber = this.currentNumber.slice(0, -1);
    }
    this.expression = this.expression.slice(0, -1);
    const lastOperatorIndex = Math.max(
      this.expression.lastIndexOf('+'),
      this.expression.lastIndexOf('-'),
      this.expression.lastIndexOf('✖'),
      this.expression.lastIndexOf('➗'),
      this.expression.lastIndexOf('%'),
      this.expression.lastIndexOf('^')
    );
    this.currentNumber = this.expression.slice(lastOperatorIndex + 1);
    this.calculateExp();
    if (this.expression === '' || isNaN(Number(this.expression[this.expression.length - 1])))
      this.result = new Decimal(0);
  }

  deleteAll() {
    this.expression = "";
    this.result = new Decimal(0);
    this.currentNumber = '';
    this.isHaveDecimal = false;
  }


  calculateExp(){
    const operators = '+-✖➗%^';

    try{
      this.result = this.getCalculate(this.expression)
      if( this.expression.length === 0){
        this.theResult = ''
      }
      else if( operators.includes(this.expression[this.expression.length - 1])){
        return
      }
      else if( this.result.toString() === 'NaN'){
        this.theResult = '表达式错误'
      }else {
        this.theResult = this.result.toString()
      }
    }catch(err){
      console.error(err + '计算错误')
    }
    this.expressionFontSize = 40
    this.expressionColor = '#000000'
    this.resultFontSize = 30
    this.resultColor = '#888888'
  }

  factorial(n: Decimal): Decimal {
    if (n.toNumber() <= 1) {
      return new Decimal(1);
    } else {
      return new Decimal(new Decimal(n).mul(this.factorial(n.sub(1))));
    }
  }

  





  getCalculate(expr:string): Decimal {

    
    let tokens: string[] = expr.match(/(d+(.d+)?|+|-|✖|➗|%|(|)|!|^)/g) || [];
    let output: string[] = []
    let operations: string[] = []

    const getPrecedence = (op:string):number => {
      switch(op){
        case '!':
          return 6
        case '^':
          return 4
        case '%':
          return 3
        case '✖':
        case '➗':
          return 2
        case '+':
        case '-':
          return 1
        default:
          return 0
      }
    }

    
    let prevToken:string = 'NaN';
    for (let token of tokens) {
      
      if (!isNaN(Number(token))) {
        output.push(token)
      } else if (token == '(') {
        operations.push(token)
      } else if (token == '!') {
        if (isNaN(Number(prevToken)))
          return new Decimal(NaN)
        else
          operations.push(token)
      } else if (token == '-' && (prevToken == 'NaN' || prevToken == '(' || '+-✖➗%^'.includes(prevToken))) {
        
        output.push('-1')
        operations.push('✖')
      } else if('+-✖➗'.includes(prevToken) && '+-✖➗'.includes(token)){
        this.expression = expr.slice(0,-1)
        this.expression += token
        operations.push(token)
      }else {
        
        while (operations.length > 0 && getPrecedence(operations[operations.length - 1]) >= getPrecedence(token)) {
          let op = operations.pop()
          if (op === '(')
            break
          if (op) {
            output.push(op)
          }
        }
        if (token != ')')
          operations.push(token)
      }
      prevToken = token;
    }

    
    while (operations.length > 0) {
      const op = operations.pop()
      if (op !== undefined)
        output.push(op)
    }

    
    let nums: Decimal[] = []

    for (let value of output) {
      if (!isNaN(Number(value))) {
        nums.push(new Decimal(Number(value)))
      } else if (value == '!') {
        let num1 = nums.pop()
        if (num1 !== undefined)
          nums.push(new Decimal(this.factorial(num1)))
      } else {
        
        let num11 = nums.pop() ?? 0
        let num22 = nums.pop() ?? 0
        let num1 = new Decimal(num11)
        let num2 = new Decimal(num22)
        switch (value) {
          case '+':
            nums.push(num2.add(num1))
            break
          case '-':
            nums.push(num2.sub(num1))
            break
          case '➗':
            if (num1.toNumber() !== 0)
              nums.push(num2.div(num1))
            else
              return new Decimal(NaN)  
            break
          case '✖':
            nums.push(num2.mul(num1))
            break
          case '%':
            nums.push(num2.mod(num1))
            break
          case '^':
            nums.push(num2.pow(num1))
            break
        }
      }
    }

    return nums[0] ?? new Decimal(NaN);
  }

}

@Component
struct KeyGrid{
  @State rowsTemp:string = '1fr 1fr 1fr 1fr 1fr'
  @State rows:number = 5
  @Link@Watch('aboutToAppear') isShowMore: boolean
  @Link expressionFontSize:number
  @Link expressionColor:string
  @Link resultFontSize:number
  @Link resultColor:string
  onKeyPress?: (key:string) => void
  deleteLast?: () => void
  deleteAll?: () => void
  calculateExp?: () => void

  @State pressedItems: Map = new Map();

  aboutToAppear(): void {
    this.JudgeIsMore(this.isShowMore)
  }

  JudgeIsMore(judge: boolean){
    if( judge == true){
      this.rowsTemp = '1fr 1fr 1fr 1fr 1fr 1fr'
      this.rows = 6
    }else{
      this.rowsTemp = '1fr 1fr 1fr 1fr 1fr'
      this.rows = 5
    }
  }

  build(){
    Grid(){
      
      if( this.isShowMore){
        GridItem(){
          Text('(')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('(')
        })
        GridItem(){
          Text(')')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress(')')
        })
        GridItem(){
          Text('!')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('!')
        })
        GridItem(){
          Text('^')
        }.KeyBoxStyleOther()
        .onClick( () => {
          if(  this.onKeyPress)
            this.onKeyPress('^')
        })
      }

      
      GridItem(){
        Text('AC')
      }.KeyBoxStyleOther()
      .onClick( () => {
        if( this.deleteAll)
        this.deleteAll()
      })
      .selected(true)
      GridItem(){
        Text('%')
      }.KeyBoxStyleOther()
      .onClick( () => {
        if(  this.onKeyPress)
            this.onKeyPress('%')
      })
      GridItem(){
        Image($r('app.media.calculator_delete'))
          .width(20)
          .height(20)
      }
      .KeyBoxStyleOther()
      .onClick( () => {
        if(  this.deleteLast)
          this.deleteLast()
      })

      GridItem(){
        Text('➗')
      }
      .KeyBoxStyleOther()
      .onClick( () => {
        if(  this.onKeyPress)
          this.onKeyPress('➗')
      })

      GridItem(){
        Text('✖')
      }
      .KeyBoxStyleOther()
      .onClick( () => {
        if(  this.onKeyPress)
          this.onKeyPress('✖')
      })
      .rowStart(this.rows - 4)
      .columnStart(3)
      .rowEnd(this.rows - 4)
      .columnEnd(3)

      GridItem(){
        Text('➖')
      }
      .KeyBoxStyleOther()
      .onClick( () => {
        if(  this.onKeyPress)
          this.onKeyPress('-')
      })
      .rowStart(this.rows - 3)
      .columnStart(3)

      GridItem(){
        Text('➕')
      }
      .KeyBoxStyleOther()
      .rowStart(this.rows - 2)
      .columnStart(3)
      .onClick( () => {
        if(  this.onKeyPress)
          this.onKeyPress('+')
      })


      
      ForEach(
        nums,
        (num:string) => {
          GridItem(){
            Text(num)
              .fontSize(20)
              .fontWeight(700)
          }
          .selected(true)
          .KeyBoxStyle()
          .backgroundColor(this.pressedItems[num] !== true ? Color.White : '#ffa2a0a0')
          .onTouch((event: TouchEvent) => {
            if (event.type === TouchType.Down) {
              this.pressedItems[num] = true;
            } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
              this.pressedItems[num] = false;
            }
          })
          .onClick( () => {
            if(  this.onKeyPress)
              this.onKeyPress(num)
          })
        })

      GridItem(){
        Text('=')
          .fontColor(Color.White)
      }.KeyBoxStyleOther()
      .rowStart(this.rows - 1)
      .columnStart(3)
      .backgroundColor('#ffff6b14')
      .onClick( () => {
        if(  this.calculateExp)
          this.calculateExp()
        this.expressionFontSize = 30
        this.expressionColor = '#888888'
        this.resultFontSize = 40
        this.resultColor = '#000000'
      })
    }
    .width('100%')
    .height(400)
    .columnsTemplate('1fr 1fr 1fr 1fr')
    .rowsTemplate(this.rowsTemp)
    .columnsGap(8)
    .rowsGap(8)
    .backgroundColor('#ffeeebeb')
    .padding(20)
    .animation({
      duration: 300,
      curve: Curve.EaseInOut,
      delay: 50,
    })


  }
}


@Styles function KeyBoxStyleOther(){
  .width(60)
    .backgroundColor('#ffe0dddd')
    .height(60)
    .borderRadius(8)
}

@Styles function KeyBoxStyle(){
  .width(60)
    .backgroundColor(Color.White)
    .height(60)
    .borderRadius(8)
}

结语

就这样,我们的HarmonyOS计算器诞生了!它不仅能够进行基本的算术运算,还能处理复杂的表达式,甚至包括阶乘和幂运算。通过实现这个看似简单的应用,我们实际上涉及了许多重要的编程概念:正则表达式、栈的使用、字符串处理、UI设计等等。

记住,每一个伟大的应用都是从简单的想法开始的。今天的计算器,明天可能就是改变世界的下一个大应用!所以,继续编码,继续创造,让我们一起用HarmonyOS改变世界!

最后,如果你在实现过程中遇到了任何问题,不要气馁。就像计算器处理复杂表达式一样,解决问题的过程可能需要一步步来。保持耐心,保持好奇,你一定会成功的!

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22332,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?