结对项目-自动生成四则运算题目程序
项目参与者:
林钦发 3118005061 唐炫韬 3118005069
项目github地址
https://github.com/Linqinf/fourOperations
项目相关要求
1. 控制生成题目的个数
- 控制题目中数值(自然数、真分数和真分数分母)的范围
- 生成的题目中计算过程不能产生负数
- 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
- 每道题目中出现的运算符个数不超过3个。
- 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
- 生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
- 四则运算题目
- 四则运算题目
8. 其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
9.在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
- 答案1
- 答案2
10. 真分数运算后仍为真分数
11.程序应能支持一万道题目的生成。
12.程序支持对给定的题目文件和答案文件
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 1920 | 2271 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 100 | 60 |
· Design Spec | · 生成设计文档 | 45 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 15 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 30 |
· Design | · 具体设计 | 100 | 300 |
· Coding | · 具体编码 | 1200 | 1350 |
· Code Review | · 代码复审 | 60 | 46 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 340 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 50 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 45 |
合计 | 1920 | 2271 |
效能分析
实例化对象分析图
方法调用耗时图
测试生成10000条题目和答案
改进前
经分析耗时最长的部分在TitleFactory类中的generateAllTitle( )的
即加入hashset的查重操作
经分析,Title的hashcode()方法存在问题,所有对象的hashcode都是一样,即所有对象都对之前的每一个对象进行equals(),导致了效能低下
改进方法:Title的hashcode返回是答案的hashcode,若答案不一样,即可判断两式子一定不一样,不用进行与之前的每一个对象equals(),可优化性能
改进后:时间缩短约5/6
设计实现过程
设计路线:
项目成品结构图:
代码说明
- Title类。在TitleFactory类中运用HashSet去重,每生成一个Title对象,add进HashSet,若返回false表明,插入失败存在重复式子。HashSet的add方法,先根据对象的hashcode()的返回值是否一样,在根据equals()返回的是否为true判断是否重复。Title中hashcode()先返回Title中answer属性的hashcode,再在equals()中遍历每一个运算数是否一样。即先判断两式子答案是否一样,在判断运算数是否一样,来达到去重的效果。
1 package com.title; 2 3 4 5 import java.util.ArrayList; 6 7 public class Title { 8 private String question; 9 private String answer; 10 private ArrayList<String> value; 11 public Title(String question,String answer,ArrayList<String> value){ 12 this.question = question; 13 this.answer = answer; 14 this.value = value; 15 } 16 17 public String getQuestion() { 18 return question; 19 } 20 21 public String getAnswer() { 22 return answer; 23 } 24 25 public ArrayList<String> getValue() { 26 return value; 27 } 28 29 @Override 30 public String toString() { 31 return question+" = "+answer; 32 } 33 //重写Question类中的两个方法 equals hashCode 34 //想要将Question对象存入HashSet集合内 让set集合帮我们去掉重复元素 35 @Override 36 public int hashCode(){ //默认hashcode一样 37 return answer.hashCode(); 38 } 39 @Override 40 public boolean equals(Object obj){//比较两个对象是否一致 41 if(answer==null||obj==null){ 42 return false; 43 } 44 if(this==obj){ //地址一样 45 return true; 46 } 47 if(obj instanceof Title){ //类型一样,都是question 48 Title other = (Title) obj; 49 if(!answer.equals(other.answer))//先检查答案是否一致 50 return false; 51 if(value.size()!=other.value.size()) 52 return false; 53 for(int i=0;i<other.value.size();i++){ 54 if(!value.contains(other.value.get(i))){ 55 return false; 56 } 57 } 58 return true; 59 } 60 return false; 61 } 62 }
- 生成题目类。数跟运算符都是随机生成,其次检测除号能否变成分数,最后在两个数之间随机生成一个括号,最后将整条式子拼接起来。
1 public class QuestionCreator { 2 Random r = new Random(); 3 private ArrayList<String> value = null; 4 public String CreateQuestion(int bound){ 5 value = new ArrayList<>(); 6 char []operator = new char[3]; 7 String []Number = new String[4]; 8 int operatorNum = r.nextInt(3)+1;//运算符数量, 不超过3个 9 int divideNum = 0; 10 11 for(int i = 0;i < operatorNum;i++){ //根据运算符数量来循环拼接 12 int temp = r.nextInt(4); 13 if(temp == 0) 14 operator[i] = '+'; 15 else if (temp == 1) 16 operator[i] = '-'; 17 else if (temp == 2){ 18 divideNum++; 19 operator[i] = '÷'; 20 } 21 else if (temp == 3) 22 operator[i] = '*'; 23 } 24 for(int i = 0;i < operatorNum+1 ; i++){ 25 26 Number[i] = " " + r.nextInt(bound) + " "; 27 } 28 if(divideNum >= 1 && operatorNum > 1) { //出现除号,要检查 29 int flag = 0; 30 for (int i = 0; i < operatorNum; i++) { //遍历所有的运算符,找到第一个除号 31 if ((operator[i] == '÷' && flag == 0) || (i == 2 && operator[2] == '÷' && flag == 1)) { 32 int Num1 = Integer.parseInt(Number[i].trim()); 33 int Num2 = Integer.parseInt(Number[i + 1].trim()); 34 if (Num2 != 0) { //除数不能为0 35 if (Num1 > Num2) { //出现了假分数,要转化 36 if (Num1 % Num2 == 0) 37 Number[i] = " " + Num1 / Num2; 38 else 39 Number[i] = " " + Num1 / Num2 + "'" + Num1 % Num2 + "/" + Num2; 40 } else { //真分数 41 Number[i] = " " + Num1 + "/" + Num2; 42 } 43 operator[i] = '\0'; 44 Number[i + 1] = ""; 45 } 46 47 if (i == 1) //如果是第二个运算符为除号,后边不检查第三个运算符的情况了。 48 flag = 2; 49 else 50 flag++; //若第一次变为1,还要检查第三个运算符除号的情况 51 } 52 } 53 } 54 //构建运算数组 55 for(int i=0;i<Number.length;i++){ 56 if(Number[i]!=""&&Number[i]!=null){ 57 value.add(Number[i].trim()); 58 } 59 } 60 //拼接运算式 61 String result = null; 62 String leftBrcket = " ("; 63 String rightBrcket = ") "; 64 if((operator[1]=='+' || operator[1]=='-') && operator[0] != '\0' && r.nextBoolean()== true) { 65 if(Number[3] == "" ) rightBrcket =" )"; 66 if(Number[3] == null) Number[3] = ""; 67 result = Number[0] + operator[0] + leftBrcket + Number[1] + //在式子中间(第二个运算符)加括号 68 operator[1] + Number[2] + rightBrcket + operator[2] + Number[3]; 69 } 70 else {//无括号 71 if ((operator[0] == '+' || operator[0] == '-') && operator[1] != '\0' && r.nextBoolean() == true) 72 result = leftBrcket + Number[0] + operator[0] + Number[1] + rightBrcket; //在最开始加括号 73 else result = Number[0] + operator[0] + Number[1]; //无括号 74 75 if ((operator[2] == '+' || operator[2] == '-') && operator[1] != '\0' && r.nextBoolean() == true) { 76 result += operator[1] + leftBrcket + Number[2] + operator[2] + Number[3] + rightBrcket; //在最后加括号 77 } else { 78 if (operatorNum > 1) result += operator[1] + Number[2]; //无括号 79 if (operatorNum == 3) result += operator[2] + Number[3]; //无括号 80 } 81 } 82 return result.replaceAll("\0"," ").trim(); 83 } 84 85 public ArrayList<String> getValue() { 86 return value; 87 } 88 }
- 计算器类。先检测该式子中是否有括号,有括号的将括号内的子表达式提取出来作为参数再次调用RUN函数进行计算,若无括号时,定位运算符,先定位*÷后,后定位+-。若运算数为分数,两数转化为同分母的分数,在进行加减,最后在进行分数的约简,成为最简分数作为答案输出。若运算数为整数则无须做约简分数。
1 public class Calculator { 2 private boolean Lawful = true;//缓存当前计算合法性 3 OperatorSearcher searcher = new OperatorSearcher(); 4 public String calculate(String title){ 5 6 String result = SimplifyFraction(Run(title)); 7 if(result!=null) 8 result = result.trim(); 9 return result; 10 } 11 public String Run(String title){ 12 Lawful = true; 13 while ((title.contains("*")||title.contains("÷")|| 14 title.contains("+")||title.contains("-"))&&Lawful){//存在运算符且当前运算仍合法 15 int index = -1,start = 0,end = 0;//记录运算符位标,括号的开始与结束位标 16 String[] Message = title.split(" ");//分割字符串 17 //检测当前表达式中各元素的合法性 18 checkExpressionLawful(Message); 19 String temp = null;//将结果替换子表达式 20 //记录括号的开始与结束位标 21 start = Search(Message,"("); 22 end = Search(Message,")"); 23 if(start!=-1&&end!=-1){//存在括号 24 StringBuilder Subexpression = new StringBuilder();//括号内表达式 25 for(int i=start+1;i<end;i++) {//构造子表达式 26 Subexpression.append(Message[i]).append(" "); 27 } 28 //重新构造题干信息 29 StringBuilder titleBuilder = new StringBuilder(); 30 for(int i = 0; i<start; i++) { 31 titleBuilder.append(Message[i]).append(" "); 32 } 33 title = titleBuilder.toString(); 34 title += Run(Subexpression.toString()); 35 StringBuilder titleBuilder1 = new StringBuilder(title); 36 for(int i = end+1; i<Message.length; i++) { 37 titleBuilder1.append(Message[i]).append(" "); 38 } 39 title = titleBuilder1.toString(); 40 continue; 41 } 42 //定位运算符,先*、÷后+、- 43 index = Search(Message,"*|÷"); 44 if(index == -1){//式子内没有乘除时 45 index = Search(Message,"+|-"); 46 } 47 48 if(index==-1){//不存在运算符时,运算结束,跳出循环 49 break; 50 } 51 String[] firstNum = transformFraction(Message[index-1]).split("/"); 52 String[] secondNum = transformFraction(Message[index+1]).split("/"); 53 54 switch (Message[index]) { 55 case "+": //加法操作 56 if (!Message[index - 1].contains("/") && !Message[index + 1].contains("/"))//不存在分数,直接相加 57 temp = Integer.parseInt(Message[index - 1]) + Integer.parseInt(Message[index + 1]) + ""; 58 else if (Message[index - 1].contains("/") && Message[index + 1].contains("/")) {//两个都为分数,化同分母,再相加 59 60 int Denominator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[1]); 61 int numerator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[0]) 62 + Integer.parseInt(secondNum[1]) * Integer.parseInt(firstNum[0]); 63 temp = numerator + "/" + Denominator; 64 } else {//只有一个分数,整数化为分数相加 65 int Denominator, numerator; 66 if (Message[index - 1].contains("/")) {//第一个操作数为分数 67 Denominator = Integer.parseInt(firstNum[1]); 68 numerator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[0]) + Integer.parseInt(firstNum[0]); 69 } else {//第二个操作数为分数 70 Denominator = Integer.parseInt(secondNum[1]); 71 numerator = Integer.parseInt(secondNum[1]) * Integer.parseInt(firstNum[0]) + Integer.parseInt(secondNum[0]); 72 } 73 temp = numerator + "/" + Denominator; 74 } 75 break; 76 case "-": //减法操作 77 78 if (!Message[index - 1].contains("/") && !Message[index + 1].contains("/"))//不存在分数 79 temp = Integer.parseInt(Message[index - 1]) - Integer.parseInt(Message[index + 1]) + ""; 80 else if (Message[index - 1].contains("/") && Message[index + 1].contains("/")) {//两个都为分数,化同分母,再相减 81 82 int Denominator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[1]); 83 int numerator = Integer.parseInt(firstNum[0]) * Integer.parseInt(secondNum[1]) 84 - Integer.parseInt(secondNum[0]) * Integer.parseInt(firstNum[1]); 85 temp = numerator + "/" + Denominator; 86 } else {//只有一个分数,整数化为分数相减 87 int Denominator, numerator; 88 if (Message[index - 1].contains("/")) {//第一个操作数是分数 89 Denominator = Integer.parseInt(firstNum[1]); 90 numerator = Integer.parseInt(firstNum[0]) - Integer.parseInt(secondNum[0]) * Denominator; 91 92 } else {//第二个操作数是分数 93 Denominator = Integer.parseInt(secondNum[1]); 94 numerator = Integer.parseInt(firstNum[0]) * Denominator - Integer.parseInt(secondNum[0]); 95 } 96 temp = numerator + "/" + Denominator; 97 } 98 break; 99 case "*": //乘法操作 100 if (!Message[index - 1].contains("/") && !Message[index + 1].contains("/")) {//不存在分数,直接相乘 101 temp = Integer.parseInt(Message[index - 1]) * Integer.parseInt(Message[index + 1]) + ""; 102 103 } else if (Message[index - 1].contains("/") && Message[index + 1].contains("/")) {//两个都为分数,分子分母相乘 104 105 int Denominator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[1]); 106 int numerator = Integer.parseInt(firstNum[0]) * Integer.parseInt(secondNum[0]); 107 temp = numerator + "/" + Denominator; 108 } else {//只有一个分数,整数乘以分子作为新分子,分母不变 109 int Denominator, numerator; 110 if (Message[index - 1].contains("/")) {//第一个操作数是分数 111 Denominator = Integer.parseInt(firstNum[1]); 112 } else {//第二个操作数是分数 113 Denominator = Integer.parseInt(secondNum[1]); 114 } 115 numerator = Integer.parseInt(firstNum[0]) * Integer.parseInt(secondNum[0]); 116 117 temp = numerator + "/" + Denominator; 118 } 119 break; 120 case "÷": //除法操作 121 if (!Message[index - 1].contains("/") && !Message[index + 1].contains("/"))//不存在分数,直接构造分数 122 if (Message[index - 1].equals("0")) 123 temp = "0"; 124 else 125 temp = Message[index - 1] + "/" + Message[index + 1]; 126 else if (Message[index - 1].contains("/") && Message[index + 1].contains("/")) {//两个都为分数,分子,分母交叉相乘 127 128 int Denominator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[0]); 129 int numerator = Integer.parseInt(firstNum[0]) * Integer.parseInt(secondNum[1]); 130 temp = numerator + "/" + Denominator; 131 } else {//只有一个分数 132 int Denominator, numerator; 133 if (Message[index - 1].contains("/")) {//第一个操作数是分数 134 Denominator = Integer.parseInt(firstNum[1]) * Integer.parseInt(secondNum[0]); 135 numerator = Integer.parseInt(firstNum[0]); 136 } else {//第二个操作数是分数 137 Denominator = Integer.parseInt(secondNum[0]); 138 numerator = Integer.parseInt(firstNum[0]) * Integer.parseInt(secondNum[1]); 139 } 140 temp = numerator + "/" + Denominator; 141 } 142 break; 143 } 144 //构造新题干 145 StringBuilder titleBuilder = new StringBuilder(); 146 for(int i = 0; i<index-1; i++) { 147 titleBuilder.append(Message[i]).append(" "); 148 } 149 title = titleBuilder.toString(); 150 title += temp+" "; 151 StringBuilder titleBuilder1 = new StringBuilder(title); 152 for(int i = index+2; i<Message.length; i++) { 153 titleBuilder1.append(Message[i]).append(" "); 154 } 155 title = titleBuilder1.toString(); 156 } 157 if(Lawful) 158 return title; 159 else 160 return null; 161 } 162 163 private int Search(String[] Msg,String tag){ 164 return searcher.searchOperator(Msg, tag); 165 } 166 private String transformFraction(String Fraction){//将真分数转化成假分数 167 Fraction = Fraction.trim(); 168 if(!Fraction.contains("'"))//不是真分数 169 return Fraction; 170 String[] split = Fraction.split("['/]"); 171 //分子,分母操作 172 int Denominator = Integer.parseInt(split[2]); 173 int numerator = Denominator*Integer.parseInt(split[0])+Integer.parseInt(split[1]); 174 return numerator+"/"+Denominator; 175 } 176 private String SimplifyFraction(String Fraction) {//将分数化简:假分数转化成真分数,分数的约分 177 if(!Lawful){ 178 return null; 179 } 180 if (Fraction.contains("(")) 181 Fraction = Fraction.replaceAll("\\(|\\)",""); 182 Fraction = Fraction.trim(); 183 int count = 0;//记录进位 184 String[] digit = null;//储存分数中每一个数字 185 int Denominator = 0 ;//分母 186 int numerator = 0 ;//分子 187 if(!Fraction.contains("/"))//不存在分数,无需化简,直接返回 188 return Fraction; 189 if(Fraction.contains("'")){ 190 count = Integer.parseInt(Fraction.split("'")[0]); 191 Fraction = Fraction.split("'")[1]; 192 } 193 digit = Fraction.trim().split("/"); 194 //生成分子,分母 195 numerator = Integer.parseInt(digit[0]); 196 Denominator = Integer.parseInt(digit[1]); 197 //分子为0 198 if(numerator==0){ 199 return "0"; 200 } 201 //分数进位操作 202 if(numerator>=Denominator){//分子大于或等于分母 203 if (Denominator == 0) { 204 Lawful = false; 205 return null; 206 } 207 count += numerator/Denominator; 208 numerator = numerator%Denominator; 209 } 210 //分数约简操作,将分子分母的因子加入hashset中,若插入不成功即该数为公约数 211 while (true) { 212 boolean exitCommon = false;//记录是否存在公约是 213 HashSet<Integer> set = new HashSet<>();//记录所有公约数 214 //加入其本身 215 for(int i=2;i<=Math.sqrt(numerator);i++){//构造分子所有公约数 216 if(numerator%i==0){ 217 set.add(i); 218 } 219 } 220 set.add(numerator); 221 for(int i=2;i<=Math.sqrt(Denominator);i++){ 222 if(Denominator%i==0){ 223 if(!set.add(i)){//出现相同公约数 224 numerator /= i; 225 Denominator /= i; 226 exitCommon = true; 227 break; 228 } 229 } 230 } 231 if (!exitCommon) {//若不出现公约数,即判定为最简分数 232 break; 233 } 234 } 235 //构造分数字符串 236 Fraction = ""; 237 if(count!=0){//进位不为0 238 Fraction += count+"'"; 239 } 240 if(numerator!=0)//分子不为0 241 Fraction += +numerator+"/"+Denominator; 242 else {//分子为0,去除符号‘'’ 243 Fraction = Fraction.replaceAll("'",""); 244 } 245 return Fraction; 246 } 247 private void checkExpressionLawful(String[] Message){//添加合法判断条件 248 //判断是否是负数 249 for (String s : Message) { 250 if (s.length() > 1 && s.contains("-")) { 251 Lawful = false; 252 break; 253 } 254 } 255 256 } 257 public boolean isLawful(){ 258 return Lawful; 259 } 260 }
测试运行
界面显示:
测试用例
点击
自动生成文件
稍后即进入考试界面
点击转换页数,显示新的内容
点击即可提交答案
点击即可重考
点击选择题目文件进行考试
点击
进入选择文件界面
选择文件后
选择的文件内容如下
点击
项目小结
1.该项目符合基本工厂模式,单一职责设计原则,项目的重构需要大量的时间。项目设计之初应当把架构设计好,再进行编码,可大大提高开发效率。合理的架构,可提高项目的聚合性,降低项目的耦合性,给多人开发带来便利。
2.熟悉GitHub等工具,可提高队员间的代码协作效率,有利于分工合作。