Github地址:https://github.com/mercuriussss/calculate

项目要求

实现一个自动生成小学四则运算题目的命令行程序

功能(已全部实现)

  1. 使用 -n 参数控制生成题目的个数。
  2. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围。
  3. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
  4. 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
  5. 每道题目中出现的运算符个数不超过3个。
  6. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
  7. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt。
  8. 程序应能支持一万道题目的生成。
  9. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt。

 

PSP表格预估

  

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

100

50

· Estimate

· 估计这个任务需要多少时间

100

50

Development

开发

3000

2500

· Analysis

· 需求分析 (包括学习新技术)

60

50

· Design Spec

· 生成设计文档

100

100

· Design Review

· 设计复审 (和同事审核设计文档)

30

50

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

20

20

· Design

· 具体设计

100

120

· Coding

· 具体编码

2000

1500

· Code Review

· 代码复审

60

60

· Test

· 测试(自我测试,修改代码,提交修改)

630

600

Reporting

报告

120

120

· Test Report

· 测试报告

20

40

· Size Measurement

· 计算工作量

40

30

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

60

50

合计

 

3220

2670

设计实现过程

  依照需求,可将程序流程划分为“生成符合条件的题目和对应答案”、“检验是否存在重复题目”、“输出题目和答案到相应的txt文档”、“校对答案” 四个部分。

  最终,我们使用了7个类:

  •   Main为主类,包含main方法,exe程序所调用的函数
  •   FractionOpe和IntegerOpe类,在整合过后,决定只负责分数跟整数的随机生成
  •   CreateSubject类,负责随机生成符合限制条件的题目和相应答案
  •   JudgeAnswer类,负责校对答案并将数据存进Grade.txt文件
  •   Calculate类,负责数字的计算部分

  以下为7个函数相互间的调用关系图,也是程序的流程图

  

关键代码

  CreateSubject类的代码

    其中在实现功能3跟4,也就是实现相减非负数、相除只能为真分数的功能时,我耍了个滑头。

    因为当两数相减为负数时,那就说明前者比后者小,刚好跟相除只能为真分数的情况所需条件一致,于是我调换了两种情况的位置。

    当两者相减为负数时,将两者的运算符改为相除;当两者相除不为真分数时,将两者的运算符改为相减。

    这样一来,两种情况都得到了解决,而且也保证了随机性。

package calculate;

import java.util.ArrayList;
import java.util.Random;

public class CreateSubject {

    private static final String[] OPERATORS = { "+", "-", "*", "÷" };
    ArrayList<String> expression = new ArrayList<String>();// 算式字符串存储
    ArrayList<String> numbs = new ArrayList<String>();
    ArrayList<String> opers = new ArrayList<String>();
    ArrayList<String> answer = new ArrayList<String>();

    Random rd = new Random();

    public CreateSubject(int max) {

        String numA = randNum(max);
        String numB = null;
        int nums = rd.nextInt(3) + 2;// 几个数字的运算
        String flag = null;
        expression.add(numA);
        numbs.add(numA);

        for (int i = 0; i < nums - 1; i++) {
            numB = randNum(max);
            flag = OPERATORS[rd.nextInt(4)];

            switch (flag) {
            case "+":
                numA = Calculate.add(numA, numB);
                break;

            case "-":
                if (!Calculate.isGreater(numA, numB)) {
                    flag = OPERATORS[3];
                    numA = Calculate.div(numA, numB);
                } else {
                    numA = Calculate.sub(numA, numB);
                }
                break;

            case "*":
                numA = Calculate.mul(numA, numB);
                break;

            case "÷":
                if(Calculate.isGreater(numA, numB)){
                    flag = OPERATORS[1];
                    numA = Calculate.sub(numA, numB);
                } else {
                    numA = Calculate.div(numA, numB);
                }
                break;
            }

            expression.add(flag);
            expression.add(numB);
            numbs.add(numB);
            opers.add(flag);
        }
        addBrackets(expression);
        expression.add("=");
        answer.add(numA);
    }

