Github地址:https://github.com/mercuriussss/calculate
项目要求
实现一个自动生成小学四则运算题目的命令行程序
功能(已全部实现)
- 使用 -n 参数控制生成题目的个数。
- 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围。
- 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
- 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
- 每道题目中出现的运算符个数不超过3个。
- 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
- 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt。
- 程序应能支持一万道题目的生成。
- 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt。
PSP表格预估
|
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
|
Planning |
计划 |
100 |
50 |
|
· Estimate |
· 估计这个任务需要多少时间 |
100 |
50 |
|
Development |
开发 |
3000 |
2500 |
|
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
50 |
|
· Design Spec |
· 生成设计文档 |
100 |
100 |
|
· Design Review |
· 设计复审 (和同事审核设计文档) |
30 |
50 |
|
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
20 |
20 |
|
· Design |
· 具体设计 |
100 |
120 |
|
· Coding |
· 具体编码 |
2000 |
1500 |
|
· Code Review |
· 代码复审 |
60 |
60 |
|
· Test |
· 测试(自我测试,修改代码,提交修改) |
630 |
600 |
|
Reporting |
报告 |
120 |
120 |
|
· Test Report |
· 测试报告 |
20 |
40 |
|
· Size Measurement |
· 计算工作量 |
40 |
30 |
|
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
60 |
50 |
|
合计 |
|
3220 |
2670 |
设计实现过程
依照需求,可将程序流程划分为“生成符合条件的题目和对应答案”、“检验是否存在重复题目”、“输出题目和答案到相应的txt文档”、“校对答案” 四个部分。
最终,我们使用了7个类:
- Main为主类,包含main方法,exe程序所调用的函数
- FractionOpe和IntegerOpe类,在整合过后,决定只负责分数跟整数的随机生成
- CreateSubject类,负责随机生成符合限制条件的题目和相应答案
- JudgeAnswer类,负责校对答案并将数据存进Grade.txt文件
- Calculate类,负责数字的计算部分
以下为7个函数相互间的调用关系图,也是程序的流程图

