结对项目

结对项目

这个作业属于哪个课程 班级链接
这个作业要求在哪里 结对项目 - 作业 - 计科22级12班 - 班级博客 - 博客园 (cnblogs.com)
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序
姓名 学号
韩其锟 3122004348

GitHub

https://github.com/chocohQL/3122004348-02

PSP

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10 10
· Estimate · 估计这个任务需要多少时间 10 10
Development 开发 400 490
· Analysis · 需求分析 (包括学习新技术) 30 20
· Design Spec · 生成设计文档 10 10
· Design Review · 设计复审 10 5
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 5
· Design · 具体设计 60 80
· Coding · 具体编码 200 300
· Code Review · 代码复审 20 10
· Test · 测试(自我测试,修改代码,提交修改) 60 60
Reporting 报告 30 40
· Test Repor · 测试报告 20 25
· Size Measurement · 计算工作量 5 5
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 5 10
合计 440 540

运行结果

生成一万条题目:

生成10条题目,随机修改3个错误答案,进行判定统计:

系统设计

系统核心的设计思路: Application 类负责整体流程运行,下层抽象出一个 ProblemGenerator 负责具体题目和答案的生成,将题目表达式和答案封装到 Problem 实体类中。由于需要进行分数的计算,将每个数统一使用 Fraction 表示,实体类内部提供分数的加减乘除运算,使用构造函数创建分数自动进行化简,重写 toString 方法生成字符串。生成答案的思路是将表达式解析为操作数栈和数值栈,不断弹栈进行分数计算,并保证操作优先级。底层封装 FileUtil 工具类提供文件输入输出方法。

调用链:

  1. Main 主函数:启动程序。

  2. Application 程序:运行程序、解析参数、选择方法、调用 ProblemGenerator 生成题目、判定答案、调用 FileUtil 输入输出文件。

  3. ProblemGenerator 问题生成器:生成具体问题和答案、判定答案等。

  4. FileUtil 文件工具类:负责输入输出字符串行到指定文件中。

实体类:

  • Problem 问题:记录表达式 exercises 和答案 answers 。
  • Fraction 分数:进行分数的加减乘除计算,构造函数传入分子和分母自动进行简化。

具体实现

Application

public class Application {
    /**
     * 启动程序
     */
    public static void run(String[] args) {
        try {
            Map<String, String> params = parseParams(args);
            if (params.get("-r") != null && params.get("-n") != null) {
                generateProblems(params.get("-n"), params.get("-r"));
            } else if (params.get("-e") != null && params.get("-a") != null) {
                judgeProblems(params.get("-e"), params.get("-a"));
            } else {
                throw new RuntimeException("请检查参数 [-r][-n] | [-e][-a]");
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    /**
     * 解析参数
     */
    public static Map<String, String> parseParams(String[] args) {
        if (args.length != 4) {
            throw new RuntimeException("无法解析参数");
        }
        Map<String, String> params = new HashMap<>();
        for (int i = 0; i < args.length; i += 2) {
            params.put(args[i], args[i + 1]);
        }
        return params;
    }

    /**
     * 生成问题
     */
    public static void generateProblems(String n, String r) {
        int num = Integer.parseInt(n);
        int range = Integer.parseInt(r);
        if (num < 0 || range < 0) {
            throw new RuntimeException("参数必须为自然数");
        }
        num = Math.min(10000, num);
        List<Problem> problems = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            // 生成题目
            problems.add(ProblemGenerator.generateProblem(range));
        }
        // 输出结果
        List<String> exercises = new ArrayList<>();
        List<String> answers = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            exercises.add(i + 1 + ". " + problems.get(i).exercises);
            answers.add(i + 1 + ". " + problems.get(i).answers);
        }
        FileUtil.writeFile("Exercises.txt", exercises);
        FileUtil.writeFile("Answers.txt", answers);
    }

    /**
     * 判定答案
     */
    public static void judgeProblems(String e, String a) {
        List<String> exerciseFile = FileUtil.readFile(e);
        List<String> answerFile = FileUtil.readFile(a);
        int correct = 0;
        int wrong = 0;
        List<String> correctIndex = new ArrayList<>();
        List<String> gradeIndex = new ArrayList<>();
        for (int i = 0; i < exerciseFile.size(); i++) {
            try {
                String[] exercise = exerciseFile.get(i).split("\\. ");
                String[] answer = answerFile.get(i).split("\\. ");
                // 判定答案
                if (ProblemGenerator.judgeProblem(exercise[1], answer[1])) {
                    correctIndex.add(exercise[0]);
                    correct++;
                } else {
                    gradeIndex.add(exercise[0]);
                    wrong++;
                }
            } catch (Exception ex) {
                throw new RuntimeException("无法解析表达式");
            }
        }
        // 输出统计结果
        List<String> grade = new ArrayList<>();
        grade.add("Correct:" + correct + " (" + String.join(", ", correctIndex) + ")");
        grade.add("Wrong:" + wrong + " (" + String.join(", ", gradeIndex) + ")");
        FileUtil.writeFile("Grade.txt", grade);
    }
}

Problem

public class Problem {
    public String exercises;
    public String answers;

