结对项目
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023/homework/13326 |
| 这个作业的目标 | 合作实现一个四则运算题目生成系统 |
GitHub链接
前端:https://github.com/Rainnnu/ForntenedCulculate.git
后端:https://github.com/lzw0312/randomExpessionGenerator.git
协作成员
林梓维 3223004212
许婉婷 3223004215
一.PSP表格
| Personal Software Process Stages | 预估耗时(分钟 | 实际耗时(分钟 |
|---|---|---|
| 计划 | 30 | 30 |
| 估计这个任务需要多少时间 | 5days | 5days |
| 开发 | 500 | 500 |
| 需求分析 (包括学习新技术) | 20 | 20 |
| 生成设计文档 | 30 | 30 |
| 设计复审 | 30 | 40 |
| 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
| 具体设计 | 20 | 20 |
| 具体编码 | 30 | 20 |
| 代码复审 | 20 | 15 |
| 测试(自我测试,修改代码,提交修改 | 30 | 30 |
| 报告 | 40 | 60 |
| 测试报告 | 30 | 30 |
| 计算工作量 | 20 | 20 |
| 事后总结, 并提出过程改进计划 | 30 | 30 |
| 合计 | 5days | 5days |
二.计算模块接口的设计与实现
项目结构

三.性能分析图

由性能分析图可知 public String[] getExercises(@RequestParam @Validated @NotBlank @Positive int num, @RequestParam @Validated @NotBlank @Positive int max),即生成四则运算的接口函数耗时最大,后续可通过优化生成算法进行优化项目
四.代码说明
1.生成四则运算算法
点击查看代码
public String[] getExercises(int num, int max) {
Random random = new Random();
//标准化之后存入set中,set里面数据个数不发生变化,重新生成
//随机运算符数量和运算符,根据运算符数目生成运算数,随机是否需要括号,对式子进行标准化(只有+或者只有*的时候才需要)
int sum = 0;
String[] exercises = new String[num]; //题目
Set<String> exercisesSet = new HashSet<>(); //判断是否重复的set集合
int count = exercisesSet.size();
while (sum < num) {
String threeNumber[] = new String[3]; //三个数
for (int i = 0; i < threeNumber.length; i++) {
threeNumber[i] = "";
}
int[] threeNum = new int[9]; //三个分数各个部分 数,分母,分子
for (int i = 0; i < threeNum.length; i++) {
threeNum[i] = -1;
}
String[] operators = new String[2]; //运算符
for (int i = 0; i < operators.length; i++) {
threeNumber[i] = "";
}
//生成
//生成运算符数目和运算符
int operatorNum = random.nextInt(2) + 1;
int[] operatorNumber = new int[operatorNum];
for (int i = 0; i < operatorNum; i++) {
int operator = random.nextInt(4);
operatorNumber[i] = operator;
switch (operator) {
case 0:
operators[i] = "+";
break;
case 1:
operators[i] = "-";
break;
case 2:
operators[i] = "*";
break;
case 3:
operators[i] = "/";
break;
default:
break;
}
}
//生成运算数,比运算符多一个
for (int k = 0; k < operatorNum + 1; k++) {
int num1 = random.nextInt(2) + 1; //1是自然数,2是分数
if (num1 == 1) {
threeNum[3 * k] = random.nextInt(max);
threeNumber[k] = String.valueOf(threeNum[3 * k]);
} else {
//分数
//第一个
threeNum[3 * (k)] = random.nextInt(max);
threeNum[3 * (k) + 1] = random.nextInt(100) + 2; //分母
threeNum[3 * (k) + 2] = random.nextInt(threeNum[3 * (k) + 1]) + 1; //分子
if (threeNum[3 * (k) + 2] == threeNum[3 * (k) + 1]) {
threeNum[3 * (k) + 1] += 1;
}
if (threeNum[3 * (k)] == 0) {
threeNumber[k] = (String.valueOf(threeNum[3 * (k) + 2] + "/" + threeNum[3 * (k) + 1]));
} else {
threeNumber[k] = (String.valueOf(threeNum[3 * (k)] + "'" + threeNum[3 * (k) + 2] + "/" + threeNum[3 * (k) + 1]));
}
}
}
//分两个运算符还是三个
//两个
//规范化前判断大小
if (operatorNum == 1) {
double[] numbers = new double[2];
for (int i = 0; i < 2; i++) {
if (threeNum[3 * i + 1] != -1) {
//分数
numbers[i] = (threeNum[3 * i] * threeNum[3 * i + 1] + threeNum[3 * i + 2]) / threeNum[3 * i + 1];
} else {
//自然数
numbers[i] = threeNum[3 * i];
}
}
if (Objects.equals(operators[0], "/") && numbers[1] == 0) {
threeNumber[1] = "1";
}
if (Objects.equals(operators[0], "-") && numbers[0] < numbers[1]) {
String temp = threeNumber[0];
threeNumber[0] = threeNumber[1];
threeNumber[1] = temp;
}
if ((Objects.equals(operators[0], "+") || Objects.equals(operators[0], "*")) && numbers[0] > numbers[1]) {
//交换
exercisesSet.add(threeNumber[1] + operators[0] + threeNumber[0]);
} else {
exercisesSet.add(threeNumber[0] + operators[0] + threeNumber[1]);
}
if (exercisesSet.size() > count) {
//没有重复
exercises[sum] = threeNumber[0] + operators[0] + threeNumber[1];
sum++;
count++;
}
} else {
//三个运算符
//三个数
double[] numbers = new double[3];
for (int i = 0; i < 3; i++) {
if (threeNum[3 * i + 1] != -1) {
//分数
numbers[i] = (double) (threeNum[3 * i] * threeNum[3 * i + 1] + threeNum[3 * i + 2]) / threeNum[3 * i + 1];
} else {
//自然数
numbers[i] = threeNum[3 * i];
}
}
//是否需要括号
int needBracket = random.nextInt(2);
if (needBracket == 0) {
//不需要括号
//组,判断运算符优先级
//优先级一样,计算结果,判断是否需要取反
for (int i = 0; i < 2; i++) {
if (operators[i].equals("/") && threeNumber[i + 1].equals("0")) {
threeNumber[i + 1] = "1";
}
}
if ((operatorNumber[0] < 2 && operatorNumber[1] < 2) || (operatorNumber[0] >= 2 && operatorNumber[1] >= 2)) {
//优先级一样
if (operators[0] == "-" && operators[1] == "-" && (numbers[0] - numbers[1] < numbers[2])) {
operators[0] = "+";
operators[1] = "+";
} else if (operators[0] == "+" && operators[1] == "-" && (numbers[0] + numbers[1] < numbers[2])) {
operators[0] = "-";
operators[1] = "+";
} else if (operators[0] == "-" && operators[1] == "+" && (numbers[0] + numbers[2] < numbers[1])) {
operators[0] = "+";
operators[1] = "-";
}
//不用规范化
exercisesSet.add(threeNumber[0] + operators[0] + threeNumber[1] + operators[1] + threeNumber[2]);
if (exercisesSet.size() > count) {
//没有重复
exercises[sum] = threeNumber[0] + operators[0] + threeNumber[1] + operators[1] + threeNumber[2];
sum++;
count++;
}
} else {
//优先级不一样
//减法为负数情况分析
String expressionString = numbers[0] + operators[0] + numbers[1] + operators[1] + numbers[2];
Expression e = new ExpressionBuilder(expressionString).build();
double result;
try {
result = e.evaluate();
} catch (Exception ex) {
// 捕获并处理 evaluate 方法可能抛出的异常
continue;
}
if (result < 0) {
for (int i = 0; i < 2; i++) {
if (operators[i] == "-") {
operators[i] = "+";
operatorNumber[i] = 0;
}
}
}
//规范化
if (operatorNumber[0] >= 2) {
if (numbers[0] >= numbers[1]) {
exercisesSet.add(threeNumber[1] + operators[0] + threeNumber[0] + operators[1] + threeNumber[2]);
} else {
exercisesSet.add(threeNumber[0] + operators[0] + threeNumber[1] + operators[1] + threeNumber[2]);
}
} else {
if (numbers[1] >= numbers[2]) {
exercisesSet.add(threeNumber[2] + operators[1] + threeNumber[1] + operators[0] + threeNumber[0]);
} else {
exercisesSet.add(threeNumber[1] + operators[1] + threeNumber[2] + operators[0] + threeNumber[0]);
}
}
if (exercisesSet.size() > count) {
//没有重复
exercises[sum] = threeNumber[0] + operators[0] + threeNumber[1] + operators[1] + threeNumber[2];
sum++;
count++;
}
}
} else {
//需要括号
//组,判断运算符优先级
//优先级一样,括号加在后面,计算结果,判断是否需要取反
for (int i = 0; i < 2; i++) {
if (operators[i].equals("/") && threeNumber[i + 1].equals("0")) {
threeNumber[i + 1] = "1";
}
}
if ((operatorNumber[0] < 2 && operatorNumber[1] < 2) || (operatorNumber[0] >= 2 && operatorNumber[1] >= 2)) {
//优先级一样
if (Objects.equals(operators[1], "/") && operators[0].equals("*") && (threeNumber[0].equals("0") || threeNumber[1].equals("0"))) {
threeNumber[1] = "1";
threeNumber[0] = "1";
} else if (Objects.equals(operators[0], "/") && operators[1].equals("*") && (threeNumber[1].equals("0") || threeNumber[2].equals("0"))) {
threeNumber[1] = "1";
threeNumber[2] = "1";
}
for (int i = 0; i < 2; i++) {
if (Objects.equals(operators[i], "/") && Objects.equals(threeNumber[i + 1], "0")) {
numbers[i + 1] += 1;
if (threeNum[3 * (i + 1) + 1] != -1) {
threeNum[3 * (i + 1)] += 1;
threeNumber[i + 1] = threeNum[3 * (i + 1)] + "'" + threeNum[3 * (i + 1) + 2] + "/" + threeNum[3 * (i + 1) + 1];
} else {
numbers[i + 1] += 1;
threeNum[3 * (i + 1)] += 1;
threeNumber[i + 1] = String.valueOf(threeNum[3 * (i + 1)]);
}
}
}
if (operators[0] == "-" && operators[1] == "-" && (numbers[0] < numbers[1] - numbers[2])) {
operators[0] = "+";
operators[1] = "+";
} else if (operators[0] == "+" && operators[1] == "-" && (numbers[1] - numbers[2] + numbers[0]) < 0) {
//operators[0]="-";
operators[1] = "+";
} else if (operators[0] == "-" && operators[1] == "+" && (numbers[1] + numbers[2] > numbers[0])) {
operators[0] = "+";
//operators[1]="-";
}
//用规范化
if (numbers[1] > numbers[2]) {
exercisesSet.add(threeNumber[2] + operators[1] + threeNumber[1] + operators[0] + threeNumber[0]);
} else {
exercisesSet.add(threeNumber[1] + operators[1] + threeNumber[2] + operators[0] + threeNumber[0]);
}
//exercisesSet.add(threeNumber[0]+operators[0]+threeNumber[1]+operators[1]+threeNumber[2]);
if (exercisesSet.size() > count) {
//没有重复
exercises[sum] = threeNumber[0] + operators[0] + "(" + threeNumber[1] + operators[1] + threeNumber[2] + ")";
sum++;
count++;
}
} else {
//优先级不一样,括号加给优先级低的
//减法为负数情况分析
for (int i = 0; i < 2; i++) {
if (operators[i].equals("/") && threeNumber[i + 1].equals("0")) {
threeNumber[i + 1] = "1";
}
}
if (operators[0].equals("-") && operators[1].equals("/") && Objects.equals(threeNumber[0], threeNumber[1])) {
numbers[0] += 1;
if (threeNum[1] != -1) {
threeNum[0] += 1;
threeNumber[0] = threeNum[0] + "'" + threeNum[2] + "/" + threeNum[1];
} else {
numbers[0] += 1;
threeNum[0] += 1;
threeNumber[0] = String.valueOf(threeNum[0]);
}
} else if (operators[1].equals("-") && operators[0].equals("/") && Objects.equals(threeNumber[2], threeNumber[1])) {
numbers[1] += 1;
if (threeNum[4] != -1) {
threeNum[3] += 1;
threeNumber[1] = threeNum[3] + "'" + threeNum[5] + "/" + threeNum[4];
} else {
//numbers[1]+=1;
threeNum[3] += 1;
threeNumber[1] = String.valueOf(threeNum[3]);
}
}
for (int i = 0; i < 2; i++) {
if (operators[i] == "-" && numbers[i] < numbers[i + 1]) {
String temp = threeNumber[i];
threeNumber[i] = threeNumber[i + 1];
threeNumber[i + 1] = temp;
}
}
//规范化
if (operatorNumber[0] >= 2) {
if (numbers[1] >= numbers[2]) {
exercisesSet.add(threeNumber[2] + operators[1] + threeNumber[1] + operators[0] + threeNumber[0]);
} else {
exercisesSet.add(threeNumber[1] + operators[1] + threeNumber[2] + operators[0] + threeNumber[0]);
}
} else {
if (numbers[0] >= numbers[1]) {
exercisesSet.add(threeNumber[1] + operators[0] + threeNumber[0] + operators[1] + threeNumber[2]);
} else {
exercisesSet.add(threeNumber[0] + operators[0] + threeNumber[1] + operators[1] + threeNumber[2]);
}
}
if (exercisesSet.size() > count) {
//没有重复
// exercises[sum]=threeNumber[0]+operators[0]+threeNumber[1]+operators[1]+threeNumber[2];
if (operatorNumber[0] >= 2) {
exercises[sum] = threeNumber[0] + operators[0] + "(" + threeNumber[1] + operators[1] + threeNumber[2] + ")";
} else {
exercises[sum] = "(" + threeNumber[0] + operators[0] + threeNumber[1] + ")" + operators[1] + threeNumber[2];
}
sum++;
count++;
}
}
}
}
//
}
return exercises;
}
点击查看代码
public String[] getAnswer(String[] exercises) {
String[] answers = new String[exercises.length];
// 示例:计算 "1 + 1/2"
for (int i = 0; i < exercises.length; i++) {
System.out.println("题目为" + exercises[i]);
String expression = exercises[i];
//String expression="7-2'20/27/7";
expression = convertToImproperFraction(expression);
String[] infixTokens = parseExpression(expression);
String[] rpnTokens = infixToRPN(infixTokens);
BigFraction result = calculateRPN(rpnTokens);
System.out.println(result);
if (result.toString().contains("/")) {
answers[i] = convertToProperFraction(result);
System.out.println("答案为" + convertToProperFraction(result));
} else {
answers[i] = result.toString();
System.out.println("答案为" + result);
}
}
return answers;
}
//将假分数转换为真分数
public static String convertToProperFraction(BigFraction input) {
int numerator = input.getNumeratorAsInt(); //分子
int denominator = input.getDenominatorAsInt();//分母
int num = 0;
if (denominator == 1) {
return String.valueOf(numerator);
} else {
num = numerator / denominator;
numerator = numerator % denominator;
}
if (num != 0) {
return num + "'" + numerator + "/" + denominator;
}
return numerator + "/" + denominator;
}
// 将真分数转换为假分数
public static String convertToImproperFraction(String input) {
// 正则表达式匹配真分数(格式为 a`b/c)
Pattern pattern = Pattern.compile("(\\d+)'(\\d+)/(\\d+)");
Matcher matcher = pattern.matcher(input);
// 用于存储转换后的字符串
StringBuffer result = new StringBuffer();
// 遍历匹配的真分数
while (matcher.find()) {
int a = Integer.parseInt(matcher.group(1)); // 整数部分
int b = Integer.parseInt(matcher.group(2)); // 分子
int c = Integer.parseInt(matcher.group(3)); // 分母
// 转换为假分数
int numerator = a * c + b; // 新分子
String improperFraction = numerator + "/" + c; // 假分数形式
// 替换原式子中的真分数
matcher.appendReplacement(result, improperFraction);
}
matcher.appendTail(result); // 添加剩余部分
return result.toString();
}
// 应用运算符
public static BigFraction applyOperation(BigFraction a, BigFraction b, String operator) {
switch (operator) {
case "+":
return a.add(b);
case "-":
return a.subtract(b);
case "*":
return a.multiply(b);
case "/":
return a.divide(b);
default:
throw new IllegalArgumentException("Unknown operator: " + operator);
}
}
// 判断是否为运算符
public static boolean isOperator(String token) {
return OPERATOR_PRECEDENCE.containsKey(token);
}
// 判断是否为数字
public static boolean isNumber(String token) {
try {
new BigFraction(Double.parseDouble(token));
return true;
} catch (NumberFormatException e) {
return false;
}
}
// 将输入字符串解析为 token 数组
public static String[] parseExpression(String expression) {
List<String> tokens = new ArrayList<>();
StringBuilder numberBuffer = new StringBuilder();
for (char ch : expression.toCharArray()) {
if (Character.isDigit(ch)) {
// 如果是数字或分数符号,添加到缓冲区
numberBuffer.append(ch);
} else if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '(' || ch == ')') {
// 如果是运算符或括号,先将缓冲区的内容作为一个 token 添加
if (numberBuffer.length() > 0) {
tokens.add(numberBuffer.toString());
numberBuffer.setLength(0); // 清空缓冲区
}
tokens.add(String.valueOf(ch)); // 添加运算符或括号
} else if (!Character.isWhitespace(ch)) {
throw new IllegalArgumentException("Invalid character: " + ch);
}
}
// 添加最后一个数字
if (numberBuffer.length() > 0) {
tokens.add(numberBuffer.toString());
}
return tokens.toArray(new String[0]);
}
// 将中缀表达式转换为逆波兰表达式
public static String[] infixToRPN(String[] infixTokens) {
List<String> output = new ArrayList<>();
Stack<String> operators = new Stack<>();
for (String token : infixTokens) {
if (isNumber(token)) {
// 如果是数字,直接添加到输出
output.add(token);
} else if (isOperator(token)) {
// 如果是运算符,弹出栈中优先级更高或相等的运算符
while (!operators.isEmpty() && isOperator(operators.peek()) && OPERATOR_PRECEDENCE.get(operators.peek()) >= OPERATOR_PRECEDENCE.get(token)) {
output.add(operators.pop());
}
operators.push(token);
} else if (token.equals("(")) {
// 如果是左括号,压入栈
operators.push(token);
} else if (token.equals(")")) {
// 如果是右括号,弹出栈中的运算符直到遇到左括号
while (!operators.isEmpty() && !operators.peek().equals("(")) {
output.add(operators.pop());
}
if (!operators.isEmpty() && operators.peek().equals("(")) {
operators.pop(); // 弹出左括号
} else {
throw new IllegalArgumentException("Mismatched parentheses");
}
} else {
throw new IllegalArgumentException("Invalid token: " + token);
}
}
// 弹出栈中剩余的运算符
while (!operators.isEmpty()) {
if (operators.peek().equals("(") || operators.peek().equals(")")) {
throw new IllegalArgumentException("Mismatched parentheses");
}
output.add(operators.pop());
}
return output.toArray(new String[0]);
}
// 计算逆波兰表达式
public static BigFraction calculateRPN(String[] rpnTokens) {
// 计算逆波兰表达式
Stack<BigFraction> stack = new Stack<>();
for (String token : rpnTokens) {
if (isNumber(token)) {
stack.push(new BigFraction(Double.parseDouble(token)));
} else if (isOperator(token)) {
try {
BigFraction b = stack.pop();
BigFraction a = stack.pop();
BigFraction result = applyOperation(a, b, token);
stack.push(result);
} catch (EmptyStackException e) {
throw new IllegalArgumentException("Invalid expression");
}
} else {
throw new IllegalArgumentException("Invalid token: " + token);
}
}
if (stack.size() != 1) {
throw new IllegalArgumentException("Invalid expression");
}
return stack.pop();
}
点击查看代码
public AnswerVO correctingExercises(String[] answers, String[] result) {
List<Integer> correct = new ArrayList<>();
List<Integer> error = new ArrayList<>();
int correctNumber = 0;
int errorNumber = 0;
for (int i = 0; i < answers.length; i++) {
if (answers[i].equals(result[i])) {
correct.add(i);
correctNumber++;
} else {
error.add(i);
errorNumber++;
}
}
AnswerVO answerVO = new AnswerVO(correct, correctNumber, error, errorNumber);
return answerVO;
}
五.单元测试展示
测试代码
点击查看代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class test {
Controller controller = new Controller();
ExercisesServiceImpl exercisesService = new ExercisesServiceImpl();
@Test
public void testGetExercises() {
// 测试生成30000条
String[] result = controller.getExercises(10000, 10);
}
// 测试max参数不合法
@Test(expected = IllegalArgumentException.class)
public void testGetExercisesWithErrorMax() {
String[] result = controller.getExercises(30000, -1);
}
// 测试num参数不合法
@Test(expected = NegativeArraySizeException.class)
public void testGetExercisesWithErrorNum() {
// 测试生成30000条
String[] result = controller.getExercises(-1, 10);
}
//测试给定题目和答案,进行修改
@Test
public void testCorrectingExercises() throws FileNotFoundException {
controller.correctingExercises(new FileDTO("C:\\Users\\Aurora\\Desktop\\Exercises.txt", "C://Users//Aurora//Desktop//Answers.txt"));
}
//测试CorrectingExercises函数参数错误情况
@Test(expected = NullPointerException.class)
public void testCorrectingExercisesWithoutFileDTO() throws FileNotFoundException {
controller.correctingExercises(null);
}
//测试convertToProperFraction函数,将假分数化为真分数
@Test
public void testConvertToProperFraction() {
String s = exercisesService.convertToProperFraction(new BigFraction(7, 2));
assert s.equals("3'1/2");
}
//测试convertToImproperFraction函数,将真分数化为假分数
@Test
public void testConvertToImproperFraction() {
String s = exercisesService.convertToImproperFraction("3'1/2");
assert s.equals("7/2");
}
//测试生成题目不重复
@Test
public void testGenerateExercises() {
String[] result = controller.getExercises(1000, 10);
Set<String> exercisesSet = new HashSet<>();
for (String s : result) {
exercisesSet.add(s);
}
assert exercisesSet.size() == result.length;
}
//测试将中缀表达式转换为后缀表达式
@Test
public void testInfixToRPN() {
String[] result = exercisesService.parseExpression("1+2*3");
String[] strings = exercisesService.infixToRPN(result);
assert strings[0].equals("1");
assert strings[1].equals("2");
assert strings[2].equals("3");
assert strings[3].equals("*");
assert strings[4].equals("+");
}
//测试计算后缀表达式
@Test
public void testCalculateRPN() {
String[] result = exercisesService.parseExpression("1+2*3");
String[] strings = exercisesService.infixToRPN(result);
BigFraction calculateRPN = exercisesService.calculateRPN(strings);
assert calculateRPN.equals(new BigFraction(7));
}
}
六.项目小结
在这次结对项目中,林梓维负责后端部分代码编写,许婉婷负责前端部分代码编写,在本次前后端分离的结对项目中,我们团队高效协作,顺利完成了任务。前端采用React框架,负责界面展示与用户交互,后端则使用SpringBoot框架,处理业务逻辑和数据存储。通过Git进行版本控制,确保代码同步。在开发过程中,我们保持密切沟通,及时解决问题,确保前后端接口匹配。项目最终实现了预期的功能,用户体验良好,响应速度快。此次合作,不仅提升了我们的技术能力,也锻炼了团队协作精神,为后续项目的顺利进行奠定了坚实基础。
浙公网安备 33010602011771号