结对项目
概况
| 完成人员 | 学号 | 
|---|---|
| 钟京洲 | 3121005063 | 
| 唐梦思 | 3221005284 | 
作业概述
| 软件工程 | 计科21级3班 + 4班 | 
|---|---|
| 作业要求 | 四则运算生成 | 
| Github地址 | 仓库 | 
PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) | 
|---|---|---|---|
| Planning | 计划 | 30 | 30 | 
| · Estimate | · 估计这个任务需要多少时间 | 30 | 30 | 
| Development | 开发 | 1755 | 2145 | 
| · Analysis | · 需求分析 (包括学习新技术) | 130 | 180 | 
| · Design Spec | · 生成设计文档 | 60 | 35 | 
| · Design Review | · 设计复审 (和同事审核设计文档) | 5 | 5 | 
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 | 
| · Design | · 具体设计 | 200 | 120 | 
| · Coding | · 具体编码 | 1200 | 1500 | 
| · Code Review | · 代码复审 | 30 | 120 | 
| · Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 | 
| Reporting | 报告 | 85 | 130 | 
| · Test Report | · 测试报告 | 60 | 30 | 
| · Size Measurement | · 计算工作量 | 10 | 10 | 
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | 90 | 
| 合计 | 1870 | 2305 | 
项目
需求概况
| 需求描述 | 是否实现 | 
|---|---|
| 控制生成题目的个数 | 是 | 
| 控制题目中数值范围 | 是 | 
| 计算过程不能产生负数,除法的结果必须是真分数,题目不能重复,运算符不能超过3个 | 是 | 
| 生成的题目存入执行程序的当前目录下的Exercises.txt文件 | 是 | 
| 题目的答案存入执行程序的当前目录下的Answers.txt文件 | 是 | 
| 能支持一万道题目的生成 | 是 | 
| 支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计 | 是 | 
| 统计结果输出到文件Grade.txt | 是 | 
需求分析
- 使用命令行操作,项目打包方式为jar包。
 - 文件读写操作。
 - 四则运算表达式生成、中缀表达式到后缀的转化。
 - 去重
 
项目设计
项目概况

计算与生成表达式
Fraction
Fraction类为存储计算数的基本类,包含用辗转相除法求最大公约数,化假分数为带分数的方法,代码如下
点击查看代码
/**
 * @author zjz
 * @date 2023/9/23
 */
@Data
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class Fraction {
    /**
     * 分母默认为1
     */
    private int denominator;
    /**
     * 分子
     */
    private int numerator;
    /**
     * 用辗转相除法求最大公约数
     * @param a 被除数
     * @param b 除数
     * @return 最大公约数
     */
    private static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    /**
     * 对分数进行约分
     */
    public void Appointment() {
        if (numerator == 0 || denominator == 1)
            return;
        // 如果分子是0或分母是1就不用约分了
        int gcd = gcd(numerator, denominator);
        this.numerator /= gcd;
        this.denominator /= gcd;
    }
    public  String transferFraction(){//对分数进行约分化简
        if (this.denominator == 0) this.denominator = 1;
        int c = this.numerator/gcd(this.numerator,this.denominator);
        int d = this.denominator/gcd(this.numerator,this.denominator);
        int e = c/d;
        int f = c%d;
        String str = "";
        if(f==0){
            str += e;
        }else if(e!=0){
            str = e+"'"+f+"/"+d;
        }else{
            str +=f+"/"+d ;
        }
        return str;
    }
    @Override
    public String toString() {
        return numerator + "/" + denominator;
    }
}
FractionUtil
包含加减乘除等一系列对Fraction的操作,以及将带分数转化为Fraction对象的方法
点击查看代码
public class FractionUtil {
    /**
     * 加法
     */
    public static Fraction add(Fraction f1, Fraction f2) {
        return new Fraction(f2.getDenominator() * f1.getDenominator(),f2.getNumerator() * f1.getDenominator() + f2.getDenominator() * f1.getNumerator());
    }
    /**
     * 减法
     */
    public static Fraction sub(Fraction f1, Fraction f2) {
        return new Fraction(f2.getDenominator() * f1.getDenominator(), f2.getDenominator() * f1.getNumerator() - f2.getNumerator() * f1.getDenominator());
    }
    /**
     * 分数的乘法运算
     */
    public static Fraction multiplication(Fraction f1, Fraction f2) { // 乘法运算
        return new Fraction(f2.getDenominator() * f1.getDenominator(),f2.getNumerator() * f1.getNumerator());
    }
    /**
     * 分数除法运算
     */
    public static Fraction div(Fraction f1, Fraction f2) {
        return new Fraction(f2.getNumerator() * f1.getDenominator(),f2.getDenominator() * f1.getNumerator());
    }
    /**
     * 带分数转化为Fraction对象
     * @param properFraction 带分数
     * @return Fraction
     */
    public static Fraction Transform(String properFraction,boolean type){
        if (type) {
            return new Fraction(1,Integer.parseInt(properFraction));
        } else {
            String[] split = properFraction.split("'");
            int denominator;
            int numerator;
            if (split.length == 1) {
                // 普通分数
                split = properFraction.split("/");
                denominator = Integer.parseInt(split[1]);
                numerator = Integer.parseInt(split[0]);
            } else {
                String[] split1 = split[1].split("/");
                denominator = Integer.parseInt(split1[1]);
                numerator = Integer.parseInt(split[0]) * denominator + Integer.parseInt(split1[0]);
            }
            Fraction fraction = new Fraction();
            fraction.setDenominator(denominator);
            fraction.setNumerator(numerator);
            return fraction;
        }
    }
}
CalculateImpl
- 中缀转后缀时,采用一个队列和一个栈,数字输入到队列,运算符压入栈。在压入栈时,检查栈顶元素的优先级,要保证压入栈的元素的优先级最高。若栈顶元素大于等于想压入栈的元素,则把栈顶元素弹出,继续检查,直到被压入栈的元素的优先级是最高的。遇到"(“先入栈,遇到”)“把”("之后的元素弹出到队列,括号不输出到队列。
 - 后缀表达式求值时,遇到数字就压入栈,遇到运算符就弹出两个数字进行运算。因为最后的结果是分数,所以从一开始运算就把运算数全部转换为分数进行计算最后化简。
 
