结对项目:自动生成小学四则运算题目的命令行程序

这个作业属于哪个课程 计科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来比较两个题目表达式是否有重复流程:

二、模块设计

  • 模块划分
  1. 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());
        }
    }
}
  1. 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;
    }
}
  1. 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();
    }
}
  1. 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);
        }
    }
}
  1. 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("无效的运算符");
        }
    }
}
  1. 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");

    }
}
  1. 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;
    }
}
  • 接口设计
  1. ValidationUtils.validateArgs方法(ValidationUtils.java)
    功能:接收命令行参数数组,对参数进行校验,并返回一个包含校验结果的Args对象。
    输入参数:String[] args,命令行参数数组。
    输出参数:Args对象,包含校验后的题目数量、题目文件和答案文件等信息。
  2. QuizGenerator.generateQuiz方法(QuizGenerator.java)
    功能:生成指定数量和数值范围的数学题目和答案。
    输入参数:int range,数值范围;int numberOfQuestions,题目数量。
    输出参数:Tuple2<List, List>,包含生成的题目列表和答案列表。
  3. QuizGenerator.generateQuizzes方法(QuizGenerator.java)
    功能:根据给定的题目数量和数值范围生成题目和答案文件。
    输入参数:int numberOfQuestions,题目数量;int range,数值范围。
    无特定输出参数,但会生成题目和答案文件,并在控制台输出相关信息。
  4. QuizGenerator.checkExercisesAnswers方法(QuizGenerator.java)
    功能:检查给定的题目文件和答案文件,并生成校验结果文件。
    输入参数:String exercisesFileName,题目文件名;String answerFileName,答案文件名。
    无特定输出参数,但会生成校验结果文件,并在控制台输出相关信息。

三、性能分析

四、优化改进

  • 代码优化方向:
  1. 多线程与并发优化: 题目生成与校验部分已经采用了 CompletableFuture 实现异步执行,可以在题目生成过程中添加一些进度反馈机制(例如当前生成的题目数量),提升用户体验。
  2. 错误处理: 当前的异常处理相对简单,主要捕获 TimeoutException 和一般的 Exception。可以考虑为不同类型的错误定制化错误信息,例如文件读取失败、题目生成失败等,增加调试信息。
  • 面向对象设计: 考虑进一步引入接口和抽象类的概念,将 FractionUtils 等工具类的逻辑拆分为更小的职责单元。可以创建一个 MathOperation 接口,每个运算符实现不同的运算逻辑。

五、测试结果

运行结果:

Exercises.txt文件:

Answers.txt文件:

Grade.txt文件:

六、项目总结

我们两人合作完成了小学四则运算题目生成程序,项目成功实现了自动生成题目、控制数量与范围、存入文件及答案校验统计等功能。过程中解决了题目重复、高效处理大量题目及确保文件格式正确数量一致等问题。通过合作,我们学会团队协作,提升了 Java 编程及软件开发能力,收获颇丰,未来将继续优化性能与拓展功能。

posted @ 2024-09-28 22:28  天建戈  阅读(27)  评论(0编辑  收藏  举报