关键代码
CreateSubject类的代码
其中在实现功能3跟4,也就是实现相减非负数、相除只能为真分数的功能时,我耍了个滑头。
因为当两数相减为负数时,那就说明前者比后者小,刚好跟相除只能为真分数的情况所需条件一致,于是我调换了两种情况的位置。
当两者相减为负数时,将两者的运算符改为相除;当两者相除不为真分数时,将两者的运算符改为相减。
这样一来,两种情况都得到了解决,而且也保证了随机性。
package calculate; import java.util.ArrayList; import java.util.Random; public class CreateSubject { private static final String[] OPERATORS = { "+", "-", "*", "÷" }; ArrayList<String> expression = new ArrayList<String>();// 算式字符串存储 ArrayList<String> numbs = new ArrayList<String>(); ArrayList<String> opers = new ArrayList<String>(); ArrayList<String> answer = new ArrayList<String>(); Random rd = new Random(); public CreateSubject(int max) { String numA = randNum(max); String numB = null; int nums = rd.nextInt(3) + 2;// 几个数字的运算 String flag = null; expression.add(numA); numbs.add(numA); for (int i = 0; i < nums - 1; i++) { numB = randNum(max); flag = OPERATORS[rd.nextInt(4)]; switch (flag) { case "+": numA = Calculate.add(numA, numB); break; case "-": if (!Calculate.isGreater(numA, numB)) { flag = OPERATORS[3]; numA = Calculate.div(numA, numB); } else { numA = Calculate.sub(numA, numB); } break; case "*": numA = Calculate.mul(numA, numB); break; case "÷": if(Calculate.isGreater(numA, numB)){ flag = OPERATORS[1]; numA = Calculate.sub(numA, numB); } else { numA = Calculate.div(numA, numB); } break; } expression.add(flag); expression.add(numB); numbs.add(numB); opers.add(flag); } addBrackets(expression); expression.add("="); answer.add(numA); } // 随机生成分数或整数 public String randNum(int max) { String num; int flag = rd.nextInt(10) + 1; if (flag % 3 == 0) { num = FractionOpe.gen(max); } else { num = IntegerOpe.gen(max); } return num; } // 生成必要的括号 public ArrayList<String> addBrackets(ArrayList<String> expression) { String lowlv = "+-"; String highlv = "*÷"; if (expression.size() == 5) { if (lowlv.contains(expression.get(1)) && highlv.contains(expression.get(3))) { expression.add(0, "("); expression.add(4, ")"); } } if (expression.size() == 7) { if (lowlv.contains(expression.get(1)) && highlv.contains(expression.get(3))) { expression.add(0, "("); expression.add(4, ")"); } if (lowlv.contains(expression.get(3)) && highlv.contains(expression.get(5))) { expression.add(0, "("); expression.add(6, ")"); } } return expression; } }
随机生成分数的代码
public static String gen(int max) { String fraction = null; Random rd = new Random(); int numerator = rd.nextInt(max) + 1; int denominator = rd.nextInt(max) + 1; // 检验是否为整数 if (numerator == denominator || numerator % denominator == 0) { fraction = numerator / denominator + ""; return fraction; } // 该数不能超过max while (numerator / denominator >= max) { numerator = rd.nextInt(max) + 1; denominator = rd.nextInt(max) + 1; } fraction = Calculate.simplify(numerator, denominator); return fraction; }
随即生成整数的代码
public static String gen(int max) { Random rd = new Random(); int integer = 0; while (integer == 0 || integer >= max) { integer = rd.nextInt(max) + 1; } return String.valueOf(integer); }
FileOutput类
里面包含创建文件、向文件追加写入数据、检验题目是否重复、将题目和答案写入相应文件的方法
package calculate; import java.io.*; import java.util.ArrayList; public class FileOutput { // 将题目和答案写入相应文件中 public static void outExeAndAns(int num, CreateSubject cs, File fileAns, File fileExe) throws IOException { cs.expression.add(0, num + ".\t"); cs.answer.add(0, num + ".\t"); outputData(cs.expression, fileExe); outputData(cs.answer, fileAns); } // 检验题目是否重复 public static boolean isRepeat(CreateSubject cs, File fileAns, File fileExe) throws IOException { boolean isRepeat = false; String strAns = null; String strExe = null; String[] opers = null; String[] numbs = null; int sameNum; int sameOpe; int allNums; int allOpes; BufferedReader brAns = new BufferedReader(new InputStreamReader(new FileInputStream(fileAns))); BufferedReader brExe = new BufferedReader(new InputStreamReader(new FileInputStream(fileExe))); while ((strAns = brAns.readLine()) != null) { strAns = strAns.replaceAll("\\s", "").substring(strAns.indexOf(".") + 1); if (cs.answer.get(0).equals(strAns)) { while ((strExe = brExe.readLine()) != null) { strExe = strExe.substring(strExe.indexOf(".") + 1, strExe.indexOf("=")); strExe = strExe.replaceAll("\\s", ""); numbs = strExe.split("[\\+\\-\\*\\÷]"); opers = strExe.split("[^\\+\\-\\*\\÷]"); sameNum = 0; sameOpe = 0; allOpes = 0; allNums = numbs.length; for (int i = 0; i < opers.length; i++) { if (!opers[i].equals("")) { allOpes++; } } for (String s : opers) { if (!s.equals("")) { if (cs.opers.contains(s)) { sameOpe++; } } } for (String s : numbs) { if (cs.numbs.contains(s)) { sameNum++; } } if (sameOpe == allOpes && sameNum == allNums) { isRepeat = true; } if (isRepeat) { break; } } } if (isRepeat) { break; } } brAns.close(); brExe.close(); return isRepeat; } // 创建文件,若该文件已存在则将其内容清空 public static File createFile(String name) throws IOException { File f = new File(name); if (f.exists()) { f.delete(); f.createNewFile(); } else { f.createNewFile(); } return f; } // 向文件追加写入数据 public static void outputData(ArrayList<String> data, File file) throws IOException { BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true))); String str = arrToString(data); bw.write(str); bw.newLine(); bw.flush(); bw.close(); } public static String arrToString(ArrayList<String> arr) { String str = ""; if (arr != null && arr.size() > 0) { for (String s : arr) { str += s + " "; } } return str; } }
JudgeAnswer类,负责校对答案并输出成绩
package calculate; import java.io.*; import java.util.ArrayList; public class JudgeAnswer { public JudgeAnswer(File ExeFile, File AnsFile) throws IOException { File grade = FileOutput.createFile("Grade.txt"); String str = null; ArrayList<String> trueAns = new ArrayList<String>(); ArrayList<String> testAns = new ArrayList<String>(); ArrayList<String> correct = new ArrayList<String>(); ArrayList<String> wrong = new ArrayList<String>(); BufferedReader brExe = new BufferedReader(new InputStreamReader(new FileInputStream(ExeFile))); BufferedReader brAns = new BufferedReader(new InputStreamReader(new FileInputStream(AnsFile))); while ((str = brExe.readLine()) != null) { str = str.trim().replaceAll("\\s", ""); str = str.substring(str.indexOf("=") + 1); testAns.add(str); } while ((str = brAns.readLine()) != null) { str = str.trim().replaceAll("\\s", ""); str = str.substring(str.indexOf(".") + 1); trueAns.add(str); } for (int num = 1; num <= trueAns.size(); num++) { if (trueAns.get(num - 1).equals(testAns.get(num - 1))) { if (num == trueAns.size()) { correct.add(String.valueOf(num)); } else { correct.add(String.valueOf(num) + ", "); } } else { if(num == trueAns.size()){ wrong.add(String.valueOf(num)); }else{ wrong.add(String.valueOf(num) + ", "); } } } plus(correct, "Correct"); plus(wrong, "Wrong"); FileOutput.outputData(correct, grade); FileOutput.outputData(wrong, grade); brExe.close(); brAns.close(); } public void plus(ArrayList<String> jud, String name) { jud.add(")"); jud.add(0, "("); jud.add(0, name + ": " + (jud.size() - 2)); } }
Calculate类,负责数字的加减乘除
package calculate; public class Calculate { //比较大小 public static boolean isGreater(String numA, String numB) { int[] num = new int[3]; num = comFraction(splFraction(numA), splFraction(numB)); return num[0]>num[1]; } // 求公因数 public static int getFactor(int numerator, int denominator) { if (numerator < denominator) { int temp = numerator; numerator = denominator; denominator = temp; } if (numerator % denominator == 0) { return denominator; } else { return getFactor(denominator, numerator % denominator); } } // 化简 public static String simplify(int numerator, int denominator) { int overnum = 0; int factor = getFactor(numerator, denominator); numerator /= factor; denominator /= factor; if(numerator == 0){ return "0"; } if(denominator == 1){ return String.valueOf(numerator); }else if (numerator > denominator) { overnum = numerator / denominator; numerator -= overnum * denominator; return overnum + "'" + numerator + "/" + denominator; }else { return numerator + "/" + denominator; } } // 判断该数是否为分数 public static boolean isFraction(String num) { if (num.contains("/")) { return true; } else { return false; } } //分割字符串分数 public static int[] splFraction(String num) { String[] str = num.split("'|/"); int[] splFra = new int[str.length]; for (int i = 0; i < str.length; i++) { splFra[i] = Integer.valueOf(str[i]).intValue(); } return splFra; } //通分 public static int[] comFraction(int[] numA,int[] numB){ //存储通分后的A分子、B分子、AB共同分母 int[] num = new int[3]; if(numA.length == 3 && numB.length == 3){ //两者皆为带分数 num[0] = (numA[0]*numA[2]+numA[1])*numB[2]; num[1] = (numB[0]*numB[2]+numB[1])*numA[2]; num[2] = numA[2]*numB[2]; }else if(numA.length == 3 && numB.length == 2){ //A为带分数,B为真分数 num[0] = (numA[0]*numA[2]+numA[1])*numB[1]; num[1] = numB[0]*numA[2]; num[2] = numA[2]*numB[1]; }else if(numA.length == 2 && numB.length == 3){ //A为真分数,B为带真分数 num[0] = numA[0]*numB[2]; num[1] = (numB[0]*numB[2]+numB[1])*numA[1]; num[2] = numA[1]*numB[2]; }else if(numA.length == 2 && numB.length == 2){ //两者皆为真分数 num[0] = numA[0]*numB[1]; num[1] = numB[0]*numA[1]; num[2] = numA[1]*numB[1]; }else if(numA.length == 1 && numB.length == 3){ //A为整数,B为带分数 num[0] = numA[0]*numB[2]; num[1] = numB[0]*numB[2]+numB[1]; num[2] = numB[2]; }else if(numA.length == 3 && numB.length == 1){ //A为带分数,B为整数 num[0] = numA[0]*numA[2]+numA[1]; num[1] = numB[0]*numA[2]; num[2] = numA[2]; }else if(numA.length == 1 && numB.length == 2){ //A为整数,B为真分数 num[0] = numA[0]*numB[1]; num[1] = numB[0]; num[2] = numB[1]; }else if(numA.length == 2 && numB.length == 1){ //A为真分数,B为整数 num[0] = numA[0]; num[1] = numB[0]*numA[1]; num[2] = numA[1]; }else{ //A、B皆为整数 num[0] = numA[0]; num[1] = numB[0]; num[2] = 1; } return num; } public static String add(String numA, String numB) { int[] num = comFraction(splFraction(numA),splFraction(numB)); return simplify(num[0]+num[1],num[2]); } public static String sub(String numA, String numB) { int[] num = comFraction(splFraction(numA),splFraction(numB)); return simplify(num[0]-num[1],num[2]); } public static String mul(String numA, String numB) { int[] num = comFraction(splFraction(numA),splFraction(numB)); return simplify(num[0]*num[1],num[2]*num[2]); } public static String div(String numA, String numB) { int[] num = comFraction(splFraction(numA),splFraction(numB)); if(num[0] == 0){ return "0"; }else{ return simplify(num[0], num[1]); } } }
Main类,包含运行时的主函数
package calculate; import java.io.File; import java.io.IOException; import java.util.Scanner; public class Main { public static void main(String[] args) throws IOException { System.out.println("\n\t 输入 “-n [数值]” 可控制将要生成的题目个数 "); System.out.println("\n\t 输入 “-r [数值]” 可控制生成的题目中,所用数值和真分数分母的最大数字(不包含该数)"); System.out.println("\n\t 输入 “-e <exercisefile>.txt -a <answerfile>.txt” 可校对答案,并将其校对结果输出到Grade.txt文件中"); System.out.println("\n\t 输入 “-exit ” 可退出程序"); System.out.println("\n\t PS: -r 指令需要在输入 -n 指令后才能实现,不过可由两者同时输入,例如“-n 10 -r 10”"); int max; int nums; while (true) { max = 0; nums = 0; System.out.println("\n请输入相应指令: "); Scanner sc = new Scanner(System.in); String commands[] = sc.nextLine().toString().trim().replaceAll(" +", " ").split(" "); if (commands[0].equals("-exit")) { break; } switch (commands[0]) { case "-n": if (commands.length == 2) { nums = Integer.valueOf(commands[1]).intValue(); System.out.println("\n请输入 “-r [数值]”指令:"); sc = new Scanner(System.in); String[] cm = sc.nextLine().toString().trim().replaceAll(" +", " ").split(" "); if (cm.length == 2 && cm[0].equals("-r")) { max = Integer.valueOf(cm[1]).intValue(); } else { System.out.println("\n输入指令错误,请重来!"); break; } build(nums, max); System.out.println("\n生成题目成功!"); } else if (commands.length == 4) { nums = Integer.valueOf(commands[1]).intValue(); max = Integer.valueOf(commands[3]).intValue(); build(nums, max); System.out.println("\n生成题目成功!"); } else { System.out.println("\n输入指令错误,请重来!"); } break; case "-e": if (commands.length == 4 && commands[2].equals("-a")) { File testAns = new File(commands[1]); File trueAns = new File(commands[3]); if (testAns.exists() && trueAns.exists()) { new JudgeAnswer(testAns, trueAns); System.out.println("\n校对成功,成绩已存入Grade.txt!"); } else { System.out.println("\n输入文件路径错误,请重来!"); } } else { System.out.println("\n输入指令错误,请重来!"); } break; default : System.out.println("\n输入指令错误,请重来!"); break; } } } public static void build(int numbs, int max) throws IOException { CreateSubject cs; File fileExe = FileOutput.createFile("Exercises.txt"); File fileAns = FileOutput.createFile("Answers.txt"); for (int num = 1; num <= numbs; num++) { cs = new CreateSubject(max); while (num > 2 && FileOutput.isRepeat(cs, fileAns, fileExe)) { cs = new CreateSubject(max); } FileOutput.outExeAndAns(num, cs, fileAns, fileExe); } } }
测试运行截图

什么答案也没填的结果


将题目中的答案填上,并将其中几个改为错误答案

代码覆盖率

10000道题的生成,文件跟代码一同上传至Github中

项目小结
这次项目,我们没有采用Github上的提交分支、合并分支、改进意见等功能去进行合作,而是直接采用了分工的方法。在开始的时候,我们通过在现实讨论,逐渐将项目细分为几个类,每个类都负责什么样的功能,需要将什么样的参数输入,之后又能得到什么样的结果等等。讨论过后再由各自选择几个类进行独立开发,互不干扰对方,不过显然这种方法不是很好,俩人风格不一致,我们在完成各自工作后又花了很长时间才将俩人的部分合成一个,并且其中还有不少BUG需要自己去修改,感觉工作量莫名的更多了,也算是长了个教训。
不过好处也是有的,比起一个人打码,俩个人在交流时更能激发灵感,在某些地方跟对方持不同看法后也能互相交流意见、弥补自身或对方的不足,而且有时自己卡个半天的问题,对方却能很快找到解决方法,所谓当局者迷旁观者清大概也就如此吧。
对于项目而言,目前还不够完善,在生成10000道题目时相比其他人,花费的时间要更长,后续需要优化里面使用关于计算与文件读写的操作代码才行。