君子生非异也,善假于物也!—— HarmonyOS 计算器实现

        前言

        风流要是贤公子,白晰仍为美少年。隐墨同学恋爱了,她爱上了隔壁班的少年,恰同学少年,风华正茂,指点江山,挥斥方遒。隐墨同学彻底的坠入了爱河,一发不可收拾,每天梦想着和少年檫出不一样的火花,来一场特殊的经历。让两个人的生活交轨,可谓衣带渐宽终不悔,为伊消得人憔悴啊!!

        正巧,少年家电脑上的计算器不能正常使用了,你眼睛一亮,真相只有一个,那就是机会,你快速在书架上找到HarmonyOS》大喊:如意如意,按我心意


        一: 需要解决的问题

        1. 小数计算的精度丢失

        有计算机基本基础的都清楚:由于数字都是双精度浮点数,在计算机中是二进制存储数据的,因此小数和非安全整数(超过整数的安全范围[-Math.pow(2, 53),Math.pow(2, 53)]的数据)在计算过程中会存在精度丢失的情况。

        小数运算时:“0.2 + 2.22 = 2.4200000000000004”

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

        隐墨同学提问道,何为要变化呢,为什么要变,如何变!

        逆波兰表达式又叫做后缀表达式。逆波兰表示法是波兰逻辑学家J・卢卡西维兹于1929年首先提出的一种表达式的表示方法   。后来,人们就把用这种表示法写出的表达式称作“逆波兰表达式”。逆波兰表达式把运算量写在前面,把算符写在后面。

        中缀表达式:就是我们平时用的表达式,比如 1+((2+3)*4)-5这种表达式

        “1+((2+3)*4)-5”转换为后缀表达式是”1 2 3 + 4 * + 5 -”,可以发现后缀表达式里没有括号

        明显的可以察觉到没有括号,因为计算机并不会去先计算括号里面的,或者根据优先级的去计算你给定的字符串(你看似输入的是表达式,但是计算机是按字符串进行处理的

        规则:

        1. 遇到数字就直接输出到后缀表达式中,遇到操作符就判断其优先级,并将其压入栈中。
        2. 如果栈顶元素的优先级大于等于当前操作符,则先将栈顶元素弹出并输出到后缀表达式中,再将当前操作符压入栈中。
        3. 如果遇到了左括号,则直接将其压入栈中,如果遇到了右括号,则弹出栈中的元素,直到遇到了左括号为止,并将这些元素输出到后缀表达式中。
        4. 将栈中剩余的元素依次弹出,并输出到后缀表达式中。

        3. 对于异常信息的处理

        仅限于隐墨同学所发现的,有其他情况可以补充呦!!

        1. 参考自己手机上的计算器,机会发现,当表达式的最后一位为符号位时,你在点击例外一个符号他会自动的进行代替。

        2. 对于输入 .3 的结果为 3 或者 ((2)+2 = 4


        二:整体的思路并且代码的实现

        1. UI布局(Grid)

        不难发现,整个计算器的布局就为网格布局(Grid)

        Grid组件为网格容器,其中容器内各条目对应一个GridItem组件

                代码如下

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

const nums:string[] = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '0', '.']

@Entry
@Component
struct Index {
  columnsTemplate: string = '1fr 1fr 1fr 1fr'
  @State rowsTemp: string = '1fr 1fr 1fr 1fr 1fr'
  @State rows: number = 5
  @State expression: string = ''
  @State result: string = '0'
  @State @Watch('aboutToAppear') isShowMore: boolean = false //用于判断是否要更多的的展示
  @State expressionFontSize: number = 40
  @State expressionColor: string = '#000000'
  @State resultFontSize: number = 30
  @State resultColor: string = '#000000'

  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() {
    Column() {
      Row() {
        Image($r('app.media.three_dot'))
          .width(50)
          .height(50)
          .margin(20)
          .onClick(() => {
            this.isShowMore = !this.isShowMore
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.End)
      .alignItems(VerticalAlign.Top) //位于顶端

      Column() {
        Row() {
          TextInput({ text: this.result })
            .fontSize(lengthSize(this.result))
            .fontColor(this.resultColor)
            .fontWeight(700)
            .textAlign(TextAlign.End)
            .backgroundColor(Color.White)
            .padding({ bottom: 20 })
            .animation({ duration: 500 })
        }
        .padding({ right: 15 })
        .justifyContent(FlexAlign.End)
        .width('100%')

        Divider().width('90%').color(Color.Black).height(20)

        Row() {
          TextInput({ text: this.expression })
            .textAlign(TextAlign.End)
            .backgroundColor(Color.White)
            .fontSize(lengthSize(this.expression))
            .fontColor(this.expressionColor)
            .animation({ duration: 500 })
        }
        .padding({ top: 5, bottom: 5 })
        .justifyContent(FlexAlign.End)
        .width('100%')
        .height(70)


        Grid() {
          //是否展开,根据按钮的不同有不同的情况
          if (this.isShowMore) {
            GridItem() {
              Image($r('app.media.rightbucket'))
                .width(30)
            }.KeyBoxStyleOther()
            .onClick(() => {
              this.expression += '('
            })

            GridItem() {
              Image($r('app.media.leftbucket'))
                .width(30)
            }.KeyBoxStyleOther()
            .onClick(() => {
              this.expression += ')'
            })

            GridItem() {
              Image($r('app.media.percent'))
                .width(30)
            }.KeyBoxStyleOther()
            .onClick(() => {
              this.expression += '!'
            })

            GridItem() {
              Image($r('app.media.mi'))
                .width(30)
            }.KeyBoxStyleOther()
            .onClick(() => {
              this.expression += '^'
            })
          }

          //原有键的布局
          GridItem() {
            Image($r('app.media.C'))
              .width(30)
          }.KeyBoxStyleOther()
          .onClick(() => { //对于数据的全部清除
            this.expression = ''
            this.result = '0'
          })

          GridItem() {
            Image($r('app.media.gf_obelus'))
              .width(30)
          }.KeyBoxStyleOther()
          .onClick(() => {
            this.expression=SymbolRecep(this.expression,'/')

          })

          GridItem() {
            Image($r('app.media.mul'))
              .width(30)
          }.KeyBoxStyleOther()
          .onClick(() => {
            this.expressionFontSize
            this.expression=SymbolRecep(this.expression,'*')
          })

          GridItem() {
            Image($r('app.media.delete_left'))
              .width(40)
          }.KeyBoxStyleOther()
          .onClick(() => { //删除最后一位数据
            this.expression = this.expression.slice(0, this.expression.length - 1)
            this.result =''
          })

          GridItem() {
            Image($r('app.media.jiecheng'))
              .width(30)
          }
          .onClick(() => {
            this.expression=SymbolRecep(this.expression,'%')
          })
          .KeyBoxStyleOther()
          .rowStart(this.rows - 1)
          .rowEnd(this.rows - 1)
          .columnStart(0)
          .columnEnd(0)

          GridItem() {
            Image($r('app.media.minus'))
              .width(30)
          }
          .onClick(() => {
            this.expression=SymbolRecep(this.expression,'-')
          })
          .KeyBoxStyleOther()
          .rowStart(this.rows - 4)
          .rowEnd(this.rows - 4)
          .columnStart(3)
          .columnEnd(3)

          GridItem() {
            Image($r('app.media.plus'))
              .width(30)
          }.KeyBoxStyleOther()
          .onClick(() => {
            this.expression=SymbolRecep(this.expression,'+')
          })
          .rowStart(this.rows - 3)
          .columnStart(3)

          GridItem() {
            Text('=')
              .fontColor(Color.White)
              .fontSize(30)
              .fontWeight(FontWeight.Bold)
          }
          .onClick(() => {
            this.expressionFontSize=lengthSize(this.expression)
            this.resultFontSize=lengthSize(this.result)

            this.result=total(this.expression).toString()
            this.expression=''
          })
          .KeyBoxStyleOther()
          .rowStart(this.rows - 2)
          .rowEnd(this.rows - 1)
          .columnStart(3)
          .height(130)
          .columnEnd(3)
          .backgroundColor('#ffff6b14')

          //循环加载数字键盘
          ForEach(nums, (item: string) => {
            GridItem() {
              Text(item)
                .fontSize(20)
                .fontWeight(700)
            }
            .onClick(() => {
              if(item=='.')
                this.expression=SymbolRecep(this.expression,'.')
              else
                this.expression+=item
            })
            .KeyBoxStyle()
          })
        }
        .rowsTemplate(this.rowsTemp)
        .columnsTemplate(this.columnsTemplate)
        .rowsGap(20)
        .columnsGap(8)
        .width('100%')
        .height(this.isShowMore == false ? '55%' : '65%')
        .animation({ duration: 300, curve: Curve.FastOutSlowIn, delay: 5 })
        .backgroundColor("#ffeeebed")
        .padding({
          bottom: 20,
          top: 20,
          left: 8,
          right: 8
        })
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.End)
    }
  }
}

    进行了相应的扩展

        2. 表达式转化和异常处理

        对于数据的处理代码如下

function total(expression: string):number {
  //检查是否含有这些符号
  if('+-x/%^'.includes((expression[expression.length-1])))
    return NaN

  //expression 为 .3 时,正则表达式中的 \d+ 期望至少有一个数字开头,因此 .3 会被分为两个部分:"." 和 "3"。
  //然而,由于正则表达式没有独立的模式来捕捉孤立的点,它仅匹配了数字 "3",因此 tokens 数组只包含 "3"。
  // 使用正则划分出 整数或小数和符号的数组,修改正则表达式以支持负数
  let tokens: string[] = expression.match(/(\d+(\.\d+)?|\+|-|\*|\/|\%|\^|\(|\)|!)/g) || [];

  //用于检查输入的符号
  for(let i=0;i<tokens.length;i++) {
    console.log(tokens[i].toString())
  }

  let output: string[] = []
  let operations: string[] = []

  // 增加前一个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&&op!='(')
        output.push(op)
    }

  console.log('以下为后缀表达式中的表示')

  for(let i=0;i<output.length;i++) {
    console.log(output[i])
  }

    //定义一个数组栈,用来计算最终结果
    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(factorial(num1))
      } else {
        //左侧为空就是右侧,要不然是左侧
        let num1 = nums.pop() ?? 0
        let num2 = nums.pop() ?? 0
        switch (value) {
          case '+':
            nums.push(num2 + num1)
            break
          case '-':
            if(num2==0) {
              nums.push(num1)
            }
            else{
              nums.push(num2 - num1)
            }
            break
          case '/':
            if (num1 !== 0)
              if(num2==0) {
                nums.push(num1)
              }
              else{
                nums.push(num2 / num1)
              }
            else
              return NaN  // 除数为零
            break
          case '*':
            num1*=100000000000
            num2*=100000000000
            if(num2==0) {
              nums.push(num1/100000000000)
            }
            else{
              nums.push(num2/100000000000 * num1 /100000000000)
            }
            break
          case '%':
            if(num2==0) {
              nums.push(num1)
            }
            else{
              nums.push(num2 % num1)
            }
            break
          case '^':
            nums.push(Math.pow(num2, num1))
            break
        }
      }
    }
    return nums[0] ?? NaN
}

