HNU个人项目互评-中小学数学卷子自动生成程序

发布时间 2023-09-21 00:01:39作者: 枯朽为灯

一、前言

本博客是对结对编程队友秦凯同学的个人项目的评价,项目采用的编程语言是Java。通过互评发现对方代码的优缺点,互相学习。

二、个人项目要求

用户:
小学、初中和高中数学老师。
功能:
1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;
2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;
3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);
4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;
5、生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;

账户、密码:

账户类型 账户 密码
小学 张三1 123
张三2 123
张三3 123
初中 李四1 123
李四2 123
李四3 123
高中 王五1 123
王五2 123
王五3 123
小学、初中、高中题目难度要求:
小学 初中 高中
难度要求 +,-,*,/ 平方,开根号 sin,cos,tan
备注 只能有+,-,*,/和() 题目中至少有一个平方或开根号的运算符 题目中至少有一个sin,cos或tan的运算符

三、项目结构

该项目的src文件夹下有为5个包和一个Test类。
与src同级的文件夹有小学、初中、高中文件夹,3个文件夹中又根据账户类型包含了对应账户的文件夹。

四、代码分析

1.autoquestion包:

里面包含了4个类和一个接口,每个类和接口中包含一个函数。

1.1 接口GetMathProblem:

让小学、初中、高中生成题目的类实现该接口,实现多态。该接口中有一个抽象方法,由子类实现。

public interface GetMathProblem {

  /**
   * 抽象函数,在子类中实现.
   *
   * @param difficulty the math difficulty.
   * @return a math problem .
   */
  public abstract String getMathProblem(String difficulty);

}
1.2 GetPrimaryMathProblem类:生成小学难度题目

该类实现了接口GetMathProblem,并重写了函数getMathProblem(String difficulty)。生成的题目仅包括+,-,*,/和括号。

public class GetPrimaryMathProblem implements GetMathProblem {
  String problem = new String();
  String[] symbol = new String[]{"+", "-", "*", "/", "²", "√", "sin", "cos", "tan"}; //符号;

  /**
   * 这个函数重写,来获得小学难度的题目.
   *
   * @param difficulty the math difficulty.
   * @return a primary problem.
   */
  @Override
  public String getMathProblem(String difficulty) {
    Random random = new Random();   // 随机数

    while (true) {
      problem = "";  // 初始化为空;
      int operatorNum = 0;   // 为运算数的个数;
      int brackets = random.nextInt(3);  // 0 1 2 表示括号数量
      int leftBracket = 0;   // 左括号数量
      int braLength = 0;      // 记录当前括号内的长度;

      while (true) { // ran为0或1,有1/2的概率在数前面加入左括号;

        if (random.nextInt(2) == 1 && brackets > 0) {
          problem += "(";
          brackets--;
          leftBracket++;    // 左括号个数+1;
          braLength = 0;
        }

        int num = random.nextInt(100) + 1; // 1-100的随机数
        problem += String.valueOf(num);
        operatorNum++;
        braLength++;   // 当存在左括号时,并且里面跨度已经有两个数字时,此时会有1/2 的概率加右括号

        if (random.nextInt(2) == 1 && leftBracket > 0 && braLength >= 2) {
          problem += ")";
          leftBracket--;
        }

        if (operatorNum >= 2 && brackets == 0 && leftBracket == 0) {
          break;
        }

        problem += symbol[random.nextInt(4)];  // 随机加入符号;
      }

      if (operatorNum <= 5) {
        break;
      }

    }
    problem += "=";
    return problem;
  }
}

1.3 GetMiddleMathProblem类:生成初中难度题目

该类实现了接口GetMathProblem,并重写了函数getMathProblem(String difficulty)。生成的题目在小学难度的基础上新增了平方和根号。

public class GetMiddleMathProblem implements GetMathProblem {
  Random random = new Random();   // 随机数
  String[] symbol = new String[]{"-", "+", "/", "*", "√", "²", "sin", "cos", "tan"}; // 符号;

