【个人项目互评】——中小学数学试卷生成系统

发布时间 2023-09-20 21:03:13作者: 名字想了俩小时
项目名称:中小学数学试卷生成系统
编程语言:java
代码作者:符南山
评价人 :秦凯


一、简介

我与符南山同学结对,并且都是采用java语言。因此下面博客,我们以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:账户,密码

附表-2:小学,初中,高中题目难度要求

三、项目文件的结构

在src文件夹下

  1. project包存放五个类 是代码的实现

  2. question文件夹 用来存放所有老师的文件夹

  3. teachers.txt 文件用来存放老师的账户信息

四、代码分析与测试

1.代码分析

1.1 Main类:

里面存放主函数main(),整个程序运行的端口;

main()函数:

  public static void main(String[] args) {
    GenerativeSystem generativeSystem = new GenerativeSystem();
    generativeSystem.runSystem();
  }
1.2 Teacher类: 抽象类

定义老师的属性,get函数获取属性;

1.3 MathTeacher类: 数学老师类
1)继承 Teacher类;
2)private属性:

List: allQuestion (用来记录当前老师已经生成题目列表) 用来实现后面的查重功能;

3)函数:

有参构造函数: 创建老师文件夹;

 public MathTeacher(String tType, String tAccount, String tPassword) {
    type = tType;
    account = tAccount;
    password = tPassword;
    allQuestion = new ArrayList<>();
    /* 如果数学老师文件夹没有创建,则新建一个文件夹 */
    String dirname = "src\\questions\\" + account;
    File d = new File(dirname);
    if (d.mkdir()) {
      System.out.println(account + "的文件夹已创建");
    }
  }

readQuestion() : 来读取该老师文件夹下的题目 存放到列表 allQuestion中;

public void readQuestion() {
    try {
      String filePath = "src" + File.separator + "questions" + File.separator + account;
      File file = new File(filePath);
      String[] fileList = file.list();
      if (fileList != null) {
        for (int i = 0; i < fileList.length; i++) {
          FileReader fileReader = new FileReader(filePath + "\\" + fileList[i]);
          BufferedReader reader = new BufferedReader(fileReader);
          String str;
          while ((str = reader.readLine()) != null) {
            if (str.equals("")) {
              str = reader.readLine();
            }
            allQuestion.add(str.substring(str.indexOf(" ") + 1));
          }
        }
      }
    } catch (Exception exception) {
      System.out.println("读取老师题目文件失败");
    }
  }

getQuestion():来获得list变量allQuestion;

public List<String> getQuestion() {
    return allQuestion;
  }

addQuestion(String aQuestion):添加题目到allQuestion,主要用于后面生成一道新题目,满足要求,那么需要这样加入到allQuestion中,完善查重功能;

  public void addQuestion(String aQuestion) {
    allQuestion.add(aQuestion);
   }
1.4 GenerativeSystem类: 题目生成系统
1)private属性:

mathTeachers 存放老师列表;

map 记录用户是否读取了对应文件夹下的题目;

2)函数:

无参构造函数 :调用readTeachers() 从teachers.txt文件中读取所有老师列表存放到mathTeachers链表中;

  public GenerativeSystem() {
    mathTeachers = new ArrayList<>();
    readTeachers();
  }

runSystem() :调用login()函数判断是否登录成功,登录成功之后调用generateQuestion函数生成题目;

public void runSystem() {
    while (true) {
      System.out.println("----------登录----------");
      System.out.println("请输入用户名 密码(空格分隔,输入-1退出系统):");
      Scanner sc = new Scanner(System.in);
      String account = sc.next();
      if (account.equals("-1")) {
        System.out.println("退出系统");
        break;
      }
      String password = sc.next();
      MathTeacher user = login(account, password);

      if (user == null) {
        System.out.println("请输入正确的用户名、密码");
      } else {
        System.out.println("----------------------");
        System.out.println("登录成功");
        /* 没有读取对应文件夹下的已生成题目 */
        if (map.get(user.getAccount()) == 0) {
          user.readQuestion();
          map.put(user.getAccount(), 1);
        }
        QuestionGeneration questionGeneration = new QuestionGeneration();
        questionGeneration.generateQuestion(user);
      }
    }
  }

readTeacher(): 通过读取teacher.txt文件将老师的账户信息 存储到 mathTeacher中 ;

优点:通过这样的方式如果后期添加老师,那么只需要在txt文件中添加相应的信息,便于后期的维护;