    // 随机生成分数或整数
    public String randNum(int max) {
        String num;
        int flag = rd.nextInt(10) + 1;
        if (flag % 3 == 0) {
            num = FractionOpe.gen(max);
        } else {
            num = IntegerOpe.gen(max);
        }
        return num;
    }

    // 生成必要的括号
    public ArrayList<String> addBrackets(ArrayList<String> expression) {
        String lowlv = "+-";
        String highlv = "*÷";
        if (expression.size() == 5) {
            if (lowlv.contains(expression.get(1)) && highlv.contains(expression.get(3))) {
                expression.add(0, "(");
                expression.add(4, ")");
            }
        }
        if (expression.size() == 7) {
            if (lowlv.contains(expression.get(1)) && highlv.contains(expression.get(3))) {
                expression.add(0, "(");
                expression.add(4, ")");
            }
            if (lowlv.contains(expression.get(3)) && highlv.contains(expression.get(5))) {
                expression.add(0, "(");
                expression.add(6, ")");
            }
        }

        return expression;
    }
}

 

    随机生成分数的代码

public static String gen(int max) {
        String fraction = null;
        Random rd = new Random();
        int numerator = rd.nextInt(max) + 1;
        int denominator = rd.nextInt(max) + 1;

        // 检验是否为整数
        if (numerator == denominator || numerator % denominator == 0) {
            fraction = numerator / denominator + "";
            return fraction;
        }

        // 该数不能超过max
        while (numerator / denominator >= max) {
            numerator = rd.nextInt(max) + 1;
            denominator = rd.nextInt(max) + 1;
        }

        fraction = Calculate.simplify(numerator, denominator);
        return fraction;
    }

    随即生成整数的代码

public static String gen(int max) {
        Random rd = new Random();
        int integer = 0;
        while (integer == 0 || integer >= max) {
            integer = rd.nextInt(max) + 1;
        }
        return String.valueOf(integer);
    }

    

    FileOutput类

      里面包含创建文件、向文件追加写入数据、检验题目是否重复、将题目和答案写入相应文件的方法

package calculate;

import java.io.*;
import java.util.ArrayList;

public class FileOutput {

    // 将题目和答案写入相应文件中
    public static void outExeAndAns(int num, CreateSubject cs, File fileAns, File fileExe) throws IOException {
        cs.expression.add(0, num + ".\t");
        cs.answer.add(0, num + ".\t");
        outputData(cs.expression, fileExe);
        outputData(cs.answer, fileAns);
    }

    // 检验题目是否重复
    public static boolean isRepeat(CreateSubject cs, File fileAns, File fileExe) throws IOException {
        boolean isRepeat = false;
        String strAns = null;
        String strExe = null;
        String[] opers = null;
        String[] numbs = null;

        int sameNum;
        int sameOpe;
        int allNums;
        int allOpes;

        BufferedReader brAns = new BufferedReader(new InputStreamReader(new FileInputStream(fileAns)));
        BufferedReader brExe = new BufferedReader(new InputStreamReader(new FileInputStream(fileExe)));

        while ((strAns = brAns.readLine()) != null) {
            strAns = strAns.replaceAll("\\s", "").substring(strAns.indexOf(".") + 1);
            if (cs.answer.get(0).equals(strAns)) {
                while ((strExe = brExe.readLine()) != null) {
                    strExe = strExe.substring(strExe.indexOf(".") + 1, strExe.indexOf("="));
                    strExe = strExe.replaceAll("\\s", "");
                    numbs = strExe.split("[\\+\\-\\*\\÷]");
                    opers = strExe.split("[^\\+\\-\\*\\÷]");
                    sameNum = 0;
                    sameOpe = 0;
                    allOpes = 0;
                    allNums = numbs.length;
                    for (int i = 0; i < opers.length; i++) {
                        if (!opers[i].equals("")) {
                            allOpes++;
                        }
                    }
                    for (String s : opers) {
                        if (!s.equals("")) {
                            if (cs.opers.contains(s)) {
                                sameOpe++;
                            }
                        }
                    }
                    for (String s : numbs) {
                        if (cs.numbs.contains(s)) {
                            sameNum++;
                        }
                    }
                    if (sameOpe == allOpes && sameNum == allNums) {
                        isRepeat = true;
                    }
                    if (isRepeat) {
                        break;
                    }
                }
            }
            if (isRepeat) {
                break;
            }
        }

        brAns.close();
        brExe.close();

        return isRepeat;
    }

