结对项目-自动生成四则运算题目程序

项目参与者:

林钦发  3118005061      唐炫韬  3118005069 

项目github地址

https://github.com/Linqinf/fourOperations

 

项目相关要求

   1. 控制生成题目的个数

  1. 控制题目中数值(自然数、真分数和真分数分母)的范围
  1. 生成的题目中计算过程不能产生负数
  2. 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
  3. 每道题目中出现的运算符个数不超过3个。
  4. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
  5.  生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
    1. 四则运算题目
    2. 四则运算题目

   8. 其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。

   9.在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:

  1. 答案1
  2. 答案2

   10. 真分数运算后仍为真分数

   11.程序应能支持一万道题目的生成。

   12.程序支持对给定的题目文件和答案文件

    统计结果输出到文件Grade.txt,格式如下:

    Correct: 5 (1, 3, 5, 7, 9)

    Wrong: 5 (2, 4, 6, 8, 10)

    其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。


 

PSP

PSP2.1Personal 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等工具,可提高队员间的代码协作效率,有利于分工合作。

 

posted @ 2020-03-31 22:39  Rabbit-Bear  阅读(231)  评论(0编辑  收藏  举报