在这里,我是直接讲老师的信息存放在程序中,这一点我需要改进优化;

public void readTeachers() {
    try {
      BufferedReader reader = new BufferedReader(new FileReader("src\\teachers.txt"));
      String str;
      while ((str = reader.readLine()) != null) {
        String[] buff = str.split(" ");
        MathTeacher temp = new MathTeacher(buff[0], buff[1], buff[2]);
        mathTeachers.add(temp);
        map.put(buff[1], 0); // 是否读取题目文件标记设置为0
      }
    } catch (Exception exception) {
      System.out.println("文件打开失败");
    }
  }

login(): 判断是否登录成功。通过循环遍历mathTeacher列表 判断是否满足登录条件;

  public MathTeacher login(String account, String password) {
    for (MathTeacher mathTeacher : mathTeachers) {
      if (account.equals(mathTeacher.getAccount())
          && password.equals(mathTeacher.getPassword())) {
        return mathTeacher;
      }
    }
    return null;
  }

saveFile(): 通过循环 将生成的题目保存到该老师文件夹下,并且生成.txt文件;

  public static void saveFile(MathTeacher mathTeacher, List<String> temp) {
    Date date = new Date();
    SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
    String path = "src\\questions\\" + mathTeacher.getAccount() + "\\"
        + dataFormat.format(date) + ".txt";
    File file = new File(path);
    try {
      if (!file.exists()) {
        if (file.createNewFile()) {
          System.out.println("文件src\\questions\\" + mathTeacher.getAccount()
              + "\\" + dataFormat.format(date) + ".txt已创建");
        }
      }
      BufferedWriter bw = new BufferedWriter(new FileWriter(path));
      int sizes = temp.size();
      for (int i = 0; i < sizes; i++) {
        bw.write(temp.get(i));
        if (i != (sizes - 1)) {
          bw.write("\n\n");
        }
      }
      bw.close();
    } catch (Exception exception) {
      System.out.println("文件创建失败");
    }
  }
1.5 QuestionGeneration :生成题目的类
1)private属性: 无 ;
2)函数:

generateQuestion():接收当前登录的老师,以及他所属的类型; 进入while循环,printOption()出示选择界面,然后调用judgeNum(),判断是否满足要求,

用num变量接收该数字;switch通过num的值,执行不同的操作; -1 break 退出登录,0 切换难度,调用changeQuestionType(),1 则开始生成题目,输入题目数量,若满足 ,则调用mathmaticalQuestion()函数,开始生成题目;

public void generateQuestion(MathTeacher loginTeacher) {
    String order;
    int num;
    String questionType = loginTeacher.getType();
    Scanner sc = new Scanner(System.in);
    System.out.println("当前选择为" + questionType + "出题");
    while (true) {
      printOptions(loginTeacher.getAccount(), loginTeacher.getType(), questionType);
      if ((num = judgeNum()) == -2) {
        continue;
      }
      switch (num) {
        case -1:
          System.out.println("退出当前用户,请重新登录\n");
          return;
        case 0:
          System.out.println("请输入切换为XX(XX为小学、初中和高中三个选项中的一个)。");
          order = sc.next();
          if (order.length() < 5) {
            System.out.println("输入项不符合要求,请输入切换为XX。");
          } else {
            questionType = changeQuestionType(questionType, order);
          }
          break;
        case 1:
          System.out.println("准备生成" + questionType + "数学题目,请输入生成题目数量(10-30):");
          if ((num = judgeNum()) == -2) {
            continue;
          }
          if (num < 10 || num > 30) {
            System.out.println("题目数量有效范围为10-30,请重新输入\n");
          } else {
            /* 生成数学题目 */
            mathematicalQuestion(loginTeacher, questionType, num);
          }
          break;
        default:
          System.out.println("请输入正确选项\n");
      }
    }
  }

printOption(): 打印选择界面,文字提示选择的选项;

优点:因为在while循环中,提出来这样一个重复的过程可以有效的减少方法的行数,简洁方便;

 public void printOptions(String account, String teacherType, String questionType) {
    System.out.println("----------------------");
    System.out.println("中小学数学卷子自动生成程序");
    System.out.println("用户:" + account + " 账户类型:" + teacherType);
    System.out.println("----------------------");
    System.out.println("操作选项:");
    System.out.println("-1.退出当前用户,重新登录");
    System.out.println(" 0.切换出题难度");
    System.out.println(" 1.开始生成题目");
    System.out.println("当前出题难度为:" + questionType);
    System.out.print("请输入选项:");
  }

