结对项目

这个作业属于哪个课程 [计科22级34班 ]——https://edu.cnblogs.com/campus/gdgy/CSGrade22-34
这个作业要求在哪里 [结对项目]——https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230
这个作业的目标 使用Java完成四则运算生成器,在实现项目的过程中和运用软件工程的知识进行学习开发,学会使用PSP表格,使用github提交项目,进行单元测试与问题处理。
成员 3122004531李晓彬 3122004539谭传兴

[个人项目github仓库]——https://github.com/aBin-L1/-

一、题目:

​ 实现一个自动生成小学四则运算题目的命令行程序,同时支持图像界面,功能与命令行程序相似。

  1. 算术表达式与符号定义

    • 自然数:如0, 1, 2, …。
    • 真分数:如1/2, 1/3, 2/3, 1/4, 1’1/2等。
    • 运算符:+, −, ×, ÷。
    • 括号:圆括号 ( )。
    • 等号:=。
    • 分隔符:空格(用于运算符和等号前后)。
  2. 四则运算题目格式

    • 题目形式:e = ,其中e为算术表达式。
    • 表达式规则:e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),其中n为自然数或真分数,e1、e2为表达式。
  3. 程序功能要求

    • 生成题目个数:使用 -n 参数控制生成题目的数量。例如:Myapp.exe -n 10 表示生成10个题目。
    • 数值范围控制:使用 -r 参数控制题目中数值的范围(自然数、真分数和真分数分母)。例如:Myapp.exe -r 10 表示生成数值在10以内(不包括10)的四则运算题目。该参数必须提供,否则程序报错并给出帮助信息。
    • 计算过程约束:
      • 不允许产生负数:e1 - e2 中应保证 e1 ≥ e2。
      • 除法结果要求为真分数:e1 ÷ e2 的结果应为真分数。
      • 每道题目的运算符个数不超过3个。
    • 题目去重:
      • 程序生成的题目不能重复。对于相同运算顺序的题目,即使交换了运算符的位置,依然视为重复。
      • 特殊情况下,如加法结合律下,1 + 2 + 3 和 3 + 2 + 1 视为不同题目。
    • 输出:
      • 生成的题目存储在 Exercises.txt 文件中。
      • 题目答案存储在 Answers.txt 文件中。
  4. 特殊处理

    • 真分数的输入输出格式:如真分数五分之三表示为3/5,二又八分之三表示为2’3/8。
    • 真分数运算的示例:1/6 + 1/8 = 7/24。
  5. 扩展功能

    • 程序能支持生成一万道题目。

    • 答案验证与统计:对给定的题目和答案文件进行答案正确性的判定,并进行统计。

      使用参数

      -e <> -a <>
      

      分别指定题目文件和答案文件。例如:

      Myapp.exe -e exercises.txt -a answers.txt
      
      • 统计结果输出到 Grade.txt 文件中,格式为:Correct: 5 (1, 3, 5, 7, 9)Wrong: 5 (2, 4, 6, 8, 10)

二、PSP表

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

三、设计实现过程

1.程序功能需求分析

  • 生成算术题目:程序应能够生成指定数量的算术题目,题目的操作数可以是自然数、真分数或带分数,运算符包括加法、减法、乘法和除法。

  • 检查答案:程序应能够检查用户提供的答案文件,并将检查结果记录在文件中。

  • 文件管理:程序需要读写多个文件,包括存储题目、答案和检查结果的文件。

2.主要功能模块划分

  • 主程序模块(Main 类):负责解析命令行参数,并调用相应的功能。

  • 题目生成模块(ProblemGenerator 类):生成随机算术题并将其存储到文件中。

  • 表达式求值模块(ExpressionEvaluator 类):负责计算生成的算术表达式的值。

  • 分数处理模块(Fraction 类):用于处理和运算涉及分数的算术表达式。

  • 文件管理模块(FileManager 类):负责与文件系统的交互,处理文件的读写操作。

  • 答案检查模块(AnswerChecker 类):检查用户提交的答案是否正确。

3. 类的设计与实现

3.1 主程序模块(Main 类)
  • 职责:解析命令行参数,根据用户的输入调用不同的功能模块。
  • 主要方法
    • main(String[] args):主入口方法,负责解析命令行参数,并根据参数调用题目生成或答案检查功能。
    • showUsage():辅助方法,用于当用户输入参数有误时,展示正确的使用方法。