    // 创建文件,若该文件已存在则将其内容清空
    public static File createFile(String name) throws IOException {
        File f = new File(name);
        if (f.exists()) {
            f.delete();
            f.createNewFile();
        } else {
            f.createNewFile();
        }
        return f;
    }

    // 向文件追加写入数据
    public static void outputData(ArrayList<String> data, File file) throws IOException {
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true)));
        String str = arrToString(data);
        bw.write(str);
        bw.newLine();
        bw.flush();
        bw.close();
    }

    public static String arrToString(ArrayList<String> arr) {
        String str = "";
        if (arr != null && arr.size() > 0) {
            for (String s : arr) {
                str += s + " ";
            }
        }
        return str;
    }
}

      

      JudgeAnswer类,负责校对答案并输出成绩

package calculate;

import java.io.*;
import java.util.ArrayList;

public class JudgeAnswer {
    public JudgeAnswer(File ExeFile, File AnsFile) throws IOException {
        File grade = FileOutput.createFile("Grade.txt");
        String str = null;

        ArrayList<String> trueAns = new ArrayList<String>();
        ArrayList<String> testAns = new ArrayList<String>();

        ArrayList<String> correct = new ArrayList<String>();
        ArrayList<String> wrong = new ArrayList<String>();

        BufferedReader brExe = new BufferedReader(new InputStreamReader(new FileInputStream(ExeFile)));
        BufferedReader brAns = new BufferedReader(new InputStreamReader(new FileInputStream(AnsFile)));

        while ((str = brExe.readLine()) != null) {
            str = str.trim().replaceAll("\\s", "");
            str = str.substring(str.indexOf("=") + 1);
            testAns.add(str);
        }
        while ((str = brAns.readLine()) != null) {
            str = str.trim().replaceAll("\\s", "");
            str = str.substring(str.indexOf(".") + 1);
            trueAns.add(str);
        }
        for (int num = 1; num <= trueAns.size(); num++) {
            if (trueAns.get(num - 1).equals(testAns.get(num - 1))) {
                if (num == trueAns.size()) {
                    correct.add(String.valueOf(num));
                } else {
                    correct.add(String.valueOf(num) + ", ");
                }
            } else {
                if(num == trueAns.size()){
                    wrong.add(String.valueOf(num));
                }else{
                    wrong.add(String.valueOf(num) + ", ");
                }
            }
        }
        plus(correct, "Correct");
        plus(wrong, "Wrong");

        FileOutput.outputData(correct, grade);
        FileOutput.outputData(wrong, grade);
        brExe.close();
        brAns.close();
    }

    public void plus(ArrayList<String> jud, String name) {
        jud.add(")");
        jud.add(0, "(");
        jud.add(0, name + ": " + (jud.size() - 2));
    }
}

      

      Calculate类,负责数字的加减乘除

package calculate;

public class Calculate {
    
    //比较大小
    public static boolean isGreater(String numA, String numB) {
        int[] num = new int[3];
        num = comFraction(splFraction(numA), splFraction(numB));
        return num[0]>num[1];
    }
    
    // 求公因数
    public static int getFactor(int numerator, int denominator) {
        if (numerator < denominator) {        
            int temp = numerator;            
            numerator = denominator;            
            denominator = temp;        
        }        
        if (numerator % denominator == 0) {        
            return denominator;        
        } else {     
            return getFactor(denominator, numerator % denominator);        
        }

    }

    // 化简
    public static String simplify(int numerator, int denominator) {
        int overnum = 0;
        int factor = getFactor(numerator, denominator);
        numerator /= factor;
        denominator /= factor;
        if(numerator == 0){
            return "0";
        }
        if(denominator == 1){
            return String.valueOf(numerator);
        }else if (numerator > denominator) {
            overnum = numerator / denominator;
            numerator -= overnum * denominator;
            return overnum + "'" + numerator + "/" + denominator;
        }else {
            return numerator + "/" + denominator;
        }
    }