changeQuestionType() : 修改题目类型(小学,初中,高中);

  public String changeQuestionType(String oldType, String order) {
    String head = order.substring(0, 3);
    String changeType = order.substring(3);
    if (head.equals("切换为") && (changeType.equals("小学") || changeType.equals("初中")
        || changeType.equals("高中"))) {
      System.out.println("\n出题难度已切换为:" + changeType);
      return changeType;
    } else {
      System.out.println("输入项不符合要求,请输入小学、初中和高中三个选项中的一个");
      return oldType;
    }
  }

judgeNum():判断所输入的是否为数字;

优点:避免用户输入非数字的类型而导致程序崩溃;

在这里南山使用的判断输入是否为数字,我是通过try catch捕获异常,来实现,两者应该都可以;

  public int judgeNum() {
    int num;
    Scanner scanner = new Scanner(System.in);
    if (scanner.hasNextInt()) {
      num = scanner.nextInt();
      scanner.nextLine();
      if (num == -2) {
        return -3;
      }
      return num;
    } else {
      scanner.nextLine();
      System.out.println("请输入正确数字\n");
      return -2;
    }
  }

mathmaticalQuestion() :来生成题目的函数,调用getAQuestion来获取一道题目,然后进行查重判断,若满足那么temp.add,最后saveFile函数保存;

  public void mathematicalQuestion(MathTeacher mathTeacher, String type, int num) {
    List<String> temp = new ArrayList<>();
    for (int i = 1; i <= num; i++) {
      String result;
      result = getAQuestion(type);
      /* 有重复题目则重新生成 */
      if (duplicateChecking(mathTeacher, result)) {
        i--;
        continue;
      }
      mathTeacher.addQuestion(result);
      result = i + ". " + result;
      temp.add(result);
    }
    GenerativeSystem.saveFile(mathTeacher, temp);
    System.out.println(type + "数学题目已生成,数量为" + num);
  }

getAQuestion():通过类型判断出操作数的个数,利用随机数先生成操作数的大小,调用addBracket()函数添加括号,若为初中,高中,那么再调用sqrtAndSquare()函数添加平方或者根号,若为高中,那么再调用trigonometricFunction()添加三角函数; 最后将运算符添加到位;

优点:根据附表二的要求,将添加括号,平方根号,三角函数这些分别进行拆分,便于后期维护;
我的做法:是设置了一个接口,然后分别定义了三个类来继承接口,分别实现,相较于南山这种实现方法,显然我的那种代码长度更长,后期维护性也比较差,尽管使用了多态。。还有待改进

一个新思路(应该定义接口,三个类分别继承接口实现,同时将括号, 平方根号,三角函数拆分成三个函数,这样实现了多态,也实现了函数拆分,便于优化和后期维护);

 public String getAQuestion(String type) {
    char[] symbol = new char[]{
        '+', '-', '*', '/'
    };
    Random r = new Random();
    StringBuilder result = new StringBuilder();
    String[] operand;
    int operandNum;
    if (type.equals("小学")) {
      operandNum = r.nextInt(4) + 2;
    } else {
      operandNum = r.nextInt(5) + 1;
    }
    /* 生成操作数 */
    operand = new String[operandNum];
    for (int i = 0; i < operandNum; i++) {
      operand[i] = String.valueOf(r.nextInt(100) + 1);
    }
    /* 添加括号 */
    operand = addBracket(operand, operandNum);
    /* 添加平方或根号 */
    if (type.equals("初中") || type.equals("高中")) {
      operand = sqrtAndSquare(operand, operandNum);
    }
    /* 添加三角函数 */
    if (type.equals("高中")) {
      operand = trigonometricFunction(operand, operandNum);
    }
    /* 添加运算符 */
    for (int i = 0; i < operandNum; i++) {
      result.append(operand[i]);
      if (i != operandNum - 1) {
        result.append(symbol[r.nextInt(4)]);
      }
    }
    result.append("=");
    return result.toString();
  }

