当前程序
目录结构:
代码
FormulaException
package exception;
/**
* 运算式异常类
* @description 提供几个含有不同的运算式异常信息的异常对象
* @author BlazingPhoenix
*
*/
public class FormulaException extends Exception {
//除零异常
private static FormulaException divideByZeroException = new FormulaException("除零异常:算式中某个除法运算的除数为0,请检查");
//定义域异常
private static FormulaException definitionDomainException = new FormulaException("定义域越界:请检查根号或次方运算等是否正确");
//运算符异常,某个运算符后紧接着乘除号或运算式开头为乘除号
private static FormulaException characterException = new FormulaException("运算符使用错误,请检查!");
//运算式中存在无法识别的符号,或数字中存在多个小数点,最终导致数字字符串无法转化为数字
private static FormulaException figureException = new FormulaException("运算式错误:存在无法识别的符号");
//左右括号不匹配
private static FormulaException bracketMismatchException = new FormulaException("左右括号不匹配");
//不明原因的异常,最可能是因为左右括号不匹配
private static FormulaException unKnowException = new FormulaException("运算式不明错误:可能是左右括号不匹配");
/*无参构造函数*/
private FormulaException(){
super();
}
//用详细信息指定一个异常
private FormulaException(String message){
super(message);
}
//用指定的详细信息和原因构造一个新的异常
private FormulaException(String message, Throwable cause){
super(message,cause);
}
//用指定原因构造一个新的异常
private FormulaException(Throwable cause) {
super(cause);
}
/**
* 除零异常
* @return 带有除零异常提示信息的对象
*/
public static FormulaException getDivideByZeroException()
{
return divideByZeroException;
}
/**
* 定义域异常
* @return 带有定义域异常提示信息的对象
*/
public static FormulaException getDefinitionDomainException()
{
return definitionDomainException;
}
/**
* 运算符异常
* @return 带有运算符使用错误信息的对象
*/
public static FormulaException getCharacterException()
{
return characterException;
}
/**
* 数字异常
* @return 带有引发该异常原因的提示信息的对象
*/
public static FormulaException getFigureException()
{
return figureException;
}
/**
* 不明异常
* @return 带有异常提示信息的对象
*/
public static FormulaException getUnKnowException()
{
return unKnowException;
}
/**
* 括号匹配异常
* @return 带有括号匹配错误信息的对象
*/
public static FormulaException getBracketMismatchException()
{
return bracketMismatchException;
}
}
Calculator
package calculator;
import java.util.Stack;
import exception.FormulaException;
/**
* 计算器
* @description 接收运算式,完成计算步骤并返回
* <br>
*
* @author BlazingPhoenix
*
*/
public class Calculator {
private int NP = 1; //标记进栈数字元素的正负性
private boolean NPCanUse = true; //NP标记是否是激活状态
private boolean flag = true; //标记,下一个符号能否是乘除号,能则为false
private FormulaException exception; //异常
private Stack<Float> figure = new Stack<Float>(); //数字栈
private Stack<Character> operator = new Stack<Character>(); //符号栈
private StringBuffer figureString = new StringBuffer(); //数字字符串,用于暂时存放读取到的单个数字字符,并将它们拼接成一个完整的数字
/**
* @description 计算运算式(主程序)
* <br>
* 根据传入的代表算式的字符串计算结果并返回
* <br>
* 如果运算式存在问题,捕获异常并抛出
* @param formula
* @return
* @throws FormulaException
*/
public float calculate(String formula) throws FormulaException
{
try {
return calculateImpl(formula);
}
catch (FormulaException e)
{
throw e;
}
catch (Exception e)
{
exception = FormulaException.getUnKnowException();
throw exception;
}
}
/**
* @description 计算运算式(实现类)
* <br>
* 根据传入的代表算式的字符串计算结果并返回
* @param formula 代表算式的字符串
* @return 计算结果
*/
public float calculateImpl(String formula) throws FormulaException
{
formula += "#";
operator.push('#');
for (int i = 0;i < formula.length();i ++)
{
//读取到非运算符(非数字符号)时进行的操作
if (!isOperator(formula.charAt(i)))
doWhenCharIsNotOperator(formula.charAt(i));
//读取到非运算符(数字符号)时进行的操作
else
{
if (!doWhenCharIsOperator(formula.charAt(i),formula.charAt(i - 1 > -1 ? i -1 : 0)))
throw exception;
}
}
if (getPriority(operator.pop()) != -4)
//字符串读取完成,但是符号栈内仍然有运算符
{
exception = FormulaException.getUnKnowException();
throw exception;
}
else
return figure.pop();
}
/**
* @description 判断即将要进行的运算是否正确
* <br>
* 检查是否是除数为0的除法运算或定义域不正确的次方运算
* @param c 运算符
* @param u 运算符左侧的数字(若运算符是次方或根号,则是进行次方或开方运算的数字)
* @param d 运算符右侧的数字
* @return
*/
public boolean operateIsRight(char c,float u,float d)
{
if (c == '/' && d == 0)
{
exception = FormulaException.getDivideByZeroException(); //除零异常
return false;
}
else if (c == '(' || c == ')') //括号不匹配异常
{
exception = FormulaException.getBracketMismatchException();
return false;
}
return true;
}
/**
* @description 当前读取到的字符不是运算符时进行的操作
* <br>
* 说明读取到的字符是数字,将它接到数字字符串之后
* <br>
* 由于下一个字符一定不会是正负号,所以激活标记睡眠
*
* @param currentCharacter 当前读取到的字符
*/
private void doWhenCharIsNotOperator(char currentCharacter)
{
figureString.append(currentCharacter);
flag = false;
NPCanUse = false; //NP标记休眠
}
/**
* @description 当前读取到的字符是运算符时进行的操作
* <br>
*
* @param currentCharacter 当前读取到的字符
*/
private boolean doWhenCharIsOperator(char currentCharacter,char proCharacter)
{
//非(当前符号是加减号且正负号标记激活状态),即当前符号不是正负号
if (!(getPriority(currentCharacter) == 1 && NPCanUse == true))
{
if (flag == true && currentCharacter != '(') //运算符后不能紧跟除左括号外的运算符
{
exception = FormulaException.getCharacterException();
return false;
}
//pushFigure尝试将数字字符串进栈数字栈(有些特殊情况不能进栈)
//operate尝试运算(有些情况当前字符不会参与运算)
if (!pushFigure(currentCharacter,proCharacter) || !operate(currentCharacter))
return false;
if (currentCharacter != ')') //除右括号外其他运算符右边均不能接乘除号
flag = true;
}
//当前符号是正负号,并且为符号时,正负号标记*-1
else
NP *= currentCharacter == '-' ? -1: 1;
//当读取到加减号或左括号,下一个加减号视为正负号,NP激活
if (getPriority(currentCharacter) == 1 || getPriority(currentCharacter) == 3)
NPCanUse = true;
return true;
}
/**
* @description 数字栈进栈
* <br>
* 根据当前符号判断是否将数字符号串内的内容转为数字并进栈数字栈
* <br>
* 并重置NP标记,并将其改为休眠状态
* <br>
* @param currentCharacter 当前读取的符号
* @param proCharacter 当前读取的符号的上一个符号
*/
private boolean pushFigure(char currentCharacter,char proCharacter)
{
//若当前符号不是左括号且前一个符号不是右括号,数字栈进栈并将数字字符串清空。否则当前数字字符串内应该是没有内容的
//若当前符号是左括号,说明之前的不是数字,那在之前数字字符串就被清空过;前一个符号是右括号时同理
if (currentCharacter != '(' && proCharacter != ')')
{
try {
figure.push(Float.valueOf(figureString.toString()) * NP); //将数字字符串的内容转为数字并进栈数字栈
}catch (Exception e){
exception = FormulaException.getFigureException(); //数字异常
return false;
}
figureString.setLength(0); //清空数字字符串
NP = 1; //重置NP及其激活状态
NPCanUse = false; //NP休眠
}
return true;
}
/**
* @description 运算操作
* <br>
* 根据当前符号的意义判断是否进行运算,如果是则进行运算
* <br>
* 如果当前符号暂时不能参与运算,根据当前符号做相应的处理
* <br>
* @param currentCharacter 当前读取的符号
*/
private boolean operate(char currentCharacter)
{
//尝试将当前符号拿来参与运算
while((getPriority(currentCharacter) <= getPriority(operator.peek())) && operator.peek() != '(' && operator.peek() != '#')
{
float d = figure.pop();
float u = figure.pop();
if (!operateIsRight(operator.peek(),u,d)) //判断将要进行的运算是否会出现异常
return false; //出现异常
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); //当前符号优先级太低没参与运算,且不是右括号,进栈符号栈
return true;
}
//判断符号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;
}
}
/**
* @description 计算
* <br>
* 根据运算符和两个数字计算结果
* @param c 运算符
* @param u 运算符左边的数字
* @param d 运算符右边的数字
* @return 计算结果
*/
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;
}
}
}
Main
package main;
import calculator.Calculator;
import exception.FormulaException;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
String formula = "1*(+6.--+2/.5))+(2*3)";
try {
System.out.println("算式为:" + formula + "\n");
System.out.println("计算结果为:" + new Calculator().calculateImpl(formula));
} catch (FormulaException e) {
// TODO Auto-generated catch block
System.out.println(e.getMessage());
}
}
}
括号匹配异常
然后接下来要做的是括号匹配问题的判断。虽然说我们定义了unknowException,捕获了所有的异常,并且会提示使用者者很可能是括号不匹配引起的。
但是!在测试时发现了,有时候左右括号不匹配,竟然!它**竟然能算出结果,而且有可能这个结果是错的!比如:
而这个算式明显括号不匹配,多了一个左括号,所以这个异常还是很有必要做一下。
基本思路
有个一定正确的方法,暂且称它傻瓜大法吧。那就是新增一个括号栈,每次读到左括号时就进栈,读到右括号时看看栈里面有没有元素,有的话就出栈,没有就说明右括号多了,匹配错误。如果算式读完了,但是栈内还有左括号,就说明左括号多了,匹配失败。
这么做一定没毛病,但是有可能有点浪费存储空间,如果能从原来的代码的逻辑中找到判别方法是最好的。
我累了,毕竟写代码是要消耗脑力的,为了防止猝死,我决定先睡一觉。
休息了一个星期,我们来继续更新。之前我们说到的“傻瓜大法”一定能够成功,但是有一定缺陷,就是比较浪费资源,而且原来的算法中已经存在括号进出栈操作,再去额外设置一个括号栈有些鸡肋。所以我们通过源代码中的括号进出栈操作来寻找突破口。
观察代码
在观察之前先思考一下,显然符号栈中括号的操作机制和“傻瓜大法”中括号进出栈类似,读取到左括号时进栈,读取到右括号时符号栈会连续出栈,直到符号栈栈顶是左括号,然后将左括号出栈为止。所以主要观察一下有关符号栈操作的代码。
主方法中,读取到运算符时执行的是doWhenCharIsOperator方法,所以进入该方法查看。而在doWhenCharIsOperator方法中,有关运算符的方法是operate方法,进入这个方法查看:
/**
* @description 运算操作
* <br>
* 根据当前符号的意义判断是否进行运算,如果是则进行运算
* <br>
* 如果当前符号暂时不能参与运算,根据当前符号做相应的处理
* <br>
* @param currentCharacter 当前读取的符号
*/
private boolean operate(char currentCharacter)
{
//尝试将当前符号拿来参与运算
while((getPriority(currentCharacter) <= getPriority(operator.peek())) && operator.peek() != '(' && operator.peek() != '#')
{
float d = figure.pop();
float u = figure.pop();
if (!operateIsRight(operator.peek(),u,d)) //判断将要进行的运算是否会出现异常
return false; //出现异常
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); //当前符号优先级太低没参与运算,且不是右括号,进栈符号栈
return true;
}
仔细观察可以发现,之前“2*((+6–+2/.5)+(2*3)”运算式能独处结果的原因是字符串读取完后,末尾的“#”进栈,然后判断栈顶元素为结束符后,直接将数字栈栈顶出栈作为结果了。
也就是说当左括号多出来的话,结束后符号栈栈顶一定是结束符,再下面一个就是多出来的那个左括号了。这个只需要在读取结束时,先讲符号栈栈顶元素出栈,然后再判断一下栈顶是不是左括号就行了。
那如果右括号多出来的话会怎么样呢?
/**
* @description 计算运算式(主程序)
* <br>
* 根据传入的代表算式的字符串计算结果并返回
* <br>
* 如果运算式存在问题,捕获异常并抛出
* @param formula
* @return
* @throws FormulaException
*/
public float calculate(String formula) throws FormulaException
{
try {
return calculateImpl(formula); //计算
}
catch (FormulaException e)
{
throw e; //捕获运算式异常
}
/*catch (Exception e) //捕获到异常,则抛出一个不明原因异常
{
exception = FormulaException.getUnKnowException();
throw exception;
}*/
}
把最后捕获异常的代码注释掉后,发现它报错,空栈异常。
仔细检查后会发现,多一个右括号会导致某次运算时,数字栈在空栈的情况下还出栈了一次,问题就出栈下方代码的float u = figure.pop();这一行:
/**
* @description 运算操作
* <br>
* 根据当前符号的意义判断是否进行运算,如果是则进行运算
* <br>
* 如果当前符号暂时不能参与运算,根据当前符号做相应的处理
* <br>
* @param currentCharacter 当前读取的符号
*/
private boolean operate(char currentCharacter)
{
//尝试将当前符号拿来参与运算
while((getPriority(currentCharacter) <= getPriority(operator.peek())) && operator.peek() != '(' && operator.peek() != '#')
{
float d = figure.pop();
float u = figure.pop();
if (!operateIsRight(operator.peek(),u,d)) //判断将要进行的运算是否会出现异常
return false; //出现异常
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); //当前符号优先级太低没参与运算,且不是右括号,进栈符号栈
return true;
}
而在operateIsRight方法中,u这个变量实际没有起到作用,所以我们可以将operateIsRight方法中的u这个变量去掉,当判断运算式没有问题时再进行出栈操作。修改后代码如下:
/**
* @description 判断即将要进行的运算是否正确
* <br>
* 检查是否是除数为0的除法运算或定义域不正确的次方运算
* @param c 运算符
* @param d 运算符右侧的数字
* @return
*/
public boolean operateIsRight(char c,float d)
{
if (c == '/' && d == 0)
{
exception = FormulaException.getDivideByZeroException(); //除零异常
return false;
}
else if (c == '(' || c == ')') //括号不匹配异常
{
exception = FormulaException.getBracketMismatchException();
return false;
}
return true;
}
/**
* @description 运算操作
* <br>
* 根据当前符号的意义判断是否进行运算,如果是则进行运算
* <br>
* 如果当前符号暂时不能参与运算,根据当前符号做相应的处理
* <br>
* @param currentCharacter 当前读取的符号
*/
private boolean operate(char currentCharacter)
{
//尝试将当前符号拿来参与运算
while((getPriority(currentCharacter) <= getPriority(operator.peek())) && operator.peek() != '(' && operator.peek() != '#')
{
float d = figure.pop();
if (!operateIsRight(operator.peek(),d)) //判断将要进行的运算是否会出现异常
return false; //出现异常
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); //当前符号优先级太低没参与运算,且不是右括号,进栈符号栈
return true;
}
这样代码就修改完成了,文章结尾附了源码下载地址。之后可能会考虑添加次方、开方、幂运算等。
上一页
源码1.0版(代码上传中)
源码1.1版(新加小功能)(代码上传中)