  /**
   * 这个函数重写,来获得初中难度的题目.
   *
   * @param difficulty the math difficulty
   * @return a middle problem
   */
  @Override
  public String getMathProblem(String difficulty) {
    String problem = new String();

    while (true) {
      problem = "";
      int operatorNum = 0;     // 操作数个数
      int squareNum = random.nextInt(2) + 1; // 初始化平方根号个数大于1的随机数
      int brackets = random.nextInt(2);
      int leftBracket = 0;
      int braLength = 0;

      while (true) {

        if (random.nextInt(2) + 1 == 1 && brackets > 0) { // 1/2概率加左括号
          problem += "(";
          brackets--;
          leftBracket++;
          braLength = 0;
        }

        int num = random.nextInt(100) + 1;
        operatorNum++;
        braLength++;

        if (random.nextInt(3) + 1 == 1) { // 1/3的概率加根号
          problem += (symbol[4] + String.valueOf(num));
          squareNum--;
        } else if (random.nextInt(3) + 1 == 2) { // 1/3的概率加平方
          problem += (String.valueOf(num) + symbol[5]);
          squareNum--;
        } else {     // 1/3的概率还是加入数字
          problem += String.valueOf(num);
        }

        if (random.nextInt(2) + 1 == 1 && leftBracket > 0 && braLength >= 2) {
          problem += ")";          // 1/2概率加右括号
          leftBracket--;
        }

        if (operatorNum >= 1 && squareNum <= 0 && brackets <= 0 && leftBracket <= 0) {
          break;
        }

        problem += symbol[random.nextInt(4)];
      }

      if (operatorNum <= 5) {
        break;
      }

    }

    problem += "=";
    return problem;
  }
}

1.4 GetHighMathProblem类:生成高中难度题目
该类也实现了接口GetMathProblem,并重写了函数getMathProblem(String difficulty)。在初中难度的基础上新增了三角函数。

public class GetHighMathProblem implements GetMathProblem {
  Random random = new Random();   // 随机数
  String[] symbol = new String[]{"-", "+", "*", "/", "√", "²", "sin", "cos", "tan"}; // 符号

  /**
   * 这个函数重写,来获得高中难度的题目.
   *
   * @param difficulty the math difficulty.
   * @return a high problem.
   */
  @Override
  public String getMathProblem(String difficulty) {
    String problem = new String();

    while (true) {
      problem = "";
      int operatorNum = 0;
      int squareNum = 1;                       //  平方根号个数
      int triangularNum = random.nextInt(3) + 1; // 初始化三角函数个数大于1的整数
      int brackets = random.nextInt(3);
      int leftBracket = 0;
      int braLength = 0;

      while (true) {

        if (random.nextInt(2) + 1 == 1 && brackets > 0) {
          problem += "(";
          brackets--;
          leftBracket++;
          braLength = 0;
        }

        int num = random.nextInt(100) + 1;
        operatorNum++;
        braLength++;

        if (random.nextInt(4) + 1 == 1) { // 1/4的概率加入平方
          problem += (String.valueOf(num) + symbol[5]);
        } else if (random.nextInt(4) + 1 == 2) { // 1/4的概率加入根号
          problem += (symbol[4] + String.valueOf(num));
        } else if (random.nextInt(4) + 1 == 3) { // 1/4的概率加入三角函数
          int symbolLocal = random.nextInt(3) + 6;
          problem += (symbol[symbolLocal] + String.valueOf(num));
          triangularNum--;
        } else { // 1/4的概率加入普通符号
          problem += String.valueOf(num);
        }

        if (random.nextInt(4) + 1 == 4 && leftBracket > 0 && braLength >= 2) {
          problem += ")";
          leftBracket--;
        }

        if (operatorNum >= 1 && triangularNum <= 0 && leftBracket <= 0) {  // 满足要求跳出
          break;
        }

        problem += symbol[random.nextInt(4)];
      }

      if (operatorNum <= 5 && braLength < 5) {
        break;
      }

    }
    problem += "=";
    return problem;
  }
}
1.5 GetPast类:

获得某个老师文件夹下之前所出过的题目。
该类有一个函数getPast(String name, String state),通过传入的类型和老师的账户获取老师目录下的所有文件,获得其中的题目信息并截取序号以后的字符串,添加到列表中,添加完成后将该列表返回。