    public Problem(String exercises, String answers) {
        this.exercises = exercises;
        this.answers = answers;
    }
}

FileUtil

public class FileUtil {
    public static List<String> readFile(String filePath) {
        List<String> lines = new ArrayList<>();
        File file = new File(filePath);
        if (!file.exists()) {
            throw new RuntimeException("无法加载文件");
        }
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            throw new RuntimeException("文件读取失败");
        }
        return lines;
    }

    public static void writeFile(String filePath, List<String> lines) {
        File file = new File(filePath);
        try (FileWriter writer = new FileWriter(file)) {
            for (String line : lines) {
                writer.write(line + "\n");
            }
        } catch (IOException e) {
            throw new RuntimeException("文件写入失败");
        }
    }
}

Fraction

public class Fraction {
    private int numerator;
    private int denominator;

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
        simplify();
    }

    public int getNumerator() {
        return numerator;
    }

    public int getDenominator() {
        return denominator;
    }

    public Fraction add(Fraction other) {
        return new Fraction(this.numerator * other.denominator + other.numerator * this.denominator,
                this.denominator * other.denominator);
    }

    public Fraction subtract(Fraction other) {
        return new Fraction(this.numerator * other.denominator - other.numerator * this.denominator,
                this.denominator * other.denominator);
    }

    public Fraction multiply(Fraction other) {
        return new Fraction(this.numerator * other.numerator,
                this.denominator * other.denominator);
    }

    public Fraction divide(Fraction other) {
        return new Fraction(this.numerator * other.denominator,
                this.denominator * other.numerator);
    }

    private void simplify() {
        int gcd = gcd(numerator, denominator);
        numerator /= gcd;
        denominator /= gcd;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public int compareTo(Fraction other) {
        return Integer.compare(this.numerator * other.denominator, other.numerator * this.denominator);
    }

    @Override
    public String toString() {
        if (numerator == 0) {
            return "0";
        }
        if (Math.abs(numerator) < denominator) {
            return numerator + "/" + denominator;
        }
        int intPart = numerator / denominator;
        int numerator1 = Math.abs(numerator) % denominator;
        return numerator1 == 0 ? String.valueOf(intPart) : intPart + "'" + numerator1 + "/" + denominator;
    }
}

ProblemGenerator

public class ProblemGenerator {
    private static final char[] OPERATORS = {'+', '-', '×', '÷'};
    private static final Random RANDOM = new Random();

    /**
     * 生成题目
     */
    public static Problem generateProblem(int r) {
        String exercises = generateExercises(r);
        String answer = calculateAnswer(exercises);
        // 循环直到题目符合要求
        while (answer == null) {
            exercises = generateExercises(r);
            answer = calculateAnswer(exercises);
        }
        return new Problem(exercises, answer);
    }

    /**
     * 判定答案
     */
    public static boolean judgeProblem(String exercises, String answer) {
        return Objects.equals(calculateAnswer(exercises), answer);
    }

    /**
     * 生成表达式
     */
    public static String generateExercises(int r) {
        StringBuilder sb = new StringBuilder();
        sb.append(generateNumber(r));
        for (int i = 0; i < RANDOM.nextInt(3) + 1; i++) {
            sb.append(" ").append(generateOperator()).append(" ").append(generateNumber(r));
        }
        return sb.toString();
    }

    /**
     * 生成运算符
     */
    private static String generateOperator() {
        return String.valueOf(OPERATORS[new Random().nextInt(OPERATORS.length)]);
    }

    /**
     * 判断运算符
     */
    private static boolean isOperator(char c) {
        for (char operator : OPERATORS) {
            if (operator == c) {
                return true;
            }
        }
        return false;
    }

