结对项目

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230
这个作业的目标 <结对完成实现一个自动生成小学四则运算题目的命令行程序>
项目成员 李响 3121002802
项目成员 欧可贵3121002717

一、Github链接

https://github.com/Placidoe/homework.git

二、PSP表格

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

三、需求

使用 -n 参数控制生成题目的个数,例如

Myapp.exe -n 10

将生成10个题目。

使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如

Myapp.exe -r 10

将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。

生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
每道题目中出现的运算符个数不超过3个。
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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
四则运算题目2
……

其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。

在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:

答案1
答案2

特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。

程序应能支持一万道题目的生成。
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:

Myapp.exe -e .txt -a .txt

统计结果输出到文件Grade.txt,格式如下:

Correct: 5 (1, 3, 5, 7, 9)

Wrong: 5 (2, 4, 6, 8, 10)

其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。

四、模块接口的设计与实现过程

两个类(DataStore、IOUtils)

DataStore 类是一个单例类,使用了双重检查锁定的方式来保证线程安全,并且提供了多种方法来生成和计算数学表达式。

public void insertOp():
随机选择一个操作符(+、-、*、/)并将其存入 tokens 数组。

public void insertNum():
随机生成一个操作数(范围为 0 到 num2)并将其转换为字符串形式存入 tokens 数组。

public void InitData(String[] args):
根据传入的参数初始化 op1、num1、op2 和 num2。如果参数数量不符合预期,则根据默认值进行初始化。

public void InitTokens():
初始化 tokens 数组并生成随机的操作数和操作符。

public int evalRPN(String[] tokens):
接收一个字符串数组(反向波兰表示法),使用栈计算结果。对于每个操作符,弹出相应的操作数进行计算,并将结果压回栈中。使用 StringBuilder 构建表达式的字符串。

public boolean isNumber(String token):
判断传入的字符串是否为数字。

public void Run():
运行生成表达式的主方法,循环指定次数生成数学表达式、计算结果,并校验表达式的唯一性。

public int caculate(String op,int num1,int num2):
根据传入的操作符和两个操作数计算并返回结果,处理基本的加、减、乘、除运算。

static public DataStore getInstance():
单例模式实现,确保在多线程环境中只创建一个 DataStore 实例。

IOUtils类

public static void saveExercises(String exercises):
参数:接收一个字符串参数 exercises,表示要保存的题目内容。
文件写入:
使用 BufferedWriter 包裹 FileWriter,使得写入操作更高效。
new FileWriter("Exercises.txt", true):以追加模式打开文件,如果文件不存在则会创建它。
writer.write(exercises):将题目写入文件。
writer.newLine():在写入的内容后添加换行符。
异常处理:
使用 try-with-resources 语句确保 BufferedWriter 在使用后自动关闭。
捕获 IOException 异常,并通过 e.printStackTrace() 打印异常信息。

public static void saveAnswers(String answers):
参数:接收一个字符串参数 answers,表示要保存的答案内容。
文件写入:采用相同的方式打开 Answers.txt 文件并写入内容。
异常处理:同样使用 try-with-resources 和 IOException 捕获,确保代码的安全性。

五、代码说明

DataStore.java文件:
整个 DataStore 类的设计思路是:

表达式生成: 随机生成一个数学表达式,确保操作数和操作符的随机性。
逆波兰表达式计算: 实现了一个计算逆波兰表达式的算法,能有效地处理各种操作符。
唯一性校验: 使用集合来确保生成的数学表达式是唯一的,避免重复。
这段代码的实现思路在于结合数学运算与数据结构(栈)来高效地生成和计算表达式,并通过策略来确保生成表达式的有效性和唯一性。

源代码:

点击查看代码
package com.lx.single;

import com.lx.utils.FractionGenerator;
import com.lx.utils.IOUtils;
import lombok.Data;

import java.util.*;

/**
 * TODO
 *
 * @Description
 * @Author Lx
 * @Date 2024/9/26 上午10:46
 **/
@Data
public class DataStore {
    private static volatile DataStore dataStore;

