1、GitHub地址:https://github.com/caiyouling/Myapp
队友:钟小敏 GitHub地址:https://github.com/zhongxiao136/Myapp
2.PSP表格
PSP | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 30 |
.Estimate | .估计这个任务需要多少时间 | 40 | 30 |
Development | 开发 | 1320 | 1330 |
.Analysis | .需求分析 | 100 | 120 |
.Design Spec | .生成设计文档 | 40 | 30 |
.Design Review | .设计复审 | 15 | 20 |
.Coding Standard | .代码规范 | 15 | 20 |
.Design | .具体设计 | 90 | 100 |
.Coding | .具体编码 | 850 | 950 |
.Code Review | .代码复审 | 90 | 90 |
.Test | .测试(自我测试,修改代码,提交修改) | 120 | 130 |
Reporting | 报告 | 180 | 160 |
.Test Report | .测试报告 | 90 | 80 |
.Size Measurement | .计算工作量 | 30 | 40 |
.Postmortem&Process Improvement Plan | .事后总结,并提出过程改进计划 | 60 | 40 |
合计 | 1540 | 1520 |
效能分析
设计实现过程
- 解题思路:由于题目为自动生成小学四则运算题目,所以需要系统随机生成运算式。运算式包含操作数和运算符,所以我们就用了一个CreateNumAndOpe类随机生成,然后利用createExpression类组合成运算式和答案(利用Calculate类进行计算,因为结果要为真分数或整数,所以我们利用Fraction类对分数进行操作化为最终形式),然后用createFile类生成题目,答案和做题文件,最后,利用Grade类进行验证答案,并生成成绩文件。
- createNumAndOpe类:定义运算符数组;包含createNumber()和createOperator(),其中createNumber()用于生成操作数(利用Random类随机生成整数或分数);createOperator()用于生成运算符,随机从运算符数组中选出运算符。
- createExpression类:用于生成随机带括号的运算式,其中exerciseAndAnswer()根据操作数数组和运算符数组生成运算式,其中需要用Random类判断是否随机生成括号,最后用Calculate()计算出结果。getExerciseAndAnswer()根据用户输入的题目个数和数字范围生成相应的题目和答案。
- Calculate类:根据运算式转为字符数组,然后逐个遍历,建立一个运算符栈和操作数栈,利用中缀表达式转为后缀表达式进行计算。
- 因为括号的优先级最高,所以我们先判断是否为'(',若是则入运算符栈;
- 判断是否为')',是则从操作数取出两个进行运算,将'('之后的运算符都拿出来算,结果再存入操作数栈中,然后左括号出栈;
- 判断是否为运算符,当运算符优先级小于运算符栈顶元素,则取两个操作数进行运算,直至栈顶优先级不高于该运算符,结果存入操作数栈中,然后该运算符入栈;
- 若为操作数,因为生成的操作数有分数,但是我们转为了字符数组,就需要根据'/'作为标志获取分子和分母,若为整数,我们将分母设为1,把操作数都转为分数进行传到calculate()进行运算,返回结果
- Fraction类:对分数进行操作,其中getFinalFraction()用于生成最终分数形式,gcd()利用辗转相除法生成最大公约数,getRealFraction()将假分数转为真分数。
- createFile类:利用System.getProperTy()获取当前路径生成printFile文件夹,利用BufferedWriter写入题目文件,答案文件和答题文件(用户进行答题的文件),存放到printFile文件夹下
- Grade类:用于验证用户答题是否正确,利用BufferedReader读取Writer文件和answer文件进行比较,算出对错的题数及题号写入Grade文件,也存在printFile文件夹下。
- 各类之间的调用如下:
- Frame类:图形化界面,用户点击‘生成文件’,即会生成练习文件,在左面板也会出现题目,中间面板进行答题,点击查看答案,则会有正确答案在右侧面板,查看成绩后会弹出成绩框。
关键代码说明
- 用于生成操作数和运算符的方法:1.利用随机生成的flag=[0,1],当flag为0时,生成整数 2.当flag为1时,生成分子和分母组成分数
public static String[] createNumber(int number, int round){
Random random = new Random();
String[] num = new String[number];
for (int i = 0; i < number; i++) {
//用于判断生成整数还是分数
int flag = (int)(Math.random()*10) % 2;
if(flag == 0){ //生成整数
int n = random.nextInt(round);
if(n == 0){
num[i] = 1 + "";
}else{
num[i] = n +"";
}
}else{ //生成分数
//随机生成分子和分母
int numerator = random.nextInt(round);
int denominator = random.nextInt(round);;
while(numerator>=denominator || numerator==0 || denominator==0){ //判断是否为真分数,且不能生成带0的分数
numerator = random.nextInt(round);
denominator = random.nextInt(round);
}
//拼装成分数形式
num[i] = numerator + "/" + denominator;
}
}
return num;
}
/**
* 随机生成运算符
* 将四则运算符放入一个静态不可变的operatorTypes[]字符数组中
* 随机产生index到数组中取操作符
*/
private static final Character[] operatorTypes = {'+' , '-' , '×' , '÷'};
public static Character[] createOperators(int number) {
Character[] operators = new Character[number];
for (int i = 0; i < number; i++) {
//随机获取运算符的类型(0~3 代表4个运算符(+、-、×、÷)的类型)
int index = (int)(Math.random()*4);
Character operatorType = operatorTypes[index];
operators[i] = operatorType;
}
return operators;
}
public static int priority(Character character) {
switch(character) {
case '×':
case '÷':return 1;
case '+':
case '-':return 0;
}
return -1;
}
private static String[] exerciseAndAnswer(String[] numbers, Character[] operators){
Random random = new Random();
//获得操作数的数量
int num = numbers.length;
//随机生成带括号的算式
int isAddBracket = (int)(Math.random()*10) %2;
if(isAddBracket == 1){ //当isAddBracket==1时,生成带括号的表达式
//当标记为1时代表该操作数已经添加了左括号
int[] leftBracket = new int[num];
//当标记为1时代表该操作数已经添加了右括号
int[] rightBracket = new int[num];
//遍历操作数数组,随机添加括号
for (int index = 0 ; index<num-1 ; index++) {
int n = (int)(Math.random()*10)%2;
if(n == 0 && rightBracket[index] != 1) {//判断当前操作数是否标记了左括号
leftBracket[index] = 1; //标记已生成左括号
numbers[index] = "(" + numbers[index]; //操作数之前加上左括号
int k = num - 1;
//生成右括号的位置(左括号的位置~最后)
int rightBracketIndex = random.nextInt(k)%(k-index) + (index+1);
//如果当前操作数有左括号,则重新生成括号位置
while (leftBracket[rightBracketIndex] == 1){
rightBracketIndex = random.nextInt(k)%(k-index) + (index+1);
}
rightBracket[rightBracketIndex] = 1; //标记已生成右括号
numbers[rightBracketIndex] = numbers[rightBracketIndex] +")";
}
}
}
//将运算符数组和操作数数组交替拼成一个算式字符串
StringBuilder str = new StringBuilder(numbers[0]);
for (int k = 0; k < operators.length; k++) { //组成算式
str.append(operators[k]).append(numbers[k + 1]);
}
//将算式转换为String
String exercise = str.toString();
//获取运算式结果
String answer = Calculate.getAnswer(exercise);
if(answer.equals("-")){ //运算过程出现负数则返回null
return null;
}
return new String[]{exercise,answer};
}
public static String getAnswer(String expression) {
Stack<Character> operators = new Stack<Character>(); //运算符栈,用于存放运算符
Stack<Fraction> fractions = new Stack<Fraction>(); //操作数栈,用于存放操作数
if(expression == null){
return "null";
}
char[] chars = expression.toCharArray(); //将表达式字符串转成字符数组
//遍历获取处理
for (int i = 0 ; i<chars.length ; i++) {
char c = chars[i];
if(c == '('){ //先处理有括号的情况,如果是左括号,入栈
operators.push(c);
} else if(c == ')'){ //当前字符为右括号
while(operators.peek() != '('){ //当运算符栈顶的元素不为‘(’,则继续
Fraction fraction1 = fractions.pop(); //拿取操作栈中的两个分数
Fraction fraction2 = fractions.pop();
//获取计算后的值
Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(),
fraction2.getNumerator(), fraction2.getDenominator());
if(result.getNumerator()<0){ //判断是否为负数
return "-";
}
//将结果压入栈中
fractions.push(result);
}
//将左括号出栈
operators.pop();
}else if(isOperator(c)){//是运算符
//当运算符栈不为空,且当前运算符优先级小于栈顶运算符优先级
while(!operators.empty() && !(priority(c)>= priority(operators.peek()))){
//拿取操作栈中的两个分数
Fraction fraction1 = fractions.pop();
Fraction fraction2 = fractions.pop();
//获取计算后的值
Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(),
fraction2.getNumerator(), fraction2.getDenominator());
if(result.getNumerator()<0){
return "-";
}
//将结果压入栈中
fractions.push(result);
}
//将运算符入栈
operators.push(c);
}else{ //是操作数
if(c >= '0' && c <= '9'){
StringBuilder fra = new StringBuilder();
//对分式进行处理
while(i<chars.length && (chars[i]=='/' || ((chars[i]>='0') && chars[i]<='9'))){
fra.append(chars[i]);
i++;
}
i--;
//到此 fra里面是一个操作数
String val = fra.toString();
//标记‘/’的位置
int flag = val.length();
for(int j = 0 ; j<val.length() ; j++){
if(val.charAt(j)=='/'){//当获取的数值存在/则标记/的位置,便于接下来划分分子和分母生成分数对象
flag = j;
}
}
//把操作数拆成分式计算
//分子
StringBuilder numeratorBuf = new StringBuilder();
//分母
StringBuilder denominatorBuf = new StringBuilder();
for(int k = 0 ; k<flag; k++){
numeratorBuf.append(val.charAt(k));
}
//判断是否为分数
if(flag != val.length()){
for(int m = flag+1 ; m<val.length() ; m++){
denominatorBuf.append(val.charAt(m));
}
}else{//如果不是分数则分母计为1
denominatorBuf.append('1');
}
//将分子分母分别入栈
fractions.push(new Fraction(Integer.parseInt(numeratorBuf.toString()),
Integer.parseInt(denominatorBuf.toString())));
}
}
}
while(!operators.empty() && fractions.size()>1){
Fraction fraction1 = fractions.pop();
Fraction fraction2 = fractions.pop();
//获取计算后的值
Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(),
fraction2.getNumerator(), fraction2.getDenominator());
if(result.getNumerator()<0){
return "-";
}
//将结果压入栈中
fractions.push(result);
}
//计算结果
Fraction result = fractions.pop();
//获取最终的结果(将分数进行约分)
return Fraction.getFinalFraction(result);
}
//用于验证答案是否正确,correct的题数用字符串拼接,再分割字符串
public static void isAnswer(String writeFilePath, String answerFilePath) {
//获取用户答题和练习答案的文件
File writeFile = new File(writeFilePath);
File answerFile = new File(answerFilePath);
//找到存放Writer.txt和Answer.txt文件的文件夹
String fileDirectory = System.getProperty("user.dir") + File.separator + "PrintFile";
File gradeFile = new File(fileDirectory, "Grade.txt");//在当前文件夹下生成Grade.txt文件
if(writeFile.isFile() && answerFile.isFile()) {
try {
BufferedReader writeReader = new BufferedReader(new InputStreamReader(new FileInputStream(writeFile)));
BufferedReader answerReader = new BufferedReader(new InputStreamReader(new FileInputStream(answerFile)));
//储存错题和对题
String correct = "";
String wrong = "";
int correctNum = 0;
int wrongNum = 0;
//记录错题对题的题号
int index = 1;
String write = null;
String answer = null;
System.out.println("开始验证答案···");
//通过一行行读取文件比较答题情况
while((( write= writeReader.readLine()) != null) && ((answer = answerReader.readLine()) != null)){
if(write.equals(answer)){
//将答对的题用字符串拼接
correct += "," + index;
index ++;
correctNum ++;
}else{
wrong += "," + index;
index ++;
wrongNum ++;
}
}
//用于生成Grade.txt文件内容
if(correctNum > 0){
correct = "Correct: " + correctNum + "(" + correct.substring(1) +")" + "\r\n" ;
}else{
correct = "Correct: 0" +"\r\n";
}
if(wrongNum > 0){
wrong = "Wrong: " + wrongNum + "(" + wrong.substring(1) +")";
}else{
wrong = "Wrong: 0";
}
//写入文件
BufferedWriter gradeFileBuf = new BufferedWriter(new FileWriter(gradeFile));
gradeFileBuf.write(correct);//将correct 和wrong写入文件
gradeFileBuf.write(wrong);
System.out.print(correct);//控制台也打印答题情况
System.out.println(wrong);
if(writeReader != null){
writeReader.close();
}
if(answerReader != null){
answerReader.close();
}
if(gradeFileBuf != null){
gradeFileBuf.close();
}
public static String getFinalFraction(Fraction fraction) {
int denominator = fraction.getDenominator(); //获得分子分母
int numerator = fraction.getNumerator();
if(numerator == 0){ //若分子为0,则输出0
return "0";
}
if(denominator == 0){
return "";
}
int a = gcd(numerator, denominator); //c是分子分母的最大公因数,将分子分母化简
if (denominator == 1 ) { //分母为1
return numerator + "";
}
if(numerator == denominator){
return 1+"";
}else {
if(numerator > denominator){ //分子大于分母化为真分数
fraction = getRealFraction(fraction); //假分数分割为商和余数,余数作为新的分子
if(fraction.getNumerator() == 0){//若余数为0,则代表整除
return fraction.getInter()+"";
}else {
return fraction.getInter() + "'" + fraction.getNumerator()/a + "/" + fraction.getDenominator()/a;
}
}else{ //其他情况化简分数
return numerator/a + "/" + denominator/a;
}
}
}
private static int gcd(int numerator, int denominator){
if(numerator%denominator == 0){
return denominator; //获取最大公因数
}else{
return gcd(denominator,numerator%denominator); //辗转相除法递归获取最大公因数
}
}
测试
1.启动main,运行控制台打印
2.用户输入后生成的文件列表
.
3.生成题目文件的题目
4.生成答案放入文件
5.在答题文件进行答题后
6.重新判别答案
7.传入参数不正确的情况
8.输入不正确的文件名
9.生成一万道题目
10.生成一万道题目的答案文件
11.初始界面
12.点击生成文件
13.输入生成个数
14.生成题目
15.进行作答
16.输出成绩
17.生成十道题
18.生成一万道
小结
- 本次结对项目中,我们都受益匪浅,结对项目需要多次讨论得出一致结论,两个人一起分析问题解决bug是比一个人好一些,虽然也有发生歧义的地方,但是不同角度看问题,才能实现1+1>2,这次编程作业让我们对java有了更深了了解,但也还有很多不足的