结对项目(JAVA)
结队项目:自动生成小学四则运算题目(JAVA)
一、Github项目地址(合作人:黄煜淇、郭沛)
https://github.com/huange7/arithmetic
二、题目叙述
2.1题目数字以及运算符要求:
- 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 自然数:0, 1, 2, …。
- 运算符:+, −, ×, ÷。
- 括号:(, )。
- 等号:=。
- 分隔符:空格(用于四则运算符和等号前后)。
- 算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
- 四则运算题目:e = ,其中e为算术表达式。
2.2 生成题目具体操作过程及格式:
- 使用 -n 参数控制生成题目的个数,例如: Myapp.exe -n 10 将生成10个题目。
- 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如 :Myapp.exe -r 10 将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
- 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
- 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
- 每道题目中出现的运算符个数不超过3个。
- 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
- 生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
- 四则运算题目1
- 四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
- 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
答案1
答案2
- 真分数的运算如下例所示:1/6 + 1/8 = 7/24。
- 程序应能支持一万道题目的生成。
- 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt 统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
三、解题思路
1、首先是生成题目的实现思路:(伪代码如下)
步骤:
if 随机方法
{
随机生成操作符数量 operatorsNumber
计算括号上限数量 limit
随机生成数字 List<String> numberList
随机生成运算符 List<Character>
}
for
if 满足括号条件 && 未达到括号数量上限
if 随机方法
左括号添加
数字添加
if 满足括号条件 && 括号数量大于0
if 随机方法
右括号添加
运算符添加
endfor
将未闭合的括号进行闭合
2、其次,得到一个表达式后,便开始对其进行计算,实现思路如下:
- 将表达式中的所有真分数转换成假分数;
- 将中缀表达式转化成后缀表达式,转换过程利用了栈的先进后出的特点;
- 之后便是后缀表达式的计算过程:从左到右遍历中缀表达式中的每一个数字和符号;若是数字则进栈;若为符号,则把栈顶的两个数字出栈,进行运算(两个数运算过程见下方叙述),运算结果再进栈,直到获得最终结果;
-
两个数运算过程:首先判断两个数是否为分数形式,若为分数形式则调用分数计算的方法,整数则对应整数的运算方法;在处理除号运算时,若分母为0,则会返回ERROR字符串;
- 最后的计算结果若为负数或者计算的结果超出用户要求的限定范围,则也会返回ERROR字符串;
四、设计实现过程
主要分成几个大类:Main、Service、ShowGraphic、Calculate以及AnswerFile,具体流程图如下:
五、关键代码说明
Main函数:
public class Main { public static void main(String[] args) { Service service = new ServiceImpl(); service.main(args); } }
ServiceImpl类:实现参数校验、调用GUI界面功能以及计算等核心功能
public class ServiceImpl implements Service { private List<String> answerList = new ArrayList<>(); public static List<LinkedList<String>> numberList = new ArrayList<>(); public static List<LinkedList<Character>> charList = new ArrayList<>(); @Override public void generateQuestion(Integer number) { // 清空答案列表 answerList.clear(); Operations operations = new Operations(); int times = 0; while (number > 0) { AnswerResult answerResult = new AnswerResult(); String operation = operations.generateOperations(); String resultString = Calculate.getResult(operation, ArgsUtil.numberBound); if ("ERROR".equals(resultString)) { numberList.remove(numberList.size() - 1); charList.remove(charList.size() - 1); continue; } if (checkExpression(resultString)){ numberList.remove(numberList.size() - 1); charList.remove(charList.size() - 1); times++; if (times == 16){ System.out.println("生成题目时冲突多次!"); break; } continue; } answerResult.setQuestion(operation + " ="); answerList.add(resultString); Controller.operationData.add(answerResult); number--; } if (!ArgsUtil.isX) { // 将题目展示在控制台 System.out.println("-----------------------------------------"); printQuestion(); } // 将答案写到文件 try { AnswerFile.writeFile(answerList, true); } catch (IOException e) { System.out.println("生成答案文件异常"); } System.out.println("答案文件已经存放在:" + AnswerFile.address); if (!ArgsUtil.isX) { downloadQuestion(); } } private boolean checkExpression(String result){ if (answerList.size() <= 0){ return false; } int now = answerList.size(); boolean flag = false; // 获取当前表达式 List<String> nowNumber = numberList.get(now); List<Character> nowChar = charList.get(now); for (int i = 0; i < answerList.size() - 1; i++){ if (!result.equals(answerList.get(i))){ continue; } List<String> iNumber = numberList.get(i); List<Character> iChar = charList.get(i); // 如果数字的大小和字符的大小相等,则进行进一步的验证 if (!(iNumber.size() == nowNumber.size() && iChar.size() == nowChar.size())){ continue; } boolean bs = false; // 查看是否存在 for (String iString : iNumber){ if (!nowNumber.contains(iString)){ bs = true; break; } } if (bs){ continue; } for (Character iC : iChar){ if (!nowChar.contains(iC)){ bs = true; break; } } if (bs){ continue; } flag = true; } return flag; } private void printQuestion() { Controller.operationData.forEach(answerResult -> { System.out.println(answerResult.questionProperty().getValue()); System.out.println("-----------------------------------------"); }); } @Override public int[] checkQuestion() { File exerciseFile = new File(ArgsUtil.questionPath); File answerFile = new File(ArgsUtil.answerPath); Map<Integer, String> result = AnswerFile.checkAnswer(exerciseFile, answerFile); if (result == null){ System.out.println("文件不存在!"); return null; } int right = 0, error = 0; String Right = ""; String Error = ""; for (int i = 1; i <= result.size(); i++) { if (result.get(i).equals("right")) { right++; if (Right.equals("")) { Right = Right + i; } else { Right = Right + ", " + i; } } else { error++; if (Error.equals("")) { Error = Error + i; } else { Error = Error + ", " + i; } } } System.out.println("Correct: " + right + "(" + Right + ")" +"\r\n" + "Wrong: " + error + "(" + Error + ")"); return new int[]{right, error}; } @Override public void downloadQuestion() { // 生成题目文件 try { AnswerFile.writeFile(Controller.operationData, false); } catch (IOException e) { System.out.println("生成题目文件时失败"); } System.out.println("题目文件已经存放在:" + AnswerFile.address); } @Override public void main(String[] args) { // 进行参数的校验 if (!ArgsUtil.verifyArgs(args, false)) { System.out.println("参数输入错误!"); return; } // 展开图形界面 if (ArgsUtil.isX) { ShowGraphic.show(args); return; } // 进行处理 if (ArgsUtil.isGenerate) { // 生成题目 generateQuestion(ArgsUtil.questionNumber); }else { // 进行答案的校对 checkQuestion(); } } }
Operation实现算术表达式的生成
public class Operations { // 记录运算符数量 private Integer operatorsNumber; // 记录括号的位置 private List<Integer> bracketsPos = new ArrayList<>(); // 括号最大值 private static final Integer MAX_BRACKETS = 2; // 括号已结束 private static final Integer USED = -1; // 记录已经使用的运算符数量 private int count = 0; // 括号的限制数量 private int limit = 0; // 存储数字 private LinkedList<String> numberList; // 存储符号 private LinkedList<Character> characters; public String generateOperations() { StringBuffer stringBuilder = new StringBuffer(); if (randomSelective()) { init(true); }else { init(false); } Iterator<String> iteratorNumber = numberList.iterator(); Iterator<Character> iteratorChar = characters.iterator(); for (int i = 0; ; i++) { if (i % 2 == 0) { generateLeftBracket(stringBuilder); stringBuilder.append(iteratorNumber.next()); } else { generateRightBracket(stringBuilder, false); stringBuilder.append(iteratorChar.next()); count++; } if (!iteratorNumber.hasNext()) { break; } stringBuilder.append(" "); } generateRightBracket(stringBuilder, true); destroy(); return stringBuilder.toString(); } private void init(Boolean isTrueFraction) { // 随机生成操作符数量 operatorsNumber = new Random().nextInt(3) + 1; // 设置括号的上限数量 limit = operatorsNumber == 3 ? 2 : operatorsNumber == 1 ? 0 : 1; // 随机生成数字 generateNumber(isTrueFraction); // 随机生成操作符 generateChar(); ServiceImpl.numberList.add(numberList); ServiceImpl.charList.add(characters); } private void destroy() { // 对数字进行归零 operatorsNumber = 0; // 对括号位置进行置零 bracketsPos.clear(); // 对操作符数量进行置零 count = 0; // 对括号上限进行置零 limit = 0; } // 随机生成数字(整数或真分数) private void generateNumber(Boolean isTrueFraction) { numberList = new LinkedList<>(); for (int i = 0; i < operatorsNumber + 1; i++) { numberList.add(buildNumber(isTrueFraction)); } } private String buildNumber(Boolean isTrueFraction) { if (isTrueFraction && randomSelective()) { // 保证生成大于0 int left; do { left = new Random().nextInt(ArgsUtil.numberBound); }while (left <= 0); // 控制分母在10以内 int mother; do{ mother = new Random().nextInt(ArgsUtil.numberBound < 11 ? ArgsUtil.numberBound : 11); }while (mother <= 0); int son; // 保证生成最简分数 do { son = new Random().nextInt(mother) + 1; }while (!isSimplest(son, mother)); return left + "'" + son + "/" + mother; } else { return String.valueOf(new Random().nextInt(ArgsUtil.numberBound)); } } // 求出最简分数 private boolean isSimplest(int son, int mother){ int tempMo = mother, tempSon = son; int r = tempMo % tempSon; while ( r > 0){ tempMo = tempSon; tempSon = r; r = tempMo % tempSon; } return tempSon == 1; } // 随机生成运算符 private void generateChar() { characters = new LinkedList<>(); String chars = "+-×÷"; for (int i = 0; i < operatorsNumber; i++) { characters.add(chars.charAt(new Random().nextInt(chars.length()))); } } // 随机选择算法 private boolean randomSelective() { return new Random().nextInt(2) == 1; } // 生成左括号 private void generateLeftBracket(StringBuffer stringBuilder) { for (int i = 0; i < limit; i++) { if (bracketsPos.size() >= limit) { break; } if (count >= operatorsNumber) { break; } if (count == 0 || (stringBuilder.charAt(stringBuilder.length() - 1) < '0' || stringBuilder.charAt(stringBuilder.length() - 1) > '9')) { if (!bracketsPos.isEmpty() && count - bracketsPos.get(0) > 1) { break; } if (!bracketsPos.isEmpty() && count == bracketsPos.get(0) && bracketsPos.get(0) == operatorsNumber - 1) { break; } // 随机算法-true则为加上括号 if (randomSelective()) { stringBuilder.append('('); bracketsPos.add(count); } } } } // 生成右括号 // flag 表示是否为表达式结尾 private void generateRightBracket(StringBuffer stringBuilder, boolean flag) { if (bracketsPos.isEmpty()) { return; } if (flag) { // 如果已经到达结尾位置,进行剩余括号的闭括号操作 for (int i = 0; i < bracketsPos.size(); i++) { if (!bracketsPos.get(i).equals(USED)) { stringBuilder.append(')'); } } // 可能发生多加一个括号的情况,将其进行剔除 if (bracketsPos.size() == MAX_BRACKETS && bracketsPos.get(0).equals(bracketsPos.get(1)) && bracketsPos.get(0) != -1) { stringBuilder.delete(stringBuilder.length() - 1, stringBuilder.length()); } return; } if (bracketsPos.size() == MAX_BRACKETS) { // 说明此时有两个括号 if (bracketsPos.get(0).equals(bracketsPos.get(1))) { // 说明此时起始位置一样 if (count - bracketsPos.get(1) == 1) { // 说明此时内括号应该结束 stringBuilder.append(')'); } else if (count - bracketsPos.get(0) == operatorsNumber - 1) { // 说明此时内括号应该结束 stringBuilder.append(')'); bracketsPos.replaceAll(pos -> USED); } } else { // 说明起始位置不同 if (count - bracketsPos.get(1) == 1) { stringBuilder.append("))"); bracketsPos.replaceAll(pos -> USED); } } } else { if (count - bracketsPos.get(0) == operatorsNumber - 1) { stringBuilder.append(')'); bracketsPos.set(0, -1); return; } if (count - bracketsPos.get(0) == 1) { if (randomSelective()) { stringBuilder.append(')'); bracketsPos.set(0, -1); } } } } }
Calculate实现具体的计算过程
public class Calculate { public static Pattern pattern = Pattern.compile("[0-9]+\\'[0-9]+\\/[1-9][0-9]*"); //scope就是用户输入的范围限制 public static String getResult(String expression, Integer scope) { //将所有空格去掉 expression = expression.replaceAll(" +", ""); //将表达式中所有的真分数转化成假分数 Matcher m = pattern.matcher(expression); while(m.find()) { expression = expression.replace(m.group(), Transform.TrueToFalse(m.group())); } //将中缀表达式转换成后缀表达式 expression = Transform.change(expression); //对后缀表达式进行运算 //存放操作符的栈 Stack<String> stack = new Stack<>(); //将后缀表达式进行切割,分成多个字符串,分割之后就是单纯的数字或者运算符 String[] strings = expression.split("\\|+"); for (int i = 0; i < strings.length;) { if (strings[i].matches("[0-9].*")) { stack.push(strings[i]); i++; continue; } String num2 = stack.pop(); String num1 = stack.pop(); String result = cout(num1, num2, strings[i]); if (result.equals("ERROR")) { return result; } stack.push(result); i++; } //使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围 String result = Transform.FinalFraction(stack.pop()); if (result.equals("ERROR")) { return result; } //对结果进行校验 if (Transform.isOutRange(scope, result)) { return "ERROR"; } return result; } //判断两个数需要进行计算的方法:num1表示数字1,num2表示数字2,temp表示运算符, private static String cout(String num1, String num2, String temp) { String result; //分两种方式运算,一种是整数的运算,一种是分数的运算 if (num1.matches("\\-{0,1}[0-9]+\\/\\-{0,1}[0-9]+") || num2.matches("\\-{0,1}[0-9]+\\/\\-{0,1}[0-9]+")) { //说明是分数,调用分数运算方法 result = FractionCount(num1, num2, temp); } else { //调用整数运算方法 result = IntCount(num1, num2, temp); } return result; } //num1表示数字1,num2表示数字2,temp表示运算符 private static String IntCount(String num1, String num2, String temp) { switch (temp) { case "+": return String.valueOf(Integer.valueOf(num1) + Integer.valueOf(num2)); case "-": return String.valueOf(Integer.valueOf(num1) - Integer.valueOf(num2)); case "×": return String.valueOf(Integer.valueOf(num1) * Integer.valueOf(num2)); case "÷": return Simplify(Integer.valueOf(num1), Integer.valueOf(num2)); default: } return null; } //将分数进行化简的式子(numerator为分子,denominator为分母) public static String Simplify(Integer numerator, Integer denominator) { if (denominator == 0) { return "ERROR"; } if (numerator == 0) { return "0"; } int p = Transform.getMax(numerator, denominator); numerator /= p; denominator /= p; if (denominator == 1) { return String.valueOf(numerator); } else { return (numerator) + "/" + (denominator); } } //分数运算:num1表示数字1,num2表示数字2,temp表示运算符 private static String FractionCount(String num1, String num2, String temp) { //将所有的数字都化成最简分数来进行计算 int[] first = Transform.changeToFraction(num1); int[] second = Transform.changeToFraction(num2); int[] result = new int[2]; //获取两个分母的最小公倍数 int min = first[1] * second[1] / Transform.getMax(first[1], second[1]); switch (temp) { case "+": //分子 result[0] = first[0] * min / first[1] + second[0] * min / second[1]; //分母 result[1] = min; return Simplify(result[0], result[1]); case "-": //分子 result[0] = first[0] * min / first[1] - second[0] * min / second[1]; //分母 result[1] = min; return Simplify(result[0], result[1]); case "×": //分子 result[0] = first[0] * second[0]; //分母 result[1] = first[1] * second[1]; return Simplify(result[0], result[1]); case "÷": //分子 result[0] = first[0] * second[1]; //分母 result[1] = first[1] * second[0]; return Simplify(result[0], result[1]); } return null; } }
AnswerFile实现题目以及答案文本生成,同时进行答案校对的功能
public class AnswerFile { //获取系统当前路径 public static final String address = System.getProperty("user.dir"); public static Pattern patten = Pattern.compile("[1-9][0-9]*\\.[0-9]+(\\'{0,1}[0-9]+\\/[0-9]+){0,1}(\\/[1-9]+){0,1}"); //将答案写入文件 public static void writeFile(List answerList, boolean isAnswer) throws IOException { File answerfile; File exercisefile; FileOutputStream outputStreamAnswer = null; FileOutputStream outputStreamExercise = null; BufferedWriter answerWriter = null; BufferedWriter exerciseWriter = null; if (isAnswer) { answerfile = new File(address + "\\answer.txt"); outputStreamAnswer = new FileOutputStream(answerfile); answerWriter = new BufferedWriter(new OutputStreamWriter(outputStreamAnswer, "UTF-8")); if (!answerfile.exists()) { answerfile.createNewFile(); } } else { exercisefile = new File(address + "\\exercise.txt"); outputStreamExercise = new FileOutputStream(exercisefile); exerciseWriter = new BufferedWriter(new OutputStreamWriter(outputStreamExercise, "UTF-8")); if (!exercisefile.exists()) { exercisefile.createNewFile(); } } int num = 1; for (Object o : answerList) { String answer; if (o instanceof String) { answer = (String) o; } else { answer = ((AnswerResult) o).questionProperty().getValue(); } answer = num++ + ". " + answer + "\r\n"; if (isAnswer){ answerWriter.write(answer); answerWriter.flush(); }else { exerciseWriter.write(answer); exerciseWriter.flush(); } } if (isAnswer){ outputStreamAnswer.close(); answerWriter.close(); }else { outputStreamExercise.close(); exerciseWriter.close(); } } //答案校对,返回的是比对结果,key是题号,value是right或error public static Map<Integer, String> checkAnswer(File exercisefile, File answerFile) { Controller.operationData.clear(); BufferedReader exerciseReader = null; BufferedReader answerReader = null; Map<Integer, String> result = new HashMap<>(); try { if (!exercisefile.exists()) { System.out.println("练习答案文件不存在"); return null; } if (!answerFile.exists()) { System.out.println("答案文件不存在"); return null; } if (!exercisefile.getName().matches(".+(.txt)$") || !answerFile.getName().matches(".+(.txt)$")) { System.out.println("文件格式不支持"); return null; } InputStreamReader exerciseIn = new InputStreamReader(new FileInputStream(exercisefile.getAbsolutePath()), StandardCharsets.UTF_8); InputStreamReader answerIn = new InputStreamReader(new FileInputStream(answerFile.getAbsolutePath()), StandardCharsets.UTF_8); exerciseReader = new BufferedReader(exerciseIn); answerReader = new BufferedReader(answerIn); //将题号和答案对应存储,以防止出现漏写某一道题的情况 Map<Integer, String> exerciseMap = new HashMap<>(); Map<Integer, String> answerMap = new HashMap<>(); String content = null; while ((content = exerciseReader.readLine()) != null) { //去除字符串的所有空格 content = content.replaceAll(" +", ""); content = content.replaceAll("\uFEFF", ""); if (!isQualified(content, false)) { System.out.println(content); System.out.println("文本的内容格式错误"); return null; } exerciseMap.put(Integer.valueOf(content.split("\\.")[0]), content.split("\\.")[1]); } while ((content = answerReader.readLine()) != null) { //去除字符串的所有空格 content = content.replaceAll(" +", ""); content = content.replaceAll("\uFEFF", ""); if (!isQualified(content, true)) { System.out.println(content); System.out.println("文本的内容格式错误"); return null; } answerMap.put(Integer.valueOf(content.split("\\.")[0]), content.split("\\.")[1]); } exerciseReader.close(); answerReader.close(); //比对结果 for (int i = 1; i <= answerMap.size(); i++) { if (exerciseMap.containsKey(i)) { //将答案切割出来(格式:3+2=5) String exercise = exerciseMap.get(i).split("=")[1]; // 将答案写入图形化界面的表格 AnswerResult answerResult = new AnswerResult(); answerResult.setQuestion(exerciseMap.get(i).split("\\=")[0]); answerResult.setAnswerByStudent(exercise); answerResult.setAnswerByProject(answerMap.get(i)); Controller.operationData.add(answerResult); if (answerMap.get(i).equals(exercise)) { //结果正确,将题号记录下来 result.put(i, "right"); } else { result.put(i, "error"); } } //说明该题漏写或者错误 else { result.put(i, "error"); } } } catch (IOException e) { System.out.println("读取文件错误"); } return result; } //判断文本的内容是否符合要求 private static Boolean isQualified(String content, Boolean isAnswer) { //如果是答案的格式 if (isAnswer) { Matcher matcher = patten.matcher(content); if (!matcher.find()) { return false; } if (matcher.group().equals(content)) { //说明内容完全匹配 return true; } return false; } else { //说明是表达式 String matches = "[1-9][0-9]*\\.[0-9,\\',\\/,\\+,\\-,\\(,\\),\\×,\\÷]+\\=[0-9]+"; if (content.matches(matches)) { return true; } return false; } } public static void main(String[] args) { String expression = "\uFEFF1.(0÷2)×6=0"; System.out.println(expression.matches("[1-9][0-9]*\\.[0-9,\\',\\/,\\+,\\-,\\(,\\),\\×,\\÷]+\\=[0-9]+")); } }
六、测试运行
1、GUI生成题目: 如直接点击生成题目或参数输入错误,便会出现以上提示;
正确输入参数之后便会显示如下界面:
当点击生成题目按钮过于频繁时,则会出现以下提示:
2、 若是在控制台生成题目,同时会自动生成exercise.txt和answer.txt;若在图形界面,则只会自动生成answer.txt,用户可通过点击下载按钮来生成exercise.txt题目文件;
生成的文件如下:
执行校对答案功能:若未选择文件,则会进行响应提示
3、控制台结果显示:
GUI界面显示
七、PSP表格
|
八、项目小结
1、对业务处理有了更清晰的认识。
2、对于表达式的计算有了更深的理解。
3、对于文件的IO流更加熟悉。
4、同时对于正则表达式的使用更加熟悉。
5、对于结对编程,队友之间的配合有了进一步提升。