项目名称:中小学数学卷子自动生成程序
分析对象:甘英龙
分析人:辜玫致
编程语言:Java
一、总体设计思路
代码设计的总体思路:
1. Paper类是试卷抽象类,然后PrimaryPaper、MiddlePaper和HighPaper这三个类都继承了Paper抽象类,分别代表小学试卷,初中试卷和高中试卷,这三个类都可以生成各自对应难度的试卷。
2. User类代表老师用户类,可以调用三个试卷类来生成不同难度的试卷。
3. ExamPaperSystem类,是main函数所在的类,主要实现登录和接收用户输入来执行相应操作的功能。
代码分为以下六个类文件:
类文件名
|
主要功能 |
ExamPaperSystem.java |
main函数所在的文件,实现用户的登录以及接收用户的输入并且调用相应的函数来执行如生成试卷和切换难度等操作。
|
User.java |
定义了用户类的字段和方法。
|
Paper.java |
是抽象类,声明了生成试卷的函数;还实现了很多辅助函数用来生成算式,以及还有对算式进行不同难度加工的函数;还实现了查重功能。
|
PrimaryPaper.java |
继承了Paper类,实现了生成试卷的函数,以及定义了一个生成小学难度题目的函数。
|
MiddlePaper.java |
继承自Paper类,也实现了生成试卷的函数,还有定义了一个生成初中难度题目的函数。
|
HighPaper.java |
继承自Paper类,并且也实现了生成试卷的函数,定义了一个生成高中难度题目的函数。
|
定义了name,password,type私有属性,可以通过get,set等函数对它们进行获取,还有定义了生成不同难度试卷的方法generate
public void generate(int numQuestions) throws IOException {
switch (this.type) {
case "小学":
Paper primaryPaper = new PrimaryPaper();
primaryPaper.generatePaper(numQuestions, this.name);
break;
case "初中":
Paper middlePaper = new MiddlePaper();
middlePaper.generatePaper(numQuestions, this.name);
break;
case "高中":
Paper highPaper = new HighPaper();
highPaper.generatePaper(numQuestions, this.name);
break;
default:
break;
}
}
声明了generatePaper函数。PrimaryPaper、MiddlePaper和HighPaper这三个类都会实现这个方法。
public abstract void generatePaper(int num_questions, String userName) throws IOException;
还有定义了查重函数isDuplicate:这段代码是读取指定文件下的以前试卷中的题目和刚生成的题目进行比较,如果相等或者满足2个操作数时的交换律就返回true表示重复,否则返回false。
protected boolean isDuplicate(Vector<String> ques, String userName) throws IOException {
File file = new File(userName);
File[] fs = file.listFiles();
assert fs != null;
for (File f : fs) {
InputStreamReader reader = new InputStreamReader(Files.newInputStream(f.toPath()));
BufferedReader br = new BufferedReader(reader);
String line;
StringBuilder finalQuestion = new StringBuilder();
for (String s : ques) {
finalQuestion.append(s);
}
String temp = finalQuestion.toString();
while ((line = br.readLine()) != null) {
if (!line.equals("")) {
line = line.substring(line.indexOf(".") + 2, line.length() - 2);
Vector<String> parseLine = parseString(line);
if (line.equals(temp)) {
return true;
} else if (parseLine.size() == 3 && ques.size() == 3) {
if (parseLine.get(0).equals(ques.get(2)) && parseLine.get(2).equals(ques.get(0))
&& parseLine.get(1).equals(ques.get(1))) {
return true;
}
}
}
}
}
return false;
}
然后是对初始算式进行三个不同等级难度的加工函数:
小学难度的加工函数levelZeroGenerate,给到来的初始式子随机加括号,初始式子中的操作数、操作符按顺序存储在一个String类型的向量中,方便后续在操作数间加括号或者sqrt等更高难度的函数符号。
// 对初始生成的题目随机加括号
protected void levelZeroGenerate(Vector<String> question) {
int len = question.size();
int operandNum = (len + 1) / 2;
Random rand = new Random();
if (operandNum >= 3) {
int seed = rand.nextInt(3);
if (seed == 0) {
return;
}
int leftBracket = rand.nextInt(operandNum - 1);
int rightBracket = rand.nextInt(operandNum - leftBracket - 1) + leftBracket + 1;
if (leftBracket == 0 && rightBracket == operandNum - 1) {
rightBracket--;
}
question.set(leftBracket * 2, "(" + question.get(leftBracket * 2));
question.set(rightBracket * 2, question.get(rightBracket * 2) + ")");
if (operandNum >= 4) {
seed = rand.nextInt(2);
if (seed == 0) {
return;
}
int doubleLeftBracket;
int doubleRightBracket;
do {
doubleLeftBracket = rand.nextInt(operandNum - 1);
doubleRightBracket = rand.nextInt(operandNum - doubleLeftBracket - 1) + doubleLeftBracket + 1;
if (doubleLeftBracket == 0 && doubleRightBracket == operandNum - 1) {
doubleRightBracket--;
}
} while ((doubleLeftBracket == leftBracket && doubleRightBracket == rightBracket)
|| doubleLeftBracket == rightBracket || doubleRightBracket == leftBracket);
question.set(doubleLeftBracket * 2, "(" + question.get(doubleLeftBracket * 2));
question.set(doubleRightBracket * 2, question.get(doubleRightBracket * 2) + ")");
}
}
}
初中难度的加工函数levelOneGenerate,随机给操作数加开方sqrt和平方符号^2。
// 对初始生成的题目进行符合初中难度的加工
protected void levelOneGenerate(Vector<String> question, int extraOpNum, int squareNum) {
int len = question.size();
int[] index = new int[(len + 1) / 2];
for (int i = 0; i < len; i += 2) {
index[i / 2] = i;
}
randomShuffle(index);
for (int i = 0; i < squareNum; i++) {
question.set(index[i], question.get(index[i]) + "^2");
}
for (int i = squareNum; i < extraOpNum; i++) {
if (question.get(index[i]).charAt(0) == '(') {
question.set(index[i], "sqrt" + question.get(index[i]));
} else {
question.set(index[i], "sqrt(" + question.get(index[i]) + ")");
}
}
}
高中难度的加工函数levelTwoGenerate,随机给操作数加上三角函数符号sin、cos、tan。
// 对初始生成的题目进行符合高中难度的加工
protected void levelTwoGenerate(Vector<String> question, int extraOpNum, int sinNum, int cosNum) {
int len = question.size();
int[] index = new int[(len + 1) / 2];
for (int i = 0; i < len; i += 2) {
index[i / 2] = i;
}
randomShuffle(index);
for (int i = 0; i < sinNum; i++) {
if (question.get(index[i]).charAt(0) == '(') {
question.set(index[i], "sin" + question.get(index[i]));
} else {
question.set(index[i], "sin(" + question.get(index[i]) + ")");
}
}
for (int i = sinNum; i < sinNum + cosNum; i++) {
if (question.get(index[i]).charAt(0) == '(') {
question.set(index[i], "cos" + question.get(index[i]));
} else {
question.set(index[i], "cos(" + question.get(index[i]) + ")");
}
}
for (int i = sinNum + cosNum; i < extraOpNum; i++) {
if (question.get(index[i]).charAt(0) == '(') {
question.set(index[i], "tan" + question.get(index[i]));
} else {
question.set(index[i], "tan(" + question.get(index[i]) + ")");
}
}
}
生成符合要求题目的函数generateQuestion:
思路:先生成只有操作数和加减乘除操作符的基础式子,操作数、操作符按顺序存储在一个String类型的向量中方便后续在操作数间加括号或者sqrt等更高难度的函数符号,然后再调用levelZeroGenerate函数进行加括号。
private Vector<String> generateQuestion() {
Random rand = new Random();
int numOperands = rand.nextInt(4) + 2;
Vector<String> question = new Vector<>();
for (int i = 0; i < numOperands; i++) {
question.add(String.valueOf(randomOperand()));
if (i < numOperands - 1) {
question.add(randomOperator());
}
}
levelZeroGenerate(question);
return question;
}
还有实现了抽象类中声明的函数generatePaper,代码如下:
先生成个人的试卷文件夹(如果没有的话),然后再根据时间创建txt文件,然后再把指定数量生成的题目加上题号写入该txt文件中。
// 生成试卷
@Override
public void generatePaper(int numQuestions, String userName) throws IOException {
Date date = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
String fileName = formatter.format(date.getTime()) + ".txt";
File dir = new File(userName);
if (!dir.exists()) {
dir.mkdir();
}
fileName = userName + "/" + fileName;
File file = new File(fileName);
if (!file.exists()) {
file.createNewFile();
}
BufferedWriter output = new BufferedWriter(new FileWriter(file, true));
for (int i = 0; i < numQuestions; i++) {
Vector<String> question = generateQuestion();
if (isDuplicate(question, userName)) {
i--;
continue;
}
StringBuilder finalQuestion = new StringBuilder();
for (String s : question) {
finalQuestion.append(s);
}
String word = String.valueOf(i + 1) + ". " + finalQuestion + " =\n\n";
output.write(word);
}
output.flush();
output.close();
}
思路和PrimaryPaper类中generateQuestion函数差不多,就是多了个levelOneGenerate函数进行加初中难度的加工。
private Vector<String> generateQuestion() {
Random rand = new Random();
int numOperands = rand.nextInt(4) + 2;
Vector<String> question = new Vector<>();
for (int i = 0; i < numOperands; i++) {
question.add(String.valueOf(randomOperand()));
if (i < numOperands - 1) {
question.add(randomOperator());
}
}
levelZeroGenerate(question);
int extraOpNum = rand.nextInt(numOperands - 1) + 1;
int squareNum = rand.nextInt(extraOpNum + 1);
levelOneGenerate(question, extraOpNum, squareNum);
return question;
}
也实现了抽象类中声明的函数generatePaper,思路跟PrimaryPaper类中的实现差不多,这里就不粘贴出来了。
思路和MiddlePaper类中generateQuestion函数也差不多,多了个调用levelTwoGenerate函数进行加高中难度的加工,可以看出重复代码有些多。这里就不粘贴出来了。
也实现了抽象类中声明的函数generatePaper,思路跟MiddlePaper类中的实现差不多。
main函数:里面只有一个login函数的调用。
findUser函数根据给定的用户名和密码来查找对应的User对象,找到了就返回该对象,否则就返回null。
login函数用来接收用户的用户名和密码的输入,进行判断,调用findUser函数来查找用户,查找到了就登录成功,然后会调用handUserRequest函数。
handUserRequest函数,接收用户输入数字来调用handleUserInput(int num, User user)生成指定数目题目的试卷。
或者接受用户输入的字符串来调用handleUserInput(String input, User user)来实现切换难度。
// 处理整数输入,一般用于接收生成题目的数量
private static void handleUserInput(int num, User user) throws IOException {
if (num == -1) {
login();
return;
}
if (num < 10 || num > 30) {
System.out.println("请输入10-30之间的整数,或输入-1退出当前用户并重新登录");
Scanner in = new Scanner(System.in);
num = in.nextInt();
handleUserInput(num, user);
} else {
user.generate(num);
System.out.println("输入1继续出题,输入其他内容则退出系统");
Scanner in = new Scanner(System.in);
if (in.hasNextInt()) {
int choice = in.nextInt();
if (choice == 1) {
handleUserRequest(user);
}
}
}
}
// 处理字符串输入,一般用于接收切换的身份
private static void handleUserInput(String input, User user) throws IOException {
switch (input) {
case "切换为小学":
user.setType("小学");
break;
case "切换为初中":
user.setType("初中");
break;
case "切换为高中":
user.setType("高中");
break;
default:
System.out.println("请输入小学、初中和高中三个选项中的一个:");
Scanner in = new Scanner(System.in);
input = in.next();
handleUserInput(input, user);
return;
}
System.out.println("切换成功!");
handleUserRequest(user);
}
总结:符合个人项目所有需求,对用户输入具有较好的容错性,输入其他无关的内容也会提示正确的输入。
运行截图:
如上图所示,登录张三1用户后让系统生成小学难度试卷,并且切换难度,生成初中和高中难度的试卷。都是设定了三十题,生成的试卷.txt文件,都会在张三1文件夹下,如下所示:
都截取了最后五题,如下所示:从左到右一次是小学,初中和高中,生成的题目都符合难度要求。
1.优点
(1)整体符合编程规范,也满足了个人项目所有需求,对用户输入具有较好的容错性,不会说不按要求输入系统就会报错不能运行。
(2)每个函数都有写上其功能注释,使得代码阅读起来不会很困难。
(3)生成的算式加括号、初中和高中难度等符号都做到了随机性。
2.缺点
(1)出现不必要的递归调用函数。
比如:ExamPaperSystem类中的handleUserInput函数,其实可以修改为for循环的,不必要的递归可能会创建很多函数,占用空间从而影响性能。
(2)部分地方代码冗余,重复代码有些多。
如PrimaryPaper、MiddlePaper和HighPaper这三个类中的generateQuestion函数代码有重复,重复部分如下:
Random rand = new Random();
int numOperands = rand.nextInt(4) + 2;
Vector<String> question = new Vector<>();
for (int i = 0; i < numOperands; i++) {
question.add(String.valueOf(randomOperand()));
if (i < numOperands - 1) {
question.add(randomOperator());
}
}
其实可以这段代码可以复用,MiddlePaper中的generateQuestion函数体实现:可以先调用PrimaryPaper的generateQuestion函数,生成小学难度的题目,然后再加上几行初中难度的加工;同理,高中的也可以调用初中的,然后再进行高中难度的加工。这样就不会重复出现上面那段代码了。
(3)没有设置用户信息的存储,仅仅是把用户数据放入缓存中,如果用户信息进行了变动比如难度切换,系统关闭后这个变动不能被存储下来,而且如果要扩展的话也不能实现用户注册和删除。
(4)没有主文件夹来放所有用户的试卷个人文件夹,直接在当前工程文件路径下创建个人试卷文件夹,这样的话如果用户多了的话当前工程文件路径下的文件夹也会很多。