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

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

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

         

二、基本实现思路

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

  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
      }
    }

    // 增加前一个token,用来判断!前是否有数字,以及判断-是否为负号
    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<string, boolean> = 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'

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:number = 0
  @State isShowMore:boolean = false

  @State expressionFontSize: number = 40
  @State expressionColor: string = '#000000'
  @State resultFontSize: number = 30
  @State resultColor: string = '#000000'

  build() {
    Column(){
      /*Header()
          .margin({top:20})*/
      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(){
        //1.计算表达式显示
        Row(){
          TextArea({text:this.expression})
            .fontSize(this.expressionFontSize)
            .fontColor(this.expressionColor)
            .fontWeight(700)
            .textAlign(TextAlign.End)
              //.direction(Direction.Rtl)
            .backgroundColor(Color.White)
            .padding({bottom:20})
            .animation( {
              duration:500
            })
        }
        .padding({right:20})
        .justifyContent(FlexAlign.End)
        .width('100%')
        Divider().width('94%').opacity(1)
        //2.计算结果显示
        Row(){
          TextArea({text:this.result.toString()})
            .textAlign(TextAlign.End)
            .backgroundColor(Color.White)
            .fontColor(this.resultColor)
            .fontSize(this.resultFontSize)
            .fontWeight(700)
            .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){
    this.expression += key
  }

  deleteLast(){
    this.expression = this.expression.slice(0,-1)
    this.calculateExp()
    if( this.expression === '' || isNaN(Number(this.expression[this.expression.length - 1])))
      this.result = 0
  }

  deleteAll(){
    this.expression = ""
    this.result = 0
  }


  calculateExp(){
    try{
      this.result = this.getCalculate(this.expression)
    }catch(err){
      console.error(err + '计算错误')
    }
    this.expressionFontSize = 40
    this.expressionColor = '#000000'
    this.resultFontSize = 30
    this.resultColor = '#888888'
  }

  factorial(n: number): number {
    if (n <= 1) {
      return 1;
    } else {
      return n * this.factorial(n - 1);
    }
}



  getCalculate(expr:string): number {
    if('+-✖➗%^'.includes((expr[expr.length - 1])))
      return NaN
    // 使用正则划分出 整数或小数和符号的数组,修改正则表达式以支持负数
    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
      }
    }

    // 增加前一个token,用来判断!前是否有数字,以及判断-是否为负号
    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)
    }

    //定义一个数组栈,用来计算最终结果
    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;
  }

}

@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<string, boolean> = 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)
}

由于完整的代码相当长,我就不在这里完整展示了。但是,我相信通过上面的讲解,你已经对整个实现过程有了清晰的理解。完整的代码包括了我们讨论过的所有部分,以及一些额外的功能,比如处理特殊运算符(如阶乘)和更多的UI细节。

结语

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

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

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

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值