四则运算题目生成程序

个人作业1——四则运算题目生成程序(基于控制台)

项目已提交到Coding.net:arith-exercise

需求分析

  • 使用 -n 参数控制生成题目的个数
  • 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
  • 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
  • 每道题目中出现的运算符个数不超过3个。
  • 其中真分数在输入输出时,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
  • 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
  • 程序应能支持一万道题目的生成 [spoiler]然而并没有规定时间哈哈哈[/spoiler]。
  • 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,并会输出所有题目中重复的题目。

功能设计

  • 根据命令行参数种类的不同来分别进入两种模式:算式生成和算式检查
  • 设计一个数字类,用来封装分数和四则运算操作
  • 题目没有给出算式生成模式时输出的答案文件名的命令行参数,所以默认为“Answer.txt”

设计实现

采用Java开发

项目结构

  • arith
    • Arith              封装了数学方法,比如算式校验和算式计算
    • Checker          检查测验文件,进行算式查重,并将答案与答案文件里的比对,输出结果到成绩文件
    • Creator          生成算式
  • model
    • Expression     表达式
    • Number             数字,封装了四则运算和约分等操作
  • Config                    各种配置,比如-n、-r等
  • Main                       程序入口,读取参数并进入相应模式

Model类

这里列出两个model类——数字和表达式,因为篇幅原因省略掉了很多方法和方法的实现,具体可以到Coding.net里查看

public class Number {
    // 把每个数字都视为分数(可能是假分数), 如果是整数的话就把分母设为1
    private int mNumerator;
    private int mDenominator;

    // 通过整数、分子和分母、格式化的数字字符串三种方式来构造
    public Number(int value) { }
    public Number(int numerator, int denominator) { }
    public Number(String number) { }

    // 根据运算符来执行对应四则运算
    public Number operate(String operator, Number number) { }

    // 封装的四则运算操作
    public Number add(Number number) { }
    public Number sub(Number number) { }
    public Number mul(Number number) { }
    public Number div(Number number) { }

    // 约分分数,用到了网上找到的最大公约数算法
    // 每次运算后都会调用一次该函数
    public void reduce() {
        if (mNumerator == 0) // 不需要约分
            return;

        // 计算最大公约数
        // Ref: http://blog.csdn.net/iwm_next/article/details/7450424
        int a = Math.abs(mNumerator);
        int b = mDenominator;
        while (a != b)
            if (a > b)
                a = a - b;
            else
                b = b - a;

        mNumerator /= a;
        mDenominator /= a;
    }
}
// 表达式是一个字符串数组,其中每个元素都是格式化的数字或者运算符,比如
// (1+2/3)*4'5/6 => ["(", "1", "+", "2/3", ")", "*", "4'5/6"]
public class Expression extends ArrayList<String> {
    // 解析一个字符串,转化为表达式类型
    public static Expression fromString(String src) { }
}

代码说明

对于表达式的计算,参考了这篇博客的算法,但是他的算法有个地方有点问题:

else { // 优先级小于栈顶运算符,则弹出
    tmp = stack.pop();

    // 这里不应该把element加入suffix里,应该把它压入栈中
    // suffix.append(tmp).append(" ").append(element).append(" ");
    suffix.append(tmp).append(" ");
    stack.push(element);
}

生成算式的方法

public static void create(Config config, boolean putAnswer) {
    if (!isConfigValid(config))
        return;

    int number = config.number;
    int range = config.range;
    List<Expression> expressions = new ArrayList<>(number);
    List<Number> answers = new ArrayList<>(number);

    for (int i = 0; i < number; i++) {
        try {
            Expression exp = createExpression(range);
            Number ans = Arith.evaluate(exp);

            // 检查是否存在重复的算式,先检查答案是否重复再检查算式本身
            if (Checker.hasSameAnswer(answers, ans) && Checker.findDuplicate(expressions, exp) != -1)
                throw new ArithmeticException("Expression duplicated: " + exp);

            expressions.add(exp);
            answers.add(ans);
        } catch (ArithmeticException e) {
            // 如果生成失败了就回退,再试一次
            i--;
        }
    }

    // 保存结果到文件
    output(config.output, expressions, answers, putAnswer);
}

生成随机数字的方法

private static Number randomNumber(int numberMax) {
    if (Math.random() < PR_INTEGER) // PR_INTEGER=0.8,是生成一个整数的概率
        // 生成一个整数
        return new Number((int) (Math.random() * numberMax - 1) + 1);
    else
        // 生成一个分数
        return new Number((int) (Math.random() * numberMax - 1) + 1, (int) (Math.random() * numberMax - 1) + 1);
}

在原算式的基础上添加一个运算的方法,随机选取一个数字,比如将1+2*3里的2替换为2-4,或者带括号的(2-4)