    // 判断该数是否为分数
    public static boolean isFraction(String num) {
        if (num.contains("/")) {
            return true;
        } else {
            return false;
        }
    }

    //分割字符串分数
    public static int[] splFraction(String num) {
        String[] str = num.split("'|/");
        int[] splFra = new int[str.length];
        for (int i = 0; i < str.length; i++) {
            splFra[i] = Integer.valueOf(str[i]).intValue();
        }
        return splFra;
    }
    
    //通分
    public static int[] comFraction(int[] numA,int[] numB){
        
        //存储通分后的A分子、B分子、AB共同分母
        int[] num = new int[3];
        
        if(numA.length == 3 && numB.length == 3){
            //两者皆为带分数
            num[0] = (numA[0]*numA[2]+numA[1])*numB[2];
            num[1] = (numB[0]*numB[2]+numB[1])*numA[2];
            num[2] = numA[2]*numB[2];
        }else if(numA.length == 3 && numB.length == 2){
            //A为带分数,B为真分数
            num[0] = (numA[0]*numA[2]+numA[1])*numB[1];
            num[1] = numB[0]*numA[2];
            num[2] = numA[2]*numB[1];
        }else if(numA.length == 2 && numB.length == 3){
            //A为真分数,B为带真分数
            num[0] = numA[0]*numB[2];
            num[1] = (numB[0]*numB[2]+numB[1])*numA[1];
            num[2] = numA[1]*numB[2];
        }else if(numA.length == 2 && numB.length == 2){
            //两者皆为真分数
            num[0] = numA[0]*numB[1];
            num[1] = numB[0]*numA[1];
            num[2] = numA[1]*numB[1];
        }else if(numA.length == 1 && numB.length == 3){
            //A为整数,B为带分数
            num[0] = numA[0]*numB[2];
            num[1] = numB[0]*numB[2]+numB[1];
            num[2] = numB[2];
        }else if(numA.length == 3 && numB.length == 1){
            //A为带分数,B为整数
            num[0] = numA[0]*numA[2]+numA[1];
            num[1] = numB[0]*numA[2];
            num[2] = numA[2];
        }else if(numA.length == 1 && numB.length == 2){
            //A为整数,B为真分数
            num[0] = numA[0]*numB[1];
            num[1] = numB[0];
            num[2] = numB[1];
        }else if(numA.length == 2 && numB.length == 1){
            //A为真分数,B为整数
            num[0] = numA[0];
            num[1] = numB[0]*numA[1];
            num[2] = numA[1];
        }else{
            //A、B皆为整数
            num[0] = numA[0];
            num[1] = numB[0];
            num[2] = 1;
        }
        
        return num;
    }
    
    public static String add(String numA, String numB) {

        int[] num = comFraction(splFraction(numA),splFraction(numB));
        return simplify(num[0]+num[1],num[2]);
    }

    public static String sub(String numA, String numB) {
        int[] num = comFraction(splFraction(numA),splFraction(numB));
        return simplify(num[0]-num[1],num[2]);
    }

    public static String mul(String numA, String numB) {
        int[] num = comFraction(splFraction(numA),splFraction(numB));
        return simplify(num[0]*num[1],num[2]*num[2]);
    }

    public static String div(String numA, String numB) {
        int[] num = comFraction(splFraction(numA),splFraction(numB));
        if(num[0] == 0){
            return "0";
        }else{
            return simplify(num[0], num[1]);
        }
    }
    

}

 

      Main类,包含运行时的主函数

