结对项目

一、作业信息

这个作业属于哪个课程 班级链接
这个作业要求在哪里 作业要求
这个作业的目标 组队设计实现一个生成小学四则运算题目的命令行程序

二、小组成员

姓名 学号 github
陈大锴 3122004816 https://github.com/ez4-cdk/ez4-cdk/tree/master/3122004816/algorithm
陈祖民 3122004822 https://github.com/MIR-mIsTEo/3122004822-02

三、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
· Estimate · 估计这个任务需要多少时间 30 30
Development 开发 420 510
· Analysis · 需求分析 (包括学习新技术) 60 30
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 30 30
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 30
· Design · 具体设计 120 180
· Coding · 具体编码 60 60
· Code Review · 代码复审 30 30
· Test · 测试(自我测试,修改代码,提交修改) 60 120
Reporting 报告 120 180
· Test Repor · 测试报告 60 120
· Size Measurement · 计算工作量 30 30
Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
· 合计 1200 570 720

四、设计实现过程

1、项目结构

2、接口设计与实现

①.Expression

此类为表达式的对象,成员为String类型的expression、构造方法Expression(String)、getter以及核心方法evaluate():String

②Fratcion

此类为分数的对象,对于分数的存储,选用"A'B/C"的带分数形式存储
加减乘除的思路为:
加法:分母分子同分,分子相加,分母取最小公倍数,返回的新分数再化简
减法:分母分子同分,分子相加,分母取最小公倍数,返回的新分数再化简
乘法:分子乘分子,分母乘分母
乘法:分子分母交叉相乘
成员主要有:
int 类型的num————整数部分
int 类型的numerator————分子部分
int 类型的denominator————分母部分
上面三个属性的getter,本分数的构造方法Fraction(Int,Int)
simplify() [将构造的分数化为带分数并且约分]
generateABiggerFraction():Fration [生成一个比本分数更大的分数]
add(Fratcion):Fraction [加]
subtract(Fratcion):Fraction [减]
multiply(Fratcion):Fraction [乘]
divide(Fratcion):Fraction [除]
fromString(String):Fraction [将表达式转化为分数]
gcd(int,int):int [求最大公约数]
toString():String [重写的tostring()方法]

③ExpressionEvaluator

此类为表达式计算器,用于计算表达式的值,先计算括号内的,再计算乘除,最后计算加减
evaluate(String):Fraction 计算一个表达式并返回一个分数
calculate(String):String 识别括号并进入括号内计算的过程
evaluateExpression(String):Fraction 计算的主体过程,包括加减乘除

④FileIO

此类为文件输入输出的工具类
readFile(String):String [读入]
writeOutput(String,Object):String [写出]

⑤Generator

此类为生成器工具类,用户生成各种需要的数据,属于关键类,轴心流程图如下:

       开始
         |
         v
  generateExpressions(num, area) —— > 生成表达式集合的核心函数,num为表达式数量,area为表达式各个参数的取值范围
         |
         v
   循环 i 从 0 到 num-1  —— > 生成每一条表达式
         |  
         v
     generateOperators()  —— > 生成表达式的操作符
         |
         v
   generateNumber(operators.size() + 1, area, operators)  —— > 生成表达式的操作数
         |
         v
    generateExpression(operators, numbers)  —— > 表达式的操作符与操作数按照需求组合
         |
         v
 添加表达式到 expressions 列表
         |
         v
        结束循环
         |
         v
    返回 expressions 列表
         |
         v
        结束

Random 类型的random————随机种子
generateNumber(int,int,List):List [随机生成分数]
generateOperators():List [生成操作符]
sort(List):void [排序]
generateExpressions(int,int):List [生成表达式集合,主要生成函数]
generateBracket(int):List [随机生成括号]
generateExpression(List,List):String [生成每一条字符串类型的表达式]
getOperatorSymbol(int):String [将加减乘除符号解析]

⑥SplashClass

此类为入口类,入口有两个,一个是生成题目与答案的入口,一个是检查答案与表达式答案的入口
main(String[]):void [main函数]
generateExercises(Integer,Integer):void [生成题目的入口]
checkAnswer(String,String):void [检查答案的入口]

五、代码说明

Generator
生成器的类是本程序的核心,负责控制生成操作符、操作数、表达式、表达式集合,层层递进

package com.softwareClass.util;

import com.softwareClass.entity.Expression;
import com.softwareClass.entity.Fraction;

import java.util.*;

public class Generator {
    
