结对项目

这个作业属于哪个课程 课程
这个作业的要求在哪里 结对项目
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序

一、合作成员

项目成员 学号 github仓库地址
黄锐 3222004335 github
王伊若 3222004382
PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
Estimate 估计这个任务需要多少时间 45 45
Development 开发 120 100
Analysis 需求分析 (包括学习新技术) 60 55
Design Spec 生成设计文档 90 75
Design Review 设计复审 (和同事审核设计文档) 45 40
Coding Standard 代码规范 (为目前的开发制定合适的规范) 15 15
Design 具体设计 30 30
Coding 具体编码 240 210
Code Review 代码复审 15 15
Test 测试(自我测试,修改代码,提交修改) 30 30
Reporting 报告 60 50
Test Report 测试报告 30 20
Size Measurement 计算工作量 15 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 15 15
合计 825 750

二、设计实现过程

这个java程序的主要功能是生成数学问题并保存到文件中,读取文件并比较答案。

下面是这个程序的主要运行流程:
从命令行参数中获取参数。参数可能包括-n和-r,表示需要生成的题目数量和题目中数字的范围,或者参数可能包括-e和-a,表示题目文件和答案文件的路径。
如果有-n和-r参数,程序将生成指定数量和范围的题目,并保存到"Exercises.txt"和 "Answers.txt"文件。
1、 生成题目的具体步骤包括:根据范围随机生成2到4个操作数,随机生成对应数量的运算符,然后将这些操作数和运算符组合成表达式。
2、 为了保证生成的题目不重复,程序还会检查新生成的题目是否和已有的题目重复。如果重复,就放弃当前题目,重新生成。
3、 生成题目后,程序会使用JavaScript引擎计算这个表达式的结果,作为正确答案。
如果有-e 和 -a 参数,程序将从指定的题目文件和答案文件中读取题目和答案,然后检查这些答案是否正确,并统计正确和错误答案的数量。
如果出现任何错误,或者没有提供必要的参数,程序将显示帮助信息。

步骤 操作
参数解析 从命令行获取参数
题目生成 根据参数-n和-r生成指定数量和范围的题目
题目保存 将生成的题目和答案保存到"Exercises.txt"和 "Answers.txt"
答案检查 从-e和-a参数指定的文件读取题目和答案,然后比较答案
结果统计 统计正确和错误的答案并保存到"Grade.txt"
错误处理 在出错时显示帮助信息

代码说明

generateProblemsAndAnswers函数,它的作用是生成指定数量(problemCount)的问题以及这些问题的答案,并将这些问题存储在expressions集合中,答案存储在answers列表中。这里的range参数代表生成每个操作数时考虑的范围。

生成问题时,代码使用了Random对象来创建随机数。在一个循环中,只要expressions的大小小于problemCount,就尝试生成新的问题并将其添加到集合中。

为了确保生成的问题是独一无二的,代码首先检查生成的表达式是否已经存在于expressions集合中。如果不存在,再用solveProblem函数解决问题并再次检查解决方案是否重复。

isDuplicate函数,它用于检查表达式是否重复。它首先通过调用normalizeExpression函数将表达式规范化,然后在expressions集合中查找是否有相同的规范化字符串,如果找到,则表达式重复,返回true;否则返回false。

函数normalizeExpression接受一个表达式字符串作为输入,并返回一个规范化的表达式字符串。规范化过程包括将字符串分割为单个的操作数和操作符,排序它们,然后再将它们拼接回一个字符串。

这种方法的一个潜在问题是它可能会因为重排序而导致原本不同的表达式被错误地视为相同的表达式。例如,"1 + 2" 和 "2 + 1"在经过规范化之后将变得无法区分。

代码的最后部分是generateSingleProblem函数,它使用提供的Random对象和范围值来生成单个数学问题。问题由2到4个操作数组成,操作数之间随机插入加减乘除运算符。操作数取自一个从1到range值的随机数。

此代码中有几个可以优化的点:

  1. 在isDuplicate函数中,每次调用时都会遍历整个expressions集合来检查重复性,这在表达式数量较多时可能效率低下。可以考虑使用更高效的查找算法或数据结构。
  2. normalizeExpression函数的排序逻辑可能会造成逻辑上不同的表达式被视为相同,如前所述。需要一种更精确的方法来确定表达式是否真的相同。
  3. 在generateSingleProblem函数中,在生成每个操作数时,可以加入更多的变化,比如支持带分数、小数点等。

综上所述,这段代码的目的是生成数学问题和答案,并防止重复问题的出现,但其规范化逻辑需要改进以避免将本质上不同的表达式视为相同。此外,代码性能也可以通过优化数据结构和算法来提高。

