计算器
文章目录
前言
笔者应组内要求,简单实现了一个可以完成简单四则运算的计算器程序。UI界面则是通过最近学习的Masonry库来实现的,而简单的四则运算内容则是通过栈来实现一个简单的四则运算。
简单的四则运算
笔者这里四则运算的思路是一个中缀表达式转后缀表达式的方式,然后再通过后缀表达式来进行一个计算,然后得到一个结果。这里中缀表达式转后缀表达式的思路主要参考这篇博客《数据结构》:中缀表达式转后缀表达式 + 后缀表达式的计算
这里简单说明一下我们为什么在计算机中要将中缀表达式转换成后缀表达式,中缀表达式的顺序是混乱的(因为有括号和每个符号优先级的问题),而转化成后缀表达式的逻辑就会变得很简单,我们只用按照栈中的顺序来进行一个运算就可以了。
中缀表达式转后缀表达式的核心思想其实就是对于我们的运算符的顺序的控制,如果遇到右括号的话,我们要一直让符号栈一直出栈直到遇到左括号才停止。遇到操作符的话,我们只需要满足下面这个条件就可以了,栈为空或者是我们的当前的操作符的优先级大于栈顶元素的操作符时候,我们的操作符栈就可以停止出栈了,然后给当前读到的操作符入栈。
对于数字我们都是进行一个直接入栈。
这里给出一个C语言版本:
objc
typedef struct Stack {
char stk[80];
int top;
}Stack;
int EmptyStack(Stack* stk) {
if (stk->top == -1) {
return 1;
} else {
return 0;
}
}
char getTopStack(Stack* stk) {
if (EmptyStack(stk)) {
return -1;
} else {
return stk->stk[stk->top];
}
}
int fullStack(Stack* stack) {
if (stack->top == 80) {
return 1;
} else {
return 0;
}
}
void pushStack(Stack* stack, char a) {
if (fullStack(stack)) {
return;
} else {
stack->stk[++stack->top] = a;
}
}
char popStack(Stack* stack) {
if (EmptyStack(stack)) {
return -1;
} else {
return stack->stk[stack->top--];
}
}
int isDigit(char a) {
int flag;
switch (a) {
case '0':
flag = 1;
break;
case '1':
flag = 1;
break;
case '2':
flag = 1;
break;
case '3':
flag = 1;
break;
case '4':
flag = 1;
break;
case '5':
flag = 1;
break;
case '6':
flag = 1;
break;
case '7':
flag = 1;
break;
case '8':
flag = 1;
break;
case '9':
flag = 1;
break;
default:
flag = 0;
break;
}
return flag;
}
char** changeStack(Stack* stk, int length, char* s, int* num1) {
char** string = (char**)malloc(sizeof(char*) * 30);
for (int i = 0; i < 30; i++) {
string[i] = (char*)malloc(sizeof(char) * 10);
}
int num = 0;
int tail = 0;
for (int i = 0; i < length; i++) {
if (s[i] == '(') {
pushStack(stk, s[i]);
} else if (s[i] == ')') {
if (tail > 0) {
string[num][tail] = '\0';
num++;
tail = 0;
}
while (!EmptyStack(stk) && getTopStack(stk) != '(') {
string[num][0] = popStack(stk);
string[num][1] = '\0';
num++;
}
popStack(stk);
} else if (isDigit(s[i]) || s[i] == '.') {
string[num][tail++] = s[i];
} else if (s[i] == '+' || s[i] == '-') {
if (i == 0 || (i > 0 && !isDigit(s[i - 1]) && s[i - 1] != ')' && s[i] == '-')) {
string[num][tail++] = s[i];
} else {
if (tail > 0) {
string[num][tail] = '\0';
num++;
tail = 0;
}
while (!EmptyStack(stk) && (getTopStack(stk) == '*' || getTopStack(stk) == '/' || getTopStack(stk) == '+' || getTopStack(stk) == '-')) {
string[num][0] = popStack(stk);
string[num][1] = '\0';
num++;
}
pushStack(stk, s[i]);
}
} else if (s[i] == '*' || s[i] == '/') {
if (tail > 0) {
string[num][tail] = '\0';
num++;
tail = 0;
}
while (!EmptyStack(stk) && (getTopStack(stk) == '*' || getTopStack(stk) == '/')) {
string[num][0] = popStack(stk);
string[num][1] = '\0';
num++;
}
pushStack(stk, s[i]);
}
}
if (tail > 0) {
string[num][tail] = '\0';
num++;
}
while (!EmptyStack(stk)) {
string[num][0] = popStack(stk);
string[num][1] = '\0';
num++;
}
*num1 = num;
return string;
}
int isNumber(char* token) {
return strlen(token) > 1 || ('0' <= token[0] && token[0] <= '9');
}
double change(char* token) {
double x = 0;
double decimalFactor = 1.0;
int index = -1;
int flag = 1;
if (token[0] == '-') {
flag = -1;
}
for (int i = 0; i < strlen(token); i++) {
if (token[i] == '-') {
continue;
}
if (token[i] == '.') {
index = i;
} else {
if (index == -1) {
x = x * 10 + (token[i] - '0');
} else {
decimalFactor *= 0.1;
x += (token[i] - '0') * decimalFactor;
}
}
}
printf("%lf\n", x * flag);
return x * flag;
}
double evalRPN(char** tokens, int tokensSize) {
int n = tokensSize;
double stk[n];
int top = 0;
for (int i = 0; i < n; i++) {
char* token = tokens[i];
if (strlen(token) == 0) {
continue;
}
if (isNumber(token)) {
stk[top++] = change(token);
} else {
double num2 = stk[--top];
double num1 = stk[--top];
switch (token[0]) {
case '+':
stk[top++] = num1 + num2;
break;
case '-':
stk[top++] = num1 - num2;
break;
case '*':
stk[top++] = num1 * num2;
break;
case '/':
stk[top++] = num1 / num2;
break;
}
}
}
return stk[top - 1];
}
这里和上面简单的版本有一点区别,这里的还考虑到了一个负数的判别和一个小数点的时候对于我们的数字的一个读取特别判断,这里如果是数字或者是一个小数点我们都要继续进行一个读取。这里我对于负数的处理是将负号存储到我们对应的数字前面,因为一个数字如果是负数的话,那他的负号是链接在运算符后面的,或者链接在左括号后面的。所以通过一个特判,来分辨我们的普通符号减和一个负数的标志。
但是在OC中给出了一个类NSDecimalNumber
这个类可以实现一个比较精确的加减乘除,下面给出我们使用这个类来实现计算的过程
objc
- (NSDecimalNumber*) evalRPN {
NSInteger n = self.ary.count;
NSLog(@"%@", self.ary);
NSMutableArray* stack = [NSMutableArray array];
//int top = 0;
for (int i = 0; i < n; i++) {
NSString* token = self.ary[i];
if (token.length == 0) {
continue;
}
if ([self isNumber:token]) {
[stack addObject: [self change:token]];
} else {
NSDecimalNumber* num2 = [stack lastObject];
[stack removeLastObject];
NSDecimalNumber* num1 = [stack lastObject];
[stack removeLastObject];
if ([token isEqualToString:@"+"]) {
[stack addObject:[num1 decimalNumberByAdding:num2]];
} else if ([token isEqualToString:@"-"]) {
[stack addObject:[num1 decimalNumberBySubtracting:num2]];
} else if ([token isEqualToString:@"×"]) {
[stack addObject:[num1 decimalNumberByMultiplyingBy:num2]];
} else if ([token isEqualToString:@"÷"]) {
[stack addObject:[num1 decimalNumberByDividingBy:num2]];
}
}
}
if (stack.count > 1) {
return nil;
} else {
return [stack lastObject];
}
}
UI界面
UI界面采用了Masonry来布局,这个界面大致有两个部分组成一个是我们的textField,剩下的部分则是我们的按钮部分,这里布局我采用了一个for循环来不断创建我们的button,并且给这些button赋值对应的tag,这样方便我们对于具有不同button的进行一个划分。
objc
UIView* preView = nil;
for (int i = 0; i < 19; i++) {
UIButton* button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[self addSubview:button];
//button.backgroundColor = UIColor.whiteColor;
[button setTitle:ary[i] forState:UIControlStateNormal];
[button setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:37];
button.tag = 100 + i;
if (i == 0) {
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self).offset(20);
make.top.equalTo(self.textField.mas_bottom).offset(10);
make.size.equalTo(@80);
}];
} else if (i % 4 == 0 && i != 16) {
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self).offset(20);
make.top.equalTo(preView.mas_bottom).offset(10);
make.size.equalTo(@80);
}];
} else if (i == 16) {
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self).offset(20);
make.top.equalTo(preView.mas_bottom).offset(10);
make.width.equalTo(@170);
make.height.equalTo(@80);
}];
} else {
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(preView.mas_right).offset(10);
make.top.equalTo(preView);
make.size.equalTo(@80);
}];
}
button.layer.cornerRadius = 80 / 2;
button.layer.masksToBounds = YES;
preView = button;
}
for (UIView* subview in self.subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
if (subview.tag < 103) {
subview.backgroundColor = UIColor.lightGrayColor;
} else if (subview.tag == 103 || subview.tag == 107 || subview.tag == 111 || subview.tag == 115 || subview.tag == 118) {
subview.backgroundColor = UIColor.orangeColor;
} else {
subview.backgroundColor = UIColor.darkGrayColor;
}
}
}
这部分代码是一个创建button的代码,然后根据button的不同tag来分配颜色以及设置对应的位置。
因为采用MVC架构,所以我这里将所有给button添加事件的函数都放在了ViewController中。
objc
for (UIView* subview in _myView.subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
UIButton* myButton = (UIButton*)subview;
if (subview.tag == 100) {
[myButton addTarget:self action:@selector(empty) forControlEvents:UIControlEventTouchUpInside];
} else if (subview.tag == 103 || subview.tag == 102 || subview.tag == 101 || subview.tag == 107 || subview.tag == 111 || subview.tag == 115 || subview.tag == 117) {
[myButton addTarget:self action:@selector(pressopator:) forControlEvents:UIControlEventTouchUpInside];
} else if (subview.tag == 118) {
[myButton addTarget:self action:@selector(pressEqual:)
forControlEvents:UIControlEventTouchUpInside];
NSLog(@"12");
} else {
[myButton addTarget:self action:@selector(pressNum:) forControlEvents:UIControlEventTouchUpInside];
}
}
}
这部分实现了一个给button添加事件函数。
这里可以注意一下textfield
的adjustsFontSizeToFitWidth
属性可以让他根据字符串长度来实现一个自适应字体的效果。
事件的逻辑
这里笔者对于输入运算符做了限制,同时也对我们输入的小数点和左右括号都做了限制。
比方说笔者在一开始只允许我们的负号输入和左括号允许输入,别的操作符被设置成无法键入符号的状态。
又或者是在输入数字的时候限制他只能输入一个小数点。
这部分的逻辑其实比较复杂,要考虑的内容也比较多。比方说判断数字的小数点个数是否符合要求或者是判断多个运算符重叠的情况。
这里我主要把这部分的判断分成了两部分,一个是通过一些全局变量来控制一些不合理的输入,另一个则是通过判断中缀表达式是否合理来然后返回一个error字符串。
这里我是通过一个dotFlag和numFlag来控制他一个数字只能输入一次小数点,从而限制输入。另一个部分就是我们开始我设置成只可以输入的符号只有负号。
小结
计算器的仿写比较困难的点在于我们需要考虑的问题比较多,以及对于字符串的处理需要注意一下。细节地方比较多。