addBracket():添加括号

  public String[] addBracket(String[] operand, int operandNum) {
    Random r = new Random();
    int bracketNum = 0;
    for (int i = 0; i < operandNum; i++) {
      int temp = r.nextInt(3);
      if (temp == 1 && i < operandNum - 1) {
        operand[i] = "(" + operand[i];
        bracketNum++;
      } else if ((temp == 2) && bracketNum > 0) {
        operand[i] = operand[i] + ")";
        bracketNum--;
      }
      if (i == operandNum - 1 && bracketNum > 0) {
        while (bracketNum > 0) {
          operand[i] = operand[i] + ")";
          bracketNum--;
        }
      }
    }
    return operand;
  }

sqrtAndSquare():添加平方根号

 public String[] sqrtAndSquare(String[] operand, int operandNum) {
    Random r = new Random();
    boolean flag = false;
    for (int i = 0; i < operandNum; i++) {
      int temp = r.nextInt(6);
      if (temp == 1) {
        operand[i] = "√" + operand[i];
        flag = true;
      } else if (temp == 2) {
        operand[i] = operand[i] + "²";
        flag = true;
      }
    }
    /* 没有根号或平方 */
    if (!flag) {
      int index = r.nextInt(operandNum);
      if (r.nextBoolean()) {
        operand[index] = "√" + operand[index];
      } else {
        operand[index] = operand[index] + "²";
      }
    }
    return operand;
  }

trigonnometricFunction() : 添加三角函数

 public String[] trigonometricFunction(String[] operand, int operandNum) {
    Random r = new Random();
    String[] tri = {"sin", "cos", "tan"};
    boolean flag = false;
    for (int i = 0; i < operandNum; i++) {
      if (r.nextInt(3) == 1) {
        operand[i] = tri[r.nextInt(3)] + operand[i];
        flag = true;
      }
    }
    if (!flag) {
      int index = r.nextInt(operandNum);
      operand[index] = tri[r.nextInt(3)] + operand[index];
    }
    return operand;
  }

duplicateChecking():查重函数

public boolean duplicateChecking(MathTeacher mathTeacher, String checkString) {
    List<String> temp = mathTeacher.getQuestion();
    return temp.contains(checkString);
  }

2.测试

2.1 登录功能测试

优点:登录界面简洁,具有文字提示;


登录失败有文字提示,并且还可以继续登录;


用户名与密码输入正确,那么成功登录;

2.2 登录成功后,界面展示


优点:此时用分割线---进行分离,界面美观;
这里我是使用****来分割,个人感觉还是---会更美观,值得学习;


当输入不合规范时,程序不会崩溃,这里就体现了前面judgeNum()函数的重要性了;

2.3 切换难度


只有当输入切换为小学/初中/高中,才会切换难度,功能正确;

2.4 出题

当输入题目数量在10-30之间会生成题目;

当输入题目数量不满足,则不会生成;

小学题目:

初中题目:

高中题目:

小学要求(+-*/) 都考虑到了,并且操作数范围是2-5 ,初中(要求至少一个平方或根号),操作数范围是1-5,高中题目(要求至少一个三角函数),操作数范围是1-5; 出题满足以上规范,功能正确;

2.5 试卷保存格式


出题文件的保存命名与格式也正确;


五、总结

针对于符南山的代码的编写与运行过程:

优点:

  • 老师的所有账户信息存储在teachers.txt文件中,这样便于老师信息的更改,便于后期维护。

  • 在出题函数中,将添加括号,添加平方根号,添加三角函数的步骤,拆分成三个方法,很巧妙。

  • 在输入时,总会使用judgeNum()函数来判断是否为整数,从而避免因用户输入不规范导致程序的崩溃。

  • 在运行中,界面的文字提示都很到位,既不会显的很臃肿,也很好的传达了意思。

不足:

  • 对于Teacher这个抽象类的使用不是很恰当,没有很好的利用上抽象类Teacher

最后还有一点,这个仁者见仁:

在运行中,切换难度如果输入不规范,在我看来,应该接着循环,让他输入想要切换的难度,南山的代码是让他重新回到选择的界面,这个大家都有不同的理解;

六、个人感悟

在分析测试完,南山所写的项目之后,感觉自己的代码还有不少可以改进的地方;

譬如:在出题函数我也可以将添加括号,添加平方根号,这些函数,独立拆分出来,这样便于后期优化维护;

同时,在程序运行过程中,自己的文字提示与界面显示 应该多加一个分割符------- 这样用户看起来更加简洁方便;

在与南山经过一起讨论分析之后,让我也认识到了互相学习,互相讨论的重要性;

这篇博客中,如果有哪个地方写的不正确,欢迎大家指出,批评;
互相讨论,学习,大家共同进步!