1.项目Github地址
https://github.com/Vertigor/FourOperation
2.题目
(四则运算题目生成程序(基于控制台)){https://edu.cnblogs.com/campus/whu/2017ASE/homework/952}
3.估计花费时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | |
· Estimate | · 估计这个任务需要多少时间 | ||
Development | 开发 | 360 | |
· Analysis | · 需求分析 (包括学习新技术) | 30 | |
· Design Spec | · 生成设计文档 | 20 | |
· Design Review | · 设计复审 (和同事审核设计文档) | ||
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | ||
· Design | · 具体设计 | ||
· Coding | · 具体编码 | 230 | |
· Code Review | · 代码复审 | ||
· Test | · 测试(自我测试,修改代码,提交修改) | ||
Reporting | 报告 | 60 | |
· Test Report | · 测试报告 | ||
· Size Measurement | · 计算工作量 | ||
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | ||
合计 | 430 |
3.
4.解题思路
在拿到题目后,一开始想的是算法的实现,后来感觉算法是一定可以实现的,然而运行在什么样的环境,是至关重要的。本来想选用最近一直在用的JAVA,但是由于我不会JAVA的界面编程,最后还是选择了之前比较熟练的C#。又想到这个项目应该有很好的兼容性,网页似乎就是一个不错的选择。但后来又发现题目要求基于控制台,感觉自己白浪费这么多时间在界面上了。汗。。。最后还是用JAVA吧,电脑上的环境已经搭好了,不用重新搭C#的环境了。
Java是一门面向对象的语言,所以分析题目中的对象是很重要的。首先,要把界面、功能模块、控制分割开,采用MVC模式设计。其次,用户操作的对象是题目,将题目作为对象生成一个类(Question)。题目中要求可以进行真分数的运算,我的想法是把每个数字都当成是真分数来运算,整数相当于分母为1,所以我又生成一个类(Fractions)。光有对象没有操作不行的,我又声明一个类(Calculate),用来计算真分数的加减乘除。最后,生成一个(Control)类,用来处理与用户交互的图形界面,这样程序的框架就搭好了。
完成框架后,具体设计还需满足以下需求:
- 操作数必须随机生成。
- 运算符的种类和顺序必须随机生成。
- 可以判断用户输入的对错。
- 使用-n参数控制生成题目的个数。
- 支持带括号的多元复合运算。
- 运算符个数随机生成。
补充:考虑真分数可以约分成整数,所以Fractions中应该包含changeToInteger()函数,将结果保存成整数。括号的数目和位置也应该随机生成,满足数学约束,并且能够嵌套。
5.设计实现过程
总流程图
子流程图
Fractions用两个int变量分别表示分子分母,提供静态函数maxCommonDivisor(int,int)和minCommonMultiple(int, int),分别是求最大公约数函数和最小公倍数函数,还包含将可转化为整数的分数转化为整数的函数changeToInteger()。
Question采用两种数组保存操作数,分别是分数操作数和整数操作数,又创建两个括号数组,分别是左括号和右括号,专门的乘除运算符数组以及用于计算的两个堆栈。包括检查括号约束情况函数checkBracket()、计算函数calculate()、优先级比较函数compare(str: char)等。
Calculate包含四个静态函数,分别是加减乘除。Control包含main函数,从控制台读取到题目个数,与用户进行交互。
6.代码说明
代码说明已存在注释中。
6.1Question类如下:
1 package question;
2
3 import java.util.Random;
4 import java.util.Stack;
5
6 import fraction.Fractions;
7 import calculate.Calculate;
8
9 public class Question {
10 private Character[] operators;//操作符数组
11 private int[] operands;//操作数数组
12 private Fractions[] operands_fra;//操作数分数数组
13 private int operators_num;//运算符数目
14 private Fractions result;//计算结果
15 private Stack<Character> priStack;// 操作符栈
16 private Stack<Fractions> numStack;// 操作数栈
17 private int[] leftBracket;//左括号
18 private int[] rightBracket;//右括号
19 private int bracketNum;//括号数量
20 private String expression;//表达式字符串
21 public Question(int operators_num){
22 if(operators_num<1||operators_num>10){
23 System.out.println("Error:operators number error!");
24 return;
25 }
26 this.operators_num = operators_num;
27 this.operands = new int[operators_num+1];
28 this.operators = new Character[operators_num];
29 this.operands_fra = new Fractions[operators_num+1];
30 this.init();
31 }
32 //初始化各种参数
33 private void init(){
34 Random random=new Random();
35 if(operators_num==1)
36 bracketNum=0;
37 else
38 bracketNum=random.nextInt(operators_num/2+operators_num%2+1);
39 leftBracket = new int[operators_num];
40 rightBracket = new int[operators_num];
41 priStack = new Stack<Character>();
42 numStack = new Stack<Fractions>();
43 initBracketArray();
44 if(bracketNum>0){
45 for(int i=0;i<this.bracketNum;i++){
46 int pos = random.nextInt(operators_num);
47 leftBracket[pos]++;
48 rightBracket[random.nextInt(operators_num-pos)+pos]++;
49
50 }
51 checkBracket();
52 }
53 for(int i=0;i<this.operands.length;i++){
54 operands[i]=random.nextInt(100)+1;
55 }
56 for(int i=0;i<this.operands_fra.length;i++){
57 operands_fra[i]=new Fractions(operands[i],1);
58 }
59 for(int i=0;i<this.operators.length;i++){
60 switch(random.nextInt(4)){
61 case 0:
62 operators[i]='+';
63 break;
64 case 1:
65 operators[i]='-';
66 break;
67 case 2:
68 operators[i]='*';
69 break;
70 case 3:
71 operators[i]='/';
72 break;
73 }
74 }
75 this.setExpression(printQuestion());
76 this.calculate();
77 }
78 //初始化括号数组
79 private void initBracketArray(){
80 for(int i=0;i<this.operators_num;i++){
81 leftBracket[i]=0;
82 rightBracket[i]=0;
83 }
84 }
85 //检查括号是否满足约束,不满足删除括号
86 private boolean checkBracket(){
87 boolean flag = true;
88 int[] lb = leftBracket.clone();
89 int[] rb = rightBracket.clone();
90 for(int i=0;i<operators_num;i++){
91 int temp =i;
92 while(rb[i]>0){
93 for(int j=i;j>-1;j--){
94 while(lb[j]>0&&rb[i]>0){
95 lb[j]--;
96 rb[i]--;
97 if(temp-1==j||temp==j||(i==operators_num-1&&j==0)){
98 deleteBracket(j, i);
99 flag = false;
100 }
101 temp=j;
102 }
103 }
104 }
105 }
106 return flag;
107 }
108 //删除括号
109 private boolean deleteBracket(int lb,int rb){
110 if(leftBracket[lb]==0||rightBracket[rb]==0)
111 return false;
112 leftBracket[lb]--;
113 rightBracket[rb]--;
114 bracketNum--;
115 return true;
116 }
117 //打印表达式字符串
118 private String printQuestion(){
119 String str="";
120 for(int i=0;i<operators_num;i++){
121 for(int j=0;j<leftBracket[i];j++){
122 str+="(";
123 }
124 str+=operands[i];
125 if(i>0){
126 for(int j=0;j<rightBracket[i-1];j++){
127 str+=")";
128 }
129 }
130 str+=operators[i].toString();
131 }
132 str+=operands[operators_num];
133 if(bracketNum>0)
134 for(int j=0;j<rightBracket[operators_num-1];j++){
135 str+=")";
136 }
137 str+="=";
138 return str;
139 }
140 //计算表达式
141 private void calculate(){
142 numStack.push(operands_fra[0]);
143 int i=0;
144 int[] lb = leftBracket.clone();
145 int[] rb = rightBracket.clone();
146 while(i<operators_num){
147 while(lb[i]>0){
148 priStack.push('(');
149 lb[i]--;
150 }
151 if(i>0){
152 if(rb[i-1]>0){
153 char ope = priStack.pop();
154 if(ope=='(')
155 continue;
156 Fractions b = (Fractions) numStack.pop();// 第二个运算数
157 Fractions a = (Fractions) numStack.pop();// 第二个运算数
158 Fractions tempresult ;
159 switch (ope) {
160 // 如果是加号或者减号,则
161 case '+':
162 tempresult = Calculate.addtion(a, b);
163 numStack.push(tempresult);
164 break;
165 case '-':
166 tempresult = Calculate.subtraction(a, b);
167 numStack.push(tempresult);
168 break;
169 case '*':
170 tempresult = Calculate.multiplication(a, b);
171 numStack.push(tempresult);
172 break;
173 case '/':
174 tempresult = Calculate.division(a, b);
175 numStack.push(tempresult);
176 break;
177 }
178 rb[i-1]--;
179 }
180 }
181 if(!compare(operators[i])){
182 Fractions b = (Fractions) numStack.pop();// 第二个运算数
183 Fractions a = (Fractions) numStack.pop();// 第二个运算数
184 char ope = priStack.pop();
185 Fractions tempresult ;
186 switch (ope) {
187 // 如果是加号或者减号,则
188 case '+':
189 tempresult = Calculate.addtion(a, b);
190 numStack.push(tempresult);
191 break;
192 case '-':
193 tempresult = Calculate.subtraction(a, b);
194 numStack.push(tempresult);
195 break;
196 case '*':
197 tempresult = Calculate.multiplication(a, b);
198 numStack.push(tempresult);
199 break;
200 case '/':
201 tempresult = Calculate.division(a, b);
202 numStack.push(tempresult);
203 break;
204 }
205 }else{
206 priStack.push(operators[i]);
207 numStack.push(operands_fra[i+1]);
208 i++;
209 }
210 }
211 while(!priStack.isEmpty()){
212 char ope = priStack.pop();
213 if(ope=='(')
214 continue;
215 Fractions b = (Fractions) numStack.pop();// 第二个运算数
216 Fractions a = (Fractions) numStack.pop();// 第一个运算数
217 Fractions tempresult ;
218 switch (ope) {
219 // 如果是加号或者减号,则
220 case '+':
221 tempresult = Calculate.addtion(a, b);
222 numStack.push(tempresult);
223 break;
224 case '-':
225 tempresult = Calculate.subtraction(a, b);
226 numStack.push(tempresult);
227 break;
228 case '*':
229 tempresult = Calculate.multiplication(a, b);
230 numStack.push(tempresult);
231 break;
232 case '/':
233 tempresult = Calculate.division(a, b);
234 numStack.push(tempresult);
235 break;
236 }
237 }
238
239 result = numStack.pop();
240 }
241 private boolean compare(char str) {
242 if (priStack.empty()) {
243 // 当为空时,显然 当前优先级最低,返回高
244 return true;
245 }
246 char last = (char) priStack.lastElement();
247 // 如果栈顶为'('显然,优先级最低,')'不可能为栈顶。
248 if (last == '(') {
249 return true;
250 }
251 switch (str) {
252 case '=':
253 return false;// 结束符
254 case '(':
255 // '('优先级最高,显然返回true
256 return true;
257 case ')':
258 // ')'优先级最低,
259 return false;
260 case '*': {
261 // '*/'优先级只比'+-'高
262 if (last == '+' || last == '-')
263 return true;
264 else
265 return false;
266 }
267 case '/': {
268 if (last == '+' || last == '-')
269 return true;
270 else
271 return false;
272 }
273 // '+-'为最低,一直返回false
274 case '+':
275 return false;
276 case '-':
277 return false;
278 }
279 return true;
280 }
281 public Fractions getResult() {
282 return result;
283 }
284 public String getExpression() {
285 return expression;
286 }
287 private void setExpression(String expression) {
288 this.expression = expression;
289 }
290
291 }
Question
6.2Calculate类如下:
1 package calculate;
2
3 import fraction.Fractions;
4 public class Calculate {
5
6 public Calculate(){
7 }
8 // 加法计算
9 public static Fractions addtion(Fractions fractions1,Fractions fractions2)
10 {
11 int result_numerator,min; // 相加后的分子以及两分数分母的最小公倍数
12 min=Fractions.minCommonMultiple(fractions1.getDenominator(), fractions2.getDenominator());
13 result_numerator=(min/fractions1.getDenominator())*fractions1.getNumerator()+(min/fractions2.getDenominator())*fractions2.getNumerator();
14 Fractions result=new Fractions(result_numerator, min);
15 return result;
16 }
17 // 减法计算
18 public static Fractions subtraction(Fractions fractions1,Fractions fractions2)
19 {
20 int result_numerator,min; // 相减后的分子以及两分数分母的最小公倍数
21 min=Fractions.minCommonMultiple(fractions1.getDenominator(), fractions2.getDenominator());
22 result_numerator=(min/fractions1.getDenominator())*fractions1.getNumerator()-(min/fractions2.getDenominator())*fractions2.getNumerator();
23 Fractions result=new Fractions(result_numerator, min);
24 return result;
25 }
26 // 乘法计算
27 public static Fractions multiplication(Fractions fractions1,Fractions fractions2)
28 {
29 int result_numerator,result_denominator; // 相乘后的分子和分母
30 result_numerator=fractions1.getNumerator()*fractions2.getNumerator();
31 result_denominator=fractions1.getDenominator()*fractions2.getDenominator();
32 Fractions result=new Fractions(result_numerator, result_denominator);
33 return result;
34 }
35 // 除法计算
36 public static Fractions division(Fractions fractions1,Fractions fractions2)
37 {
38 int result_numerator,result_denominator; // 相除后的分子和分母
39 // 分数相除问题转换成分数相乘问题
40 result_numerator=fractions1.getNumerator()*fractions2.getDenominator();
41 result_denominator=fractions1.getDenominator()*fractions2.getNumerator();
42 Fractions result=new Fractions(result_numerator, result_denominator);
43 return result;
44 }
45 }
Calculate
6.3Control类如下:
package control;
import java.util.Random;
import java.util.Scanner;
import fraction.Fractions;
import question.Question;
public class Control {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("本次共有"+args[1]+"道题。");
if(args[0].equals("-n")){
Scanner scanner=new Scanner(System.in);
Integer num=new Integer(args[1]);
int correct=0;
String answer[]=new String[num];//用户输入的答案
Random random = new Random();
boolean judge[] = new boolean[num];//保存用户输入对错
Question[] questions = new Question[num];
for(int i=0;i<num;i++){
questions[i] = new Question(random.nextInt(10)+1);
System.out.print((i+1)+":"+questions[i].getExpression());
answer[i] = scanner.nextLine();
Fractions result = questions[i].getResult();
int result_int = result.changeToInteger();
if(!answer[i].equals("")){
if(result_int!=Integer.MAX_VALUE){
if(Integer.parseInt(answer[i])==result_int){
judge[i]=true;
System.out.println("正确!");
correct++;
}else{
judge[i]=false;
System.out.println("不正确!正确答案:"+result_int);
}
}else{
String splits[] = answer[i].split("/");
if(splits.length==2&&Integer.parseInt(splits[0])==result.getNumerator()&&Integer.parseInt(splits[1])==result.getDenominator()){
judge[i]=true;
System.out.println("正确!");
correct++;
}else{
judge[i]=false;
System.out.println("不正确!正确答案:"+result.printFraction());
}
}
}else{
judge[i]=false;
System.out.println("未回答!正确答案:"+result.printFraction());
}
}
double score = (double)correct/(double)num*100.00;
System.out.println("本次得分:"+score);
scanner.close();
}else{
System.out.println("命令有误");
}
}
}
Control
6.4Fractions类如下:
1 package fraction;
2
3 public class Fractions {
4 private int numerator; //分子
5 private int denominator; //分母
6 // 无参数构造器
7 public Fractions(){
8 }
9 //参数构造器
10 public Fractions(int numerator,int denominator){
11 this.setValue(numerator, denominator);
12 }
13 // 设置分子分母
14 public void setValue(int numerator,int denominator)
15 {
16 if(numerator==0){
17 this.numerator=0;
18 this.denominator=1;
19 return;
20 }
21 if(denominator==0){
22 System.out.println("Error:denominator equals zero!");
23 }
24 int temp=maxCommonDivisor(denominator, numerator); //temp为最大公约数
25 this.numerator=numerator/temp;
26 this.denominator=denominator/temp;
27 }
28 // 求最大公约数
29 public static int maxCommonDivisor(int d, int n)
30 {
31 if (d < n) {// 保证d>n,若d<n,则进行数据交换
32 int temp = d;
33 d = n;
34 n = temp;
35 }
36 while (d % n != 0) {// 在余数不能为0时,进行循环
37 int temp = d % n;
38 d = n;
39 n = temp;
40 }
41 return n;// 返回最大公约数
42 }
43 // 求最小公倍数
44 public static int minCommonMultiple(int m, int n) {
45 return m * n / maxCommonDivisor(m, n);
46 }
47 // 打印分数
48 public String printFraction()
49 {
50 return (this.numerator+"/"+this.denominator).toString();
51 }
52 // 获取分子
53 public int getNumerator()
54 {
55 return this.numerator;
56 }
57 // 获取分母
58 public int getDenominator()
59 {
60 return this.denominator;
61 }
62 //判断是否可以转化为整数
63 private boolean isInteger(){
64 if(this.denominator==1||this.denominator==-1)
65 return true;
66 else return false;
67 }
68 //转换为整数
69 public int changeToInteger(){
70 if(this.isInteger())
71 return this.getNumerator();
72 else
73 return Integer.MAX_VALUE;
74 }
75 }
Fractions
7.测试运行
7.1程序测试
程序截图如下:
由上图可知,程序界面显示没问题,满足需求,括号也满足了嵌套,数学约束,结果由人工手算也是正确的。当然,这张图只是程序跑一次的结果,这个程序我跑了50次左右,结果都是正确(其中出BUG的部分也被修补过了)。在运行中,一开始结果是不正确的,错误集中在calculate()函数中,栈的入栈出栈顺序有问题,编程的时候没发现,发现错误的时候很难找出来。在括号数学约束中,bug也是很多的,会出现(34)+23这样的表达式,这段代码我看了很多遍,一直都没发现错误,后来才发现是少减了1,(⊙﹏⊙)b这个bug我纠结了2个小时。
7.2单元测试
7.2.1Calculate功能测试
先展示单元测试的结果图:
在此次单元测试中,分别测试了Calculate类中加减乘除的运算,例如:测试加法函数,输入(1/2+1/2),对比结果是否为1。具体测试类如下:
1 package calculate;
2
3 import static org.junit.Assert.*;
4
5 import org.junit.Before;
6 import org.junit.Test;
7
8 import fraction.Fractions;
9
10 public class CalculateTest {
11
12 @Before
13 public void setUp() throws Exception {
14 }
15
16 @Test
17 public void testAddtion() {
18 Fractions result = Calculate.addtion(new Fractions(1,2), new Fractions(1,2));
19 assertEquals(1, result.getNumerator());
20 assertEquals(1, result.getDenominator());
21 }
22
23 @Test
24 public void testSubtraction() {
25 Fractions result = Calculate.subtraction(new Fractions(1,2), new Fractions(1,2));
26 assertEquals(0, result.getNumerator());
27 assertEquals(1, result.getDenominator());
28 }
29
30 @Test
31 public void testMultiplication() {
32 Fractions result = Calculate.multiplication(new Fractions(1,2), new Fractions(1,2));
33 assertEquals(1, result.getNumerator());
34 assertEquals(4, result.getDenominator());
35 }
36
37 @Test
38 public void testDivision() {
39 Fractions result = Calculate.division(new Fractions(1,2), new Fractions(1,2));
40 assertEquals(1, result.getNumerator());
41 assertEquals(1, result.getDenominator());
42 }
43
44 }
7.2.2Fractions功能测试
Fractions类的测试结果如图:
在Fractions中主要测试的是最大公约数,最小公倍数函数,分数转整数,打印分数,设置分数值。代码如下:
1 package fraction;
2
3 import static org.junit.Assert.*;
4
5 import org.junit.Before;
6 import org.junit.Test;
7
8 public class FractionsTest {
9
10 private Fractions fraction;
11
12 @Before
13 public void setUp() throws Exception {
14 fraction = new Fractions(1,1);
15 }
16
17 @Test
18 public void testSetValue() {
19 fraction.setValue(2, 3);
20 assertEquals(2, fraction.getNumerator());
21 assertEquals(3, fraction.getDenominator());
22 }
23
24 @Test
25 public void testMaxCommonDivisor() {
26 int divisor = fraction.maxCommonDivisor(9, 6);
27 assertEquals(3, divisor);
28 }
29
30 @Test
31 public void testMinCommonMultiple() {
32 int multiple = fraction.minCommonMultiple(9, 6);
33 assertEquals(18, multiple);
34 }
35
36 @Test
37 public void testPrintFraction() {
38 assertEquals("1/1", fraction.printFraction());
39 }
40
41 @Test
42 public void testChangeToInteger() {
43 assertEquals(1, fraction.changeToInteger());
44 }
45
46 }
7.3代码覆盖测试
代码覆盖测试使用的是EclEmma插件,运行结果截图如下:
程序覆盖率在82%,其中有一些是判断失败的语句以及一些表达式并没有生成括号,所以一些代码没有运行到。
8.实际花费的时间
600 | |||
420 | |||
330 | |||
990 |
9.项目小结
此次项目帮助我深入了解了PSP的工作流程,从计划、开发到测试,每一步都亲身体验,从中学到了许多,掌握了很多Eclipse的插件,比如:Eclipse自带的Git插件、GEF插件、单元测试等。我深刻体会到软件工程的系统性和复杂性,正如:
软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。它涉及程序设计语言、数据库、软件开发工具、系统平台、标准、设计模式等方面。
在现代社会中,软件应用于多个方面。典型的软件有电子邮件、嵌入式系统、人机界面、办公套件、操作系统、编译器、数据库、游戏等。同时,各个行业几乎都有计算机软件的应用,如工业、农业、银行、航空、政府部门等。这些应用促进了经济和社会的发展,也提高了工作效率和生活效率 。
由于一开始没有单元测试的概念,导致单元测试都是在完成整个软件的时候才开始的。单元测试应该在完成每一个功能模块的时候就进行,这样才能保证每一个功能的正确性。并且在更改和更新的时候还需要做回归测试,保证原有功能正常,不受新功能的影响。