一、学生信息及 GitHub 仓库地址
姓名 | 学号 |
---|---|
黄晓楷 | 3118005327 |
黄裕煜 | 3118005328 |
GitHub 仓库地址:https://github.com/Boyle-Coffee/SE_homework_arithmetic
二、作业信息
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Networkengineering1834 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Networkengineering1834/homework/11148 |
这个作业的目标 | 学会使用PSP表格规划项目开发计划,学习结算项目的流程、熟悉 Git 以及 GitHub 的基本操作,学习逆波兰算法解决四则运算计算问题 、学习单元测试 |
三、PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 65 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 30 |
Development | 开发 | 1640 | 1700 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 30 |
· Design Spec | · 生成设计文档 | 30 | 40 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 1000 | 1100 |
· Code Review | · 代码复审 | 240 | 230 |
· Test | · 测试(自我测试,修改代码,提交修改) | 200 | 190 |
Reporting | 报告 | 100 | 90 |
· Test Repor | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 50 |
· 合计 | 1590 | 1800 |
本次项目工作量如下:
- 代码量:总共 3122 行代码
- GitHub 总共签入 83 次
四、开发计划
五、功能及接口设计
5.1 功能设计
(一)、参数获取功能
该功能主要用于获取一些生成题目以及存储文件时必要的参数,该项目有两种实现方式:
-
通过命令行获取参数,如下:
# 获取生成题目数量的参数 Myapp.exe -n 10 # 获取生成题目范围的参数 Myapp.exe -r 10 # 答案判别的参数 Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
-
通过 web 网站的图形界面获取参数
(二)、文件存取功能
该功能主要用于将题目、答案或成绩存进指定文件中,主要实现以下几个功能点:
- 题目文件或答案文件的读取,读取指定的文件信息,并生成一个列表
- 题目文件或答案文件的存储,读取指定的文件信息以及列表,并将结果存进指定文件中
- 成绩信息的存储,将用户的答案判定结果按要求格式存进指定文件中
(三)、答案计算与查错功能
该功能主要包括两个功能点:
- 采用逆波兰计算器对给定的式子进行计算,即实现将算式转换为逆波兰式,并实现逆波兰计算(计算模块主要基于逆波兰式实现,具体可参考文档逆波兰式算法.md
- 将结果与给定答案进行比对,以实现答案判定。
(四)、题目生成功能
该功能主要实现符合需求的题目的生成,主要功能点如下:
- 随机生成符合数学规律的算式,具体算法参考文档 生成题目算法.md
- 对生成的数学式,进行查重功能,并剔除重复算式,去重算法可参考文档逆波兰式算法.md
- 判定数学式是否符合题目要求,并选取出其中符合要求的题目:
- 出现的数字在限定的数字范围内
- 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如(e_1− e_2)的子表达式,那么(e_1≥ e_2)。
- 生成的题目中如果存在形如(e_1div e_2)的子表达式,那么其结果应是真分数。
5.2 接口与方法
(一)、文件接口
1、文件的输入
/**
* 将题目文件或答案文件转换为集合
* @param fileName 题目文件或答案文件的文件名
* @return 返回集合
*/
public List<String> readFromFile(String fileName);
2、文件的输出
/**
* 将生成的题目或答案输出至文件中
* @param fileName 题目文件或答案文件的文件名
* @param textList 题目或答案集合
*/
public void writeToFile(String fileName, List<String> textList);
3、成绩文件的输出
/**
* 将成绩输出至文件
*
* @param fileName 文件名
* @param textList 文本集合
*/
public void gradeToFile(String fileName, List<String> textList);
(二)、生成接口
1、题目的生成
/**
* 生成题目
*
* @param naturalNumber 自然数的最大值
* @return 返回题目
*/
public String generateQuestion(Integer naturalNumber);
(三)、运算接口
1、逆波兰式的生成
/**
* 生成逆波兰式
* @param question 题目
* @return 返回逆波兰式
*/
public String generateReversePoland(String question);
2、答案的生成
/**
* 生成答案
* @param againstPoland 逆波兰式
* @return 返回答案
*/
public String generateAnswer(String reversePoland);
(四)、检查接口
1、题目重复率的检查
/**
* 检查生成的题目的重复率
*
* @param question 题目
* @param againstPolandSet 逆波兰式的集合
* @return 返回检查结果
*/
public Boolean checkQuestion(String question, Set<String> againstPolandSet);
2、答案正确率的检查
/**
* 检查我的答案的正确率
*
* @param myAnswerList 我的答案
* @param trueAnswerList 正确答案
* @return 返回错误题号
*/
public List<String> checkAnswer(List<String> myAnswerList, List<String> trueAnswerList);
六、算法说明及伪代码
6.1 逆波兰算法
逆波兰表达式
波兰表达式又叫做后缀表达式,是1929年波兰的逻辑学家卢卡西维兹(Jan Lucasiewicz)提出的一种将运算符放在运算项后面的逻辑表达式,具体转换如下:
中缀表达式 | 后缀表达式 |
---|---|
(a+b) | (a,b,+) |
(a+(b-c)) | (a,b,c,-,+) |
(a+(b-c)*d) | (a,b,c,-,d,*,+) |
(a+d*(b-c)) | (a,d,b,c,-,*,+) |
中缀表达式转为后缀表达式
步骤如下:
- 初始化两个栈:运算符栈
s1
和储存中间结果的栈s2
。 - 从左至右扫描中缀表达式。
- 遇到操作数时,将其压
s2
。 - 遇到运算符时,比较其与
s1
栈顶运算符的优先级:- 如果
s1
为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈; - 否则,若优先级比栈顶运算符的高,也将运算符压入
s1
; - 否则,将
s1
栈顶的运算符弹出并压入到s2
中,再次转到4.1与s1
中新的栈顶运算符相比较。
- 如果
- 遇到括号时:
- 如果是左括号"(",则直接压入
s1
; - 如果是右括号")",则依次弹出
s1
栈顶的运算符,并压入s2
,直到遇到左括号为止,此时将这一对括号丢弃;
- 如果是左括号"(",则直接压入
- 重复步骤 2—5 ,直到扫描到表达式的最右边。
- 将
s1
中剩余的运算符依次弹出并压入s2
。 - 依次弹出
s2
中的元素并输出,结果的逆序即为 中缀表达式 对应的 后缀表达式。
形如:
中缀表达式“1+((2+3)×4)-5”,转为后缀表达式的结果是:
1 2 3 + 4 × + 5 –
伪代码如下:
input: prefixExper(中缀表达式)
s1 = new stack()
s2 = new stack()
for item in prefixExper: # 判断是否是数字
if isFigure(item):
s2.push(item)
elif isOpt(item): # 判断是否是符号
while True:
if s1.empty() or isLeft(s1.peek()):
s1.push(item)
if item.level() > s1.peek().lavel(): # 比较优先级
s1.push(item)
else:
s2.push(s1.pop())
break
elif isLeft(item): # 判断是否是左括号
s1.push(item)
elif isRight(item): # 判断是否是右括号
while not isLeft(s1.peek()):
s2.push(s1.pop())
s1.pop()
else:
return Error
end if
end for
postfixExper = new String()
while not s2.empty():
postfixExper.add(s2.pop()) # 将栈2依次弹出
end while
postfixExper = postfixExper.reserve # 栈2弹出结果的逆序就是后缀表达式
return postfixExper
后缀表达式的计算
步骤如下:
- 初始化一个存储计算结果的栈
s3
。 - 从左至右扫描后缀表达式。
- 如果遇到数字,就直接入栈,否则转4。
- 如果遇到运算符,就去除栈顶的两个操作数,执行后将结果入栈。
- 重复步骤 2—4 ,直到扫描到表达式的最右边。
- 如果后缀表达式没有异常,最后栈中应该只剩一个元素,这时将操作数出栈,即为最后结果。
伪代码如下:
input: postfixExper(后缀表达式)
s3 = new stack();
for item in postfixExper:
if isFigure(item): # 判断是否是数字
s3.push(item)
elif isOpt(item): # 判断是否是符号
a = s3.pop()
b = s3.pop()
temp = operation(item, a, b) # 进行计算
s3.push(temp) # 将结果入栈
else:
return Error
end if
end for
result = s3.pop()
return result
6.2 题目生成算法
生成算法主要流程
输入:题目个数 n ,数值范围 m
输出:题目列表 prod_list
prod_list = []
for i in range(n):
exper = '' # 初始化算式
sign_num = randint(1, 3) # 符号个数
fig_0 = create_rand_fig()
exper.expand(fig_0) # 将第一个数字加入式子中
for j in range(sign_num):
sign_j = create_rand_sign() # 将符号加入式子中
fig_j = create_rand_fig() # 将数字加入式子中
exper.expand(sign_j)
exper.expend(fig_J)
end for
exper = add_brac(exper, sign_num)
end for
数字生成
整数
主要采用随机数生成
# m 为数字的范围
fig = randint(m)
分数
辗转相除法
分为真分数的生成和带分数的生成:
- 对于真分数,先生成规定范围内的分子和分母,再进行约分,并舍弃多出来的整数
- 如,随机生成分子 2 和分母 3,即为“2/3”
- 在如,生成分子 10 和分母 4,先约分为 “5/2”即“2'1/2”,再舍弃多出来的整数为“1/2”
- 对于带分数,先生成规定范围内的分子和分母,再进行约分,并舍弃多出来的整数,最后在随机生成规定范围内的整数部分
- 如,生成分子 10 和分母 4,先约分为 “5/2”即“2'1/2”,再舍弃多出来的整数为“1/2”,然后在随机生成了 3 ,最后结果为“3'1/2”
括号的插入
分析后发现,当符号数为 1 时,无法插入括号;
当符号数为 2 时,可以插入括号,一共 2 种插入方式,加上不插入一共 3 中状态
当符号数为 3 时,可以插入括号,一共 6 种插入方式,加上不插入一共 7 中状态
对于不同状态赋予不同的权重,就能按一定比例生成各种情况的括号
6.3 基于逆波兰式的查重算法
对于相同运算符、运算数的算式,只要计算次序相同,其后缀表达式就是相同的,可以利用这个规律,可以实现表达式生成过程中的去重问题,伪代码如下:
input: experNum(算式个数)
result = new list()
postfixSet = new set()
while result.length < experNum:
exper = createExperRand() # 随机生成合法
postfixExper = toPrefixExper(exper) # 生成后缀表达式
if postfixExper in postfixSet:
continue
else:
result.add(exper)
postfixSet.add(postfixExper)
end if
end while
return result
七、设计实现过程
八、关键代码展示及说明
逆波兰式生成算法
详细算法参考上文六、算法说明及伪代码,这里主要通过注释进行说明
public String generateReversePoland(String question) {
/**
*@Description: 逆波兰式生成,将中缀表达式转换为后缀表达式
*@Param: [question]
*@return: java.lang.String
*@Author: Boyle
*@date: 2020/10/8
*/
if (question == null || "".equals(question.trim())) {
return null;
}
// 根据空格分割字符串
String[] preExper = question.trim().split(Constants.REGEX);
// 初始化运算符栈
Stack<String> optStack = new Stack<>();
// 存储中间结果的栈
Stack<String> tempStack = new Stack<>();
Stack<String> resultStack = new Stack<>();
int experSize = preExper.length;
try {
for (int i = 0; i < experSize; i++) {
String item = preExper[i].trim();
if ("".equals(item)) { // 舍弃多余的空格
continue;
}
if (DigitStringUtil.isFigure(item)) { // 遇到整数时,直接压入存储结果的栈
tempStack.push(item);
} else if (DigitStringUtil.isOpt(item)) { // 遇到运算符,根据栈顶元素进行操作
while (optStack.size() != 0
&& (!optStack.peek().equals(Constants.LEFT))
&& DigitStringUtil.getValue(optStack.peek()) >= DigitStringUtil.getValue(item)) {
tempStack.push(optStack.pop());
}
optStack.push(item);
} else if (item.equals(Constants.LEFT)) { // 遇到左括号时,直接压入运算符栈
optStack.push(item);
} else if (item.equals(Constants.RIGHT)) { // 遇到右括号一次弹出栈元素,直到遇到左括号
while (!optStack.peek().equals(Constants.LEFT)) {
tempStack.push(optStack.pop());
}
optStack.pop();
} else {
return null;
}
}
while (!optStack.empty()) {
tempStack.push(optStack.pop());
}
while (!tempStack.empty()) {
resultStack.push(tempStack.pop());
}
return stack2RPN(resultStack);
} catch (Exception e) {
return null;
}
}
该算法的流程图如下:
后缀表达式的计算
详细算法参考上文六、算法说明及伪代码,这里主要通过注释进行说明
public String generateReversePoland(String question) {
/**
*@Description: 逆波兰式生成,将中缀表达式转换为后缀表达式
*@Param: [question]
*@return: java.lang.String
*@Author: Boyle
*@date: 2020/10/8
*/
if (question == null || "".equals(question.trim())) {
return null;
}
// 根据空格分割字符串
String[] preExper = question.trim().split(Constants.REGEX);
// 初始化运算符栈
Stack<String> optStack = new Stack<>();
// 存储中间结果的栈
Stack<String> tempStack = new Stack<>();
Stack<String> resultStack = new Stack<>();
int experSize = preExper.length;
try {
for (int i = 0; i < experSize; i++) {
String item = preExper[i].trim();
if ("".equals(item)) { // 舍弃多余的空格
continue;
}
if (DigitStringUtil.isFigure(item)) { // 遇到整数时,直接压入存储结果的栈
tempStack.push(item);
} else if (DigitStringUtil.isOpt(item)) { // 遇到运算符,根据栈顶元素进行操作
while (optStack.size() != 0
&& (!optStack.peek().equals(Constants.LEFT))
&& DigitStringUtil.getValue(optStack.peek()) >= DigitStringUtil.getValue(item)) {
tempStack.push(optStack.pop());
}
optStack.push(item);
} else if (item.equals(Constants.LEFT)) { // 遇到左括号时,直接压入运算符栈
optStack.push(item);
} else if (item.equals(Constants.RIGHT)) { // 遇到右括号一次弹出栈元素,直到遇到左括号
while (!optStack.peek().equals(Constants.LEFT)) {
tempStack.push(optStack.pop());
}
optStack.pop();
} else {
return null;
}
}
while (!optStack.empty()) {
tempStack.push(optStack.pop());
}
while (!tempStack.empty()) {
resultStack.push(tempStack.pop());
}
return stack2RPN(resultStack);
} catch (Exception e) {
return null;
}
}
九、单元测试及代码覆盖率
计算模块测试
测试类如下:
package com.company.model.calculatemodel;
import com.company.common.Constants;
import com.company.model.filemodel.ReadFromFile;
import org.junit.jupiter.api.*;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class CalculateRPNTest {
static private ReadFromFile fileMdlService = new ReadFromFile();
private CalculateRPN calculateMdlService = new CalculateRPN();
static List<String> questionList;
List<String> RPNList = new ArrayList<String>();
String RPNu;
@BeforeAll
static void readFile() {
questionList = fileMdlService.readFromFile("Exercises.txt");
}
@Test
@DisplayName("转换10000题")
void generateReversePoland() {
for(String question:questionList) {
RPNu = calculateMdlService.generateReversePoland(question);
RPNList.add(RPNu);
System.out.println(RPNu);
}
}
@Test
@DisplayName("计算10000个答案")
void generateAnswer() {
for(String item:RPNList) {
System.out.println(calculateMdlService.generateAnswer(item));
}
}
@Test
@DisplayName("输入错误中缀表达式")
void generateReversePolandError() {
String question = "3 2 4";
String answer;
answer = calculateMdlService.generateReversePoland(question);
assertNull(answer);
}
@Test
@DisplayName("输入式子计算过程出现负数")
void generateReversePolandNegative() {
String question = "3 - 4";
String RPN;
String answer;
RPN = calculateMdlService.generateReversePoland(question);
answer = calculateMdlService.generateAnswer(RPN);
assertEquals(answer, Constants.NEGATIVE);
}
@Test
@DisplayName("输入式子中除数为0")
void generateReversePolandDivZero() {
String question = "3 ÷ 0";
String RPN;
String answer;
RPN = calculateMdlService.generateReversePoland(question);
answer = calculateMdlService.generateAnswer(RPN);
assertEquals(answer, Constants.DIVZERO);
}
}
计算模块的各个测试样例耗时如下:
将 10000 道题由中缀表达式转换为后缀表达式
输入数据:通过@BeforeAll
修饰器的方法从存储有 10000 道题的文件中读取的题目,题目存储在一个String
类型的数组中,读取文件的方法如下
@BeforeAll
static void readFile() {
questionList = fileMdlService.readFromFile("Exercises.txt");
}
测试方法:
@Test
@DisplayName("转换10000题")
void generateReversePoland() {
for(String question:questionList) {
RPNList.add(calculateMdlService.generateReversePoland(question));
}
}
预期结果:转换 10000 道题
测试结果:生成 10000 道题,耗时 435 ms,速度还是比较快的,部分结果如下
输入错误的中缀表达式
输入数据:错误的中缀表达式"3 2 4"
测试方法:
@Test
@DisplayName("输入错误中缀表达式")
void generateReversePolandError() {
String question = "3 2 4";
String answer;
answer = calculateMdlService.generateReversePoland(question);
assertNull(answer);
}
预期结果:检测到错误表达式,返回空值
测试结果:返回空值
计算 10000 道题目
输入数据:10000道后缀表达式
测试方法:
@Test
@DisplayName("计算10000个答案")
void generateAnswer() {
for(String item:RPNList) {
System.out.println(calculateMdlService.generateAnswer(item));
}
}
预期结果:计算出正常结果
测试结果:计算出正常结果,耗时不到 1 ms,速度很快
计算过程中出现负数
输入数据:计算出现负数的表达式3 - 4
测试方法:
@Test
@DisplayName("输入式子计算过程出现负数")
void generateReversePolandNegative() {
String question = "3 - 4";
String RPN;
String answer;
RPN = calculateMdlService.generateReversePoland(question);
answer = calculateMdlService.generateAnswer(RPN);
assertEquals(answer, Constants.NEGATIVE);
}
预期结果:返回超参数Constants.NEGATIVE
的值,即表示计算过程中出现负数
测试结果:与预期一样,返回超参数Constants.NEGATIVE
的值
计算过程中除法的除数为 0
输入数据:计算除数为 0 的表达式3 ÷ 0
测试方法:
@Test
@DisplayName("输入式子中除数为0")
void generateReversePolandDivZero() {
String question = "3 ÷ 0";
String RPN;
String answer;
RPN = calculateMdlService.generateReversePoland(question);
answer = calculateMdlService.generateAnswer(RPN);
assertEquals(answer, Constants.DIVZERO);
}
预期结果:返回超参数Constants.DIVZERO
的值,即表示算式中出现除数为 0 的情况
测试结果:与预期一样,返回超参数Constants.DIVZERO
的值
文件模块测试
测试类如下:
package com.company.model.filemodel;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ReadFromFileTest {
private ReadFromFile fileMdlService = new ReadFromFile();
@Test
@DisplayName("读取文件并打印内容")
void readFromFile() {
List<String> questionList;
questionList = fileMdlService.readFromFile("Exercises.txt");
for(String question: questionList) {
System.out.println(question);
}
}
@Test
@DisplayName("读取不存在的文件")
void readFromFileNotExist() {
List<String> questionList;
questionList = fileMdlService.readFromFile("NotExist.txt");
assertNull(questionList); // 读取的文件不存在将返回空值
}
}
文件模块的各个测试样例耗时如下:
读取正常文件并打印
输入数据:存在文件的文件名
测试方法:
@Test
@DisplayName("读取文件并打印内容")
void readFromFile() {
List<String> questionList;
questionList = fileMdlService.readFromFile("Exercises.txt");
for(String question: questionList) {
System.out.println(question);
}
}
预期结果:打印出文件中的内容
测试结果:打印出文件中的内容,耗时 969 ms,打印结果如下
读取不存在的文件
输入数据:不存在文件的文件名
测试方法:
@Test
@DisplayName("读取不存在的文件")
void readFromFileNotExist() {
List<String> questionList;
questionList = fileMdlService.readFromFile("NotExist.txt");
assertNull(questionList); // 读取的文件不存在将返回空值
}
预期结果:返回空值,表示文件不存在
测试结果:返回空值
生成模块测试
测试类如下:
package com.company.model.generatemodel;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class GenerateQuestionTest {
private GenerateQuestion generateMdlService = new GenerateQuestion();
@Test
@DisplayName("生成10以内的10000道题")
void generateQuestion() {
for(int i=0; i<10000; i++) {
System.out.println(generateMdlService.generateQuestion(10));
}
}
}
生成 10000 道 10 以内的算式并打印
输入数据:生成题目的数量以及范围
测试方法:
@Test
@DisplayName("生成10以内的10000道题")
void generateQuestion() {
for(int i=0; i<10000; i++) {
System.out.println(generateMdlService.generateQuestion(10));
}
}
预期结果:打印出生成的 10000 道题目
测试结果:打印出 10000 道题目,耗时 1ms 部分结果如下
生成题目以及答案功能测试
测试样例
生成 10 以内的 10000 道题
输入数据:生成题目的数量 10000 以及范围 10,命令行如下
Myapp.jar -n 10000 -r 10
预期结果:生成 10000 道题及其对应答案
测试结果:生成 10000 道题及其对应答案,经人工验证,分层抽样的 500 个答案均正确,且表示中未出现不合法的表示方式(如分数的分母为 1 或分子为 0 ,分子比分母大,分数未进行约分等),可认为答案均正确,测试成功,详细请参考文件 Exercises.txt 以及 Answers.txt,耗时为 707 ms部分结果如下:
Exercises.txt
Answers.txt
当输入不存在的命令行参数时
输入数据:命令行如下
Myapp.jar -i 10000 -r 10
预期结果:检测出参数错误,并打印提示信息
测试结果:检测出参数错误,并打印提示信息
参数异常
当输入的数值范围太小或题目数量太大时
输入数据:命令行如下
Myapp.jar -r 2 -n 10000
预期结果:程序中设定连续 10000 次生成的算式都与前面的算式重复就认为算式的生成空间已经被消耗完,该测试样例预期会由于多次尝试生成算式失败而认为算式的生成空间已经被消耗完,退出打印提示信息
测试结果:
生成失败,可能是算式的生成空间已经消耗完,请尝试减少题目数量
程序运行时间为: 2455ms
代码覆盖率
答案检查功能测试
测试样例
输入算式文件和待检测文件
输入数据:包含算式和待检测答案的两个文件,命令行如下:
Myapp.jar -e Exercises.txt -a myAnswers.txt
预期结果:输出到 Grade.txt 文件,结果如下:
Correct: 6 (1, 2, 3, 6, 8, 10)
Wrong: 4 (4, 5, 7, 9)
测试结果:成功输出到 Grade.txt 文件,耗时 11ms 结果如下:
Correct: 6 (1, 2, 3, 6, 8, 10)
Wrong: 4 (4, 5, 7, 9)
参数中的文件不存在
输入数据:不存在的文件名,命令行如下:
Myapp.jar -e NotExist.txt -a myAnswers.txt
预期结果:检测到文件不存在,并打印提示信息
测试结果:
检查出错,这可能是读取的文件异常或不存在导致,请检查你的参数
代码覆盖率
十、效能分析
十一、网页版展示
除了命令行版,我们还设计了网页版,网页版主要分为三个页面,在第一个页面,用户可以选择生成题目或者检查答案
生成题目页面
检查答案页面
十二、解决问题
负数是如何解决的
问题分析
题目要求:生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
(一)、计算过程中的负数产生
负数的产生是因为e1 < e2所导致的,因此在生成题目的过程中,当随机生成的符号为“-”时,需要进行“-“的前一个数值与后一个数值的大小比较。由于我们设计的题目生成算法中,数值的生成可能为:自然数、真分数、带分数。这三种不同的形式,所以在进行数值比较的过程中需要进行转换。同时还需要对”-“进行处理。
(二)、计算结果中负数的产生
计算结果中出现负数,这种情况相较于计算过程中出现负数的情况而言,更加复杂。因为这种情况涉及到了多个运算符以及括号的计算,因此不可如同计算过程中出现负数的情况的处理方案一样。
解决方法
(一)、计算过程中负数产生的解决方法
在经过讨论后,我们得出了两套解决方法。
1、通过抛出异常的方式,终止此次题目生成,并返回一个特定的字符串常量,告诉调用方,生成的题目中存在负数,调用方获取这个信息后,会继续调用生成模块生成新的题目,直到生成合理的算式为止。
2、通过修改符号的方式,当出现”-“时,并且”-“的前一个数值减去后一个数值结果为负数时,需进行符号更改。通过讨论,我们决定将”-“改为”+“。
在经过一番讨论后,我们认为当生成题目号较大时,出现计算过程中出现负数的概率是较大的,且更改符号有可能会生成已生成过的算式,为了提高生成题目的效率,我们最终选择了第一套方案。
(二)、计算结果中负数产生的解决方法
同计算过程中出现负数一样,我们第一时间也是想到了抛异常的方式。但考虑到了生成数量较大时一样会出现上面的情况,于是选择了放弃该方案。
之后,我们通过讨论决定当出现计算结果是负数时,丢弃此次生成的题目与答案,不将其存入集合中。
在这个方案中,我们也发现了之前设计的一个不足之处,原先的设计是通过一个for循环,不断的生成题目与答案。在原先的设计中如果出现计算结果为负数时,虽然丢弃了该题目与答案但for循环的计数依旧加1。于是通过部分的调整,最终完善了计算结果出现负数的情况。
分母为零或除数为零是如何解决的
问题分析
在随机生成的题目中,由于是随机产生而并非采用启发式算法产生,所以难免会出现分母为 0 或除数为 0 的情况。这是因为在计算过程中,虽然直接产生的分数分母不可能为 0 ,但在计算过程中遇到某些情况则可能产生这类情况,以下情况:
( 2 + 3 ) ÷ ( 4'3/4 - 4'3/4 )
= 5 ÷ 0
注意,这里虽然直接生成的数分母不为 0 ,但计算过程却产生了这个情况,这是生成模块解决不了的问题。
解决方案
我们第一时间也是想到了交换分子分母的方式。但考虑到了可能出现已生成的题目的情况,于是选择了放弃该方案。
最后,通过我们的一致讨论,决定采用前一个问题的解决思路,当发现计算过程中出现分母为 0 或除数为 0 的情况时,即终止运算,并返回一个特定的字符串常量,告诉调用方,生成的题目中存在分母为 0 或除数为 0 ,调用方获取这个信息后,会继续调用生成模块生成新的题目,直到生成合理的算式为止。
分数计算
问题分析
由于题目中涉及到整数、分数以及带分数多种类型数字的计算,分数的计算无法调用现有的方法进行操作,于是计算模块中要解决的一大难题就是分数的计算。
解决方案
为了解决这个问题,我们定义了一个分数类FractionObj
,将所有类型的数字的计算都用该类进行,这个分数类包括两个属性,int numerator
表示分子,int denominator
表示分母,规定如下:
- 该类有一个
getString()
方法用于得到该数字的字符串形式 - 当数字为整数时,
numerator
为数字的值,denominator
的值为 1 。 - 当数字为 0 时,
numerator
为 0 ,denominator
为 1 。 - 当计算过程导致分母为 0 时,
getString()
得到该数字的字符串为一个特定的字符串常量,用户告诉调用方计算出现异常 - 定义了以下四则运算方法,输入为第二个操作数(分数类类型):
public FractionObj add(FractionObj r); // 加法
public FractionObj sub(FractionObj r); //减法
public FractionObj multi(FractionObj r); //乘法
public FractionObj div(FractionObj r); // 除法
- 在每次初始化分子分母或者得到二元运算时,都对结果的分子分母进行约分。
十三、项目小结
通过这一次的项目经验,我们两个人通过讨论项目需求,分析项目模块,进而进行分工。
在一开始的讨论中,我们将其大致分为三个模块:“生成模块”、“文件模块”以及“检查模块”。其中“生成模块”中我们又将其细分为“题目生成”、“逆波兰式生成”以及“答案生成”。在文件模块中我们将其细分为“文件读入”以及“文件读取”。最后的“检查模块”中,我们将其分为“错误率检查”以及“重复率检查”。
通过分析项目需求,模块分工后。我们通过百度等方式查询到了我们做这个项目所需要的知识点例如逆波兰式的生成。在通过数天的合作以及努力中我们完成了命令行版本的四则运算生成的核心功能,并可支持10000道题目的生成。但在后续的测试中,我们发现了一些情况并没有考虑中。例如生成的分数会存在为0或1的情况,亦或者是当要求生成的题目数量为1000,而自然数的最大值为3时,将无法生成需求数量的题目。通过考虑多个方面的测试,进而不断完善代码,命令行版本的四则运算终于大功告成。
在完成了命令行版本后的四则运算生成,我们再次制作了网页版的四则运算,由于我们两人对于前端的知识并不是很丰富,导致我们这部分需要先去百度学习页面制作的相关内容。通过百度以及请教他人后,终于做出一个勉强可观看的页面版本。
在这一次的合作中,我们也发现了在git使用上还需要多锻炼,有时会容易出现push失败等的情况。同时也意识到了我们的一些不足之处,例如前端知识的匮乏。