private static void addOperation(Expression exp, int numberMax) {
    int size = exp.size();
    int position = 0;
    int loops = 0;

    // 随机选择一个数字
    while (true) {
        if (Arith.isNum(exp.get(position).charAt(0)) && Math.random() > 0.5)
            break;

        // 如果搜索结束了还没有选中数字,就回退到起点然后重新搜索
        if (++position == size)
            position = 0;

        if (loops++ == 50) {
            System.out.println("Oops, something went wrong...?");
            return;
        }
    }

    boolean addBrackets = Math.random() > PR_BRACKET; // PR_BRACKET=0.5,是插入一个括号的概率
    if (addBrackets)
        exp.add(position++, "(");

    exp.add(++position, randomOperator());
    exp.add(++position, randomNumber(numberMax).toString());

    if (addBrackets)
        exp.add(++position, ")");
}

从文件读取并检查算式结果的算法

// 示例:
// Exercise.txt >       1. 1+2*3=7
// Answer.txt >         1. 7

while ((expLine = exerciseReader.readLine()) != null) {
    separator = expLine.indexOf('=');
    exp = Expression.fromString(expLine.substring(expLine.indexOf('.') + 2, separator));

    ansLine = answerReader.readLine();
    rightAnsStr = ansLine.substring(ansLine.indexOf('.') + 2);

    // 检查答案
    if (separator + 1 < expLine.length() // 填有答案
            && rightAnsStr.equals(expLine.substring(separator + 1))) // 等于正确答案
        corrects.add("" + index);
    else
        wrongs.add("" + index);

    // 检查重复
    rightAns = new Number(rightAnsStr);
    if (hasSameAnswer(rightAnswers, rightAns) && (duplicate = findDuplicate(expressions, exp)) != -1)
        // 暂存重复的两个表达式的下标和对象
        repeats.add(new ExpressionPair(duplicate + 1, index, expressions.get(duplicate), exp));

    expressions.add(exp);
    rightAnswers.add(rightAns);

    index++;
}

测试运行

运行截图,耗时还是比较长……

生成10000个算式,最大数字10

这里因为÷号不是ascii所以读取出来的是乱码,需要加上 -Dfile.encoding=utf-8 参数

检查算式

算式、答案、成绩文件

现在的查重还是做不到检测交换顺序后的重复,只能检测到数字顺序一样并且运算符和优先级一致的算式,具体来说就是去掉或加上括号的那种重复

PSP

PSP2.1 | Personal Software Process Stages | Time Predicted | Time

  • | - | :-: | :-:
    Planning | 计划 | 5 | 5
    · Estimate | 估计这个任务需要多少时间 | 5 | 5
    Development | 开发 | 420 | 973
    · Analysis | 需求分析 (包括学习新技术) | 10 | 15
    · Design Spec | 生成设计文档 | - | -
    · Design Review | 设计复审 | - | -
    · Coding Standard | 代码规范 | - | -
    · Design | 具体设计 | - | -
    · Coding | 具体编码 | 200 | 491
    · Code Review | 代码复审 | 10 | 270
    · Test | 测试(自我测试,修改代码,提交修改) | 200 | 197
    Reporting | 报告 | 100 | 94
    . | 测试报告 | 90 | 94
    . | 计算工作量 | 10 | -
    . | 并提出过程改进计划 | - | -

做这个项目还是花了很多时间的,虽然明明可以把功能完成得差不多就得了,但是为什么还要这么拼呢,对啊为什么呢……
大概是因为完美主义吧,就像我玩游戏一定要做全成就一样

这个表格的时间我也算得比较严格,误差应该在30分钟之内

具体编码这部分虽然一开始就觉得会花很久,结果最后花的时间还是比预期的要多

测试这部分虽然没有超出估计值,但还是比我之前做项目时花的时间多得多,也是第一次用了单元测试,感觉太棒了,非常好用啊

后来还花了3个半小时来写文档和注释(大部分时间在查单词- -),不知道该归类在哪,就写到代码复审里了

还有一些步骤我没做或者不知道哪些属于它,就没写时间了

另外,最烦人的部分是git,带着各种匪夷所思的错误,为了git的一个push花了3个小时,还是在寂寞的半夜

最后写博客花的时间我没记,不过加起来也得有3、4个小时吧

小结

这个项目虽然耗费了我大量娱乐时间,但也让我学到了很多有用的东西

比如JUnit,以前我都懒得写测试,但现在我第一次体会到了单元测试的好处,再也不用为了测一段代码就得反复把整个程序跑起来,还各种模拟操作了

再比如javadoc,这是我第一次这么认真地写javadoc,也基本了解了它的语法和一些表达习惯

还有git,虽然第一次配置配得我头都快秃了,但配完之后用起来还是很好用的,对代码管理也是大有帮助

posted @ 2017-09-18 23:59  Dr_Pepper  阅读(488)  评论(8编辑  收藏  举报