by 李世潇 刘润活
项目要求
题目:实现一个自动生成小学四则运算题目的命令行程序
PSP表格预估
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 1190 | 1730 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 90 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 80 |
· Coding | · 具体编码 | 800 | 1200 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | 120 | 150 |
· Test Report | · 测试报告 | 80 | 100 |
· Size Measurement | · 计算工作量 | 20 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1370 | 1940 |
功能列表
[完成] 使用 -n 参数控制生成题目的个数。
[完成] 使用 -r 参数控制题目中数值的范围。
[完成] 生成的题目中计算过程不能产生负数。
[完成] 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
[完成] 程序一次运行生成的题目不能重复,生成的题目存入执行程序的当前目录下的Exercises.txt文件。
[完成] 每道题目中出现的运算符个数不超过3个。
[完成] 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。
[完成] 程序应能支持一万道题目的生成。
设计实现过程
数值生成
定义整数类型的分子分母以此来构造Fraction类型的值,以numerator/denominator的形式存在,若为假分数则化为以上形式,并化简
表达式生成
随机生成固定格式表达式(含括号)中的一种,作为表达式雏形,可能含有运算后出现负数和除号后面出现零的情况。将表达式雏形构建成树,构建的过程中发现运算后出现负数和除号后面出现零的情况则分别调用ridMeaningless与ridNegative方法,前者交换除数前后的值(若交换后仍出现"÷0"的情况则将除号后面的值置为1),后者交换树中"-"结点的左右子树。此时按照中序遍历可获得正确的String表达式。sum方法用于计算正确表达式的结果。
文本生成
将随机生成的每一条表达式与对应的答案分别写入Exercises.txt与Answers.txt中
代码说明
Fraction类
public class Fraction {
int numerator;//分子
int denominator;//分母
boolean isNegative=false;//是否为负
/*
* 构造分数
*/
public void createFraction(int numerator,int denominator) {
if(denominator==0) {
throw new RuntimeException("分母不能为0!");
}
//记录负数的标志
int isNegative = (numerator*denominator<0)? -1 : 1;
if(isNegative==-1) {
this.isNegative=true;
}
numerator = Math.abs(numerator);
denominator = Math.abs(denominator);
int c = gcd(numerator, denominator);
//保证只有a才会小于0
this.numerator = numerator / c ;
this.denominator = denominator / c;
}
public Fraction(int numerator,int denominator) {
createFraction(numerator, denominator);
}
public Fraction(String farction) {
int numerator;
int denominator;
farction.trim();
int indexOfPoite=farction.indexOf("'");
int indexOfLine=farction.indexOf("/");
if(indexOfPoite!=-1) {
int integer=Integer.valueOf(farction.substring(0, indexOfPoite));
denominator=Integer.valueOf(farction.substring(indexOfLine+1));
numerator=integer*denominator+Integer.valueOf(farction.substring(indexOfPoite+1, indexOfLine));
}else if(indexOfLine!=-1) {
denominator=Integer.valueOf(farction.substring(indexOfLine+1));
numerator=Integer.valueOf(farction.substring(0,indexOfLine));
}else {
numerator=Integer.valueOf(farction);
denominator=1;
}
createFraction(numerator, denominator);
}
/*
* 求两数的最大公因数
*/
public int gcd(int a,int b) {
if(a%b==0) {
return b;
}else {
return gcd(b,a%b);
}
}
/*
* 加法
*/
public Fraction add(Fraction f) {
return new Fraction(this.numerator*f.denominator+this.denominator*f.numerator,
this.denominator*f.denominator);
}
/*
* 减法
*/
public Fraction subtract(Fraction f) {
return new Fraction(this.numerator*f.denominator-this.denominator*f.numerator,
this.denominator*f.denominator);
}
/*
* 乘法
*/
public Fraction multiply(Fraction f) {
return new Fraction(this.numerator*f.numerator,this.denominator*f.denominator);
}
/*
* 除法
*/
public Fraction devide(Fraction f) {
return new Fraction(this.numerator*f.denominator,this.denominator*f.numerator);
}
@Override
public String toString() {
if(denominator==1) {
return String.valueOf(numerator);
}else
if(numerator>denominator) {
return String.format("%d'%d/%d", numerator/denominator,numerator%denominator,denominator);
}else {
return String.format("%d/%d", numerator,denominator);
}
}
}
Node类
/*
* 交换左右子树
*/
public static void swap(Node n) {
Node temp;
temp=n.left;
n.left=n.right;
n.right=temp;
}
Expression类
/*
* 生成表达式
* @return 表达式字符串
*/
public String expressionGenerate() {
int numOfFraction=random.nextInt(3)+2;//生成分数个数
int numOfOperator=numOfFraction-1;//生成符号个数
boolean bracketsGenerate=random2.nextBoolean();//是否随机生成括号
StringBuilder sb=new StringBuilder();
try {
for (int i=1;i<=numOfFraction;i++) {
if(i!=numOfFraction) {
sb.append(new Fraction(random.nextInt(defualtRange),random.nextInt(defualtRange-1)+1).toString()+" ");
sb.append(operator[random.nextInt(4)]+" ");
}else {
sb.append(new Fraction(random.nextInt(defualtRange),random.nextInt(defualtRange-1)+1).toString());
}
}
//分数三个或四个就随机生成括号
if(numOfFraction!=2&&bracketsGenerate) {
String[] s = sb.toString().split(" ");
if("+".equals(s[s.length-2])||"-".equals(s[s.length-2])) {
if(s.length==5) {
return s[0]+" "+s[1]+" ( "+s[2]+" "+s[3]+" "+s[4]+" ) ";
}else {
return s[0]+" "+s[1]+" "+s[2]+" "+s[3]+" ( "+s[4]+" "+s[5]+" "+s[6]+" ) ";
}
}
}
return sb.toString();
}catch(RuntimeException a) {
return expressionGenerate();
}
}
/*
* 获取正确表达式的树
*/
public Node getCorrectTree() {
Node node=null;//对生成的表达式建树
while(true) {
String s=expressionGenerate();
node = buildTree(s);
ridMeaningless(node.clone(), node);
if(isTrue) {
break;
}else {
isTrue = true;
}
}
ridNegative(node.clone(),node);//去掉负数
return node;
}
/*
* 输入树的结点,生成正确的表达式
* @param Node
* @return String表达式
*/
public void nodeTraToStr(Node node,StringBuilder sb) {
if(node!=null) {
nodeTraToStr(node.left, sb);
if(Node.isSymbolNode(node)&&Node.isSymbolNode(node.right)&&(!Node.isSymbolNode(node.right.right))&&(Node.isSymbolNode(node.left))
&&Expression.comparePriority(node.value, node.right.value)>=0) {
sb.append(node.value+" ( ");
}else{
sb.append(node.value+" ");
}
nodeTraToStr(node.right, sb);
}
}
/*
* 对表达式建树
* @param 表达式雏形
* @return 树的根节点
*/
public Node buildTree(String expression) {
String ex[]=expression.trim().split(" ");
if(ex.length==3) {//两位操作数的情况(无括号)
Node node0=new Node(ex[0],null,null);
Node node2=new Node(ex[2],null,null);
Node node1=new Node(ex[1],node0,node2);
return node1;
}else if(ex.length==5) {//三位操作数的情况(无括号)
//把前三个字符构成树,索引为1的作为根,索引为0或2的分别作左右子树
Node node0=new Node(ex[0],null,null);
Node node2=new Node(ex[2],null,null);
Node node1=new Node(ex[1],node0,node2);
if("+".equals(ex[1])||"-".equals(ex[1])) {//第一个符号为加或减
if("+".equals(ex[3])||"-".equals(ex[3])) {//第二个符号为加或减
//把第二个的符号作为新根连接旧根和最后一个结点
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node1,node4);
return node3;
}else {//第二个符号为乘或除
//第二个符号作为父节点连接相邻的两数,再作为根节点的右子树
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node2,node4);
node1.right=node3;
return node1;
}
}else{//第一个符号为乘或除
//把第二个的符号作为新根连接旧根和最后一个结点
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node1,node4);
return node3;
}
}else if(ex.length==7) {//三位操作数有括号和四位操作数的情况
if("(".equals(ex[2])) {//三位操作数有括号
//必定是先算符号
Node node0=new Node(ex[0],null,null);
Node node2=new Node(ex[3],null,null);
Node node4=new Node(ex[5],null,null);
Node node3=new Node(ex[4],node2,node4);
Node node1=new Node(ex[1],node0,node3);
return node1;
}else {//四位操作数
Node node0=new Node(ex[0],null,null);
Node node1=new Node(ex[1],node0,null);
if("+".equals(ex[1])||"-".equals(ex[1])) {//第一个符号为加或减
if("+".equals(ex[3])||"-".equals(ex[3])) {//第二个符号为加或减
//把前三个字符构成树,索引为1的作为根,索引为0或2的分别作左右子树
Node node2=new Node(ex[2],null,null);
node1.right=node2;
if("+".equals(ex[5])||"-".equals(ex[5])) {//第三个符号为加或减
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node1,node4);
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node3,node6);
return node5;
}else {//第三个符号为乘或除
Node node4=new Node(ex[4],null,null);
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node4,node6);
Node node3=new Node(ex[3],node1,node5);
return node3;
}
}else {//第二个符号为乘或除
//必定有第二和第三个操作数相乘或相除
Node node2=new Node(ex[2],null,null);
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node2,node4);
if("+".equals(ex[5])||"-".equals(ex[5])) {//第三个符号为加或减
node1.right=node3;
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node1,node6);
return node5;
}else {//第三个符号为乘或除
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node3,node6);
node1.right=node5;
return node1;
}
}
}else {//第一个符号为乘或除
//必定有前两个操作数向运算,把前三个字符构成树,索引为1的作为根,索引为0或2的分别作左右子树
Node node2=new Node(ex[2],null,null);
node1.right=node2;
if("+".equals(ex[3])||"-".equals(ex[3])) {//第二个符号为加或减
if("+".equals(ex[5])||"-".equals(ex[5])) {//第三个符号为加或减
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node1,node4);
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node3,node6);
return node5;
}else {//第三个符号为乘或除
Node node4=new Node(ex[4],null,null);
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node4,node6);
Node node3=new Node(ex[3],node1,node5);
return node3;
}
}else{//第二个符号为乘或除
//第一第二个符号都是乘除的话必定按顺序运算
Node node4=new Node(ex[4],null,null);
Node node3=new Node(ex[3],node1,node4);
Node node6=new Node(ex[6],null,null);
Node node5=new Node(ex[5],node3,node6);
return node5;
}
}
}
}else {//四个操作数加括号
//必定有括号里面的数相加减,定义一个node6,左右孩子为最后两个数
Node node5=new Node(ex[5],null,null);
Node node7=new Node(ex[7],null,null);
Node node6=new Node(ex[6],node5,node7);
if(("+".equals(ex[1])||"-".equals(ex[1]))&&("×".equals(ex[3])||"÷".equals(ex[3]))) {
Node node2=new Node(ex[2],null,null);
Node node3=new Node(ex[3],node2,node6);
Node node0=new Node(ex[0],null,null);
Node node1=new Node(ex[1],node0,node3);
return node1;
}else {
Node node0=new Node(ex[0],null,null);
Node node2=new Node(ex[2],null,null);
Node node1=new Node(ex[1],node0,node2);
Node node3=new Node(ex[3],node1,node6);
return node3;
}
}
}
/*
* 计算某结点的两个叶子结点
* @Param Node
* @return
*/
public Fraction calculate(Node node) {
if("+".equals(node.value)) {
return new Fraction(node.left.value).add(new Fraction(node.right.value));
}else if("-".equals(node.value)) {
return new Fraction(node.left.value).subtract(new Fraction(node.right.value));
}else if("×".equals(node.value)) {
return new Fraction(node.left.value).multiply(new Fraction(node.right.value));
}else
return new Fraction(node.left.value).devide(new Fraction(node.right.value));
}
/**
* @param root
* @return Fraction
*/
public void sum(Node node1,Node node2) {
if(Node.isSymbolNode(node2)) {
sum(node1.left,node2.left);
sum(node1.right,node2.right);
Fraction f =calculate(node2);
node2.value=f.toString();
}
}
/*
* 去掉负数
* @param Node
*/
public void ridNegative(Node node1,Node node2) {
if(Node.isSymbolNode(node1)) {
ridNegative(node1.left,node2.left);
ridNegative(node1.right,node2.right);
Fraction f=calculate(node1);
if(f.isNegative) {
Node.swap(node1);
Node.swap(node2);
}
node1.value=f.toString();
}
}
/*
* 去掉除数为0
* @param Node
*/
boolean isTrue=true;
public void ridMeaningless(Node node1,Node node2) {
if(Node.isSymbolNode(node1)) {
ridMeaningless(node1.left,node2.left);
ridMeaningless(node1.right,node2.right);
if("÷".equals(node1.value)&&"0".equals(node1.right.value)) {//
Node.swap(node1);
Node.swap(node2);
if("0".equals(node1.right.value)) {//如果交换之后还是0,不好意思,只能把你丢弃了
node1.value = "1";
isTrue = false;
return;
}
}
Fraction f=calculate(node1);
node1.value=f.toString();
}
}
/*
* 以下两个方法比较符号
*/
public static int getPriority(String s) {
switch (s) {
case "+":
case "-":
return -1;
case "×":
case "÷":
return 1;
default:
return 0;
}
}
public static int comparePriority(String s1,String s2) {
if(Expression.getPriority(s1)>Expression.getPriority(s2)) {
return 1;
}else if(Expression.getPriority(s1)<Expression.getPriority(s2)) {
return -1;
}else {
return 0;
}
}
}
test类
实现上述类的方法,并将随机生成的每一条表达式与对应的答案分别写入Exercises.txt与Answers.txt中
public class Test {
public static void main(String[] args) throws IOException {
BufferedWriter fw;
BufferedWriter fw2;
BufferedWriter fw3;
//-e C:/Users/Administrator/Desktop/Exercises.txt -a C:/Users/Administrator/Desktop/Answers.txt
Scanner scanner = new Scanner(System.in);
System.out.println("MyApp.exe ");
System.out.println("请输入命令生成题目数目'-n number'");
System.out.println("请输入命令数值范围数目'-r number'");
System.out.println("查询请输入'-e <exercisefile>.txt -a <answerfile>.txt'\n");
Expression e=new Expression();
//Exercise exercise=new Exercise();
boolean b=true;
while (b) {
try {
System.out.println("请输入命令:");
String str=scanner.nextLine();
String[] input=str.trim().split(" ");
if(input.length==2) {
if(Integer.valueOf(input[1])>0) {
if(input[0].equals("-n")) {
e.defualtNumOfQuestion=Integer.valueOf(input[1]);
}else if(input[0].equals("-r")){
e.defualtRange=Integer.valueOf(input[1]);
}else {
throw new Exception();
}
fw=new BufferedWriter(new FileWriter(new File("C:/Users/Administrator/Desktop/Exercises.txt")));
fw2=new BufferedWriter(new FileWriter("C:/Users/Administrator/Desktop/Answers.txt"));
for(int i=1;i<=e.defualtNumOfQuestion;i++) {
Node n = e.getCorrectTree();
StringBuilder sb = new StringBuilder();
e.nodeTraToStr(n, sb);
e.sum(n);
fw2.write(i+"、"+n.value);
fw2.newLine();
fw2.flush();
if(sb.toString().contains("(")) {
sb.append(")");
}
fw.write(i+"、"+sb.toString()+"=");
fw.newLine();
fw.flush();
}
fw.close();
fw2.close();
}else {
throw new Exception();
}
}else if(input.length==4&&input[0].equals("-e")&&input[2].equals("-a")){//检验答案
File fileE=new File(input[1]);
File fileA=new File(input[3]);
if(fileE.exists()&&fileA.exists()) {
BufferedReader bra=new BufferedReader(new FileReader(fileA));
BufferedReader bre=new BufferedReader(new FileReader(fileE));
fw3=new BufferedWriter(new FileWriter("C:/Users/Administrator/Desktop/Grade.txt"));
//s为读入的题目
String s;
while((s=bre.readLine())!=null) {
String ss[]=s.split("、");
s=ss[1];
//对每条题目进行计算
Node tn=e.getCorrectTree(s);
e.sum(tn);
//cAnswer为正确答案
String cAnswer=tn.value;
//tAnswer为输入的答案
String[] sss=bre.readLine().split("[、=]");
String tAnswer=sss[2];
if(cAnswer.equals(tAnswer)) {
fw3.write(sss[0]);
}
}
fw3.flush();
fw3.close();
bra.close();
bre.close();
}else
throw new FileNotFoundException("文件不存在");
}else if("-q".equals(str)){
System.out.println("已退出!");
b=false;
}else {
throw new Exception();
}
}catch(Exception ex){
System.out.println("命令错误,请重新输入命令\n");
}
}
}
}
测试运行
控制台输入
题目生成
答案
代码覆盖率
总结
这次的结对编程虽然我和队友世潇严重缺乏编程经验,但是世潇不畏艰辛持之以恒全神贯注的样子着实让人钦佩不已。我们两人讨论了很多,交换了各种想法,也相互促进产生很多灵感。在我想要放弃一开始的思路即先产生表达式并以此构造表达式二叉树进行计算、查重的时候,世潇坚持的态度感染了我,他正直地想完成这项任务。但由于我们技术薄弱,将理想化为现实异常困难,于是我们上网查阅了很多资料,也虚心向同学请教,最终克服了一个又一个的困难,最终完成了先生成表达式再构建树并能进行修正、计算等功能,获得了小小的成就感。