项目Github仓库链接
这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 个人项目 |
这个作业的目标 | 实现一个自动生成简单四则运算题目并进行计算的程序,同时提供核对答案是否正确的功能 |
一、PSP表格
PSP2.1 | Personal Software Process Stages |
预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要 多少时间 |
800 | 1200 |
Development | 开发 | 300 | 240 |
Analysis | 需求分析 (包括学习新技术) |
240 | 300 |
Design Spec | 生成设计文档 | 60 | 60 |
Design Review | 设计复审 | 60 | 60 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) |
60 | 60 |
Design | 具体设计 | 240 | 360 |
Coding | 具体编码 | 240 | 360 |
Code Review | 代码复审 | 60 | 60 |
Test | 测试(自我测试, 修改代码,提交修改) |
120 | 60 |
Reporting | 报告 | 60 | 60 |
Test Repor | 测试报告 | 60 | 30 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan |
事后总结, 并提出过程改进计划 |
60 | 30 |
合计 | 1650 | 1770 |
二、计算模块接口的设计与实现过程
模块接口
Fraction类是分数类,包括分子和分母两个数据类型,以及分数相加、相减、相乘、化简和求分子分母最大公约数的函数。Calculator类
继承自Fraction类,两个主要函数为integerCalculator和fractionCalCaltor,分别实现整数运算和分数运算。OPerationsCreate类以中缀表达式的
形式生成一个符合该项目要求的四则运算算式。PostfixExpression类可以将OPerationsCreate类中生成的算式转换成便于程序计算的后缀表达式。而
txtOperation类实现文件的读取与写入,同时可以匹配一组题目文件和答案文件,检查答案文件中各个算式的答案的正确与否。
关键函数
整数计算
点击查看代码
public static int integerCalculator(List<String> equation){//整数计算
Stack<String> s1=new Stack<>();
int result=0;
for (String s : equation) {
if (!PostfixExpression.isOperator(s)) {
s1.push(s);//逐个扫描字符,若为数字则直接入栈
} else {//若为符号则弹出两个数字与当前符号进行运算,并将结果入栈
result = Calculate(s1.pop(), s1.pop(), s);
s1.push(String.valueOf(result));
}
}
return result;
private static int Calculate(String a,String b,String c){//整数计算
int result=0;
int ai=Integer.parseInt(a);
int bi=Integer.parseInt(b);
switch(c) {//根据传入的符号进行相应的计算
case "+" -> result = ai + bi;
case "-" -> result = bi - ai;//对于减法,应考虑两个数字入栈的顺序
case "*" -> result = ai * bi;
}
return result;
}
分数计算
点击查看代码
public static String fractionCalculator(List<String> equation){//分数计算
for(int j=0;j<2;j++){
if(equation.contains("'")){//判断分数形式,若为带"'"的真分数形式则化简为假分数形式
int temp=equation.indexOf("'");//返回"'"所在的下标
int t1=Integer.parseInt(equation.get(temp-1));//获取"'"前后的数字
int t2=Integer.parseInt(equation.get(temp+1));
int t3=Integer.parseInt(equation.get(temp+2));
int t4=t1*t3+t2;//将"'"前的数字乘以分母后与分子相加
equation.remove(temp+1); equation.remove(temp);
equation.set(temp-1,String.valueOf(t4));
}
}
int i=0;
String n1= equation.get(i++);//提取两个分数的分子与分母
String d1= equation.get(i++);
i++;//跳过符号
String n2= equation.get(i++);
String d2= equation.get(i++);
i++;
return fractionCalculate(d1,n1,d2,n2,equation.get(i));
}
private static String fractionCalculate(String a1,String b1,String a2,String b2,String c){//分数具体计算
Fraction f1=fractionCreate(a1,b1);//根据传入的分子分母数据构造分数
Fraction f2=fractionCreate(a2,b2);
switch (c){//根据符号选择对应的计算
case "+"->f1= f1.addFraction(f2);
case "-"->f1=f1.subFraction(f2);
case "*"->f1=f1.multFraction(f2);
}
return f1.numerator+"/"+f1.denominator;//以分数形式返回结果
}
中缀表达式转后缀表达式
点击查看代码
public static List<String> postfixChange(String fix){//中缀表达式转后缀表达式
Stack<String> s1=new Stack<>();
List<String> s2=new ArrayList<>();
List<String> infix=toList(fix);
int size=infix.size();
int priority;
int i=0;
while(i<size){
if(isOperator(infix.get(i))){//如果该元素不是数字
if(infix.get(i).compareTo("(")==0){//左括号无条件入栈s1
s1.push(infix.get(i));
i++;
continue;
}
else if(infix.get(i).compareTo(")")==0){
while(s1.peek().compareTo("(")!=0){//当碰见右括号时依次让s1栈顶的元素出栈并加入到s2,直到栈顶元素为左括号
s2.add(s1.pop());
}
if(Objects.equals(s1.peek(), "("))//左括号出栈
s1.pop();
i++;
continue;
}
priority=getPriority(infix.get(i));//若不是左括号,则计算该符号的优先级
if(s1.isEmpty() || priority > getPriority(s1.peek())){//若栈空或优先级高于栈顶元素优先级,则直接入栈
s1.push(infix.get(i));
}
else{
while (!s1.isEmpty() && priority <= getPriority(s1.peek()))
{
s2.add(s1.pop());//若优先级小于栈顶则栈顶出栈并加入s2,直到优先级大于栈顶或栈空
}
s1.push(infix.get(i));
}
}
else{
s2.add(infix.get(i));//若为数字则加入s2
}
i++;
}
while (!s1.isEmpty()) {//将s1剩余元素加入到s2
s2.add(s1.pop());
}
return s2;
}
算式生成
点击查看代码
public static List<String> operationsCreate(int n, int r){
Random random=new Random();
List<String> op=new ArrayList<>();
int t;
String operation;
String[] symbol ={" "," "," "};//一个算式最多包含三个符号
for(int i=0;i<n;i++){
t= random.nextInt(4);//根据随机数的结果选择生成何种算式
if(t<2){//随机数结果小于2,生成只包含整数的算式
for(int j=0;j<3;j++){
t= random.nextInt(3);//根据随机数结果生成符号
if(t==0)
symbol[j]="+";
else if(t==1)
symbol[j]="-";
else
symbol[j]="*";
}
t=random.nextInt(5);//根据随机数结果生成某种形式的算式
if(t==0)
//生成包含两个数字一个符号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1);
else if(t==1)
//生成包含三个数字两个符号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " " + symbol[1] + " " + (random.nextInt(r)+1);
else if(t==2)
//生成包含四个数字三个符号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " " + symbol[1] + " " + (random.nextInt(r)+1) + " " +
symbol[2] + " " + (random.nextInt(r)+1);
else if(t==3)
//生成包含四个数字三个符号且后两个数字带有括号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " " + symbol[1] + " " + "( " + (random.nextInt(r)+1) + " " +
symbol[2] + " " + (random.nextInt(r)+1) + " )";
else
//生成包含四个数字三个符号且前两个数字带有括号的算式
operation = "( " + (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " ) " + symbol[1] + " " + (random.nextInt(r)+1) + " " +
symbol[2] + " " + (random.nextInt(r)+1);
}
else{//生成只包含分数的算式
symbol[0]="/"; symbol[2]="/";
t=random.nextInt(3);
switch (t){
case 0->symbol[1]="+";
case 1->symbol[1]="-";
case 2->symbol[1]="*";
}
t=random.nextInt(3);
if(t>0)
//生成包含两个分数一个符号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " " + symbol[1] + " " + (random.nextInt(r)+1) + " " +
symbol[2] + " " + (random.nextInt(r)+1);
else
//生成包含"'"、两个分数和一个符号的算式
operation = (random.nextInt(r)+1) + " " + symbol[0] + " " +
(random.nextInt(r)+1) + " " + symbol[1] + " " + (random.nextInt(r)+1) + " ' " +
(random.nextInt(r)+1)+ " " + symbol[2] + " " + (random.nextInt(r)+1);
}
op.add(operation);
}
return op;
}
效能分析
总览
分析
由于本项目生成的算式、计算出的答案等数据均以字符串的形式保存,并以List的形式管理,所以在内存开销上byte类型和String类占用的最多。
设计之初计划将数字与运算符号分开存储,但实现起来较为复杂,且需要来回读取不同数据类型的List表,不能做到一次读取和一次写入,所以重新考虑,将整
个算数都以字符串的形式保存,另写一个函数用来辨别一个元素是否为数字。
单元测试
1.txtOperation类测试模块
通过打开正确文件、错误格式文件、错误路径文件的方式测试能否正常读写文件;通过对手动输入指定文档测试文件的读取与写入功能。
点击查看代码
public class txtOperationTest {
@Test
public void read() throws IOException {
List<String> t1= txtOperation.Read("F:\\program\\c\\software\\txt\\1.txt");
System.out.println(t1);
//读取不存在的文件
List<String> t2=txtOperation.Read("F:\\program\\c\\software\\txt\\111.txt");
//读取错误格式的文件
List<String> t3=txtOperation.Read("F:\\program\\c\\software\\txt\\");
}
@Test
public void writeOperations() throws IOException {
List<String> op= List.of(new String[]{"1", "2", "3"});
txtOperation.writeOperations(op);
}
@Test
public void writeAnswers() throws IOException {
List<String> answer= List.of(new String[]{"4", "5", "6"});
txtOperation.writeOperations(answer);
}
@Test
public void answersMatch() throws IOException {
List<String> answer= List.of(new String[]{"1. 4", "2. 5", "3. 6"});
txtOperation.answersMatch(answer,"F:\\program\\Java\\ideaWorkplace\\Operations\\1.txt");
}
2.Fraction类测试模块
通过手动创建两个分数并让两个分数相互计算,检查结果是否正确。
点击查看代码
public class FractionTest {
@Test
public void addFraction() {
Fraction f1=new Fraction(5,1);
Fraction f2=new Fraction(5,2);
f1=f1.addFraction(f2);
System.out.println(f1.numerator + "/" + f1.denominator);
}
@Test
public void subFraction() {
Fraction f1=new Fraction(5,2);
Fraction f2=new Fraction(5,4);
f1=f1.subFraction(f2);
System.out.println(f1.numerator + "/" + f1.denominator);
}
@Test
public void multFraction() {
Fraction f1=new Fraction(5,1);
Fraction f2=new Fraction(3,2);
f1=f1.multFraction(f2);
System.out.println(f1.numerator + "/" + f1.denominator);
}
}
3.###Calculator类测试
计算手动输入的后缀表达式,并检查结果是否正确。
点击查看代码
public class CalculatorTest {
@Test
public void integerCalculator() {
List<String> op1= List.of(new String[]{"1", "2", "9", "*", "+"});
List<String> op2= List.of(new String[]{"1", "2", "+", "9", "*"});
List<String> op3= List.of(new String[]{"20", "9", "2", "-", "+"});
System.out.println(Calculator.integerCalculator(op1));
System.out.println(Calculator.integerCalculator(op2));
System.out.println(Calculator.integerCalculator(op3));
}
@Test
public void fractionCalculator() {
List<String> op1= List.of(new String[]{"1", "5", "/", "3", "5", "/", "+"});
List<String> op2= List.of(new String[]{"4", "5", "/", "3", "5", "/", "-"});
List<String> op3= List.of(new String[]{"1", "5", "/", "3", "5", "/", "*"});
System.out.println(Calculator.fractionCalculator(op1));
System.out.println(Calculator.fractionCalculator(op2));
System.out.println(Calculator.fractionCalculator(op3));
}
}
4.OperationsCreate类测试
创建不同数目的算式。
点击查看代码
public class OperationsCreateTest {
@Test
public void operationsCreate() {
System.out.println(OperationsCreate.operationsCreate(1,5));
System.out.println(OperationsCreate.operationsCreate(5,10));
System.out.println(OperationsCreate.operationsCreate(10,20));
}
}
5.PostfixExpression类测试
手动输入计算符号或其他字符,检查能否正确判断。手动输入中缀表达式,检查函数能否正确转换为后缀表达式。
点击查看代码
public class PostfixExpressionTest {
@Test
public void isOperator() {
if(PostfixExpression.isOperator("+"))
System.out.println("该符号是计算符号");
if(PostfixExpression.isOperator("-"))
System.out.println("该符号是计算符号");
if(PostfixExpression.isOperator("*"))
System.out.println("该符号是计算符号");
if(PostfixExpression.isOperator("/"))
System.out.println("该符号是计算符号");
if(!PostfixExpression.isOperator("3"))
System.out.println("该符号不是计算符号");
if(!PostfixExpression.isOperator("111"))
System.out.println("该符号不是计算符号");
if(!PostfixExpression.isOperator("ra"))
System.out.println("该符号不是计算符号");
}
@Test
public void postfixChange() {
System.out.println(PostfixExpression.postfixChange("1 + 2 * 9"));
System.out.println(PostfixExpression.postfixChange("( 1 + 2 ) * 9"));
System.out.println(PostfixExpression.postfixChange("4 * 2 * 9"));
System.out.println(PostfixExpression.postfixChange("5 - 2 * 9"));
System.out.println(PostfixExpression.postfixChange("( 5 - 2 ) * 9"));
System.out.println(PostfixExpression.postfixChange("1 / 5 + 2 / 5"));
System.out.println(PostfixExpression.postfixChange("4 / 5 - 2 / 5 * 3 / 5"));
System.out.println(PostfixExpression.postfixChange("( 4 / 5 - 2 / 5 ) * 3 / 5"));
}
}