    /*
      策略1:
      1.从栈顶弹出a1-操作数
      2.
      从栈顶弹出a2-操作符(如果是+或-)则继续下一步单暂不运算,而是判断后面一个是否为(*或/),依次循环往后看,直到遇到(+或-)。
      如果是(*或/)继续下一步就直接进行运算
      3.从栈顶弹出a3-操作数
      4.从栈顶弹出a4-操作符
      ...
      ...
      ...

      策略2:补偿保证e1>=e2
      当弹出来操作数a1和操作符a2,判断a3,是否a1<a3,如果 a1<a3,则要把a1和a2弹出,重新,入栈一个新的操作数和操作符。直到满足了解,否则继续弹出。


      策略3:式子不能重复
      1.可以通过将式子字符串进行hash运算,然后存到set中,可能会存在误判,但结果肯定可以保证都是唯一的
     **/

    static String op1;
    static int num1;//生成的题目个数
    static String op2;
    static int num2;//生成的值的范围[0~num2)
    static Random random;
    static Stack<Object> st;//用来存放操作数和操作符。自栈顶向下,就是自表达式左向右
    static StringBuilder stringBuilder;
    static HashSet set;
    static StringBuilder HashString;
    static int index;
    static String[] tokens;

    String[] ops={"+","-","*","/"};
    static {
        num1=3;
        num2=2;
        random = new Random();
        st=new Stack<>();
        set=new HashSet();
        stringBuilder=new StringBuilder();
        tokens=new String[1000];
        HashString=new StringBuilder();
    }
    public void insertOp(){//入操作符
        int val = random.nextInt(4);
//        stringBuilder.append(ops[val]);
        tokens[index++]=ops[val];
    }

    public void insertNum(){//入操作数
//        stringBuilder.append(random.nextInt(num2+1));
        tokens[index++]= String.valueOf(FractionGenerator.generateTrueFraction());
    }
    public void InitData(String[] args){

        if(args.length==4){
            op1=args[0];
            num1=Integer.parseInt(args[1]);
            op2=args[2];
            num2=Integer.parseInt(args[3]);
        }else if(args.length==2){
            op1=args[0];
            num1=Integer.parseInt(args[1]);
        }

    }
    public void InitTokens(){
        tokens=new String[1000];
        index=0;
        //1.生成运算符的个数
        int val=random.nextInt(num2+1);
        int count = val==0?1:val;//左闭右开[0~num2)

        //2.入栈操作数和操作符
        insertNum();
        for(int i=0;i<count;i++){
            insertNum();// 1 2 + 3 / 4 - 6
            insertOp();
        }
    }

    public Double evalRPN(String[] tokens) {
        Deque<Double> stack = new LinkedList<Double>();
        Deque<String> express = new LinkedList<String>();

        express.push(tokens[0]);
        int n = index;
        for (int i = 0; i < n; i++) {
            String token = tokens[i];
            if (isNumber(token)) {
                stack.push(Double.parseDouble(token));
            } else {
                Double num2 = stack.pop();
                Double num1 = stack.pop();
//
                StringBuilder HashString1 = new StringBuilder();
                switch (token) {
                    case "+":
                        String str1 = express.pop();
                        HashString1.append("(");
                        HashString1.append(str1);
                        HashString1.append("+");
                        HashString1.append(num2);
                        HashString1.append(")");
                        express.push(HashString1.toString());

                        stack.push(num1 + num2);
                        break;
                    case "-":
                        String str2 = express.pop();
                        HashString1.append("(");
                        HashString1.append(str2);
                        HashString1.append("-");
                        HashString1.append(num2);
                        HashString1.append(")");
                        express.push(HashString1.toString());

                        stack.push(num1 - num2);
                        break;
                    case "*":
                        String str3 = express.pop();
                        HashString1.append("(");
                        HashString1.append(str3);
                        HashString1.append("*");
                        HashString1.append(num2);
                        HashString1.append(")");
                        express.push(HashString1.toString());
                        stack.push(num1 * num2);
                        break;
                    case "/":
                        if(num2==0)num2++;
                        String str4 = express.pop();
                        HashString1.append("(");
                        HashString1.append(str4);
                        HashString1.append("/");
                        HashString1.append(num2);
                        HashString1.append(")");
                        express.push(HashString1.toString());

                        stack.push(num1 / num2);
                        break;
                    default:
                }
            }
        }
        HashString=new StringBuilder(express.pop());
        return stack.pop();
    }