点击查看代码
public class CalculateImpl implements Calculate{
    @Override
    public Fraction fourKindCalculate(Fraction f1,Fraction f2, String operator) {
        Fraction result;
        switch (operator) {
            case "+":
                result = FractionUtil.add(f1,f2);
                break;
            case "-":
                result = FractionUtil.sub(f1,f2);
                break;
            case "*":
                result = FractionUtil.multiplication(f1,f2);
                break;
            case "/":
                // 除数不为0则返回结果,除数为0则报错
                if (f2.getDenominator() != 0) {
                    result = FractionUtil.div(f1,f2);
                } else {
                    throw new DivisionWrongException();
                }
                break;
            default:
                throw new WrongOperationException();
        }
        return result;
    }
    @Override
    public String calculate(String str) {
        String[] compute = str.split(" ");
        //存后缀表达式
        Stack<String> stack = new Stack<>();
        //放操作符
        Stack<String> optStack = new Stack<>();
        //放数字结果
        Stack<Fraction> numStack = new Stack<>();
        for (String s : compute) {
            // 带分数
            if (s.matches("^(\\d+)'(\\d+)\\/(\\d+)$")) {
               // 直接压入栈中,作为一个数来计算
                stack.push(s);
            }
            //匹配数字
            else if (s.matches("-?\\d+(\\.\\d+)?")) {
                stack.push(s);
            } else if (optStack.isEmpty()) {
                optStack.push(s);
            }
            //判断符号
            else if ("(".equals(s)) {
                optStack.push(s);
            } else if (")".equals(s)) {
                while (!"(".equals(optStack.peek())) {
                    stack.push(optStack.pop());
                }
                //找到(
                optStack.pop();
            }
            //加减乘除入栈
            else if ("+".equals(s) || "-".equals(s) || "*".equals(s)
                    || "/".equals(s)) {
                while ((!optStack.isEmpty()) && (PriorityTable.Priority(s) <= PriorityTable.Priority(optStack.peek()))) {
                    stack.push(optStack.pop());
                }
                optStack.push(s);
            }
            //数字小数点直接入栈
            else {
                stack.push(s);
            }
        }
        //当操作符栈不为空,将操作符全部弹入stack
        while (!optStack.isEmpty()) {
            stack.push(optStack.pop());
        }
        //numStack=
        Stack<Fraction> traversing = Traversing(stack, numStack);
        return traversing.pop().transferFraction();
    }
    @Override
    public Stack<Fraction> Traversing(Stack<String> stack,Stack<Fraction> numStack){
        for (String l : stack) {
            Fraction num1, num2;
            //正则表达式匹配分数
            if (l.matches("^(\\d+)(?:'(\\d+))?/(\\d+)$")){
                numStack.push(FractionUtil.Transform(l,false));
            } else if (l.matches("^\\d+$")){
                numStack.push(FractionUtil.Transform(l,true));
            }
            else {
                if(numStack.isEmpty()){
                    num1 = new Fraction(1,0);
                }
                else {
                    num1 = numStack.pop();
                }
                if(numStack.isEmpty()){
                    num2 = new Fraction(1,0);
                }
                else {
                    num2 = numStack.pop();
                }
                Fraction calculate = fourKindCalculate(num2,num1,l);
                numStack.push(calculate);
            }
        }
        return  numStack;
    }
}
ExpressionImpl
- List
createExpression(Integer n, Integer r) 
运算数和运算符是随机生成的,每一次循环生成一个运算数和一个运算符,limit控制循环的次数,即运算符的数目,在这个方法中对不合法和重复的式子进行剔除,不合法的情况有分母为零,出现负数。在调用Calculate方法计算的时候,把含有以上两种情况的式子都会剔除。 
点击查看代码
public class ExpressionImpl implements Expression {
    /**
     * 生成的表达式集合
     */
    ArrayList<String> expressionList = new ArrayList<>();
    /**
     * 写入文件的表达式的集合
     */
    ArrayList<String> expressionListToFile = new ArrayList<>();
    @Override
    public List<String> createExpression(Integer n, Integer r) {
        String[] operate = new String[]{"+", "-", "*", "/"};
        Random random = new Random();
        //随机产生的运算符个数
        int count;
        StringBuilder temp;
        String tempToFile;
        for (int i = 0; i < n; i++) {
            //[1,4)
            temp = new StringBuilder();
            count = random.nextInt(3) + 1;
            for (int j = 1; j <= count; j++) {
                Fraction fraction = new Fraction(random.nextInt(n), random.nextInt(n));
                String opt = operate[random.nextInt(4)];
                // 这里要判断是否分母可能为0,如果为0则按1来计算,分数类分母默认为1
                temp.append(fraction.transferFraction()).append(" ").append(opt).append(" ");
            }
            int x2 = random.nextInt(n);
            temp.append(x2);
            tempToFile = i+1+"、"+temp + "=";
            expressionList.add(temp.toString());
            expressionListToFile.add(tempToFile);
        }
        return expressionList;
    }
    @Override
    public void writeToFile(String fileName) {
        FileUtil.writeToFile(expressionListToFile,fileName);
    }
    @Override
    public void answerToFile(List<String> answerList, String fileName) {
        FileUtil.writeToFile(answerList,fileName);
    }
}
表达式结果校验与输入输出
Check
其对应的实现类CheckImpl,使用FileUtil将答题情况写入Grade文件,调用Calculate计算对应结果并且校验,使用linkedHashMap存放题目和答案。
点击查看代码
 /**
     * 读取题目和答案文件
     *
     * @param answerFile    答案文件
     * @return exercises集合
     */
    Map<String,String> readFile(File answerFile) throws Exception;
    /**
     * 对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
     *
     */
    Result checkAnswer(Map<String,String> map) throws IOException;
    /**
     * 对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
     */
    Result checkPaper(String answerFile);