private static void generateProblemsAndAnswers(int problemCount, int range) {
        Random rand = new Random();
        while (expressions.size() < problemCount) {
            String expr = generateSingleProblem(rand, range);
            if (!expressions.contains(expr + " = ")) {  // 确保表达式唯一
                String answer = solveProblem(expr);
                if (answer != null && !isDuplicate(expr, answer)) {
                    expressions.add(expr + " = ");
                    answers.add(answer);
                }
            }
        }
    }

    static boolean isDuplicate(String expr, String answer) {
        // 生成一个可以用来比较的规范化表达式字符串
        String normalizedExpr = normalizeExpression(expr);

        // 遍历已有的表达式列表,检查是否有相同的规范化字符串
        for (String existingExpr : expressions) {
            String existingNormalized = normalizeExpression(existingExpr);
            if (normalizedExpr.equals(existingNormalized)) {
                return true;  // 发现重复
            }
        }

        return false;  // 未发现重复
    }

    static String normalizeExpression(String expr) {
        // 分割表达式成操作数和操作符
        String[] tokens = expr.split(" ");

        // 由于只使用了自然数和基本运算符,规范化只需排序
        Arrays.sort(tokens);

        // 再将排序后的操作数和操作符连接成字符串
        return String.join(" ", tokens);
    }

    static String generateSingleProblem(Random rand, int range) {
        String[] operators = {"+", "-", "*", "/"};
        // 操作数的数量在2到4之间
        int numOfOperands = 2 + rand.nextInt(3);
        // 创建一个包含操作数和操作符的List
        List<String> components = new ArrayList<>();
        for (int i = 0; i < numOfOperands; ++i) {
            components.add(Integer.toString(rand.nextInt(range) + 1));  // 生成操作数
            if (i != numOfOperands - 1) {
                components.add(operators[rand.nextInt(operators.length)]);  // 生成操作符
            }
        }
        return String.join(" ", components);
    }

这段代码定义了一个方法solveProblem,它使用Java的ScriptEngineManager来计算一个字符串形式的数学表达式。方法使用名为"JavaScript"的引擎对表达式进行评估,并返回计算结果。如果表达式无法计算,比如有语法错误,它会打印出异常信息并返回null。这种方式简洁且便利,但可能在表达式较复杂或需要高安全性的场景下不太适用。


static String solveProblem(String expr) {
        ScriptEngineManager mgr = new ScriptEngineManager();
        ScriptEngine engine = mgr.getEngineByName("JavaScript");
        try {
            Object result = engine.eval(expr);
            return result.toString();
        } catch (ScriptException e) {
            e.printStackTrace();
            return null;  // 或者您可以返回一些错误信息
        }
    }

该方法checkAndGrade读取练习问题和学生答案,比较学生答案是否正确,并记录下正确和错误答案的序号。最后,它将结果写入一个文件中。如果读取文件过程中发生错误,它会输出错误信息。

static void checkAndGrade(String exerciseFile, String answerFile) {
        // 存储正确和错误答案的列表
        List<Integer> correctAnswers = new ArrayList<>();
        List<Integer> wrongAnswers = new ArrayList<>();

        try {
            List<String> givenAnswers = Files.readAllLines(Paths.get(answerFile));
            List<String> exercises = Files.readAllLines(Paths.get(exerciseFile));

            for (int i = 0; i < givenAnswers.size(); i++) {
                // 移除题目字符串尾部的等号和可能的空格
                String exercise = exercises.get(i).split(" =")[0];
                String correctAnswer = solveProblem(exercise);
                if (givenAnswers.get(i).equals(correctAnswer)) {
                    correctAnswers.add(i + 1);
                } else {
                    wrongAnswers.add(i + 1);
                }
            }

            // 写入Grade.txt文件
            saveToFile("Grade.txt", Arrays.asList(
                    "Correct: " + correctAnswers.size() +  correctAnswers ,
                    "Wrong: " + wrongAnswers.size() + wrongAnswers
            ));
        } catch (IOException e) {
            System.err.println("Error reading from files.");
            e.printStackTrace();
        }
    }

这段代码解析命令行参数,将参数名和参数值存储到一个映射表中,并处理不带值的参数。

static Map<String, String> parseArguments(String[] args) {
        Map<String, String> arguments = new HashMap<>();
        // 遍历args数组
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            // 检查arg是否是一个参数键,即它是否以'-'开头
            if (arg.charAt(0) == '-') {
                // 如果它是一个参数键,我们检查是否还有更多的参数并且下一个参数不是另一个键
                if (i + 1 < args.length && args[i + 1].charAt(0) != '-') {
                    // 将下一个参数作为这个键的值保存
                    arguments.put(arg, args[i + 1]);
                    i++; // 增加i以跳过下一个参数(因为它已经作为当前键的值被处理)
                } else {
                    // 如果下一个参数是另一个参数键或者没有更多的参数,那么我们可以认为这个参数键没有值
                    arguments.put(arg, "");
                }
            }
        }
        return arguments;
    }

测试运行

测试覆盖率

单元测试成功运行




项目小结

通过这次的项目编写,不仅在代码编写方面有所收获:
1、功能划分:项目通过细分功能模块(如生成题目、保存文件、验证答案等)提高了代码的可管理性和可扩展性,有助于后续功能的增加与维护。
2、用户交互设计:实现了命令行界面参数解析,这样的设计使得用户能够通过简洁的命令操作程序,提升了用户体验。
3、异常处理:在关键操作如文件读写中实施了异常捕获,保证了程序的健壮性,也为遇到错误时提供了更清晰的指导。
在合作编程这块也有深刻的体会:
1、互补长处:每个人都有自己的长处和弱点。在结对编程中,一个人的优点可以弥补另一个人的不足,反之亦然。
2、即时交流与反馈:结对编程允许即时的交流与反馈。当一人正在编写代码时,另一人可以立即进行审查,提出建议或指出错误。这可以减少代码审查和调试的时间,提高功效。
3、共享知识与经验:结对编程不仅仅是共同编写代码,它也是一个良好的学习机会。双方都可以从对方那里学习新的编程技术或方法,加深对代码或项目本身的理解。

posted @ 2024-09-28 20:30  黄锐001  阅读(13)  评论(0编辑  收藏  举报