结对项目:自动生成小学四则运算题目的命令行程序
这个作业属于哪个课程 | 计科22级12班 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-12/homework/13221 |
姓名 | 学号 | github |
---|---|---|
曾繁曦 | 3122004841 | https://github.com/Fancy-Tsang/3122004841/tree/main/src/main/java/com/tsang/fancy_3122004841/Maths |
吴健民 | 3122004667 |
PSP表格
一、流程图
- 生成题目表达式的流程图:
- 使用HashMap来比较两个题目表达式是否有重复流程:
二、模块设计
- 模块划分
- Main模块(Main.java)
功能描述:程序的入口点,负责接收命令行参数,进行参数校验,调用其他模块的方法来生成题目、检查答案以及处理可能出现的异常情况,如超时异常和其他一般异常。
点击查看代码
package com.tsang.fancy_3122004841.Maths;
import com.tsang.fancy_3122004841.Maths.entity.Args;
import com.tsang.fancy_3122004841.Maths.utils.ValidationUtils;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.tsang.fancy_3122004841.Maths.utils.QuizGenerator.checkExercisesAnswers;
import static com.tsang.fancy_3122004841.Maths.utils.QuizGenerator.generateQuizzes;
public class Main {
public static void main(String[] args){
int num = 0;
try{
//校验参数
Args argsObj = ValidationUtils.validateArgs(args);
Integer numberOfQuestions = argsObj.getNumberOfQuestions();
num = numberOfQuestions;//?
Integer range = argsObj.getRange();
// 判题
checkExercisesAnswers(argsObj.getExercisesFileName(), argsObj.getAnswerFileName());
// 生成题目
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> generateQuizzes(numberOfQuestions, range));
// 5秒超时
task.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.err.println("数据范围不支持生成" + num + "道题,请调整参数!");
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}
- DuplicateChecker模块(DuplicateChecker.java)
功能描述:用于检查生成的数学表达式是否重复。通过维护一个特定结构的映射来存储已生成的表达式的结果、长度以及每个操作数和操作符的出现次数,以判断新生成的表达式是否与已有的重复。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import com.tsang.fancy_3122004841.Maths.entity.Fraction;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class DuplicateChecker {
// key:表达式的结果;value:map(key:表达式的长度,value:map集合(key:表达式中的每一个操作数或操作符;value:该字符串出现的次数))
private static final Map<String, Map<Integer, List<Map<String, Integer>>>> DUMPLICATE_MAP = new HashMap<>();
public static boolean isDuplicate(Fraction result, String expression) {
// 已创建的表达式中,如果有计算结果相同,且表达式中的所有字符和出现的次数都一样,就认为是重复的
String resultStr = result.toString();
expression = expression.replaceAll("[()]", "");
Integer length = expression.length();
// 统计表达式中每个操作数和操作符出现的次数
Map<String, Integer> characterCountMap = Arrays.stream(expression.split("\\s+"))
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.collectingAndThen(Collectors.counting(), Long::intValue))
);
Map<Integer, List<Map<String, Integer>>> expressionLengthMap = DUMPLICATE_MAP.get(resultStr);
if (expressionLengthMap != null) {
// 存在计算结果相同的表达式
List<Map<String, Integer>> characterCountMapList = expressionLengthMap.get(length);
if (characterCountMapList != null) {
// 存在长度相同的表达式
boolean isDuplicate = characterCountMapList.stream()
.anyMatch(map -> map.equals(characterCountMap));
if (isDuplicate) {
// 存在操作数和操作符出现次数相同的表达式
return true;
}
}
} else {
expressionLengthMap = new HashMap<>();
DUMPLICATE_MAP.put(resultStr, expressionLengthMap);
}
List<Map<String, Integer>> characterCountMapList = expressionLengthMap.computeIfAbsent(length, k -> new ArrayList<>());
characterCountMapList.add(characterCountMap);
return false;
}
}
- ExpressionUtils模块(ExpressionUtils.java)
功能描述:提供了将中缀表达式转换为后缀表达式的方法,判断字符串是否为数字或操作符的方法,以及对后缀表达式进行求值的方法,包括允许负数和不允许负数的情况。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import com.tsang.fancy_3122004841.Maths.entity.Fraction;
import java.util.Deque;
import java.util.LinkedList;
import java.util.StringTokenizer;
public class ExpressionUtils {
public static String infixToPostfix(String infix) {
StringBuilder postfix = new StringBuilder();
Deque<Character> operatorStack = new LinkedList<>();
StringTokenizer tokens = new StringTokenizer(infix, "()+-÷×", true);
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken().trim();
if (token.isEmpty()) {
continue;
}
if (isNumber(token)) {
postfix.append(token).append(' ');
} else if ("(".equals(token)) {
operatorStack.push('(');
} else if (")".equals(token)) {
while (!operatorStack.isEmpty() && !operatorStack.peek().equals('(')) {
postfix.append(operatorStack.pop()).append(' ');
}
operatorStack.pop(); // Remove '('
} else if (isOperator(token.charAt(0))) {
while (!operatorStack.isEmpty() && precedence(operatorStack.peek()) >= precedence(token.charAt(0))) {
postfix.append(operatorStack.pop()).append(' ');
}
operatorStack.push(token.charAt(0));
}
}
while (!operatorStack.isEmpty()) {
postfix.append(operatorStack.pop()).append(' ');
}
return postfix.toString().trim();
}
private static int precedence(char op) {
switch (op) {
case '+':
case '-':
return 1;
case '×':
case '÷':
return 2;
}
return -1;
}
public static boolean isNumber(String operator) {
String regex = "^\\d+'\\d+/\\d+$|^\\d+/\\d+$|^\\d+$";
return operator.matches(regex);
}
private static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '×' || c == '÷';
}
public static Fraction evaluatePostfix(String postfix) {
Deque<Fraction> stack = new LinkedList<>();
String[] tokens = postfix.split(" ");
for (String token : tokens) {
if (isNumber(token)) {
stack.push(Fraction.parseFraction(token));
} else if (isOperator(token.charAt(0))) {
Fraction operand2 = stack.pop();
Fraction operand1 = stack.pop();
Fraction result = FractionUtils.calculate(operand1, operand2, token.charAt(0));
if (result.isNegative()) {
// 负数
return null;
}
stack.push(result);
}
}
return stack.pop();
}
public static Fraction evaluatePostfixAllowNegative(String postfix) {
Deque<Fraction> stack = new LinkedList<>();
String[] tokens = postfix.split(" ");
for (String token : tokens) {
if (isNumber(token)) {
stack.push(Fraction.parseFraction(token));
} else if (isOperator(token.charAt(0))) {
Fraction operand2 = stack.pop();
Fraction operand1 = stack.pop();
Fraction result = FractionUtils.calculate(operand1, operand2, token.charAt(0));
stack.push(result);
}
}
return stack.pop();
}
}
- FileUtils模块(FileUtils.java)
功能描述:包含了一些与文件操作相关的实用方法,如验证文件名是否为有效的.txt格式、检查文件是否存在、删除已存在的文件等。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import java.io.File;
public class FileUtils {
private static final String TXT_FILE_PATTERN = "^[a-zA-Z0-9_-]+\\.txt$";
public static boolean isValidTxtFileName(String fileName) {
return fileName.matches(TXT_FILE_PATTERN);
}
public static boolean isNotValidTxtName(String fileName) {
return !isValidTxtFileName(fileName);
}
public static void deleteFileIfExists(String filePath) {
File file = new File(filePath);
if (file.exists() && file.delete()) {
System.out.println("旧题目文件已删除: " + filePath);
}
}
public static void validateFileExists(String filePath) {
if (!new File(filePath).exists()) {
throw new IllegalArgumentException("文件不存在: " + filePath);
}
}
}
- FractionUtils模块(FractionUtils.java)
功能描述:提供了生成随机真分数和随机操作数的方法,以及根据给定的操作符对两个分数进行计算的方法。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import com.tsang.fancy_3122004841.Maths.entity.Fraction;
import java.util.Random;
public class FractionUtils {
private static final Random RANDOM = new Random();
//随机生成真分数
public static Fraction generateRandomFraction(int range) {
//分子 0 ~ range-1
int numerator = RANDOM.nextInt(range);
//分母 1 ~ range-1
int denominator = RANDOM.nextInt(range - 1) + 1;
return new Fraction(numerator, denominator);
}
public static Fraction generateRandomOperand(String operator, int range) {
if (RANDOM.nextBoolean()) {
// 真分数
return generateRandomFraction(range);
} else {
// 自然数
if ("÷".equals(operator)) {
return new Fraction(RANDOM.nextInt(range - 1) + 1);
}
return new Fraction(RANDOM.nextInt(range));
}
}
public static Fraction calculate(Fraction operand1, Fraction operand2, char operator) {
switch (operator) {
case '+':
return operand1.add(operand2);
case '-':
return operand1.subtract(operand2);
case '×':
return operand1.multiply(operand2);
case '÷':
return operand1.divide(operand2);
default:
throw new IllegalArgumentException("无效的运算符");
}
}
}
- QuizGenerator模块(QuizGenerator.java)
功能描述:负责生成数学题目和答案的核心模块。包括随机生成操作符、操作符数量、数学表达式,添加随机括号,生成题目和答案文件,以及检查给定的题目文件和答案文件的正确性。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import cn.hutool.core.io.FileUtil;
import com.tsang.fancy_3122004841.Maths.entity.Fraction;
import groovy.lang.Tuple2;
import io.micrometer.common.util.StringUtils;
import java.io.File;
import java.util.*;
public class QuizGenerator {
private static final String[] OPERATORS = {"+", "-", "×", "÷"};
private static final Random RANDOM = new Random();
public static String generateRandomOperator() {
return OPERATORS[RANDOM.nextInt(OPERATORS.length)];
}
public static int generateRandomOperatorCounts() {
return RANDOM.nextInt(3) + 1;
}
public static Tuple2<List<String>, List<String>> generateQuiz(int range, int numberOfQuestions) {
int duplicateCount = 0;
int negativeCount = 0;
int totalCount = 0;
List<String> quizzes = new ArrayList<>(numberOfQuestions);
List<String> answers = new ArrayList<>(numberOfQuestions);
for(int i = 1; i <= numberOfQuestions; i++) {
int maxOperators = generateRandomOperatorCounts();
while(true) {
totalCount++;
List<String> operands = new ArrayList<>();
List<String> operators = new ArrayList<>();
for (int j = 0; j < maxOperators + 1; j++){
String lastOperator = operators.isEmpty() ? "" : operators.get(operators.size() - 1);
Fraction operand = FractionUtils.generateRandomOperand(lastOperator, range);
operands.add(operand.toString());
if (j < maxOperators) {
operators.add(generateRandomOperator());
}
}
StringBuilder quiz = new StringBuilder();
for (int j = 0; j < operands.size(); j++) {
quiz.append(operands.get(j));
if (j < operators.size()) {
quiz.append(" ").append(operators.get(j)).append(" ");
}
}
String expression = quiz.toString();
if (operands.size() > 2 && RANDOM.nextBoolean()) {
expression = addRandomParentheses(expression);
}
String postfix = ExpressionUtils.infixToPostfix(expression);
Fraction result = ExpressionUtils.evaluatePostfix(postfix);
if (Objects.nonNull(result)) {
if (!DuplicateChecker.isDuplicate(result, expression)) {
quizzes.add(i + ". " + expression);
answers.add(i + ". " + result);
break;
} else {
duplicateCount++;
}
} else {
negativeCount++;
}
}
}
System.out.println( numberOfQuestions + "道题目已生成,生成题目总次数:" + totalCount + ",重复次数(已去除该题目):" + duplicateCount + ",负数次数(已去除该题目):" + negativeCount);
return new Tuple2<>(quizzes, answers);
}
public static void generateQuizzes(int numberOfQuestions, int range) {
System.out.printf("生成题目的个数:" + numberOfQuestions + ",题目中数值的范围:0~%d(不包含%d)\n", range, range);
Tuple2<List<String>, List<String>> quizAndAnswers = QuizGenerator.generateQuiz(range, numberOfQuestions);
//当前用户目录("user.dir")(即工程根目录)
String generateExercisesFilePath = System.getProperty("user.dir") + "/Exercises.txt";
String generateAnswerFilePath = System.getProperty("user.dir") + "/Answers.txt";
// 删除文件
File exercisesFile = new File(generateExercisesFilePath);
File answerFile = new File(generateAnswerFilePath);
FileUtils.deleteFileIfExists(exercisesFile.getName());
FileUtils.deleteFileIfExists(answerFile.getName());
// 将题目和答案写入文件
FileUtil.writeUtf8Lines(quizAndAnswers.getFirst(), exercisesFile);
FileUtil.writeUtf8Lines(quizAndAnswers.getSecond(), answerFile);
System.out.println("新生成的题目问题存入执行程序的当前目录下的Exercises.txt文件,路径如下:" + generateExercisesFilePath);
System.out.println("新生成的题目答案存入执行程序的当前目录下的Exercises.txt文件,路径如下:" + generateAnswerFilePath);
}
private static String addRandomParentheses(String expression) {
String[] tokens = expression.split(" ");
StringBuilder result = new StringBuilder();
List<Integer> addSubIndices = new ArrayList<>();
for (int i = 1; i < tokens.length; i += 2) {
String operator = tokens[i];
if ("+".equals(operator) || "-".equals(operator)) {
addSubIndices.add(i);
}
}
if (!addSubIndices.isEmpty()) {
Integer index = addSubIndices.get(RANDOM.nextInt(addSubIndices.size()));
for (int i = 0; i < tokens.length; i++) {
if (i == index - 1) {
result.append("(");
}
result.append(tokens[i]);
if (i == index + 1) {
result.append(")");
} else if (i < tokens.length - 1) {
result.append(" ");
}
}
} else {
result.append(expression);
}
return result.toString();
}
public static void checkExercisesAnswers(String exercisesFileName, String answerFileName) {
if (StringUtils.isBlank(exercisesFileName) && StringUtils.isBlank(answerFileName)) {
return;
}
String exercisesFilePath = System.getProperty("user.dir") + "/" + exercisesFileName;
String answerFilePath = System.getProperty("user.dir") + "/" + answerFileName;
FileUtils.validateFileExists(exercisesFilePath);
FileUtils.validateFileExists(answerFilePath);
List<String> exercises = FileUtil.readUtf8Lines(exercisesFilePath);
List<String> answers = FileUtil.readUtf8Lines(answerFilePath);
if (exercises.size() != answers.size()) {
throw new IllegalStateException("题目和答案的数量不一致!");
}
System.out.println("开始校验题目和答案...");
List<String> rightAnswers = new ArrayList<>();
List<String> wrongAnswers = new ArrayList<>();
for (int i = 0; i < exercises.size(); i++) {
String[] parts = exercises.get(i).trim().split("\\.\\s+");
String exercise = parts[1];
String answer = answers.get(i).trim().split("\\.\\s+")[1];
String infixToPostfix = ExpressionUtils.infixToPostfix(exercise);
Fraction result = ExpressionUtils.evaluatePostfixAllowNegative(infixToPostfix);
if (Objects.equals(result.toString(), answer)) {
rightAnswers.add(String.valueOf(parts[0]));
} else {
wrongAnswers.add(String.valueOf(parts[0]));
}
}
List<String> gradeList = new ArrayList<>();
gradeList.add("Correct: " + rightAnswers.size() + "(" + String.join(", ", rightAnswers) + ")");
gradeList.add("Wrong: " + wrongAnswers.size() + "(" + String.join(", ", wrongAnswers) + ")");
FileUtil.writeUtf8Lines(gradeList, System.getProperty("user.dir") + "/Grade.txt");
System.out.println("校验完成,结果已保存至 " + System.getProperty("user.dir") + "/Grade.txt");
}
}
- ValidationUtils模块(ValidationUtils.java)
功能描述:用于验证命令行参数的合法性。从命令行参数中读取和解析特定的参数,如题目数量参数-n,题目文件参数-e和答案文件参数-a,并进行相应的合法性检查。
点击查看代码
package com.tsang.fancy_3122004841.Maths.utils;
import com.tsang.fancy_3122004841.Maths.entity.Args;
import java.util.Objects;
public class ValidationUtils {
public static Args validateArgs(String[] args) {
Args argsObj = new Args();
String exercisesFileName = null;
String answerFileName = null;
//从命令行参数读取 -n 和 -r 参数
for(int i = 0; i < args.length; i++){
if(Objects.equals("-n", args[i]) && i+1 < args.length) {
try{
int numberOfQuestions = Integer.parseInt(args[i+1]);
argsObj.setNumberOfQuestions(numberOfQuestions);
}catch (NumberFormatException e) {
throw new IllegalArgumentException("题目个数参数不合法:" + args[i+1]);
}
} else if (Objects.equals("-e", args[i]) && i+1 < args.length) {
exercisesFileName = args[i + 1];
} else if (Objects.equals("-a", args[i]) && i+1 < args.length) {
answerFileName = args[i + 1];
}
}
//验证 -e 和 -a 参数的存在性?
if ((exercisesFileName == null && answerFileName != null) || (exercisesFileName != null && answerFileName == null)){
throw new IllegalArgumentException("如果需要对给定题目文件和答案文件进行校验,则参数 -e 和 -a 必须同时给出");
}
//如果 -e 和 -a 参数都存在,校验文件名并进行答案校验
if (exercisesFileName != null) {
if(FileUtils.isNotValidTxtName(exercisesFileName)) {
throw new IllegalArgumentException("题目文件格式不正确:" + exercisesFileName + ",必须为txt文件");
}
if(FileUtils.isNotValidTxtName(answerFileName)) {
throw new IllegalArgumentException("答案文件格式不正确:" + answerFileName + ",必须为txt文件");
}
argsObj.setExercisesFileName(exercisesFileName);
argsObj.setAnswerFileName(answerFileName);
}
return argsObj;
}
}
- 接口设计
- ValidationUtils.validateArgs方法(ValidationUtils.java)
功能:接收命令行参数数组,对参数进行校验,并返回一个包含校验结果的Args对象。
输入参数:String[] args,命令行参数数组。
输出参数:Args对象,包含校验后的题目数量、题目文件和答案文件等信息。 - QuizGenerator.generateQuiz方法(QuizGenerator.java)
功能:生成指定数量和数值范围的数学题目和答案。
输入参数:int range,数值范围;int numberOfQuestions,题目数量。
输出参数:Tuple2<List, List >,包含生成的题目列表和答案列表。 - QuizGenerator.generateQuizzes方法(QuizGenerator.java)
功能:根据给定的题目数量和数值范围生成题目和答案文件。
输入参数:int numberOfQuestions,题目数量;int range,数值范围。
无特定输出参数,但会生成题目和答案文件,并在控制台输出相关信息。 - QuizGenerator.checkExercisesAnswers方法(QuizGenerator.java)
功能:检查给定的题目文件和答案文件,并生成校验结果文件。
输入参数:String exercisesFileName,题目文件名;String answerFileName,答案文件名。
无特定输出参数,但会生成校验结果文件,并在控制台输出相关信息。
三、性能分析
四、优化改进
- 代码优化方向:
- 多线程与并发优化: 题目生成与校验部分已经采用了 CompletableFuture 实现异步执行,可以在题目生成过程中添加一些进度反馈机制(例如当前生成的题目数量),提升用户体验。
- 错误处理: 当前的异常处理相对简单,主要捕获 TimeoutException 和一般的 Exception。可以考虑为不同类型的错误定制化错误信息,例如文件读取失败、题目生成失败等,增加调试信息。
- 面向对象设计: 考虑进一步引入接口和抽象类的概念,将 FractionUtils 等工具类的逻辑拆分为更小的职责单元。可以创建一个 MathOperation 接口,每个运算符实现不同的运算逻辑。
五、测试结果
运行结果:
Exercises.txt文件:
Answers.txt文件:
Grade.txt文件:
六、项目总结
我们两人合作完成了小学四则运算题目生成程序,项目成功实现了自动生成题目、控制数量与范围、存入文件及答案校验统计等功能。过程中解决了题目重复、高效处理大量题目及确保文件格式正确数量一致等问题。通过合作,我们学会团队协作,提升了 Java 编程及软件开发能力,收获颇丰,未来将继续优化性能与拓展功能。