    public boolean isNumber(String token) {
        return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
    }


    public void Run(){

        //运行指定的次数
        for(int j=0;j<num1;j++){
            //1.初始化tokens
            InitTokens();
            for(int i=0;i<index;i++){
                System.out.println(tokens[i]);
            }
            //2.运算(运算+补偿+拼接表达式)
            Double res=evalRPN(tokens);
            IOUtils.saveExercises(HashString.toString());
            HashString.append("=");
            HashString.append(res);
            IOUtils.saveAnswers(HashString.toString());
            //3.校验表达式是否唯一,不唯一则要重试
            if(set.contains(HashString)){
                j--;
                HashString=new StringBuilder();
                continue;
            }
            set.add(HashString);
            //4.完成表达式
            System.out.println(HashString.toString());
            HashString=new StringBuilder();
        }
    }

    public int caculate(String op,int num1,int num2){
        switch (op){
            case "+":
                return num1+num2;
            case "-":
                if(num1<num2){

                    return num2-num1;
                }
                return num1-num2;
            case "*":
                return num1*num2;
            case "/":
                return num1/num2;
            default:
                return 0;
        }
    }

    static public DataStore getInstance(){
        if(dataStore==null){
            synchronized (DataStore.class){
                if(dataStore==null)
                    dataStore=new DataStore();
            }
        }
        return dataStore;
    }
}

IOUtils.java文件:
IOUtils 类提供了两个静态方法,saveExercises 和 saveAnswers,用于将练习题和答案分别写入不同的文本文件。这种设计允许用户在不创建 IOUtils 类实例的情况下,直接调用这些方法进行文件操作。异常处理确保了在写入文件过程中,如果发生任何输入输出错误,可以得到相关的错误信息以供调试。

源代码:

点击查看代码
package com.lx.utils;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

/**
 * IOUtils类用于处理输入和输出操作
 * @Author Okg
 * @Date 2024/9/28 下午8:01
 **/
public class IOUtils {

    // 存储题目到文件
    public static void saveExercises(String exercises) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("Exercises.txt", true))) {
            // 将题目写入文件
            writer.write(exercises);
            writer.newLine(); // 换行
        } catch (IOException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }

    // 存储答案到文件
    public static void saveAnswers(String answers) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("Answers.txt", true))) {
            // 将答案写入文件
            writer.write(answers);
            writer.newLine(); // 换行
        } catch (IOException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }
}

六、运行测试

测试结果:
Exercises.txt文件:

Answers.txt文件:

七、项目总结

  • 欧可贵:这是我第一次接触合作开发一个项目,所以有很多合作的部分会比较生疏,本地代码上传不了GitHub、GitHub上的代码拉不下来、java环境配置等等都是需要解决的问题,由于以前使用python较多,所以主要负责帮忙解决I/O功能,以及计算功能中的注释添加,计算功能由李响同学进行开发完成。当然这次结对项目让我认识到了团队协作的重要性,让我们更能清楚团队配合之间主要问题会存在哪里并解决它。
  • 李响:本次我主要负责主要功能的代码开发,看到题目我们经过讨论决定先随机生成一个数学表达式,然后使用逆波兰表达式处理各种操作符号,由于代码经验较丰富,所以我负责主要的代码开发。但是由于开发时间过长,破坏了我们原有的打算,效能分析没有能够完成(如果要完成的话可能会导致不能在规定时间内提交作业),我会吸取经验,争取下次做更加合理的时间规划,完成每一个任务要求。
posted @ 2024-09-28 21:39  去码头整点薯条778  阅读(49)  评论(0编辑  收藏  举报