    /**
     * 生成数字
     */
    private static String generateNumber(int r) {
        return new Fraction(
                RANDOM.nextInt(r - 1) + 1,
                RANDOM.nextInt(r - 1) + 1
        ).toString();
    }

    /**
     * 计算答案
     */
    private static String calculateAnswer(String exercises) {
        Stack<Fraction> numStack = new Stack<>();
        Stack<Character> operator = new Stack<>();
        // 加入操作栈
        for (String str : exercises.split(" ")) {
            if (str.length() == 1 && isOperator(str.charAt(0))) {
                operator.push(str.charAt(0));
            } else {
                // 转为分数计算
                numStack.push(getFraction(str));
            }
        }
        while (!operator.isEmpty()) {
            char op = operator.pop();
            Fraction num2 = numStack.pop();
            Fraction num1 = numStack.pop();
            if (op == '+' || op == '-') {
                // 优先计算乘除
                if (!operator.isEmpty() && (operator.peek() == '×' || operator.peek() == '÷')) {
                    Fraction num3 = numStack.pop();
                    Character op1 = operator.pop();
                    if (op1 == '×') {
                        num1 = num1.multiply(num3);
                    } else if (op1 == '÷') {
                        if (num3.getNumerator() == 0) {
                            return null;
                        }
                        num1 = num1.divide(num3);
                    }
                }
                if (op == '+') {
                    numStack.push(num1.add(num2));
                } else {
                    // 如果存在形如e1− e2的子表达式,那么e1≥e2
                    if (num1.compareTo(num2) >= 0) {
                        numStack.push(num1.subtract(num2));
                    } else {
                        return null;
                    }
                }
            } else if (op == '×') {
                numStack.push(num1.multiply(num2));
            } else if (op == '÷') {
                if (num2.getNumerator() == 0) {
                    return null;
                }
                numStack.push(num1.divide(num2));
            }
        }
        return numStack.size() == 1 ? numStack.pop().toString() : null;
    }

    /**
     * 转换为分数
     */
    private static Fraction getFraction(String str) {
        if (str.contains("'")) {
            String[] parts = str.split("[/']");
            int intPart = Integer.parseInt(parts[0]);
            int numerator = Integer.parseInt(parts[1]);
            int denominator = Integer.parseInt(parts[2]);
            numerator += intPart * denominator;
            return new Fraction(numerator, denominator);
        }
        if (str.contains("/")) {
            String[] parts = str.split("/");
            return new Fraction(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
        }
        return new Fraction(Integer.parseInt(str), 1);
    }
}

项目测试

测试用例

public class MainTest {
    @Test
    public void test1() {
        Main.main(new String[]{});
    }

    @Test
    public void test2() {
        Main.main(new String[]{"-n", "10", "-r", "10"});
    }

    @Test
    public void test3() {
        Main.main(new String[]{"-n", "-1", "-r", "10"});
    }

    @Test
    public void test4() {
        Main.main(new String[]{"-n", "10005", "-r", "10"});
    }

    @Test
    public void test5() {
        Main.main(new String[]{"-e", "e.txt", "-a", "a.txt"});
    }

    @Test
    public void test6() {
        Main.main(new String[]{"-e", "e", "-a", "a"});
    }

    @Test
    public void test7() {
        Main.main(new String[]{"-e", "Exercises.txt", "-a", "Answers.txt"});
    }

    @Test
    public void test8() {
        Main.main(new String[]{"-e", "Exercises.txt", "-a", "ErrorAnswers.txt"});
    }

    @Test
    public void test9() {
        Main.main(new String[]{"-e", "Exercises.txt", "-b", "ErrorAnswers.txt"});
    }

    @Test
    public void test10() {
        Main.main(new String[]{"-n", "10", "-f", "10"});
    }
}

覆盖率测试

除文件输入输出流异常外覆盖了项目所有分支。

性能测试

选择生成一万条题目进行性能测试,主要耗时在于文件写入、问题生成和答案生成。

项目小结

本次项目运用软件系统设计各方面知识,包括选择合理的算法、使用分治思想抽象系统模块、使用面向对象编程抽象题目和分数、将系统划分为多层进行解耦合、编写测试接口并进行性能分析等。此次任务能够增强我软件开发的能力并提高我系统设计的综合能力。

posted @ 2024-09-27 20:53  chocoh  阅读(26)  评论(0编辑  收藏  举报