public class GetPast {

  /**
   * 这个函数来获得某个老师文件夹下之前所出过的题目.
   *
   * @param name  the teacher name.
   * @param state the teacher state.
   * @return the teacher past problem(arraylist).
   */
  public static ArrayList<String> getPast(String name, String state) {
    String path = state + "\\" + name;
    File f = new File(path);   //  获取当前教师的目录;
    File[] fs = f.listFiles();    // 该目录下的文件
    ArrayList<String> pastProblem = new ArrayList<>();  // 字符串数组;

    for (File f1 : fs) {            //  遍历

      if (!f1.isDirectory()) {   //若是不是目录则txt文件

        try {
          BufferedReader br = new BufferedReader(new FileReader(f1));
          String l;
          while ((l = br.readLine()) != null && l.length() > 0) {
            l = l.substring(2, l.length() - 1);
            pastProblem.add(l);     // 非空行,将题目加到数组后面;
          }
          br.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    return pastProblem;
  }
}
1.6 GetMathPaper类:

获得数学试卷并保存到相应文件中。
函数:getQuestion(String name, String state, int num, String difficulty)。
通过传入的账户和类型获取当前账户已生成的题目信息,num、difficulty分别为要生成题目的数量和难度。根据不同的类型,使用不同的实例来实现多态。
如果出现重复题目,则i--并重新生成。

public class GetMathPaper {
  /**
   * 这个函数获得数学卷,然后写入文件中.
   *
   * @param name       the teacher name.
   * @param state      the teacher state.
   * @param num        the problem num.
   * @param difficulty the problem difficulty.
   */
  public static void getQuestion(String name, String state, int num, String difficulty) {
    ArrayList<String> pastQuestion = GetPast.getPast(name, state);  // 得到该老师之前的题目
    String dirpath = state + "\\" + name;     // 获得当前教师文件下的路径;
    Date fileDate = new Date();  // 获得创建txt文件的具体时间;
    // 建立日期格式;
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss");
    String fileName = simpleDateFormat.format(fileDate);
    String path = dirpath + "\\" + fileName + ".txt";

    try {
      FileWriter fw = new FileWriter(path, true); // 允许追加;
      for (int i = 0; i < num; i++) {
        GetMathProblem getMathProblem;         // 定义接口类型对象;

        if (difficulty.equals("小学")) {
          getMathProblem = new GetPrimaryMathProblem();
        } else if (difficulty.equals("初中")) {
          getMathProblem = new GetMiddleMathProblem();
        } else {
          getMathProblem = new GetHighMathProblem();
        }

        String problem = getMathProblem.getMathProblem(difficulty);

        if (!pastQuestion.contains(problem)) {
          pastQuestion.add(problem);
          problem = String.valueOf(i + 1) + "." + problem;  // 添加序号;
          fw.write(problem + "\n" + "\n");  // 追加
        } else {
          i--;   //  重复了那么i要-1 回到正常数量;
        }

      }

      System.out.println("试卷生成完毕!");
      System.out.println("******");
      fw.close();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

优点:autoQuestion包下的类通过接口实现了不同的功能,各个类的功能也比较单一,易于维护。

2.primarysystem包、middlesystem包,highsystem包:

这3个包中的类实现的功能基本相同,都是用于账户的登录和文件夹的生成。
所以接下来就以primarysystem包为例子进行分析。

2.1 primarysystem包:小学老师的登录判断

包含两个类PriLogin,priTeaFile。

public class PriLogin {

  /**
   * 这个类来判断是否为高中老师登录.
   *
   * @param name     the user name.
   * @param password the user password.
   * @return a num to judge.
   */
  public static int priLogin(String name, String password) {

    if ((name.equals("张三1") || name.equals("张三2") || name.equals("张三3"))
        && password.equals("123")) {
      return 1;
    }
    return 0;

  }
}
public class PriTeaFile {

  /**
   * 这个函数来创建小学文件夹,并且在该文件夹下创建三个老师的文件夹.
   */
  public static void priTeaFile() {

    File f = new File("./小学");
    f.exists();
    f.mkdir();
    f.isDirectory();
    f.exists();

    f = new File("./小学/张三1");
    f.exists();
    f.mkdir();
    f.isDirectory();
    f.exists();

    f = new File("./小学/张三2");
    f.exists();
    f.mkdir();
    f.isDirectory();
    f.exists();

    f = new File("./小学/张三3");
    f.exists();
    f.mkdir();
    f.isDirectory();
    f.exists();

  }
}

将小学、初中和高中的登录分开来有利于管理和维护,不过我觉得这3个包可以合并为一个包,两个函数,创建文件夹的函数可以通过传递两个参数账户和类型获取路径,两者应该都可以。

3.login包:包含登录相关的类

这个包下有4个类。

3.1 SystemSpeak类:输出系统的文字界面。
public static void autoQusestionSpeak(String difficulty) {

    System.out.println("*******************");
    System.out.println("准备生成" + difficulty + "数学题目的试卷,请输入生成题目的数量(10-30)");
    System.out.println("*******************");


    System.out.println("若输入0   可以选择切换难度");
    System.out.println("若输入-1  将退出当前用户,退出登录");

    System.out.println("*******************");

  }

优点:将重复出现的代码封装为一个函数,使代码看起来简洁明了。

3.2 ExceptionCatch类:抛出异常
public static int exceptionCatch() {
    Scanner sc = new Scanner(System.in);
    int num = 0;

    try {
      String input = sc.next();
      num = Integer.parseInt(input);
    } catch (NumberFormatException e) {
      System.err.println("输入错误");
      num = 2;
    }

    return num;
  }

优点:通过获取字符串转整形时的异常,设置num的值,根据num的值判断输入是否为数字。
我是通过hasNextInt()函数判断输入的是否为数字,还需考虑输入后的换行符,通过try,catch来实现似乎更简单。

3.3 Login类:账户登录系统

该类包含login()函数,用于初始时账户的登录。
输入账户密码后调用相应函数,并根据返回值设置state的值,然后调用函数QuestionLogin.questionLogin(name, state)进入出题界面。在该界面输入-1可以退出系统。

public static void login() {
    Scanner sc = new Scanner(System.in);
    System.out.println("欢迎来到自动出题系统");
    String state;

    while (true) {                   //  输入用户名,密码;
      System.out.println("登录请输入用户名和密码(两者之间请用空格隔开):");
      System.out.println("(输入-1,那么退出自动出题系统)");
      String name = sc.next();
      String w = "-1";                   // 接收-1

      if (name.equals(w)) {
        System.out.println("退出系统!");
        break;
      }

      String password = sc.nextLine();       // 输入密码;
      int local = 0;

      for (int i = 0; i < password.length() - 1; i++) {
        if (password.substring(i, i + 1).equals(" ")
            && !password.substring(i + 1, i + 2).equals(" ")) {
          local = i;
          break;
        }
      }

      password = password.substring(local + 1, password.length());
      int pri = 0;          // 用来确定属于什么类型的教师;
      int mid = 0;
      int high = 0;
      pri = primarysystem.PriLogin.priLogin(name, password);   // package.class.method();
      mid = middlesystem.MidLogin.midLogin(name, password);
      high = highsystem.HighLogin.highLogin(name, password);

      if (pri + mid + high == 1) {
        System.out.println("登陆成功");

        if (pri == 1) {
          state = "小学";
        } else if (mid == 1) {
          state = "初中";
        } else {
          state = "高中";
        }

        System.out.println("当前选择为" + state + "出题");
        QuestionLogin.questionLogin(name, state);
      } else {
        System.out.println("请输入正确的用户名,密码,重新输入用户名,密码");
        System.out.println("*************");
      }

    }
  }
3.4 QuestionLogin类:出题的界面系统

包含questionLogin(String name, String state)函数。
进入出题界面后可以直接输入题目的数量,满足输入要求调用函数autoquestion.GetMathPaper.getQuestion(name, state, num, difficulty)生成题目。或者输入-1退出登录,输入0切换出题难度。

public static void questionLogin(String name, String state) {
    String difficulty = state;

    while (true) {
      SystemSpeak.autoQusestionSpeak(difficulty);
      Scanner sc = new Scanner(System.in);
      int num = 0;

      while (true) {
        num = ExceptionCatch.exceptionCatch();

        if ((num >= 10 && num <= 30) || num == 0 || num == -1) {
          break;
        } else {
          System.out.println("输入错误,请按照规范输入");
          SystemSpeak.autoQusestionSpeak(difficulty);
        }

      }

      if (num == -1) {
        System.out.println("账号退出成功");
        System.out.println("*********");
        break;
      } else if (num == 0) {
        System.out.println("命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个");

        while (true) {
          String possibleDifficulty = sc.nextLine();

          if (possibleDifficulty.equals("切换为小学") || possibleDifficulty.equals("切换为初中")
              || possibleDifficulty.equals("切换为高中")) {

            for (int i = 0; i < possibleDifficulty.length(); i++) {
              if (possibleDifficulty.substring(i, i + 1).equals("为")) {
                difficulty = possibleDifficulty.substring(i + 1, i + 3);
              }
            }

            break;
          }

          System.out.println("难度输入不符(请输入:切换为xx(xx为小学或初中或高中))");
        }
      } else {
        System.out.println("准备生成数学题目"); //  num = ExceptionCatch.exceptionCatch();

        if (num >= 10 && num <= 30) {
          System.out.println("题目正在生成");
          System.out.println("******"); // 生成题目
          autoquestion.GetMathPaper.getQuestion(name, state, num, difficulty);
        } else {
          System.out.println("题目数量不正确,应该在10-30之间的整数");
        }

      }
    }
  }

4.Test类:

包含一个main函数,首先创建文件,然后调用Login.login()运行程序。

public static void main(String[] args) {


    //主函数
    //文件创建
    PriTeaFile.priTeaFile();
    HighTeaFile.highTeaFile();
    MidTeaFile.midTeaFile();

    //登陆系统
    Login.login();


  }

五、运行测试

1.登录

登录时有文字提示,在登录过程中的错误输入也都有相应的处理,输入正确的用户名和密码后可以正常登录系统。

2.切换出题难度

在出题界面选择0,并输入切换为XX,输入项符合要求时可以切换出题的难度。当输入项的切换类型错误,或整个输入项均不符合要求时,都会输出文字提示信息,并要求重新输入。

3.出题验证

当前登录用户为李四1,在出题界面输入正确的题目数量后,可以正确生成初中难度的题目。

把出题难度切换为小学或高中后,输入正确题目数量也可以生成小学或高中难度的题目。

在输入题目数量时,输入的题目数量小于10或大于30,以及输入的是一个字符串,会有错误信息提示。

4.退出登录(系统)

所有的功能测试正常。

六、总结

  • 优点

    1.项目结构清晰,将不同功能的类放在不同的包下,每个方法的功能单一,并且每个方法行数不超过40行,有利于代码的维护和理解。

    2.编码规范符合要求,且为每个类,方法,接口编写了javadoc。

    3.很好的处理了输入项的异常情况,输入项不符合要求和规范时输出文字提示错误信息。

    4.使用了接口实现多态,通过使用不同的实例执行不同的功能,可扩展性、灵活性好。

    5.界面交互性好,很好的考虑到用户的使用。

  • 缺点

    1.账户和密码写在相应的类里,当账户数量变多时会有些麻烦。

  • 可以改进的地方
    在输入密码时将原来的nextLine()修改为next(),在输入时next()会忽略有效字符前的空格,这样就不需要再去截取密码。

    在设置日期格式时,小时的设置使用了hh,获取的小时将以12小时制显示,无法判断是上午还是下午的时间,或许使用HH(24小时制)会更好一些。

  • 总结
    通过分析秦凯的代码,我也发现了自己在编写代码时没有考虑到的问题。
    例如所有的类都写在一个包里,可能不方便维护。可以通过抓取异常来防止输入类型不正确时的程序崩溃。
    通过与秦凯的讨论交流以及互评代码,我认识到了自己的不足以及代码可以改进的地方。