测试
生成表达式
public void testCreateExpression() {
        List<String> expression = this.expression.createExpression(5, 5);
        for (String s : expression) {
            System.out.println(s);
        }
    }

存入txt文本

问题:

答案:

一万道题目生成及其性能分析
 Expression expression = new ExpressionImpl();
        Calculate calculate = new CalculateImpl();
        String answerTemp;
        List<String> expression1 = expression.createExpression(10000, 10);
        List<String> answerList = new ArrayList<>();
        expression.writeToFile("Exercises.txt");
        for (String s : expression1) {
            String result = calculate.calculate(s);
            answerTemp = s + " =" + result;
            System.out.println(answerTemp);
            answerList.add(answerTemp);
        }
        expression.answerToFile(answerList, "Answers.txt");
    }




答案校验
题目:

答案:

其中修改了前三题的答案,所以grade输出应该是前三题错误,其余正确
校验:

七、总结
收获:
1 锻炼了团队协作和沟通的能力,学会倾听和尊重队友的意见,并在最终的决策过程达成共识。
2 为完成项目需求,需要分析具体的实现方式来解决遇到的难题,学习新的思维模式,采用相关技术克服难题,在这个过程锻炼了逻辑思维,提升了编程能力
3 结对编程体现了合作的关键性,在项目经过需求分析和详细设计后,要进入真正实现的过程,对于程序的推进,需要明确各自的目标,尽可能达到最优的实现方案,完成负责的模块,提高开发效率
不足的地方:
用Java来实现数据结构的效率不够高,没有图形化界面

                
            
浙公网安备 33010602011771号