package calculate;

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws IOException {
        System.out.println("\n\t 输入 “-n [数值]” 可控制将要生成的题目个数 ");
        System.out.println("\n\t 输入 “-r [数值]” 可控制生成的题目中,所用数值和真分数分母的最大数字(不包含该数)");
        System.out.println("\n\t 输入 “-e <exercisefile>.txt -a <answerfile>.txt” 可校对答案,并将其校对结果输出到Grade.txt文件中");
        System.out.println("\n\t 输入 “-exit ” 可退出程序");
        System.out.println("\n\t PS: -r 指令需要在输入 -n 指令后才能实现,不过可由两者同时输入,例如“-n 10 -r 10”");
        int max;
        int nums;
        while (true) {

            max = 0;
            nums = 0;

            System.out.println("\n请输入相应指令:     ");
            Scanner sc = new Scanner(System.in);
            String commands[] = sc.nextLine().toString().trim().replaceAll(" +", " ").split(" ");
            if (commands[0].equals("-exit")) {
                break;
            }

            switch (commands[0]) {
            case "-n":
                if (commands.length == 2) {
                    nums = Integer.valueOf(commands[1]).intValue();
                    System.out.println("\n请输入 “-r [数值]”指令:");
                    sc = new Scanner(System.in);
                    String[] cm = sc.nextLine().toString().trim().replaceAll(" +", " ").split(" ");
                    if (cm.length == 2 && cm[0].equals("-r")) {
                        max = Integer.valueOf(cm[1]).intValue();
                    } else {
                        System.out.println("\n输入指令错误,请重来!");
                        break;
                    }
                    build(nums, max);
                    System.out.println("\n生成题目成功!");
                } else if (commands.length == 4) {
                    nums = Integer.valueOf(commands[1]).intValue();
                    max = Integer.valueOf(commands[3]).intValue();
                    build(nums, max);
                    System.out.println("\n生成题目成功!");
                } else {
                    System.out.println("\n输入指令错误,请重来!");
                }
                break;
            case "-e":
                if (commands.length == 4 && commands[2].equals("-a")) {
                    File testAns = new File(commands[1]);
                    File trueAns = new File(commands[3]);
                    if (testAns.exists() && trueAns.exists()) {
                        new JudgeAnswer(testAns, trueAns);
                        System.out.println("\n校对成功,成绩已存入Grade.txt!");
                    } else {
                        System.out.println("\n输入文件路径错误,请重来!");
                    }
                } else {
                    System.out.println("\n输入指令错误,请重来!");
                }
                break;
            default :
                System.out.println("\n输入指令错误,请重来!");
                break;
            }
            
        }
        
    }

    public static void build(int numbs, int max) throws IOException {
        CreateSubject cs;
        File fileExe = FileOutput.createFile("Exercises.txt");
        File fileAns = FileOutput.createFile("Answers.txt");
        for (int num = 1; num <= numbs; num++) {
            cs = new CreateSubject(max);
            while (num > 2 && FileOutput.isRepeat(cs, fileAns, fileExe)) {
                cs = new CreateSubject(max);
            }
            FileOutput.outExeAndAns(num, cs, fileAns, fileExe);
        }
    }
}

 

测试运行截图

    

 

  什么答案也没填的结果

  

 

将题目中的答案填上,并将其中几个改为错误答案

 

代码覆盖率

 

10000道题的生成,文件跟代码一同上传至Github中

10000道题目

10000道题目的答案

 

 

项目小结

  这次项目,我们没有采用Github上的提交分支、合并分支、改进意见等功能去进行合作,而是直接采用了分工的方法。在开始的时候,我们通过在现实讨论,逐渐将项目细分为几个类,每个类都负责什么样的功能,需要将什么样的参数输入,之后又能得到什么样的结果等等。讨论过后再由各自选择几个类进行独立开发,互不干扰对方,不过显然这种方法不是很好,俩人风格不一致,我们在完成各自工作后又花了很长时间才将俩人的部分合成一个,并且其中还有不少BUG需要自己去修改,感觉工作量莫名的更多了,也算是长了个教训。

  不过好处也是有的,比起一个人打码,俩个人在交流时更能激发灵感,在某些地方跟对方持不同看法后也能互相交流意见、弥补自身或对方的不足,而且有时自己卡个半天的问题,对方却能很快找到解决方法,所谓当局者迷旁观者清大概也就如此吧。

  对于项目而言,目前还不够完善,在生成10000道题目时相比其他人,花费的时间要更长,后续需要优化里面使用关于计算与文件读写的操作代码才行。