    public List generateOperators(){
        int randomArea = 3;
        List list = new ArrayList<>();
        int operatorsCount = random.nextInt(3)+1;
        for(int i=0; i generateNumber(int count, int area, List operators) {
        List numbers = new ArrayList<>();
        try {
            
            //循环生成数量为count的分数集合
            while (numbers.size() < count) {
                int denominator = random.nextInt(area-3) + 3;
                int numerator = random.nextInt((area - 1) * denominator) + 1;
                Fraction fraction = new Fraction(numerator, denominator);
                numbers.add(fraction);
            }
            
            //排序
            sort(numbers);
            
            //对于除法,选择生成一个比前一个更大的分数
            for (int i = 0; i < operators.size(); i++) {
                if (operators.get(i) == 3) {
                    numbers.set(i+1,numbers.get(i).generateABiggerFraction());
                }
            }
            
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("随机种子初始化失败,请重试.");
        }
        return numbers;
    }

    //排序(降序)
    private void sort(List numbers) {
        numbers.sort((f1, f2) -> {
            int value1 = f1.getNum();
            int value2 = f2.getNum();
            return Integer.compare(value2, value1);
        });
    }

    //主生成函数——生成表达式集合
    public List generateExpressions(int num,int area) {
        List expressions = new ArrayList<>();
        while (expressions.size() < num){
            List operators = generateOperators();
            List numbers = generateNumber(operators.size()+1, area,operators);
            Expression e = new Expression(generateExpression(operators,numbers));
            if (Fraction.fromString(e.evaluate()).getNum()>=0){
                expressions.add(e);
            }
        }
        return expressions;
    }

    //生成括号——在特定索引下生成括号,如果索引合法,则返回索引集合,非法则不生成括号
    public List generateBracket(int num){
        List brackets = new ArrayList<>();
        int leftBracket = random.nextInt(num);
        int rightBracket = random.nextInt(num);
        if (leftBracket operators, List numbers) {
        List numberList = new ArrayList<>(numbers);
        List brackets = generateBracket(numberList.size());
        StringBuilder expressionBuilder = new StringBuilder();
        //类似于e=()这种情况就不用加括号,比如e=(1+2/3)
        if (brackets!=null&&brackets.get(0)==0&&brackets.get(1)==numberList.size()-1){
            brackets = null;
        }
        //每次加入一个分数和一个符号
        for (int i = 0; i < operators.size(); i++) {
            //左括号索引
            if (brackets!=null&&brackets.get(0)==i){
                expressionBuilder.append("(");
            }
            //右括号索引
            expressionBuilder.append(numberList.get(i));
            if (brackets!=null&&brackets.get(1)==i){
                expressionBuilder.append(")");
            }
            expressionBuilder.append(getOperatorSymbol(operators.get(i)));
        }
        //添加最后一个分数
        expressionBuilder.append(numberList.getLast());
        
        //如果右括号没合上,则合上
        if (brackets!=null&&(brackets.get(1)+1)==numberList.size()){
            expressionBuilder.append(")");
        }
        return expressionBuilder.toString();
    }

    // 辅助方法,用于根据操作符的整数值返回相应的符号
    public String getOperatorSymbol(int operator) {
        return switch (operator) {
            case 0 -> " + ";
            case 1 -> " - ";
            case 2 -> " * ";
            case 3 -> " / ";
            default -> throw new IllegalArgumentException("Invalid operator");
        };
    }

}

Fraction
分数的类,至关重要,对每个分数的加减乘除和解析负责

package com.softwareClass.entity;

public class Fraction {
    private int num; // 整数部分
    private int numerator;   // 分子
    private int denominator; // 分母
    
    //三个属性的getter
    public int getNum() {
        return num;
    }

    public int getNumerator() {
        return numerator;
    }

    public int getDenominator() {
        return denominator;
    }

    //构造函数
    public Fraction(int numerator, int denominator) {
        if (denominator == 0) throw new IllegalArgumentException("分母不能为零");
        this.numerator = numerator;
        this.denominator = denominator;
        simplify(); // 在构造时简化分数
    }

    //生成一个比本分数还大的分数
    public Fraction generateABiggerFraction(){
        if (this.num==0&&this.numerator==0)return new Fraction(1,1);
        int thisNumerator = this.num*this.denominator+this.numerator;
        int thisDenominator = this.denominator;
        if (thisDenominator/2 == 0){
            thisDenominator=3;
        }else {
            thisDenominator=thisDenominator/2;
        }
        return new Fraction(thisNumerator,thisDenominator);
    }

    //从字符串解析出分数
    public static Fraction fromString(String str) {
        int denominator,numerator,num;
        String[] temp1,temp2;
        if (str.contains("/")){
            if (str.contains("'")){
                temp1 = str.split("'");
                num = Integer.parseInt(temp1[0]);
                temp2 = temp1[1].split("/");
                denominator = Integer.parseInt(temp2[1]);
                numerator = Integer.parseInt(temp2[0]);
                return new Fraction(num*denominator+numerator,denominator);
            }else{
                temp1 = str.split("/");
                denominator = Integer.parseInt(temp1[1]);
                numerator = Integer.parseInt(temp1[0]);
                return new Fraction(numerator,denominator);
            }
        }else{
            return new Fraction(Integer.parseInt(str),1);
        }
    }

    //加法,分母相乘,分子交叉乘分母并相加
    public Fraction add(Fraction other) {
        int commonDenominator = this.denominator * other.denominator;
        int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator +
                (other.numerator + other.num * other.denominator) * this.denominator;
        return new Fraction(newNumerator, commonDenominator);
    }

    //减法,分母相乘,分子交叉乘分母并相减
    public Fraction subtract(Fraction other) {
        int commonDenominator = this.denominator * other.denominator;
        int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator -
                (other.numerator + other.num * other.denominator) * this.denominator;
        return new Fraction(newNumerator, commonDenominator);
    }

    //乘法,分子分母对应相乘
    public Fraction multiply(Fraction other) {
        int newNumerator = (this.numerator + this.num * this.denominator) * (other.numerator + other.num * other.denominator);
        int newDenominator = this.denominator * other.denominator;
        return new Fraction(newNumerator, newDenominator);
    }
    
    //除法,分子分母交叉相乘
    public Fraction divide(Fraction other) {
        if (other.numerator == 0 && other.num == 0) throw new IllegalArgumentException("不能除以零");
        int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator;
        int newDenominator = this.denominator * (other.numerator + other.num * other.denominator);
        return new Fraction(newNumerator, newDenominator);
    }

    //化简分数
    private void simplify() {
        int gcd = gcd(Math.abs(numerator), Math.abs(denominator)); // 计算最大公约数

        // 更新整数部分与分子、分母
        this.num = this.numerator/this.denominator;
        this.numerator = this.numerator % this.denominator;
        this.numerator /= gcd;
        this.denominator /= gcd;

        // 确保分母为正
        if (denominator < 0) {
            numerator = -numerator;
            denominator = -denominator;
        }
    }

    //求最大公约数
    private int gcd(int a, int b) {
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return Math.abs(a); // 返回绝对值以确保结果为正
    }

    @Override
    public String toString() {
        if (num == 0) {
            if (numerator == 0) {
                return "0"; // 处理0的情况
            } else {
                return numerator + "/" + denominator; // 只返回分数
            }
        } else {
            if (numerator == 0) {
                return String.valueOf(num); // 只返回整数部分
            }
            return num + "'" + numerator + "/" + denominator; // 返回带分数
        }
    }
}

六、测试运行

测试代码

import com.softwareClass.SplashClass;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class SplashClassTest {
    @Test
    public void testGenerateExercises(){
        try {
            SplashClass.main(new String[]{"Myapp.exe","-n","10","-r","10"});
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testCheckAnswer(){
        try {
            SplashClass.main(new String[]{"Myapp.exe","-e","Exercises.txt","-a","Answer.txt"});
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

testGenerateExercises()测试结果

在当前目录下生成了两个文件Exercises.txt和Answer.txt


使用标准答案测试testCheckAnswer()

在生成的Grade.txt文档里全部都是正确的,经过数学验算也可得知答案没有出错
修改Answer.txt:答案(1,3,5,7)分别加1

使用修改后的答案测试testCheckAnswer()
(为了更好地对比,不加清除文本原来内容的处理逻辑)

生成一万条题目
修改测试代码

检查生成结果

七、效能分析

Jprofile 数据




Idea内置的IntelliJ Profile数据

可以看到耗时最大的函数为文件输入输出的函数FileIO


一次一次地写入文件是一件很耗时的操作,所以我们改进了一下写入操作:


private static void generateExercises(Integer num,Integer area) throws IOException {

        int index=0;
        try {

            String EXERCISES_FILE = "Exercises.txt";
            String ANSWERS_FILE = "Answer.txt";
            
            // 使用 StringBuilder 进行批量写入
            StringBuilder exerciseContent = new StringBuilder();
            StringBuilder answerContent = new StringBuilder();

            for (Expression e : new Generator().generateExpressions(num, area)) {
                index++;
                exerciseContent.append((index + 1))
                  .append(". ")
                  .append(e.getExpression())
                  .append(System.lineSeparator());
                answerContent.append((index + 1))
                  .append(". ")
                  .append(e.evaluate())
                  .append(System.lineSeparator());
            }

            // 一次性写入文件
            FileIO.writeOutput(EXERCISES_FILE, exerciseContent.toString());
            FileIO.writeOutput(ANSWERS_FILE, answerContent.toString());

        } catch (IOException exception) {
            throw new IOException(exception.getMessage());
        }
    }

八、项目小结

陈祖民:合作最重要的一点是沟通,在合作做项目时,我们需要合理的分配两个人需要完成的部分,偶尔双方会因为想法不同导致项目停工,需要不停更改项目,但从最终结果来看,我们还算是圆满的完成任务。总之,合作完成项目于我而言是一次非常好的经历,它让我明白项目永远不能只靠一个人的埋头苦干,还是需要大家一起集思广益,才能写出更大更好的项目。
陈大锴:本次程序的开发总体上比较成功,在完成了基本需求之后也完成了附加需求。在本次开发过程中,本人不仅收获了分模块开发的技巧,还收获了与他人合作互相交流想法的经验,虽然在某些区域也有些分歧,但是经过一番讨论后也达成了意见统一。我的搭档不仅提供了算法的思路,还协助进行了单元测试,找出了开发过程中的漏洞,希望下一次开发能继续合作。

posted @ 2024-09-27 19:45  CDucK  阅读(23)  评论(0编辑  收藏  举报