3.2 题目生成模块(ProblemGenerator 类)
  • 职责:生成指定数量的随机算术题,确保题目不重复且结果有效(非负)。
  • 主要方法:
    • generateProblems():生成指定数量的题目,并将其写入文件。
    • generateRandomProblem(Random random):生成随机算术题。
    • generateSubExpression(Random random, int numOperators):生成含有多个操作数和运算符的子表达式。
    • generateRandomOperand(Random random):生成随机操作数,包括自然数、真分数和带分数。
    • generateRandomOperator(Random random):随机生成运算符。
    • simplifyFraction(int numerator, int denominator):化简分数。
    • gcd(int a, int b):计算最大公约数,用于分数化简。

3.3 表达式求值模块(ExpressionEvaluator 类)

  • 职责:将中缀表达式转换为后缀表达式,并计算表达式的结果。
  • 主要方法:
    • evaluate(String expression):评估表达式,返回结果。
    • infixToPostfix(String infix):将中缀表达式转换为后缀表达式。
    • evaluatePostfix(String postfix):计算后缀表达式的值。
    • isOperator(char c):判断字符是否为运算符。
    • precedence(char c):确定运算符的优先级。
    • calculate(Fraction a, Fraction b, char operator):根据运算符计算两个分数的运算结果。
3.4 分数处理模块(Fraction 类)
  • 职责:表示分数,并提供分数的四则运算。
  • 主要方法
    • Fraction(int numerator, int denominator):构造函数,创建分数并化简。
    • fromString(String str):从字符串解析分数,包括带分数和真分数。
    • add(Fraction other):分数加法。
    • subtract(Fraction other):分数减法。
    • multiply(Fraction other):分数乘法。
    • divide(Fraction other):分数除法。
    • gcd(int a, int b):计算最大公约数,用于分数化简。
    • toString():将分数转换为字符串表示,可能是整数、真分数或带分数形式。
3.5 文件管理模块(FileManager 类)
  • 职责:管理与文件系统的交互,处理题目、答案和成绩的读写操作。
  • 主要方法
    • clearFile(String fileName):清空指定文件的内容。
    • writeProblem(String problem):将题目写入题目文件。
    • writeAnswer(String answer):将答案写入答案文件。
    • writeGrade(List<Integer> correct, List<Integer> wrong):将成绩写入成绩文件。
    • readFile(String fileName):读取指定文件的内容。
3.6 答案检查模块(AnswerChecker 类)
  • 职责:检查用户提交的答案文件是否正确,并将检查结果写入文件。
  • 主要方法
    • checkAnswers():对比用户提供的答案与程序计算的正确答案,记录正确和错误的题目编号。
    • readFile(String fileName):读取文件内容,用于加载题目和答案。
4. 类之间的关系
  • Main 类:作为程序的入口,解析命令行参数,决定调用 ProblemGenerator 还是 AnswerChecker
  • ProblemGenerator 类:负责生成题目,并通过 FileManager 将题目和答案写入文件,同时依赖 ExpressionEvaluator 评估题目的正确答案。
  • ExpressionEvaluator 类:作为独立的工具类,为 ProblemGeneratorAnswerChecker 提供表达式求值功能。
  • Fraction 类:为 ExpressionEvaluator 提供基础分数计算支持。
  • FileManager 类:独立管理文件读写操作,为 ProblemGeneratorAnswerChecker 提供服务。
  • AnswerChecker 类:依赖 FileManager 读取题目和答案文件,依赖 ExpressionEvaluator 评估用户提供的答案。

image-20240929005134289

五、代码说明

1.ProblemGenerator类中的生成子表达式方法

private String generateSubExpression(Random random, int numOperators) {
    List<String> operands = new ArrayList<>();
    List<String> operators = new ArrayList<>();

    // 生成第一个操作数并初始化当前结果
    String currentOperand = generateRandomOperand(random);
    double currentResult = evaluateOperand(currentOperand);
    operands.add(currentOperand);

    for (int i = 0; i < numOperators; i++) {
        String operator = generateRandomOperator(random);
        String nextOperand = generateRandomOperand(random);
        double nextValue = evaluateOperand(nextOperand);

        // 调整运算符和操作数以确保不产生负数
        if (operator.equals("-")) {
            if (currentResult < nextValue) {
                // 如果减法会导致负数,交换操作数和符号
                operator = "+";
            }
        } else if (operator.equals("/")) {
            if (nextValue == 0 || currentResult < nextValue) {
                // 确保除数不为零,且避免分子小于分母导致结果小于1
                operator = "*";
            }
        }

        // 更新当前结果
        currentResult = calculateNewResult(currentResult, nextValue, operator);

        operators.add(operator);
        operands.add(nextOperand);
    }

    // 随机决定是否将某些操作数和运算符包围在括号中,确保括号不包围整个表达式
    if (numOperators > 1) {
        int startIdx = random.nextInt(numOperators); // 括号的起始位置
        int endIdx = startIdx + random.nextInt(numOperators - startIdx) + 1; // 括号的结束位置

        if (startIdx > 0 || endIdx < numOperators) {
            operands.set(startIdx, "(" + operands.get(startIdx));
            operands.set(endIdx, operands.get(endIdx) + ")");
        }
    }

    // 拼接表达式
    StringBuilder subExpression = new StringBuilder();
    for (int i = 0; i < numOperators; i++) {
        subExpression.append(operands.get(i)).append(" ").append(operators.get(i)).append(" ");
    }
    subExpression.append(operands.get(numOperators));

    return subExpression.toString();
}

