软工作业NO.2小学生线上杨永信——四则运算题目生成
项目题目:实现一个自动生成小学四则运算题目的命令行程序
github地址:https://github.com/a249970271/Formula
驾驶员:梁沛诗
副驾驶:曾祎祺
项目说明
自然数:0, 1, 2, …。
- 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 运算符:+, −, ×, ÷。
- 括号:(, )。
- 等号:=。
- 分隔符:空格(用于四则运算符和等号前后)。
- 算术表达式:e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
- 四则运算题目:e = ,其中e为算术表达式。
项目要求
1. 使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
2. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
3. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
4. 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
5. 每道题目中出现的运算符个数不超过3个。
6. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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. 四则运算题目1
2. 四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
7. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
1. 答案1
2. 答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
8. 程序应能支持一万道题目的生成。
9. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
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、需求六的实现不是和完美,不能对比1+2+3和3+(1+2)的情况
遇到的困难及解决办法
▪ 困难描述
驾驶员:“和我一起合作的小伙伴比较熟悉Java,而我比较熟悉c,经讨论后决定这个项目用Java来写,因为觉得时间比较紧迫,来不及详细的去学一下Java,所以只能硬着头皮上,有不会的就上网查,虽然用的是Java来写,但Java的很多简洁的语句都没能写出来,用着c的知识去写Java,所以整段代码看起来比较冗余,会有比较多废话的语句,当出现错误或修改参数是有点麻烦。
我负责的是实现四则运算算法方面的代码实现,最大的困难是有分数这个算数的存在,答案结果要把它变成带分数,这一点有点麻烦,还有检验有无重复运算式,我最后写出的只能验证有无完全相同的运算式,当出现交换律等问题时,则无法判别。”
副驾驶:“在这一个项目里面我主要是负责那个一小部分需求的实现以及代码审查还有测试方面。因为上一个wc项目也是跟文件读取相关,所以我负责的文档输出方面对我来说还比较简单,基本上没有什么困难,除了那个答案对比批改的部分吧,不太知道怎么对比,然后要用什么方式去实现这个需求。然后另一个难点是,输出的那些式子太难了,我也不太会做。”
▪ 做过哪些尝试
1、尝试过把每条式子的所有算数和答案都转换成字符串进行对比有无重复,后来发现这种方法还是不能检测出1+2+3和3+(1+2)这种情况,以及这样要把每条式子的整形变量都转换成字符串有点麻烦,后来就使用了ArrayList,直接添加。
2、把多个运算符的情况都按一个运算符一样分情况写而不利用一个运算符的情况,后来发现这样写会更冗余且麻烦。
3、尝试性的把代码变得简洁些
▪ 是否解决
代码已经是经过我简化后的产物,目前只有需求六不能完全解决
▪ 有何收获
1、边写边学Java,即使还是学习的不够深入
2、懂得了随机函数的各种应用
设计实现过程
驾驶员:一开始是先从一个随机数以及一个运算符开始写,先写出那些函数,然后再写一个运算符的式子的实现,期间对过程中出现的负数以及除数为零这两个问题进行了必要的处理,写在同一个函数中,接下来是真分数的转换,然后就是式子是否有重复输出的对比。至此,一个运算符的式子就大致完全地实现了,这时候再接下来实现存在多个运算符以及括号的式子。然后这一部分就完成了,没有分开多个类,算上主函数一共有11个函数。
副驾驶:然后是文档输出方面,我呢先是找到驾驶员写出来的式子和答案的那个List,然后想办法把它转换成String,跟着就输出到一个新建的文档。接下来自己答案的批改部分就是,从文档里面读取出来的东西也存进一个List,再跟答案作对比,结果再存进一个String里,再输出到文档,至此我的部分就完成了。
关键代码及说明
生成分数的函数:
public static int[] fenshu(int range){//生成分数 int[] a=new int[2]; int q =1+(int)(Math.random()*range);//随机生成分子 int Q =1+(int)(Math.random()*range);//随机生成分母 if(q!=Q){//当分子分母相等时为整数 a[0]=q;//分子 a[1]=Q;//分母 } else fenshu(range); return a; }
求公倍数和公约数的函数:
public static int gongyueshu( int m, int n){ //辗转相除法求m和n的公约数 int r; while (n!=0){ r=m % n; m=n; n=r; } return m; } public static int gongbeishu(int m,int n){ //求m和n的最小公倍数 return m*n/gongyueshu(m,n); }
生成随机整数函数:
public static int suijishu(int range){//生成随机整数 int n=1+(int)(Math.random()*range); return n; }
生成随机运算数函数:
public static int[] suiji(int range){//返回随机运算数(包括分数、整数) int count[]=new int[8]; for(int p = 0;p<8;p=p+2){ int b=1+(int)(Math.random()*2); if(b==1){ count[p]=suijishu(range); count[p+1]=1; }else if(b==2){ int[] d=new int[2]; d=fenshu(range); count[p]=d[0]; count[p+1]=d[1]; } } return count; }
将假分数变成真分数的函数:
public static String zhenfenshu(int m,int n){//将假分数变为真分数 int t=m; int i=1; while(t>n){ t=m-(n*i); i++; } int[] d=new int[3]; String b=""; d[0]=i-1; d[1]=t; d[2]=n; if(d[0]==0){ if(d[1]==d[2]){ b="1"; }else if((d[1]!=d[2])&&d[1]!=0){ b=String.valueOf(t)+"/"+String.valueOf(n); }else if(d[1]==0){ b="0"; } }else if(d[0]!=0){ if(d[1]==d[2]){ b=String.valueOf(i); }else if(d[1]!=d[2]&&d[1]!=0){ b=String.valueOf(i-1)+"′"+String.valueOf(t)+"/"+String.valueOf(n); }else if(d[1]==0){ b="0"; } } return b; }
检验是否有相同式子的函数:
public static boolean duibi(ArrayList<String> Question){//避免生成相同的式子 for(int i=0;(i+1)<Question.size();i++){ boolean x=Question.get(i).equals(Question.get(i+1)); if(x==true){ return x;} } return false; }
储存答案的函数:
public static void daan(String da,ArrayList<String>Answer){//储存答案 Answer.add(da); }
当只有一个运算符情况的函数:
供多个运算符及一个运算符调用
public static boolean danyunsuan(int a,int b,int c, int d,ArrayList<Integer>Answer1,ArrayList<String>Question1,char o) {//当只有一个运算符的情况 String d1=""; String d2=""; int[] count=new int[]{a,b,c,d}; if(gongyueshu(count[0],count[1])==0||gongyueshu(count[2],count[3])==0){//避免除数为零 return false; } int x=count[0]/gongyueshu(count[0],count[1]); int y=count[1]/gongyueshu(count[0],count[1]); int z=count[2]/gongyueshu(count[2],count[3]); int p=count[3]/gongyueshu(count[2],count[3]); d1=zhenfenshu(x,y); d2=zhenfenshu(z,p); int n1=gongbeishu(count[1],count[3]); if(o=='-'){//当运算符为"-"号 if(count[1]==0||count[3]==0){return false;} int an1=(count[0]*(n1/count[1]))-(count[2]*(n1/count[3])); if(an1>=0){ Question1.add(d1+" "+o+" "+d2); Answer1.add(an1/gongyueshu(an1,n1)); Answer1.add(n1/gongyueshu(an1,n1)); }else{return false;} }else if(o=='÷'){//当运算符为"÷"号 if(count[2]==0){//除数为0时无意义 return false; }else if(count[2]!=0){ int an2=count[0]*count[3]; int an3=count[1]*count[2]; Question1.add(d1+" "+o+" "+d2); Answer1.add(an2/gongyueshu(an2,an3)); Answer1.add(an3/gongyueshu(an2,an3)); } }else if(o=='+'){//当运算符为"+"号 if(count[1]==0||count[3]==0){return false;} int an4=(count[0]*(n1/count[1]))+(count[2]*(n1/count[3])); Question1.add(d1+" "+o+" "+d2); Answer1.add(an4/gongyueshu(an4,n1)); Answer1.add(n1/gongyueshu(an4,n1)); }else if(o=='x'){//当运算符为"x"号 int an5=count[0]*count[2]; int an6=count[1]*count[3]; Question1.add(d1+" "+o+" "+d2); Answer1.add(an5/gongyueshu(an5,an6)); Answer1.add(an6/gongyueshu(an5,an6)); } return true; }
主函数的部分代码:
相关声明以及相关参数的输入
@SuppressWarnings("resource") Scanner input=new Scanner(System.in); System.out.println("请输入生成题目数:Myapp.exe -"); int number=input.nextInt();//题目数 System.out.println("请输入公式数值以及分数分母范围:Myapp.exe -"); int range=input.nextInt();//数值范围 int[] count =new int[8]; int i=0; ArrayList<String>Answer=new ArrayList<String>();//储存答案 ArrayList<String>Question=new ArrayList<String>();//储存式子 ArrayList<Integer>Answer1=new ArrayList<Integer>();//储存运算过程中产生的答案 ArrayList<String>Question1=new ArrayList<String>();//储存运算过程中需要的式子
当一个运算符或者多个运算符的情况直接调用一个运算符情况的函数就可以了。
把ArrayList<String>转换成String类,然后输出式子和答案:
//输出文件 public static void TextToFile(String strFilename, String strBuffer) { try { //创建文件 File fileText = new File(strFilename); FileWriter fileWriter = new FileWriter(fileText); fileWriter.write(strBuffer); fileWriter.write("\n"); //关闭输入流 fileWriter.close(); }catch (IOException e) { e.printStackTrace(); } } //格式转换 public static String listostring(ArrayList<String> list) { if(list == null) { return null; } StringBuffer result = new StringBuffer(); int i = 1; for(String string : list) { result.append("[" + i++ + "]" + string+System.getProperty("line.separator")); } return result.toString(); }
下面这个是对上面这两个函数的调用:
String shizi = export.listostring(Question); String answer = export.listostring(Answer); export.TextToFile("Exercises", shizi); export.TextToFile("Answers", answer);
自己的答案文件的批改函数:
//读取文件内容 public static ArrayList<String> getlines(String filename1) { File file = new File(filename1); ArrayList<String>Answer1=new ArrayList<String>(); if(!file.exists()) { System.out.println("未找到目标文件。");//指定路径下的文件不存在则输出:未找到目标文件 }else { try { BufferedReader br = new BufferedReader(new FileReader(file)); String linetext=null; while ((linetext=br.readLine())!= null) { Answer1.add(linetext); } br.close();//关闭输入流 }catch (IOException ex) { ex.printStackTrace(); } } return Answer1; } //对比答案 public static StringBuffer duibi (ArrayList<String> A , ArrayList<String> B) { StringBuffer Right = new StringBuffer(); //存正确的题号 StringBuffer Fault = new StringBuffer();//存错误的题号 StringBuffer Daan = new StringBuffer();//存错误的题号 int r = 0; int f = 0; int j = 0; for(String i : A) { if ((A.get(j).compareTo(B.get(j)) == 0)){ r = r + 1; Right.append(j+1 + ","); } else { f = f + 1; Fault.append(j+1 + ","); } j++; } Daan.append("Correct:" + r + "(" + Right + ")" + System.getProperty("line.separator")); Daan.append("Wrong:" + f + "(" + Fault + ")"); System.out.println("Correct:" + r + "(" + Right + ")"); System.out.println("Wrong:" + f + "(" + Fault + ")"); return Daan; }
以及它的调用:
System.out.println("请输入需要批改的文件名:Myapp.exe -a "); Scanner or =new Scanner(System.in); String file=or.nextLine();//从键盘中输入文件名 ArrayList<String>Answer2 = export.getlines(file); String D = export.duibi(Answer, Answer2).toString(); export.TextToFile("Grade1", D); int o = 1; while (o<10000) { o++; System.out.println("批改下一份作业(Y/N)"); Scanner a =new Scanner(System.in); String b=a.nextLine(); if(b.trim().equals("Y")) { System.out.println("请输入需要批改的文件名:Myapp.exe -a "); Scanner or1 =new Scanner(System.in); String file1=or1.nextLine();//从键盘中输入文件名 ArrayList<String>Answer21 = export.getlines(file1); String D1 = export.duibi(Answer, Answer21).toString(); export.TextToFile("Grade"+o, D1); }else {System.out.println("感谢您的使用。");break;}
测试结果
之所以确定我的程序是正确的是因为它没有报错而且能够正确运行。
性能分析
大概花费了半天的时间,比如把一个运算符的情况独立出去,单独作为一个函数来调用,尽量地减少主函数里的代码行数,改良了一下代分数的输出形式,改掉了一些冗余的函数,尽量减少代码行。
然后运行了一万行大概用时一秒左右。
项目小结
驾驶员:看起来好像很简单的项目可是实操起来还是蛮难的,有很多需要去考虑的问题,在做每一步前都要深思熟虑这种方法有无缺点,会不会有些情况会概括不到,对于Java,还需要继续学习。和我结对的小伙伴在我出现一些比较难解决的问题时能帮我解答一下疑惑,让我能更顺畅的写下去。小伙伴也起到了督促的作用,督促我能更快的完成这个项目任务。
副驾驶:这次的项目里面我还是没有很好地学会该怎么测试以及效能分析。不过这次驾驶员非常优秀,虽然在时间方面抓得还不够紧凑,不过还是比较完美地实现了这个项目。
PSP
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
10 |
10 |
· Estimate |
· 估计这个任务需要多少时间 |
10 |
10 |
Development |
开发 |
100*60 |
120*60 |
· Analysis |
· 需求分析 (包括学习新技术) |
20 |
10 |
· Design Spec |
· 生成设计文档 |
5 |
5 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
10 |
10 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
15 |
15 |
· Design |
· 具体设计 |
30 |
60 |
· Coding |
· 具体编码 |
80*60 |
117*60 |
· Code Review |
· 代码复审 |
30 |
30 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
15 |
15 |
Reporting |
报告 |
20 |
30 |
· Test Report |
· 测试报告 |
15 |
20 |
· Size Measurement |
· 计算工作量 |
10 |
10 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
15 |
15 |
合计 |
|
6030 |
7240 |