一个计算器的诞生【ArkTS】
逆波兰表达式又称后缀表达式,即每一种运算都放置在运算对象后;我们平时书写的像3+5x6 ,每一种运算符都放置在运算对象中间,所以叫做中缀表达式。上述式子改写为后缀表达式即为3 5 6 x +这种式子乍一看可能很难让人理解这byd到底是咋算出来答案的,而要解释原理,我们又要引出一位神秘嘉宾,那就是数据结构中的——栈。
一、基本思路
1.UI界面
首先我们肯定需要一个计算器的界面,上部显示运算结果,下部则为进行交互的按钮。
左图为本蒟蒻 自行编写,左图为iOS系统自带计算器
2.核心计算---逆波兰表达式
界面有了之后,我们需要通过对按钮的点击来输入要进行的计算,这里可以创建一个变量用来储存输入的表达式,本例使用字符串的形式储存。
let ing:string[]
需注意这里使用的是字符串数组,至于原因在后续会讲到
获得表达式之后肯定不能直接运算,我们需要对其进行一些处理,此时就要引入一个概念
逆波兰表达式
首先我们要知道什么是逆波兰表达式?
逆波兰表达式又称后缀表达式,即每一种运算都放置在运算对象后;
我们平时书写的像 3+5x6 ,每一种运算符都放置在运算对象中间,所以叫做中缀表达式。
上述式子改写为后缀表达式即为 3 5 6 x +
这种式子乍一看可能很难让人理解这byd到底是咋算出来答案的,而要解释原理,我们又要引出一位神秘嘉宾,那就是数据结构中的——栈
计算原理
首先我们需要创建一个栈用于处理数据,然后遍历表达式,遇到数字则将其直接入栈,遇到运算符则提出栈中里栈顶最近的两个元素进行计算,随后将结果再次入栈,随后栈中唯一的数字变为答案。
为什么要用逆波兰表达式?
最大的一个原因还是,在该表达式中可以忽略运算的优先级,即按顺序遍历后得到结果,不易出错
3.辅助工具
在完成上述过程中我们肯定需要一些工具来辅助我们
计算方面,首先我们需要把中缀表达式变为后缀,此时我们的数据都还是string类型的,我们还需要将其变为number型,再进行计算。
UI方面,我们可以使用@Extend,@Builder等修饰器来简化代码,提高可读性
二、功能构建
为提高代码简洁性,在此为每个功能单独构建一个类
1.栈类
export class stack{
public st :Array<number|string> = []
push(item:string|number){
this.st.push(item)
}
pop(){
if(this.st.length!=0){
this.st.pop()
}
}
peek(){
return this.st.length === 0? 0:this.st[this.st.length-1]
}
is_Empty(){
return this.st.length === 0
}
clear(){
this.st = []
}
size(){
return this.st.length
}
printf(){
for(let i =0 ;i<this.st.length ;i++){
console.log(this.st[i].toString())
}
}
}
其中printf()方法方便在运行时查看栈中状态
2.字符串转数字类
export class bbutton{
public num:string = ''
change(item:string){
switch (item){
case '0':return 0;
case '1':return 1;
case '2':return 2;
case '3':return 3;
case '4':return 4;
case '5':return 5;
case '6':return 6;
case '7':return 7;
case '8':return 8;
case '9':return 9;
}
return 0
}
get_number(num:string){
let j = 0; let anw = 0;let k = num.length;let q = 0
for(let i = 0;i<num.length;i++){
if(num[i] == '.'){
k=i
break;
}
}
for(let i = k-1 ;i>=0; i--,j++){
if(num[i] == '-'){
q = 1
break;
}
let a = this.change(num[i])
anw += a*Math.pow(10,j)
}
j = -1
for(let i = k+1 ;i<num.length; i++,j--){
let a = this.change(num[i])
anw += a*Math.pow(10,j)
}
if(q == 1){
return -1*anw
}else{
return anw
}
//return parseFloat(num)
}
get_string(){
return this.num
}
}
其中get_number()先通过遍历获得小数点位置,随后以此将整个字符串分割成整数部分和小数部分,分别处理后再相加,避免了小数点的影响
3.中缀表达式转后缀类
export class Key_creat{
public suffix :Array<number|string> = []
public st = new stack()
handle(cou:string[]){
//加法
for(let i =0;i<cou.length;i++){
if(cou[i] == '+'){
if(this.st.is_Empty()){
this.st.push('+')
}else{
while(!this.st.is_Empty()){
this.suffix.push(this.st.peek())
this.st.pop()
}
this.st.push('+')
}
//减法
}else if(cou[i] == '-'){
if(this.st.is_Empty()){
this.st.push('-')
}else{
while(!this.st.is_Empty()){
this.suffix.push(this.st.peek())
this.st.pop()
}
this.st.push('-')
}
//乘法
}else if(cou[i] == 'x'){
if(this.st.is_Empty()){
this.st.push('x')
}else if(this.st.peek()=='-'||this.st.peek()=='+'){
this.st.push('x')
}else{
while(!this.st.is_Empty()){
this.suffix.push(this.st.peek())
this.st.pop()
}
this.st.push('x')
}
//除法
}else if(cou[i] == '÷'){
if(this.st.is_Empty()){
this.st.push('÷')
}else if(this.st.peek()=='-'||this.st.peek()=='+'){
this.st.push('÷')
}else{
while(!this.st.is_Empty()){
this.suffix.push(this.st.peek())
this.st.pop()
}
this.st.push('÷')
}
}else{
this.suffix.push(cou[i])
}
}
}
head_1(){
while(!this.st.is_Empty()){
this.suffix.push(this.st.peek())
this.st.pop()
}
}
get(){
return this.suffix
}
clear(){
this.st.clear()
this.suffix = []
}
}
我们首先需要创建一个字符串数组用于储存后缀表达式(为将每个运算对象隔开使用字符串数组,与基本思路中提到同理),一个栈用于储存运算符。
当遇到数字时将其直接存入数组中
而遇到运算符则要分以下四种情况:
1.栈为空,此时直接入栈;
2.栈顶元素运算优先级(例如‘x’大于‘+’)高于或等于自己,此时提出栈顶元素放入数组中,然后与下一元素比较,直到遇到运算优先级小于自己的元素,入栈。
3.栈顶元素低于自己,此时直接入栈
4.运算处理类
export class answer{
public anw = new stack()
public num = new bbutton()
get_anw(n:string[]){
for(let i =0 ;i<n.length;i++){
if(n[i] == '+'){
this.jia()
}else if(n[i] == '-'){
this.jian()
}else if(n[i] == 'x'){
this.cheng()
}else if(n[i] == '÷'){
this.chu()
}else if(n[i] == '='){
this.get()
}else {
this.anw.push(this.num.get_number(n[i] as string))
}
}
}
jia(){
let b = this.anw.peek() as number
this.anw.pop()
let a = this.anw.peek() as number
this.anw.pop()
this.anw.push(a+b)
}
jian(){
let b = this.anw.peek() as number
this.anw.pop()
let a = this.anw.peek() as number
this.anw.pop()
this.anw.push(a-b)
}
cheng(){
let b = this.anw.peek() as number
this.anw.pop()
let a = this.anw.peek() as number
this.anw.pop()
this.anw.push(a*b)
}
chu(){
let b = this.anw.peek() as number
this.anw.pop()
let a = this.anw.peek() as number
this.anw.pop()
this.anw.push(a/b)
}
get():string|number{
let a:number= this.anw.peek() as number
a = parseFloat(a.toFixed(6).toString())
return a
}
clear(){
this.anw.clear()
}
}
当主函数接受到需要处理的后缀表达式时,会遍历数组并运用字符串转数字类将其转化为number类型后进行计算,最后将计算结果返回。
三、主界面构建
主界面我选择用网格布局Grid结合ForEach渲染出主界面,代码如下
@Entry
@Component
struct page {
@State ing :string[] = []
@State answer:string = '0'
@State jiancha:string = ''
@State jiancha2:string = ''
number :string = '0'
suffix = new Key_creat()
anwe = new answer()
st_num = new bbutton()
/////
eventType: string = '';
bool :boolean = false
star_x = 0
star_y = 0
distance = 0
dain :boolean =false
fuhao :boolean = false
arr: string[] = ['AC', '+/-', '%', '÷', '7', '8', '9', 'x', '4',
'5', '6', '-', '1', '2', '3', '+', '0', '.', '=']
aboutToAppear(): void {
window.getLastWindow(getContext())
.then(win => {
win.setWindowLayoutFullScreen(true)
})
}
layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
onGetRectByIndex: (index: number) => {
if (index == 16) { // key1是“0”按键对应的index
return [4, 4, 1, 2]
} else {
return [0, 0, 0, 0]
}
}
}
build() {
Column(){
Column({space:30}){
Text(this.jiancha)
.fontColor(Color.White)
.fontSize(20)
.height(20)
Text(this.jiancha2)
.fontSize(25)
.fontColor(Color.White)
.width('100%')
.height(20)
.margin({top:80})
.textAlign(TextAlign.End)
.opacity(0.6)
.margin({bottom:20})
Text(this.answer)
.fontSize(50)
.fontColor(Color.White)
.height(40)
.margin({top:30})
.width('100%')
.textAlign(TextAlign.End)
.onTouch((event?: TouchEvent) => {
if(event){
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.star_x = event.touches[0].x
this.star_y = event.touches[0].y
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
this.bool = false
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
this.distance = Math.abs(event.touches[0].x-this.star_x)
if(this.distance>20&&this.bool == false&&this.number.length>1){
this.number=this.number.slice(0,-1)
this.answer = this.number
this.bool = true
console.log('1')
}
if(this.number.length == 1){
this.answer = this.number = '0'
console.log('2')
}
}
}
})
}
.height('25%')
.width('100%')
.alignItems(HorizontalAlign.End)
Grid(undefined, this.layoutOptions){
ForEach(this.arr,(item:string,index:number) =>{
GridItem(){
if(check_num(item)){
if(item=='0'){
Button(item,{type:ButtonType.Capsule})
.button_zero('#333333','#FFFFFF',item)
.onClick(()=>{
if(this.st_num.get_number(this.number) != 0||this.dain == true){
this.number += '0'
}
this.answer = this.number
this.fuhao = false
})
}else if(check_num(item)){
Button(item,{type:ButtonType.Capsule})
.button('#333333','#FFFFFF',item)
.onClick(()=>{
console.log(this.number)
if(this.number[this.number.length-1]=='0'){
this.number = ''
}
this.number += item
this.answer = this.number
this.fuhao = false
})
}
}else{
if(index<3){
Button(item,{type:ButtonType.Capsule})
.button('#A5A5A5','#000000',item)
.onClick(()=>{
if(item == 'AC'){
this.ing = []
this.suffix.clear()
this.anwe.clear()
this.answer = '0'
this.number = '0'
this.ing = []
this.jiancha = ''
this.jiancha2 = ''
this.dain = false
this.fuhao = false
}else if(item == '+/-'){
let a = this.st_num.get_number(this.number)
a*=-1
this.number = a.toString()
this.answer = this.number.slice(0,12)
}else if(item == '%'){
let a = this.st_num.get_number(this.number)
a/=100
this.number = a.toString()
this.answer = this.number.slice(0,12)
}
})
}else if(item == '.'){
Button(item,{type:ButtonType.Capsule})
.button('#333333','#FFFFFF',item)
.onClick(()=>{
if(this.dain == false){
this.number += '.'
this.answer = this.number
this.dain = true
}
})
}else{
Button(item,{type:ButtonType.Capsule})
.button('#FEA00C','#FEFEFE',item)
.onClick(()=>{
if(item == 'x'||item == '÷'){
st1 = 1
}else{
st1 = 0
}
this.dain = false
if(this.fuhao == false){
this.ing.push(this.number)
this.ing.push(item)
}
this.fuhao = true
this.number = '0'
ing = this.ing
if(this.check(this.ing)){
let a =ing.pop()
this.suffix.handle(ing)
ing.push(a as string)
}else{
this.suffix.handle(this.ing)
}
this.suffix.head_1()
this.anw()
if(st1>st2){
}else{
let a :string=this.anwe.get().toString()
if(a == 'Infinity'){a = '错误'}
this.answer = a
}
st2 = st1
if(item == '='){
this.number = this.answer
this.ing = []
}
this.suffix.clear()
this.anwe.anw.clear()
})
}
}
}
})
}
// .columnsGap(10)
.rowsGap(20)
.maxCount(4)
.columnsTemplate('1fr 1fr 1fr 1fr ')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.height(500)
.margin({top:40,bottom:10})
}
.backgroundColor(Color.Black)
.height('100%')
.width('100%')
}
anw(){
let st = this.suffix.get()
this.jiancha = ''
for(let i =0;i<st.length;i++)
this.jiancha+=st[i]
this.jiancha2 = ''
for(let i =0;i<this.ing.length;i++)
this.jiancha2+=(this.ing[i]+' ')
this.anwe.get_anw(st as string[])
this.anwe.anw.printf()
}
check(st:string[]){
let a = new bbutton()
let num = st[st.length-1]
if(num == '+'||num == '-'||num == 'x'||num == '÷'){
return true
}else{
return false
}
}
}
@Extend(Button)
function button(back:ResourceColor,word:ResourceColor,item:string){
.fontSize(35)
.fontColor(word)
.backgroundColor(back)
.borderRadius(50)
.height(85)
.width(85)
}
@Extend(Button)
function button_zero(back:ResourceColor,word:ResourceColor,item:string){
.fontSize(35)
.fontColor(word)
.backgroundColor(back)
.borderRadius(50)
.height(85)
.width(165)
}
function check_num(item:string){
if(item == '0'||item == '1'||item == '2'||item == '3'||item == '4'||item == '5'||item == '6'||item == '7'||item == '8'||item == '9'){
return true
}else{
return false
}
}
其中为了更加贴近IOS系统计算器,可以构建一个GridLayoutOptions对象,使组件实现跨行跨列布局,详细用法如下
在网格中,可以通过onGetRectByIndex返回的[rowStart,columnStart,rowSpan,columnSpan]来实现跨行跨列布局,其中rowStart和columnStart属性表示指定当前元素起始行号和起始列号,rowSpan和columnSpan属性表示指定当前元素的占用行数和占用列数。
所以“0”按键横跨第一列和第二列,“=”按键横跨第五行和第六行,只要将“0”对应onGetRectByIndex的rowStart和columnStart设为5和0,rowSpan和columnSpan设为1和2,将“=”对应onGetRectByIndex的rowStart和columnStart设为4和3,rowSpan和columnSpan设为2和1即可。
layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
onGetRectByIndex: (index: number) => {
if (index == 16) { // key1是“0”按键对应的index
return [4, 4, 1, 2]
} else {
return [0, 0, 0, 0]
}
}
}
其中Index即为ForEach循环中的Index,对应按键‘0’,实现效果如下
其中按键‘0’则占据两列布局,上方的‘1’则为正常的一行一列
至此一个属于自己的专属计算器就此诞生了!
更多推荐
所有评论(0)