操作数和运算符:首先,生成第一个操作数,并将其存入 operands 列表。然后,循环生成剩余的运算符和操作数。生成每个操作数后,立即评估其数值,并根据数值判断是否需要调整运算符,以防止出现负数或无效除法(如除以零)。

运算符调整:对于减法操作,如果当前结果小于下一个操作数,则将减法改为加法,避免产生负数。同样地,对于除法操作,如果除数为零或当前结果小于下一个操作数,则将除法改为乘法,避免无效结果。

括号处理:为了增加表达式的复杂性,随机决定是否在表达式的某部分添加括号。这通过随机生成的起始和结束位置来实现,但括号不会包围整个表达式。

表达式拼接:最后,将生成的操作数和运算符拼接成一个完整的表达式。

2.ProblemGenerator类中的生成子表达式方法

private String generateRandomOperand(Random random) {
    int type = random.nextInt(3); // 调整为3种类型:自然数、真分数、带分数
    switch (type) {
        case 0: // 生成自然数
            return String.valueOf(random.nextInt(range) + 1);
        case 1: // 生成真分数并化简
            int numerator, denominator;
            do {
                numerator = random.nextInt(range - 1) + 1;
                denominator = random.nextInt(range - 1) + 2; // 确保分母大于分子,且分母最小为2
            } while (numerator >= denominator); // 重复生成直到分子小于分母
            return simplifyFraction(numerator, denominator);
        case 2: // 生成带分数并化简真分数部分
            int wholeNumber = random.nextInt(range - 1) + 1; // 整数部分的最大值由 range 决定
            do {
                numerator = random.nextInt(range - 1) + 1;
                denominator = random.nextInt(range - 1) + 2; // 确保分母大于分子,且分母最小为2
            } while (numerator >= denominator); // 重复生成直到分子小于分母
            String simplifiedFraction = simplifyFraction(numerator, denominator);
            return wholeNumber + "'" + simplifiedFraction;
        default:
            return String.valueOf(random.nextInt(range) + 1);
    }
}

生成三种类型的操作数:操作数可以是自然数、真分数或带分数。根据 random.nextInt(3) 的结果决定生成哪种类型。

  • 自然数:生成范围内的随机自然数。
  • 真分数:通过生成随机的分子和分母,并确保分子小于分母,同时对生成的分数进行化简。
  • 带分数:生成一个随机的整数部分和一个真分数部分,并确保真分数部分是化简后的形式。

分数化简:调用 simplifyFraction 方法,将生成的分数进行化简,确保结果是最简形式。

3.ExpressionEvaluator类

import java.util.Stack;

public class ExpressionEvaluator {

    public String evaluate(String expression) {
        try {
            // 将表达式转化为后缀表达式
            String postfix = infixToPostfix(expression);
            // 计算后缀表达式的值
            String result = evaluatePostfix(postfix);
            return result;
        } catch (Exception e) {
            return null;
        }
    }

    private String infixToPostfix(String infix) {
        // 将中缀表达式转化为后缀表达式
        Stack<Character> stack = new Stack<>();
        StringBuilder postfix = new StringBuilder();

        for (char c : infix.toCharArray()) {
            if (Character.isDigit(c) || c == '/' || c == '\'') {
                postfix.append(c);
            } else if (c == '(') {
                stack.push(c);
            } else if (c == ')') {
                while (!stack.isEmpty() && stack.peek() != '(') {
                    postfix.append(' ').append(stack.pop());
                }
                stack.pop();
            } else if (isOperator(c)) {
                while (!stack.isEmpty() && precedence(c) <= precedence(stack.peek())) {
                    postfix.append(' ').append(stack.pop());
                }
                postfix.append(' ');
                stack.push(c);
            }
        }

        while (!stack.isEmpty()) {
            postfix.append(' ').append(stack.pop());
        }

        return postfix.toString();
    }

    private boolean isOperator(char c) {
        return c == '+' || c == '-' || c == '*' || c == '/';
    }