//计算出等级
function getPrecedence (op:string):number {
  switch(op){
    case '!':
      return 6
    case '^':
      return 4
    case '%':
      return 3
    case '*':
    case '/':
      return 2
    case '+':
    case '-':
      return 1
    case '(':
      case ')':
      return 0
    default :
      return -1
  }
}

//计算阶乘
function factorial(num: number): number {
  let result = 1;
  for (let i = 2; i <= num; i++) {
    result *= i;
  }
  return result;
}

        现在让我们开始分析哈:

                2.1 正则表达式

let tokens: string[] = expression.match(/(\d+(\.\d+)?|\+|-|\*|\/|\%|\^|\(|\)|!)/g) || [];

        这是一串正则表达式,能起到识别和分离每一个字符的作用。

        正则表达式的定义和作用:正则表达式是一种文本模式,它使用特定的字符序列来描述、匹配和处理字符串中的字符组合。正则表达式广泛应用于编程语言和文本处理工具中,用于执行诸如搜索、替换、验证和提取等操作。通过正则表达式,可以快速地对大量文本数据进行复杂的模式匹配和操作。

        小提醒:如果看不懂的话可以可以通过设置日志的方式找BUG!!!!!!

        举例对于输入 2*(-3*2)-6,以下为token数组中所存放的数据

 

                2.2 后缀表达式转换

        我上述所讲比较的粗糙,具体可以参考  后缀表达式转换

        压入栈的时候同时处理多括号的问题

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

        通过日志可以看出被转化后的表达式

        计算机就可以成功的进行识别和处理了表达式了

                2.3 精度丢失 

        小数运算时:“0.2 + 2.22 = 2.4200000000000004”,当前示例的解决方法是将小数扩展到整数进行计算,计算完成之后再将结果缩小,计算过程为“(0.2 * 100 + 2.22 * 100) / 100 = 2.42”。

 case '*':
            num1*=100000000000
            num2*=100000000000
            if(num2==0) {
              nums.push(num1/100000000000)
            }
            else{
              nums.push(num2/100000000000 * num1 /100000000000)
            }
            break

              通过测试发现成功的解决  

        

        2.4 布局美化和限制问题

//保证运算符只出现一次
function SymbolRecep(expression: string,chara:string): string {
  if (expression[expression.length-1] == '+' || expression[expression.length-1] == '-' ||
    expression[expression.length-1] == 'x' || expression[expression.length-1] == '/' ||
    expression[expression.length-1] == '%' || expression[expression.length-1] == '.') {
    return expression.slice(0, expression.length - 1) + chara
  } else
    return expression + chara
}

        如果输入的数字过长,就可以改变字体的大小,更加适合

function lengthSize(expression: string): number {
  if(expression.length<12)
    return 40
  else if(expression.length<20)
    return 25
  else
    return 20
}

         效果图如下


        后言 

        隐墨累了。她的计算器成功写好了,但是终究入不了少年的眼!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值