结对项目(java实现)
一 、Github项目地址:https://github.com/734635746/MyApp
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1310 | 1460 |
· Analysis | · 需求分析 | 120 | 120 |
· Design Spec | · 生成设计文档 | 60 | 70 |
· Design Review | · 设计复审 | 40 | 60 |
· Coding Standard | · 代码规范 | 30 | 40 |
· Design | · 具体设计 | 100 | 90 |
· Coding | · 具体编码 | 800 | 900 |
· Code Review | · 代码复审 | 60 | 80 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 100 |
Reporting | 报告 | 130 | 150 |
· Test Report | · 测试报告 | 60 | 80 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 40 | 40 |
合计 | 1470 | 1640 |
三、效能分析
1. 使用JProfiler进行效能分析,查看cpu时间,进行针对性的优化。
2. 优化过程:
a. 在获取结果的getFinalResult中,取消重复调用Fraction类的get方法。(1.0%->0.4% 约快10s/1million条)
b. 在对结果的化简时,使用非递归获取最大公约数。(5.4%->1.2% 约快400ms/10万条)
c. 计算过程中对一次计算(两个操作数一个运算符)中,取消重复调用Fraction类的构造方法生成结果。(2.6%->2.1% 约快40ms/10万条)
3. 优化前后:生成一百万条十以内的题目(-n 1000000 -r 10)
四、设计实现过程
在阅读完题目后,我们两个人一开始是想通过设计一个运算式的类来处理。将随机生成的运算符和操作数作为类的属性,然后通过类的方法来进行相应的处理计算。但最后还是选择了直接生成运算式的字符串来处理。这样处理起来会更加直接方便。
具体的思路是接收-n和-r参数来控制运算式的数量和数值的范围。通过随机生成的运算符和操作数来生成运算式字符串。在生成运算式字符串的过程中可以通过随机数来随机生成带括号的运算式。接下来就是这个项目的难点所在如何计算随机生成运算式的结果数值。
因为运算式的运算符个数和类型是随机的(1~3个),运算符的优先级也有高低之分,以及括号带来的运算先后顺序也不一样。这就使得结果的计算变得复杂起来,最终通过查阅资料发现这个问题可以通过调度场算法来解决。调度场算法可以是一个用于将中缀表达式转换为后缀表达式的经典算法,通过这个算法可以解决运算式的结果计算难题。
在这个项目中,所有的操作数包括整数在计算的时候都作为分数进行运算(整数的分母为1)。为此设计了一个分数类(Fraction)来进行处理,包括分数的整数部分、分子部分、分母部分。这样做的好处是在两个操作数进行运算的时候可以通过分数运算规则来进行。
项目支持通过-r -n 参数命令随机(运算符1~3个,操作数在-n所指定的范围内,括号随机)生成指定数量和数值范围的题目文件(Exercises.txt)和答案文件(Answers.txt),支持通过-e -a 参数命令验证用户所提交答案文件的正确程度,并生成结果文件(Grade.txt)。
项目结构如下图:
代码结构功能说明:
Fraction : 分数类,项目中所有的操作数在计算的时候都转化成Fraction对象进行计算
SymbolConstant :常量类,定义了项目中用到的常量
CalculateUtil : 运算工具类,封装了运算表答式计算所需要的方法
ExpressionUtil :运算表达式工具类,封装了生成运算式的所需方法
NumberUtil : 操作数工具类,封装了用于操作数生成的方法
OperatorUtil : 封装了生成运算符所需要的方法
PrintFileUtil :封装了用于生成题目文件以及答案验证的方法
ValidateUtil : 封装了重要数据的检查方法
Main :主类,用于接收参数调用具体功能
调用关系流程图:
说明:1. 主类在接受用户的-r -n/-e -a参数后首先会调用ValidateUtil工具类的checkParams(String command)方法进行参数合法性校验(包括对参数顺序的支持)。
2. 如果是-r -n参数命令
2.1 命令程序会调用ExpressionUtil工具类的generate(int n,int round)方法获取指定数量和参数范围的运算式以及答案的Map集合。
2.2 ExpressionUtil工具类的generate(int n,int round)方法依赖OperatorUtil工具类的getOperators(int num)方法和NumberUtil工具类的getNumbers(int num, int round)获取随机的运算符和操作数,同时还在generate方法里面进行负数以及查重的解决。
2.3 generate(int n,int round)方法依赖同时还需要getExpressValue(String express)获取题目的答案
2.4 在获取到运算式以及答案的集合后程序会调用PrintFileUtil工具类的printExerciseFileAndAnswerFile(Map<String, String> questionAndResultMap) 进行题目文件和答案文件的生成
3. 如果是-e -a参数命令
3.1 程序会调用PrintFileUtil工具类的validateAnswerFile(String exerciseFileUrl, String answerFileUrl)方法进行题目的验证。
3.2 在这个过程中需要通过调CalculateUtil工具类的getExpressValue(String express)获取运算式的答案与用户提交的答案进行比较。
五、 代码说明
分数类
项目的所有数值运算都会化成分数形式进行运算,整数则分母部分为1,分数类的整数部分是用于真分数的生成
1 public class Fraction { 2 private int inter;//整数部分 3 private int numerator;//分子 4 private int denominator;//分母 5 6 //省略构造方法 set、get方法 7 }
项目所用常量
1 public class SymbolConstant { 2 public static final Character PLUS = '+'; 3 public static final Character MINUS = '-'; 4 public static final Character MULTIPLY = '*'; 5 public static final Character DIVIDE = '÷'; 6 public static final Character EQUALS = '='; 7 public static final String PRINT_FILE_URL = System.getProperty("user.dir")+ File.separator+"question_bank";//"F:\\file";10.3修改生成文件地址 8 9 }
随机操作数和运算符的生成代码
运算符的类型和数量(num)是随机且符合要求的,操作数的数量(num+1)、数值类型以及数值范围也是随机且合法的
1 class NumberUtil { 2 3 /** 4 * 随机获取num个操作数的数组 5 */ 6 static String[] getNumbers(int num, int round) { 7 8 Random random = new Random(); 9 String[] numbers = new String[num]; 10 11 for (int i = 0; i < num; i++) { 12 //用于判断生成整数还是分数 13 int flag = (int)(Math.random()*10) % 2; 14 15 if(flag==0){//生成整数 16 int n = random.nextInt(round); 17 numbers[i] = (n==0?1:n)+""; 18 }else{//生成分数 19 //随机生成分子和分母,为了避免分子分母生成0进行了+1的改进 20 int numerator = (random.nextInt(round))+1; 21 int denominator = (random.nextInt(round))+1;; 22 23 while(numerator>=denominator||numerator==0||denominator==0){//判断是否为真分数,且不能生成带0的分数 24 numerator = (random.nextInt(round))+1; 25 denominator = (random.nextInt(round))+1; 26 } 27 //拼装成分数形式 28 numbers[i] = numerator+"/"+denominator; 29 } 30 } 31 return numbers; 32 } 33 34 } 35 36 public class OperatorUtil { 37 38 private final static Character[] operatorTypes = new Character[]{SymbolConstant.PLUS,SymbolConstant.MINUS,SymbolConstant.MULTIPLY,SymbolConstant.DIVIDE}; 39 40 /** 41 * 随机获取num个运算符的数组 42 */ 43 static Character[] getOperators(int num) { 44 45 Character[] operators = new Character[num]; 46 47 for (int i = 0; i < num; i++) { 48 //随机获取运算符的类型(0~3 代表4个运算符的类型) 49 int operatorTypeIndex = (int)(Math.random()*4); 50 Character operatorType = operatorTypes[operatorTypeIndex]; 51 operators[i] = operatorType; 52 } 53 54 return operators; 55 } 56 57 58 }
运算式生成的代码
1 运算式的生成是根据上述的随机运算符和操作数来确定的,同时在生成运算式的时候也随机生成括号
2 在生成运算式后会调用结果生成的方法进行结果的获取同时验证运算式及其结果的正确性
1 /** 2 * @author liuyoubin 3 * @date 2019/9/27 - 22:09 4 */ 5 public class ExpressionUtil { 6 7 /** 8 * 获取指定个数和数值范围的运算式字符串和结果 9 */ 10 public static Map<String,String> generate(int n,int round){ 11 12 //运算式和结果的集合 13 Map<String,String> questionAndResultMap = new HashMap<String,String>(); 14 //结果集合,用于判断是否重复 15 Set<String> result = new HashSet<String>(); 16 for (int i = 0; i < n; i++) { 17 //随机获取运算符的个数(1~3个) 18 int num = (int)(Math.random()*3)+1; 19 //随机获取num个运算符 20 Character[] curOperators = OperatorUtil.getOperators(num); 21 //随机获取num+1个操作数 22 String[] curNumbers = NumberUtil.getNumbers(num+1,round); 23 //获取运算式表达式 24 String[] questionAndResult = getExpressStr(curOperators, curNumbers); 25 26 if(questionAndResult==null){//判断运算过程是否出现负数 27 i--; 28 }else if (result.contains(questionAndResult[1])){//判断是否重复 29 i--; 30 }else { 31 result.add(questionAndResult[1]); 32 questionAndResultMap.put(questionAndResult[0],questionAndResult[1]); 33 } 34 } 35 return questionAndResultMap; 36 } 37 38 /** 39 * 根据运算符数组和操作数数组生成运算式表达式 40 * @param curOperators 运算符数组 41 * @param curNumbers 操作数数组 42 * @return 运算式字符串以及其结果 43 */ 44 private static String[] getExpressStr(Character[] curOperators, String[] curNumbers){ ---->运算符数组和操作数数组是随机生成的 45 //操作数的数量 46 int number = curNumbers.length; 47 //随机判断是否生成带括号的运算式 48 int isAddBracket = (int)(Math.random()*10) % 2; -----> 该运算式是否带括号也是通过随机数来判断的 49 //随机生成器 50 Random random = new Random(); 51 52 if(isAddBracket==1){//生成带括号的表达式 53 //当标记为1时代表该操作数已经添加了左括号 --------->这两个数组是用来标记当前操作数是否添加了左、右括号 54 int[] lStamp = new int[number]; 55 //当标记为1时代表该操作数已经添加了右括号 56 int[] rStamp = new int[number]; 57 //遍历操作数数组,随机添加括号 58 for (int index=0;index<number-1;index++) { -------------->遍历操作数来随机添加左括号,这里没有遍历到最后一个操作数是由于最后一个操作数不可能添加左括号 59 int n = (int)(Math.random()*10) % 2; 60 if(n == 0 && rStamp[index] != 1) {//判断当前操作数是否标记了左括号 61 lStamp[index] = 1;//标记左括号 62 curNumbers[index] = "(" + curNumbers[index]; //操作数之前加上左括号 63 int k = number - 1; 64 //生成右括号的位置 65 int rbracketIndex = random.nextInt(k)%(k-index) + (index+1); 66 //如果当前操作数有左括号,则重新生成优括号位置 67 while (lStamp[rbracketIndex] == 1){ 68 rbracketIndex = random.nextInt(k)%(k-index) + (index+1); 69 } 70 rStamp[rbracketIndex] = 1; 71 curNumbers[rbracketIndex] = curNumbers[rbracketIndex] +")"; 72 73 } 74 } 75 } 76 77 //将运算符数组和操作数数组拼成一个运算式字符串 78 StringBuilder str = new StringBuilder(curNumbers[0]); 79 for (int k = 0; k < curOperators.length; k++) { 80 str.append(curOperators[k]).append(curNumbers[k + 1]); 81 } 82 //生成的运算式 83 String express = str.toString(); 84 //获取运算式结果 85 String value = CalculateUtil.getExpressValue(express); 86
87 if(value.equals("#")){//运算过程出现负数
88 return null;
89 } 90 return new String[]{express,value}; 91 92 } 93 }
运算式结果计算相关代码
1 运算式的结果生成是根据调度场算法给出的实现
2 在进行数值运算的时候会同一化成分数的形式,根据分数运算式规则进行运算
3 同时还进行了结果的处理,通过辗转相除法生成的最大公约数来计算真分数,同时化成符合规范表达结果字 1 /**
2 * @author liuyoubin 3 * @date 2019/9/28 - 15:06 4 * 运算工具类 5 */ 6 public class CalculateUtil { 7 8 9 运算式的结果计算采用了调度场算法吗,该算法的思想是将我们常见的中缀表达式 转成后缀表达式。算法如下: 10 + 当还有记号可以读取时: 11 -读取一个记号。 12 - 如果这个记号表示一个数字,那么将其添加到输出队列中。 13 - 如果这个记号表示一个函数,那么将其压入栈当中。 14 -如果这个记号表示一个函数参数的分隔符(例如,一个半角逗号,): 15 -从栈当中不断地弹出操作符并且放入输出队列中去,直到栈顶部的元素为一个左括号为止。如果一直没有遇到左括号,那么要么是分隔符放错了位置,要么是括号不匹配。 16 + 如果这个记号表示一个操作符,记做o1,那么: 17 -只要存在另一个记为o2的操作符位于栈的顶端,并且 18 -如果o1是左结合性的并且它的运算符优先级要小于或者等于o2的优先级,或者 19 -如果o1是右结合性的并且它的运算符优先级比o2的要低,那么 20 -将o2从栈的顶端弹出并且放入输出队列中(循环直至以上条件不满足为止); 21 + 然后,将o1压入栈的顶端。 22 + 如果这个记号是一个左括号,那么就将其压入栈当中。 23 + 如果这个记号是一个右括号,那么: 24 -从栈当中不断地弹出操作符并且放入输出队列中,直到栈顶部的元素为左括号为止。 25 -将左括号从栈的顶端弹出,但并不放入输出队列中去。 26 -如果此时位于栈顶端的记号表示一个函数,那么将其弹出并放入输出队列中去。 27 -如果在找到一个左括号之前栈就已经弹出了所有元素,那么就表示在表达式中存在不匹配的括号。 28 +当再没有记号可以读取时: 29 -如果此时在栈当中还有操作符: 30 -如果此时位于栈顶端的操作符是一个括号,那么就表示在表达式中存在不匹配的括号。 31 -将操作符逐个弹出并放入输出队列中。 32 +退出算法 33 /** 34 * 采用调度场算法,获取指定运算式的结果值 35 * 36 * @param express 运算式 37 * @return 38 */ 39 public static String getExpressValue(String express){ 40 //运算符栈,用于存放运算符包括 +、-、*、÷、(、) 41 Stack<Character> operators = new Stack<Character>(); 42 //操作数栈,用于存放操作数 43 Stack<Fraction> fractions = new Stack<Fraction>(); 44 //将表达式字符串转成字符数组 45 char[] chars = express.toCharArray(); 46 //遍历获取处理 47 for (int i=0;i<chars.length;i++) { 48 //获取当前的字符 49 char c = chars[i]; 50 51 if(c=='('){//如果是左括号,入栈 52 operators.push(c); 53 }else if(c==')'){//当前字符为右括号 54 //当运算符栈顶的元素不为‘(’,则继续 55 while(operators.peek()!='('){ 56 //拿取操作栈中的两个分数 57 Fraction fraction1 = fractions.pop(); 58 Fraction fraction2 = fractions.pop(); 59 //获取计算后的值 60 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 61 fraction2.getNumerator(), fraction2.getDenominator());
62
63 if(result.getNumberator<0){//运算过程出现负数
64 return "#";
65 }
66 62 //将结果压入栈中 63 fractions.push(result); 64 } 65 //将左括号出栈 66 operators.pop(); 67 }else if(c=='+'||c=='-'||c=='*'||c=='÷'){//是运算符 68 //当运算符栈不为空,且当前运算符优先级小于栈顶运算符优先级 69 while(!operators.empty()&&!priority(c, operators.peek())){ 70 //拿取操作栈中的两个分数 71 Fraction fraction1 = fractions.pop(); 72 Fraction fraction2 = fractions.pop(); 73 //获取计算后的值 74 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 75 fraction2.getNumerator(), fraction2.getDenominator());
76
77 if(result.getNumerator()<0){
78 return "#";
79 }
80 76 //将结果压入栈中 77 fractions.push(result); 78 } 79 //将运算符入栈 80 operators.push(c); 81 }else{//是操作数 82 if(c>='0'&&c<='9'){ 83 StringBuilder buf = new StringBuilder(); 84 //这一步主要是取出一个完整的数值 比如 2/5、9、9/12 85 while(i<chars.length&&(chars[i]=='/'||((chars[i]>='0')&&chars[i]<='9'))){ 86 buf.append(chars[i]); 87 i++; 88 } 89 i--; 90 //到此 buf里面是一个操作数 91 String val = buf.toString(); 92 //标记‘/’的位置 93 int flag = val.length(); 94 for(int k=0;k<val.length();k++){ 95 if(val.charAt(k)=='/'){//当获取的数值存在/则标记/的位置,便于接下来划分分子和分母生成分数对象 96 flag = k; 97 } 98 } 99 //分子 100 StringBuilder numeratorBuf = new StringBuilder(); 101 //分母 102 StringBuilder denominatorBuf = new StringBuilder(); 103 for(int j=0;j<flag;j++){ 104 numeratorBuf.append(val.charAt(j)); 105 } 106 //判断是否为分数 107 if(flag!=val.length()){ 108 for(int q=flag+1;q<val.length();q++){ 109 denominatorBuf.append(val.charAt(q)); 110 } 111 }else{//如果不是分数则分母计为1 112 denominatorBuf.append('1'); 113 } 114 //入栈 115 fractions.push(new Fraction(Integer.parseInt(numeratorBuf.toString()), Integer.parseInt(denominatorBuf.toString()))); 116 } 117 } 118 } 119 120 while(!operators.empty()){ 121 Fraction fraction1 = fractions.pop(); 122 Fraction fraction2 = fractions.pop(); 123 124 //获取计算后的值 125 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 126 fraction2.getNumerator(), fraction2.getDenominator());
127
128 if(result.getNumberator()<0){
129 return "#";
130 }
131
132 127 128 //将结果压入栈中 129 fractions.push(result); 130 } 131 132 //计算结果 133 Fraction result = fractions.pop(); 134 //获取最终的结果(将分数进行约分) 135 return getFinalResult(result); 136 137 } 138 ----------------------------------------------------------------------------------- 139 private static String getFinalResult(Fraction result) { 140 int denominator = result.getDenominator(); 141 int numerator = result.getNumerator(); 142 if(denominator==0){ 143 return "0"; 144 } 145 //获取最大公约数 146 int gcd = gcd(numerator,denominator); 147 148 if(denominator/gcd==1){//分母为1 149 return String.valueOf(numerator/gcd); 150 }else{ 151 //如果分子大于分母则化成真分数的形式 152 if(result.getNumerator()>denominator){ 153 result = getRealFraction(result); 154 return result.getInter()+"'"+result.getNumerator()/gcd+"/"+result.getDenominator()/gcd; 155 }else{ 156 return numerator/gcd+"/"+denominator/gcd; 157 } 158 } 159 } 160 ----------------------------------------------------------------------------------- 161 /** 162 * 化成真分数 163 * @param result 164 * @return 165 */ 166 private static Fraction getRealFraction(Fraction result){ 167 int numerator = result.getNumerator(); 168 int denominator = result.getDenominator(); 169 //计算分子部分 170 int newNumerator = numerator % denominator; 171 //计算整数部分 172 int inter = numerator/denominator; 173 Fraction fraction = new Fraction(newNumerator, denominator); 174 fraction.setInter(inter); ------------------------------->整数部分 175 return fraction; 176 } 177 ----------------------------------------------------------------------------------- 178 /** 179 * 判断两个运算符的优先级 180 * 当opt1的优先级大于opt2时返回true 181 * 这是根据调度场算法给出的实现 182 * @return 183 */ 184 private static boolean priority(char opt1,char opt2){ --------------------------->只有当opt1的优先级小于或等于opt2的优先级时才放回true。这是根据调度场算法给出的实现 185 if((opt1=='+'||opt1=='-')&&(opt2=='*'||opt2=='÷')){ 186 return false; 187 }else if((opt1=='+'||opt1=='-')&&(opt2=='+'||opt2=='-')){ 188 return false; 189 }else if((opt1=='*'||opt1=='÷')&&(opt2=='*'||opt2=='÷')){ 190 return false; 191 }else{ 192 return true; 193 } 194 } 195 ----------------------------------------------------------------------------------- 196 /** 197 * 对两个分数进行相应的运算,获取结果 198 * @param opt 运算符 199 * @param num1 分子1 200 * @param den1 分母1 201 * @param num2 分子2 202 * @param den2 分母2 203 * @return 结果 204 */ 205 private static Fraction calculate(Character opt, int num1, int den1, int num2, int den2){ 206 //结果数组,存放结果的分子与分母 207 int[] result = new int[2]; 208 /* 209 * 这里有一个陷阱,因为用于计算的两个数是通过栈来存储,所以取出计算结果的时候两个数的顺序会颠倒 210 * 比如式子 1/2*9/12 我取出来的时候其实是 9/12 和 1/2 所以调用此函数的时候是calculate('*',9,12,1,2),所以下面的实现要注意不要踩坑 211 */ 212 switch (opt){ 213 case'+': 214 result[0] = num1*den2 + num2*den1; result[1]= den1*den2; 215 break; 216 case '-': 217 result[0] = num2*den1 - num1*den2; result[1]= den1*den2; 218 break; 219 case '*': 220 result[0] = num1*num2; result[1] = den1*den2; 221 break; 222 case '÷': 223 result[0] = num2*den1; result[1] = num1*den2; 224 break; 225 } 226 return new Fraction(result[0],result[1]); 227 } 228 ----------------------------------------------------------------------------------- 229 /** 230 * 获取分子分母的最大公约数,辗转相除法 231 * @param numerator 分子 232 * @param denominator 分母 233 * @return 最大公约数 234 */ 235 private static int gcd(int numerator,int denominator){ 236 numerator = Math.abs(numerator); 237 denominator = Math.abs(denominator); 238 while (denominator != 0) { 239 // 求余 240 int remainder = numerator % denominator; 241 // 交换数,等同递归调用 242 numerator = denominator; 243 denominator = remainder; 244 } 245 return numerator; 246 } 247 }
验证方法相关代码
1 public class ValidateUtil { 2 3 /* 4 * @Description: 参数检查 只有两种合法输入 5 * @param command 6 * @return: 返回定好顺序的命令数组 7 */ 8 public static String[] checkParams(String command) { -------------->此方法用于验证用户输入参数的正确性,同时通过此方法可以支持参数的顺序颠倒 9 String[] s = command.split(" "); 10 //其实只能输入长度为4的指令 11 if (s.length == 4){ 12 String first = s[0];String third = s[2];String second = s[1];String fourth = s[3]; 13 //1. -n i -r j / -r i -n j 14 if((first.equals("-n")&&third.equals("-r")) || (first.equals("-e")&&third.equals("-a"))){ 15 return s; 16 } 17 if((first.equals("-r")&&third.equals("-n")) || (first.equals("-a")&&third.equals("-e"))){ 18 s[0] = third; 19 s[1] = fourth; 20 s[2] = first; 21 s[3] = second; 22 return s; 23 } 24 return null; 25 }else { 26 return null; 27 } 28 } 29 30 /* 31 * @Description: 对绝对路径和相对路径的支持 32 * @param path 33 * @return: 正确路径 34 */ 35 public static String improvePath(String path) { ------------------------>此方法用于将相对路径转成绝对路径,方便用户上传规范路径下文件的文件 36 if (path.contains("/")){//替换分隔符 37 path = path.replace("/", File.separator); 38 } 39 String p = ""; 40 if (path.indexOf(":") > 0) {//绝对路径 41 p = path; 42 } else {//相对路径 43 p = SymbolConstant.PRINT_FILE_URL + File.separator + path; 44 } 45 return p; 46 } 47 }
题目/答案文件生成,验证的相关代码
public class PrintFileUtil { /** * 根据运算式子生成练习文件和答案文件 * */ public static void printExerciseFileAndAnswerFile(Map<String, String> questionAndResultMap) { File dir = new File(SymbolConstant.PRINT_FILE_URL); if (!dir.exists()) { //解决FileNotFound dir.mkdir(); } File exerciseFile = new File(SymbolConstant.PRINT_FILE_URL, "Exercises.txt"); File answerFile = new File(SymbolConstant.PRINT_FILE_URL, "Answers.txt"); try { OutputStream exerciseFileOutputStream = new FileOutputStream(exerciseFile); OutputStream answerFileOutputStream = new FileOutputStream(answerFile); StringBuilder exerciseBuffer = new StringBuilder(); StringBuilder answerFileBuffer = new StringBuilder(); System.out.println("正在写出到文件..."); for(Map.Entry<String, String> entry:questionAndResultMap.entrySet()){ exerciseBuffer.append(entry.getKey()).append("\r\n"); answerFileBuffer.append(entry.getValue()).append("\r\n"); } exerciseFileOutputStream.write(exerciseBuffer.toString().getBytes()); answerFileOutputStream.write(answerFileBuffer.toString().getBytes()); exerciseFileOutputStream.close(); answerFileOutputStream.close(); System.out.println("操作成功!!!"); } catch (IOException e) { System.out.println("文件操作异常,请重试"); } } /** * 验证答案的正确率 */ public static void validateAnswerFile(String exerciseFileUrl, String answerFileUrl) { File exerciseFile = new File(ValidateUtil.improvePath(exerciseFileUrl));//SymbolConstant.PRINT_FILE_URL, exerciseFileUrl); File answerFile = new File(ValidateUtil.improvePath(answerFileUrl)); File gradeFile = new File(SymbolConstant.PRINT_FILE_URL, "Grade.txt"); if (exerciseFile.isFile() && answerFile.isFile()) { BufferedReader exerciseReader = null; BufferedReader answerReader = null; OutputStream gradeFileOutputStream = null; List<Integer> Correct = new ArrayList<Integer>(); List<Integer> Wrong = new ArrayList<Integer>(); try { exerciseReader = new BufferedReader(new InputStreamReader(new FileInputStream(exerciseFile))); answerReader = new BufferedReader(new InputStreamReader(new FileInputStream(answerFile))); String exerciseStr = ""; String answerStr = ""; int line = 0;//记录行数 System.out.println("开始验证..."); while ((exerciseStr = exerciseReader.readLine()) != null && (answerStr = answerReader.readLine()) != null) { //获取运算式的正确答案 String realAnswer = CalculateUtil.getExpressValue(exerciseStr); if (realAnswer.equals(answerStr)) { line++; Correct.add(line); } else { line++; Wrong.add(line); } } String result = "Correct:" + Correct.size() + Correct + "\r\n" + "Wrong:" + Wrong.size() + Wrong; //保存成绩文件 gradeFileOutputStream = new FileOutputStream(gradeFile); gradeFileOutputStream.write(result.getBytes()); //打印结果 System.out.print(result); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (exerciseReader != null) { try { exerciseReader.close(); } catch (IOException ignored) { } } if (answerReader != null) { try { answerReader.close(); } catch (IOException ignored) { } } if (gradeFileOutputStream != null) { try { gradeFileOutputStream.close(); } catch (IOException ignored) { } } } } else { System.out.println("文件不存在!!!"); } } }
主类代码
1 public class Main { 2 3 public static void main(String[] args) { 4 5 System.out.println("***************欢迎使用四则运算题目生成程序****************"); 6 System.out.println("***************使用-n 参数控制题目生成数目 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围****************"); 7 System.out.println("***************使用 -e <exercisefile>.txt -a <answerfile>.txt 检查答案的正确率***************"); 8 9 while(true){ 10 11 Scanner scanner = new Scanner(System.in); 12 String command = scanner.nextLine(); 13 if(command.equals("bye")){ 14 break; 15 } 16 String[] split = ValidateUtil.checkParams(command);//command.split(" ");先校验输入避免出现不可靠输入导致程序退出 17 if (split!=null){ 18 if (split[0].equals("-n")) {//-n i -r j 19 int num = Integer.valueOf(split[1]); 20 if (num <= 0) { 21 System.out.println("-n参数输入错误,请重新输入"); 22 //break;//break->程序退出 23 }else { 24 int round = Integer.valueOf(split[3]); 25 if (round <= 0) { 26 System.out.println("-r参数输入错误,请重新输入"); 27 } else { 28 //获取运算式数组 29 System.out.println("正在生成题目..."); 30 Map<String, String> questionAndResultMap = ExpressionUtil.generate(num, round); 31 System.out.println("生成完成,请等待..."); 32 PrintFileUtil.printExerciseFileAndAnswerFile(questionAndResultMap); 33 } 34 } 35 }else {// -e x -a y 36 String exerciseFileUrl = split[1]; 37 String answerFileUrl = split[3]; 38 //验证答案 39 PrintFileUtil.validateAnswerFile(exerciseFileUrl, answerFileUrl); 40 } 41 }else { 42 System.out.println("参数输入有误,请重新输入"); 43 } 44 45 46 47 } 48 } 49 50 }
六、测试运行
1. 测试-n -r 功能:
a. 单道题目生成和结果正确
b. 两道题目生成和结果正确,-n参数处理正确(与a比较)
c. -r参数处理正确(与a比较)
d 测试生成10000题目
2 测试验证功能-e -a
a 测试正确的答案文件
b 测试修改后的答案文件
3 机测:
测试代码:
1 @Test 2 public void fanRen() throws IOException { 3 int n = 10000,r = 10; 4 //1.生成10000道操作数在10以内的题目 5 Map<String,String> questionAndResults = ExpressionUtil.generate(n, r); 6 //2.判断生成内容是否有误 7 Assert.assertEquals("题目数量",n,questionAndResults.keySet().size());//数量 8 for (String express : questionAndResults.keySet()){ 9 for (String a: express.replaceAll("[^0-9]",",").split(",")){ 10 if (!a.equals("")) { //运算符和括号相连出现"" 11 int num = Integer.parseInt(a); 12 Assert.assertTrue("操作数范围", num < r); //范围 13 } 14 } 15 int opCount = 0; 16 for (char a: express.replaceAll("\\d+","").toCharArray()){ 17 if (a==SymbolConstant.DIVIDE||a==SymbolConstant.MINUS||a==SymbolConstant.MULTIPLY||a==SymbolConstant.PLUS) 18 opCount++; 19 } 20 Assert.assertTrue("运算符数量不大于3",opCount<4); //运算符数量 21 } 22 23 }
测试结果:通过
七、项目小结
1. 本次项目使用Java进行功能实现、Maven作为构建工具、Junit作单元测试、JProfiler进行效能分析。基本上完成了项目所要求的功能。
2. 在项目过程中,两个人项目相互配合分工合作。共同完成了xc从需求分析、结构设计、编码开发、代码复审、程序优化、程序测试整个过程。在相互配合过程中我们都发现了自身的不足同也从对方学到了很多。
3. 这次项目是第一从接触结对开发这种开模式,获益匪浅。互相配合协同开发对于提高开发效率是有明显帮助的。
项目成员:刘友滨 3117004622 陈景山 3117004606