这个作业属于哪个课程 | 计科22级12班 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-12/homework/13221 |
姓名 | 学号 |
曾繁曦 | 3122004841 |
吴健民 | 3122004667 |
PSP表格
一、流程图
二、模块设计
1.模块划分
- Main模块(Main.java)
功能描述:程序的入口点,负责接收命令行参数,进行参数校验,调用其他模块的方法来生成题目、检查答案以及处理可能出现的异常情况,如超时异常和其他一般异常。
点击查看代码
```plaintext
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());}}
}
</details>
2. DuplicateChecker模块(DuplicateChecker.java)
功能描述:用于检查生成的数学表达式是否重复。通过维护一个特定结构的映射来存储已生成的表达式的结果、长度以及每个操作数和操作符的出现次数,以判断新生成的表达式是否与已有的重复。
<details>
<summary>点击查看代码</summary>
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;}
}
</details>
3. ExpressionUtils模块(ExpressionUtils.java)
功能描述:提供了将中缀表达式转换为后缀表达式的方法,判断字符串是否为数字或操作符的方法,以及对后缀表达式进行求值的方法,包括允许负数和不允许负数的情况。
<details>
<summary>点击查看代码</summary>
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();}
}
</details>
4. FileUtils模块(FileUtils.java)
功能描述:包含了一些与文件操作相关的实用方法,如验证文件名是否为有效的.txt格式、检查文件是否存在、删除已存在的文件等。
<details>
<summary>点击查看代码</summary>
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);}}
}
</details>
5. FractionUtils模块(FractionUtils.java)
功能描述:提供了生成随机真分数和随机操作数的方法,以及根据给定的操作符对两个分数进行计算的方法。
<details>
<summary>点击查看代码</summary>
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-1int numerator = RANDOM.nextInt(range);//分母 1 ~ range-1int 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("无效的运算符");}}
}
</details>
6. QuizGenerator模块(QuizGenerator.java)
功能描述:负责生成数学题目和答案的核心模块。包括随机生成操作符、操作符数量、数学表达式,添加随机括号,生成题目和答案文件,以及检查给定的题目文件和答案文件的正确性。
<details>
<summary>点击查看代码</summary>
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");}
}
</details>
7. ValidationUtils模块(ValidationUtils.java)
功能描述:用于验证命令行参数的合法性。从命令行参数中读取和解析特定的参数,如题目数量参数-n,题目文件参数-e和答案文件参数-a,并进行相应的合法性检查。
<details>
<summary>点击查看代码</summary>
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;}
}
</details>
2.接口设计
1. ValidationUtils.validateArgs方法(ValidationUtils.java)
功能:接收命令行参数数组,对参数进行校验,并返回一个包含校验结果的Args对象。
输入参数:String[] args,命令行参数数组。
输出参数:Args对象,包含校验后的题目数量、题目文件和答案文件等信息。
2. QuizGenerator.generateQuiz方法(QuizGenerator.java)
功能:生成指定数量和数值范围的数学题目和答案。
输入参数:int range,数值范围;int numberOfQuestions,题目数量。
输出参数:Tuple2<List<String>, List<String>>,包含生成的题目列表和答案列表。
3. QuizGenerator.generateQuizzes方法(QuizGenerator.java)
功能:根据给定的题目数量和数值范围生成题目和答案文件。
输入参数:int numberOfQuestions,题目数量;int range,数值范围。
无特定输出参数,但会生成题目和答案文件,并在控制台输出相关信息。
4. QuizGenerator.checkExercisesAnswers方法(QuizGenerator.java)
功能:检查给定的题目文件和答案文件,并生成校验结果文件。
输入参数:String exercisesFileName,题目文件名;String answerFileName,答案文件名。
无特定输出参数,但会生成校验结果文件,并在控制台输出相关信息。##三、性能分析
![](https://img2024.cnblogs.com/blog/3512942/202409/3512942-20240928215116850-1867623322.png)##四、优化改进
* 代码优化方向:
1. 多线程与并发优化: 题目生成与校验部分已经采用了 CompletableFuture 实现异步执行,可以在题目生成过程中添加一些进度反馈机制(例如当前生成的题目数量),提升用户体验。
2. 错误处理: 当前的异常处理相对简单,主要捕获 TimeoutException 和一般的 Exception。可以考虑为不同类型的错误定制化错误信息,例如文件读取失败、题目生成失败等,增加调试信息。
* 面向对象设计: 考虑进一步引入接口和抽象类的概念,将 FractionUtils 等工具类的逻辑拆分为更小的职责单元。可以创建一个 MathOperation 接口,每个运算符实现不同的运算逻辑。##五、测试结果
运行结果:
![](https://img2024.cnblogs.com/blog/3512942/202409/3512942-20240928220019584-1770128857.png)
Exercises.txt文件:
![](https://img2024.cnblogs.com/blog/3512942/202409/3512942-20240928220117912-694444517.png)
Answers.txt文件:
![](https://img2024.cnblogs.com/blog/3512942/202409/3512942-20240928220141267-1133094941.png)
Grade.txt文件:
![](https://img2024.cnblogs.com/blog/3512942/202409/3512942-20240928220208269-1088567821.png)##六、项目总结
我们两人合作完成了小学四则运算题目生成程序,项目成功实现了自动生成题目、控制数量与范围、存入文件及答案校验统计等功能。过程中解决了题目重复、高效处理大量题目及确保文件格式正确数量一致等问题。通过合作,我们学会团队协作,提升了 Java 编程及软件开发能力,收获颇丰,未来将继续优化性能与拓展功能。