    private int precedence(char c) {
        switch (c) {
            case '+':
            case '-':
                return 1;
            case '*':
            case '/':
                return 2;
            default:
                return -1;
        }
    }

    private String evaluatePostfix(String postfix) {
        // 计算后缀表达式的值
        Stack<Fraction> stack = new Stack<>();
        String[] tokens = postfix.split(" ");

        for (String token : tokens) {
            if (isOperator(token.charAt(0))) {
                Fraction b = stack.pop();
                Fraction a = stack.pop();
                stack.push(calculate(a, b, token.charAt(0)));
            } else {
                stack.push(Fraction.fromString(token));
            }
        }

        return stack.pop().toString();
    }

    private Fraction calculate(Fraction a, Fraction b, char operator) {
        switch (operator) {
            case '+':
                return a.add(b);
            case '-':
                return a.subtract(b);
            case '*':
                return a.multiply(b);
            case '/':
                return a.divide(b);
            default:
                return new Fraction(0, 1);
        }
    }
}

ExpressionEvaluator 类是程序中的一个关键组件,用于处理和评估算术表达式的值。它的主要功能包括将中缀表达式转换为后缀表达式(逆波兰表达式),然后根据后缀表达式计算最终的结果。这个类的设计主要是为了支持算术题目的生成和答案检查功能。

该类的结构与主要方法
1. evaluate(String expression)
  • 功能:这是 ExpressionEvaluator 类的核心方法,用于计算输入的算术表达式的值。
  • 逻辑:
    • 首先将中缀表达式转换为后缀表达式(使用 infixToPostfix() 方法)。
    • 然后使用后缀表达式计算表达式的最终结果(使用 evaluatePostfix() 方法)。
    • 如果表达式不合法或计算过程中出现错误,返回 null 表示计算失败。

2. infixToPostfix(String infix)

  • 功能:将中缀表达式转换为后缀表达式。
  • 逻辑:
    • 使用一个栈来处理运算符的优先级和括号。
    • 依次扫描表达式中的每个字符,将操作数直接添加到后缀表达式中,对于运算符则根据其优先级进行栈的操作。
    • 确保括号内的运算符正确地按顺序出现在后缀表达式中。
    • 在扫描完表达式后,将栈中剩余的运算符依次弹出并添加到后缀表达式。

3. evaluatePostfix(String postfix)

  • 功能:根据后缀表达式计算表达式的值。
  • 逻辑:
    • 使用一个栈来处理操作数。
    • 依次扫描后缀表达式的每个令牌(token),如果是操作数就压入栈中,如果是运算符则弹出栈顶的两个操作数进行计算。
    • 计算结果再次压入栈中,直到表达式扫描完成,栈顶元素即为最终结果。

4. calculate(Fraction a, Fraction b, char operator)

  • 功能:执行具体的加减乘除运算。
  • 逻辑:
    • 根据传入的运算符,对两个分数进行相应的运算操作。
    • 使用 Fraction 类的加法、减法、乘法和除法方法,返回运算结果。

5. 辅助方法

  • isOperator(char c):判断一个字符是否为运算符(+-*/)。
  • precedence(char c):返回运算符的优先级。乘法和除法优先级较高,加法和减法优先级较低。

六、测试运行

  • 1.使用命令
java -jar demo.jar -n 10 -r 10

Exercises.txt文件存放生成的表达式,表达式有1~3个运算符,算数有三种类型。

Answers.txt文件生成表达式的答案

  • 2.使用命令
java -jar demo.jar -e Exercises.txt -a Answers.txt

题目对错的结果存入Grade.txt文件中,此时Answers.txt中的文件没有经过更改,答案是全对的。

现在将Answers.txt中的答案进行更改,可以看到更改后运行批改命令,Grade.txt生成相应的答案对错信息。

  • 3.命令参数输入不正常,异常处理

  • 4.生成10000到题目,运行的时候需要点时间

性能分析:

覆盖率:

生成1000道题目时性能状况。

七、项目小结

在这个结对项目中,我们的目标是开发一个能够自动生成和检查四则运算的命令行工具。项目初期,我们明确了每个人的角色分工,虽然一开始进展顺利,但很快我们就遇到了一个棘手的问题:在处理复杂的带分数运算时,表达式评估的准确性一直无法达到我们的预期。这个问题让我们犯了难,几乎让项目进度停滞。不过经过我们的讨论还是找到的解决的办法。这次作业让我们体会到了团队开发项目是一件不简单的事情,开发过程中需要进行有效的沟通才能推进项目。这次的作业也是收获满满,对团队开发的认识又多了一些。

posted @ 2024-09-29 00:01  aBin_L  阅读(18)  评论(0编辑  收藏  举报