附加功能
附加功能主要包括小数点识别,正负号识别,识别算式错误能力这三个。
- 小数点识别:当一个数字一小数点开头,默认这个数字为0.开头;
- 正负号识别:当一个数字之前有连续的加减号,要求将其视为正负号,并能够通过加减号的数量判断数字的正负;
- 错误识别能力:如果输入的式子不是标准的四则运算式,出了上述两种情况外,均视为算式错误,要求程序能够识别错误的算式。
我们先来实现最简单的吧,小数点识别。
小数点识别
算法描述
这个非常容易实现,我们的数字都是暂时存在数字字符串中的,所以只要在数字存入字符串中对它处理即可。
在读取过程中,当读取到非运算符且为小数点时,若此时数字字符串为空,则存入0.即可。
让人高兴的是,经过测试和查阅资料发现,这个功能Java的字符串转数字的方法已经实现了,Float.valueOf(String)方法,当String是小数点开头的,会自动转化为0.开头。所以代码不用改。
那我们就直接从第二个功能开始做。
正负号识别
思路: 要解决的问题有两个,一是怎么判断加减号是运算符还是正负号;二是正负号对运算的影响。
先要搞清楚正负号出现的位置,它一定会出现在数字之前不会出现在运算符前;有可能会出现在开头、加减号之后、左括号之后。应该没有别的可能吧?
应该说出现在开头、加减号之后、左括号之后的一定是不能看做加减号,只可能看做正负号,如果它后面跟着的是加减符号和数字以外的符号,说明算式有问题。
那我们就要设置一个标记,标记前一个字符是什么,即当前位置出现的加减号能不能是加减号,如果不能,就把它当做正负号处理。
解决了判断加减号是正负号还是运算符的问题,接下来就要思考正负号对运算的影响的问题了。
观察一下很容易可以发现,当数字前每多一个符号,相当于这个数字乘上了一个-1,每多一个加号,相当于在这个数字前乘上了一个1。
所以我们可以设置一个正负号标记,初始值为1;再为它设置一个激活标记,标记它是否被激活,初始值为true,表示激活。
当读取到加减号或左括号时,将它激活,之后,若读取到加减号,这个加减号不再进栈符号栈,并且若读取到的是减号,则将正负号标记*-1。在数字将要进栈时,我们先把这个数字乘上这个正负号标记,然后将正负号标记重新赋值为1。如果读取到了数字,则将激活标记改为“休眠”,让正负号标记等待再次被“激活”。
算法描述
在计算方法开始时初始化一个int变量(正负标记),赋值为1,在设置一个激活标记,初始值为true
当读取到加减号或左括号时,激活标记设为true;
当读取到加减号时且季候标记为true,若读取到的是减号,则正负标记*-1;
当读取到数字时,激活标记改为false;
当数字要进栈时,将正负标记与这个数字相乘的结果进栈并将正负标记重新赋值为1。
代码修改
在原程序上修改:
/**
* 根据传入的代表算式的字符串计算结果并返回
* @param formula 代表算式的字符串
* @return 计算结果
*/
public float calculate(String formula)
{
formula += "#";
int NP = 1; //标记进栈数字元素的正负性
boolean NPCanUse = true; //NP标记是否被激活
boolean flag = true; //标记,若前一个运算符不是右括号,则为true
Stack<Float> figure = new Stack<Float>(); //数字栈
Stack<Character> operator = new Stack<Character>(); //符号栈
operator.push('#');
StringBuffer figureString = new StringBuffer(); //数字字符串,用于暂时存放读取到的数字
for (int i = 0;i < formula.length();i ++)
{
//读取到非运算符,连接到StringBuffer
if (!isOperator(formula.charAt(i)))
{
figureString.append(formula.charAt(i));
NPCanUse = false; //NP标记睡眠
}
//若读取到加减号且NP标记已被激活,则将加减号视为正负号,根据加减号改变
else if (!(getPriority(formula.charAt(i)) == 1 && NPCanUse == true))
{
//读取到运算符:若当前符号是左括号或前一个符号是右括号,数字栈不进栈(左括号前不应当是数字而是运算符,右括号后同理),否则数字栈进栈
if (formula.charAt(i) != '(' && flag == true)
{
figure.push(Float.valueOf(figureString.toString()) * NP); //将数字字符串的内容转为数字并进栈数字栈
figureString.setLength(0); //清空数字字符串
NP = 1; //重置NP及其激活状态
NPCanUse = false;
}
flag = true;//getPriority(formula.charAt(i)) == 1;
while((getPriority(formula.charAt(i)) < getPriority(operator.peek()) || getPriority(formula.charAt(i)) == getPriority(operator.peek())) && getPriority(operator.peek()) != 3 && getPriority(operator.peek()) != -4)
{
float d = figure.pop();
float u = figure.pop();
figure.push(operate(operator.pop(),u,d));
}
if (getPriority(formula.charAt(i)) < getPriority(operator.peek()) && getPriority(formula.charAt(i)) + getPriority(operator.peek()) == 0)
{
operator.pop(); //去括号
flag = false;
}
else
operator.push(formula.charAt(i));
}
//当读取到加减号或左括号,NP激活
if (getPriority(formula.charAt(i)) == 1 || getPriority(formula.charAt(i)) == 3)
{
//读取到减号且NP被激活,则NP*-1
NP *= formula.charAt(i) == '-' && NPCanUse ? -1: 1;
NPCanUse = true;
}
}
if (getPriority(operator.pop()) != -4)
//字符串读取完成,但是符号栈内仍然有运算符
return 0;
else
return figure.pop();
}
这样就完成了正负号的判断,经过一些简单的测试,发现功能都没有问题,如:
代码整理
虽然代码的编写完成了,目前想要实现的功能都已经实现了,但是这样的代码可读性比较差。一般一个方法在超过约30行,它的可理解性就开始直线下降,算法的代码就更是如此。而且在编写过程中,涂涂改改,导致它很多注释都已经不清楚甚至出现了错误。
所以把calculate这个方法整理一下,使它的逻辑清晰一些。
package calculator;
import java.util.Stack;
public class Calculator {
private int NP = 1; //标记进栈数字元素的正负性
private boolean NPCanUse = true; //NP标记是否是激活状态
//private boolean flag = true; //用于标记前一个符号是否是')',若是,则当前数字字符串一定为空
private Stack<Float> figure = new Stack<Float>(); //数字栈
private Stack<Character> operator = new Stack<Character>(); //符号栈
private StringBuffer figureString = new StringBuffer(); //数字字符串,用于暂时存放读取到的单个数字字符,并将它们拼接成一个完整的数字
/**
* @description 计算运算式(主程序)
* <br>
* 根据传入的代表算式的字符串计算结果并返回
* @param formula 代表算式的字符串
* @return 计算结果
*/
public float calculate(String formula)
{
formula += "#";
operator.push('#');
for (int i = 0;i < formula.length();i ++)
{
//读取到非运算符(非数字符号)时进行的操作
if (!isOperator(formula.charAt(i)))
{
doWhenCharIsNotOperator(formula.charAt(i));
}
//读取到非运算符(数字符号)时进行的操作
else
doWhenCharIsOperator(formula.charAt(i),formula.charAt(i - 1));
}
if (getPriority(operator.pop()) != -4)
//字符串读取完成,但是符号栈内仍然有运算符
return 0;
else
return figure.pop();
}
//判断符号c是否为运算符,若是则返回true
private boolean isOperator(char c)
{
return getPriority(c) != -5;
}
//获取字符c的优先级并返回
private int getPriority(char c)
{
switch(c)
{
case '+':return 1;
case '-':return 1;
case '*':return 2;
case '/':return 2;
case '(':return 3;
case ')':return -3;
case '#':return -4;
default:return -5;
}
}
//根据运算符和两个数字计算结果
private float operate(char c,float u,float d)
{
switch(c)
{
case '+':return u + d;
case '-':return u - d;
case '*':return u * d;
case '/':return u / d;
default:return 0;
}
}
/**
* @description 当前读取到的字符不是运算符时进行的操作
* <br>
* 说明读取到的字符是数字,将它接到数字字符串之后
* <br>
* 由于下一个字符一定不会是正负号,所以激活标记睡眠
*
* @param currentCharacter 当前读取到的字符
*/
private void doWhenCharIsNotOperator(char currentCharacter)
{
figureString.append(currentCharacter);
NPCanUse = false; //NP标记休眠
}
/**
* @description 当前读取到的字符是运算符时进行的操作
* <br>
*
* @param currentCharacter 当前读取到的字符
*/
private void doWhenCharIsOperator(char currentCharacter,char proCharacter)
{
//不是(当前符号是加减号且正负号标记激活状态),即当前符号不是正负号
if (!(getPriority(currentCharacter) == 1 && NPCanUse == true))
{
pushFigure(currentCharacter,proCharacter); //尝试将数字字符串进栈数字栈(有些特殊情况不能进栈)
operate(currentCharacter); //尝试运算(有些情况当前字符不会参与运算)
}
//当前符号是正负号,并且为符号时,正负号标记*-1
else
NP *= currentCharacter == '-' ? -1: 1;
//当读取到加减号或左括号,下一个加减号视为正负号,NP激活
if (getPriority(currentCharacter) == 1 || getPriority(currentCharacter) == 3)
NPCanUse = true;
}
/**
* @description 数字栈进栈
* <br>
* 根据当前符号判断是否将数字符号串内的内容转为数字并进栈数字栈
* <br>
* 并重置NP标记,并将其改为休眠状态
* <br>
* @param currentCharacter 当前读取的符号
* @param proCharacter 当前读取的符号的上一个符号
*/
private void pushFigure(char currentCharacter,char proCharacter)
{
//若当前符号不是左括号且前一个符号不是右括号,数字栈进栈并将数字字符串清空。否则当前数字字符串内应该是没有内容的
//若当前符号是左括号,说明之前的不是数字,那在之前数字字符串就被清空过;前一个符号是右括号时同理
if (currentCharacter != '(' && proCharacter != ')')
{
figure.push(Float.valueOf(figureString.toString()) * NP); //将数字字符串的内容转为数字并进栈数字栈
figureString.setLength(0); //清空数字字符串
NP = 1; //重置NP及其激活状态
NPCanUse = false; //NP休眠
}
}
/**
* @description 运算操作
* <br>
* 根据当前符号的意义判断是否进行运算,如果是则进行运算
* <br>
* 如果当前符号暂时不能参与运算,根据当前符号做相应的处理
* <br>
* @param currentCharacter 当前读取的符号
*/
private void operate(char currentCharacter)
{
//尝试将当前符号拿来参与运算
while((getPriority(currentCharacter) <= getPriority(operator.peek())) && operator.peek() != '(' && operator.peek() != '#')
{
float d = figure.pop();
float u = figure.pop();
figure.push(operate(operator.pop(),u,d));
}
if (getPriority(currentCharacter) < getPriority(operator.peek()) && getPriority(currentCharacter) + getPriority(operator.peek()) == 0)
{
operator.pop(); //去括号
}
else
operator.push(currentCharacter); //当前符号优先级太低没参与运算,且不是右括号,进栈符号栈
}
}
这样代码就清晰多了,在主程序中可以看到代码的整体思路,具体功能的实现在各个子方法中,查看具体操作时只需要鼠标移到方法上,ctrl+左键跳转。代码出现问题也可以迅速定位到是具体哪个子方法出现了问题,局部修改即可。
接下来做运算式正确性判断,如果成功的话后期可以再考虑加入次方运算等。