四则运算 - java实现(叶尚文, 张鸿)
四则运算生成程序
by 叶尚文 and 张鸿
GitHub地址:https://github.com/MansonYe/Calculate
一.需求分析
1. 使用-n 参数控制生成题目的个数
2. 使用-r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
3. 生成的题目中计算过程不能产生负数
4. 生成的题目中如果存在形如e1 / e2的子表达式,那么其结果应是真分数
5. 每道题目中出现的运算符个数不超过3个
6. 程序生成的题目不能重复,任何两道题目不能通过交换律变换为同一道题目
7. 生成题目存入程序当前目录下Exercises.txt文件
答案存入程序当前目录下Answers.txt文件
8. 程序应能支持一万道题目的生成
9. 参数“-e”“-a”对给定的题目文件和答案文件,判定对错并进行数量统计
可见,功能可分为几大类:
1.调用其他类函数进行文件读写的主类
2. 随机生成后缀式并调节成中缀式的创建类
3. 对后缀式进行计算并判断结果和计算过程是否符合要求的计算类
4. 把读入程序的算式转换为后缀式的转换类(用于需求9)
二. PSP2.1表格
三、设计实现过程:
主类:
在主类调用另外三个大类的函数完成程序的功能:
创建类:
先随机创建后缀式算式;
然后转换为中缀式;
并暂存生成的后缀式及中缀式。
计算类:
读取后缀式,以栈的形式完成计算,生成结果的分子和分母;
判断结果是否符合要求;
把符合要求的结果的分子分母转换为真分数
转换类:
因为中缀式转后缀式比较麻烦,故分出一个类完成这项操作;
后缀式栈 CaluBack存储后缀式
运算符栈 Operate 存储运算符
Priority和compare方法用来比较运算符优先级
先将中缀表达式字符串转换成字符数组
然后判断是否为运算符
若为操作数则count自增1
若为运算符,则先将前一个操作数存入后缀式栈
若当前运算符为括号或优先级高于Operate栈顶则另做处理
否则将该运算符存入Operate栈
将字符数组中和Operate栈中剩余元素分别存入后缀式栈中
四、代码说明:
主类:
Build方法:
1 private void Build(int Range, int Number) { 2 CreateClass Create = new CreateClass(); 3 CountClass Count = new CountClass(); 4 5 FileOutputStream outputStream1 = null; 6 PrintWriter printWriter1 = null; 7 FileOutputStream outputStream2 = null; 8 PrintWriter printWriter2 = null; 9 int i = 1; 10 11 String[] CaluBack;//暂存后缀式 12 String Calu;//保存中缀式 13 String Result;//保存结果 14 15 try { 16 outputStream1 = new FileOutputStream("Exercises.txt"); 17 printWriter1 = new PrintWriter(outputStream1); 18 outputStream2 = new FileOutputStream("Answers.txt"); 19 printWriter2 = new PrintWriter(outputStream2); 20 21 do { 22 CaluBack = Create.CaluCreate(Range);//创建后缀式,并保存(字符数组) 23 Create.EquationConstruct();//转换为中缀式 24 25 if(Count.CaluCount(CaluBack)) //根据后缀式计算结果,返回值表示计算结果是否符合要求 26 { 27 Result = Count.getResult();//保存结果(字符串) 28 Calu = Create.getCalu();//保存中缀式(字符串) 29 30 printWriter1.println(i + ". " + Calu); 31 32 printWriter2.println(i + ". " + Result); 33 34 i++; 35 36 } 37 else {//不符合要求,则Number自加,以便重做 38 Number++; 39 } 40 41 }while(--Number != 0); 42 43 44 } catch (IOException e) { 45 System.out.println("Sorry, there has been a problem opening or writing to the file!"); 46 } finally { 47 if(printWriter1 != null) { 48 printWriter1.close(); 49 } 50 if(printWriter2 != null) { 51 printWriter2.close(); 52 } 53 } 54 55 }
Judge方法:
1 private void Judge() { 2 File fileE = new File("Exercises.txt"); 3 File fileA = new File("Answers.txt"); 4 BufferedReader readerE = null; 5 BufferedReader readerA = null; 6 Scanner input = new Scanner(System.in); 7 int Tnum = 0; 8 int Fnum = 0; 9 int Anum = 1; 10 String True = ""; 11 String False = ""; 12 13 try { 14 15 readerE = new BufferedReader(new FileReader(fileE));//reader打开文件内容 16 readerA = new BufferedReader(new FileReader(fileA)); 17 18 String LineE = ""; 19 String LineA = ""; 20 21 LineE = readerE.readLine();//do first 22 LineA = readerA.readLine(); 23 24 while(LineE != null){ 25 System.out.print(LineE + " = "); 26 27 String temp = (Anum++) + ". " + input.nextLine(); 28 if(LineA.equals(temp)) {//读入答案,并判断对错 29 System.out.println("True"); 30 Tnum++; 31 } 32 else { 33 System.out.println("False"); 34 Fnum++; 35 } 36 37 LineE = readerE.readLine();//do again 38 LineA = readerA.readLine(); 39 } 40 41 readerE.close(); 42 readerA.close(); 43 if(True != "") 44 True = True.substring(0, (True.length()-2) ); 45 if(False != "") 46 False = False.substring(0, (False.length()-2) ); 47 System.out.println("\nCorrect: " + Tnum + " (" + True + ")"); 48 System.out.println("Wrong: " + Fnum + " (" + False + ")"); 49 50 51 } catch(IOException e) { e.printStackTrace(); } 52 53 input.close(); 54 }
Compare方法:
private void Compare(String ExeFileAddress, String AnsFileAddress) {//比较answer文档和正确答案 File fileE = new File(ExeFileAddress); File fileA = new File(AnsFileAddress); BufferedReader readerE = null; BufferedReader readerA = null; Transform caltest = new Transform(); CountClass Count = new CountClass(); int Anum = 0; int Tnum = 0; int Fnum = 0; String True = ""; String False = ""; String[] CaluBack = new String[7];//后缀式 String Result; try { readerE = new BufferedReader(new FileReader(fileE));//reader打开文件内容 readerA = new BufferedReader(new FileReader(fileA)); String LineE = ""; String LineA = ""; LineE = readerE.readLine();//do first LineA = readerA.readLine(); while(LineE != null){ Anum++; LineE = LineE.substring((Anum+"").length()+2); caltest.prepare(LineE);//中缀转后缀 CaluBack = caltest.getPostfixStack(); Count.CaluCount(CaluBack); Result = Count.getResult(); if(LineA.equals(Anum + ". " + Result)) {//读入答案,并判断对错 True += Anum + ", "; Tnum++; } else { False += Anum + ", "; Fnum++; } LineE = readerE.readLine();//do again LineA = readerA.readLine(); } readerE.close(); readerA.close(); if(True != "") True = True.substring(0, (True.length()-2) ); if(False != "") False = False.substring(0, (False.length()-2) ); System.out.println("\nCorrect: " + Tnum + " (" + True + ")"); System.out.println("Wrong: " + Fnum + " (" + False + ")"); } catch(IOException e) { e.printStackTrace(); } }
创建类:
import java.util.Stack; public class CreateClass { private String[] CaluBack = new String[7]; private String Calu = ""; private boolean isDigit(String strNum){ return strNum.matches("[0-9]{1,}"); } public String[] CaluCreate(int Rance) { //创建一个逆波兰式的算式 String[] SingCollection = {"+", "-", "*", "/", "#"}; int n = 0; int c = 0; int i=0; do { if( ((int)(Math.random()*2) != 1) && (n-1 > c) ) { //随机但后缀式中算符数量不可大于数字数量(从左到右) CaluBack[i++] = SingCollection[(int)(Math.random()*(5 - c/2))]; c++; } else if(n < 4){ //生成4个数字了就不再生成数字 CaluBack[i++] = (int)(Math.random()*Rance) + ""; n++; } }while(n+c < CaluBack.length); return CaluBack; } public String EquationConstruct() { //后缀式转中缀式 Stack<String> CaluMid = new Stack<String>(); int loop1; String s1, s2; for(loop1=0; loop1<CaluBack.length; loop1++) {//注释参考隔壁CountClass if(isDigit(CaluBack[loop1])) { CaluMid.push(CaluBack[loop1] + ""); } else { s1 = CaluMid.pop(); s2 = CaluMid.pop(); switch(CaluBack[loop1]) { case "+": CaluMid.push(s2 + " + " + s1); break; case "-": CaluMid.push(s2 + " - " + s1); break; case "*": CaluMid.push(s2 + " * " + s1); break; case "/": CaluMid.push(s2 + " / " + s1); break; case "#": CaluMid.push(s2); break; default: break; } if(loop1 != CaluBack.length - 1 && CaluBack[loop1] != "#") { CaluMid.push("(" + CaluMid.pop() + ")"); } } } Calu = CaluMid.pop(); return Calu; } public void CreateTest() { //测试 for(int i=0; i<CaluBack.length; i++) System.out.print(CaluBack[i]); System.out.println("\n" + Calu); } public String getCalu() { return Calu; } public String[] getCaluBack() { return CaluBack; } }
计算类:
public class CountClass { private String Result = ""; private boolean isDigit(String strNum){ return strNum.matches("[0-9]{1,}"); } public boolean CaluCount(String[] CaluBack) { int loop1 = 0;//循环后缀式 int loop2 = 0;//模仿栈 int mole = 0;//分子 int deno = 0;//分母 int d1, d2, m1, m2; Elem[] num = new Elem[4]; while(loop2 < 4) num[loop2++] = new Elem(); loop2 = 0; for(loop1 = 0; loop1 < CaluBack.length && CaluBack[loop1] != null; loop1++) { if( isDigit(CaluBack[loop1]) ) { //判断是否为数字,目前只做了个位数判断,后期视情况修改 num[loop2].setMole( Integer.parseInt(CaluBack[loop1]) * num[loop2].getDeno() ); loop2++; //利用num[loop2]模仿栈,把数字放入栈中((int)-48) } else {//是算符的话,根据情况作计算 m1 = num[loop2-2].getMole();//取出伪栈中2个数据进行计算 m2 = num[loop2-1].getMole(); d1 = num[loop2-2].getDeno(); d2 = num[loop2-1].getDeno(); switch(CaluBack[loop1]) { case "+": mole = m1 * d2 + m2 * d1; deno = d1 * d2; break; case "-": mole = m1 * d2 - m2 * d1; deno = d1 * d2; if(mole < 0)//防止计算过程中出现负数 return false; break; case "*": mole = m1 * m2; deno = d1 * d2; break; case "/": mole = m1 * d2; deno = d1 * m2; break; case "#": mole = m1; deno = d1; break; default: break; } num[loop2-2].setDeno(deno);//计算结果入栈 num[loop2-2].setMole(mole); num[loop2-1] = new Elem();//清空出栈元素位置 loop2--; }//end else }//end for if(num[0].getDeno() <= 0 || num[0].getMole() < 0) {//如果结果不符合要求返回false return false; } else { ResultConstruct(num[0]);//符合要求则将假分数转换为真分数 } return true; } private void ResultConstruct(Elem elem) {//将假分数转换为真分数,并存入Result int mole = elem.getMole(); int deno = elem.getDeno(); int Integer = mole/deno; int a = mole;//a, b and c are used to count divisor int b = deno; int c = 0; mole = mole % deno; Result = ""; if(mole == 0) Result = Integer + ""; else { while(a % b != 0) { c = a % b; a = b; b = c; } mole /= b; deno /= b; if(Integer == 0) Result = mole + "/" + deno; else Result = Integer + "'" + mole + "/" + deno; } } public String getResult() { return Result; } public void CountTest() { System.out.println(Result); } }
转换类(针对需求9)
import java.util.Collections; import java.util.Stack; public class Transform { private Stack<String> CaluBack = new Stack<String>();// 后缀式栈 private Stack<Character> Operate = new Stack<Character>();// 运算符栈 private int[] priority = new int[] { 0, 3, 2, 1, -1, 1, 0, 2 }; //分别对应( ) * + , - . / private boolean isOpera(char c) { return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')'; } private boolean isOperaS(String c) { return c == "+" || c == "-" || c == "*" || c == "/" || c == "(" || c == ")"; } private boolean isDigit(String strNum){ return strNum.matches("[0-9]{1,}"); } public boolean compare(char cur, char peek) {// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低 return priority[(peek) - 40] >= priority[(cur) - 40]; } public String[] getPostfixStack() { String[] CaluBacktest = new String[7]; String Temp; int i = 0; Collections.reverse(CaluBack); while(!CaluBack.isEmpty()) { Temp = CaluBack.pop().trim(); if(!Temp.isEmpty()) { CaluBacktest[i] = Temp; i++; } } return CaluBacktest; } public void prepare(String expression) { Operate.push(',');// 运算符放入栈底元素逗号,此符号优先级最低 char[] arr = expression.toCharArray(); int location = 0;// 当前字符的位置 int count = 0;// 两次算术运算符的字符的长度 char currentOp, topOp;// 当前操作符和栈顶操作符 for (int i = 0; i < arr.length; i++) { currentOp = arr[i]; if (isOpera(currentOp)) {//如果当前字符是运算符 if (count > 0) { CaluBack.push(new String(arr, location, count));// 取两个运算符之间的数字 } topOp = Operate.peek(); if (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号 while (Operate.peek() != '(') { CaluBack.push(String.valueOf(Operate.pop())); } Operate.pop(); } else { while (currentOp != '(' && topOp != ',' && compare(currentOp, topOp)) { CaluBack.push(String.valueOf(Operate.pop())); topOp = Operate.peek(); } Operate.push(currentOp); } count = 0; location = i + 1; } else {//否则间距自加 count++; } } if (count > 1 || (count == 1 && !isOpera(arr[location]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中 CaluBack.push(new String(arr, location, count)); } while (Operate.peek() != ',') { CaluBack.push(String.valueOf(Operate.pop()));// 将操作符栈中的剩余的元素添加到后缀式栈中 } } }
五、测试结果:
以Eclipse中单元测试功能作测试
1 import static org.junit.Assert.*; 2 import org.junit.Test; 3 4 public class test1 extends MainClass{ 5 6 @Test 7 public void test() { 8 System.out.println("test"); 9 MainClass testMain = new MainClass(); 10 String[] test1 = {"-n", "5", "-r", "10"}; 11 String[] test2 = {"-a", "answersfile.txt", "-e", "exercisesfile.txt"}; 12 String[] test3 = {"-n", "10000", "-r", "10"}; 13 14 testMain.main(test1); 15 } 16 17 }
基础功能(test1):
需求9(test2):
压力测试(test3)仅生成测试(加上输入怕是要按坏键盘):
六、小结:
这次项目比上次复杂,而且自己也挺久没用java了,生疏之余,忘记了许多应该注意的细节,更没有想到的事需求9的难度比想象中的难度大。
这次的结对编程还是学到了很多东西,例如一开始构思的时候,我就打算直接生成一条从左到右的线性算式,然后再加工,从而无视后缀式、中缀式等结构转换问题,但同学的反对虽然使起步的速度降低了,但在后续编程方面,后缀式的引入无疑给程序提供了极大的便利。此外,虽然同学没怎么打代码,但是,他利用丰富的知识储备和多样的学习方法(baidu)帮助我攻克了不少bug,再次感谢牺牲打游戏时间帮我debug的同学。
那么!为什么又拖到那么晚交呢?我也不想熬夜啊!看了眼现在都是在打dnf的合作伙伴。。。emmmmm
原因可能是博文太难写吧(主类的图我画了半天)