结对项目——实现一个自动生成小学四则运算题目的命令行程序—陈泽瀚and林桂旭

发布时间 2023-09-28 17:46:31作者: Chen泽瀚

软工作业3:结对项目:实现一个自动生成小学四则运算题目的命令行程序

作业属于课程 课程首页 - 计科21级1班 - 广东工业大学 - 班级博客 - 博客园
作业要求 个人项目-作业2- 计科21级1班 - 广东工业大学 - 班级博客 - 博客园
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序

gitee代码仓库

成员信息

姓名 学号
陈泽瀚 3121004818
林桂旭 3121004830

PSP表格

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

开发工具及依赖

  • 编程语言:java

  • IDE:Intellij IDEA 2023.1

  • 项目构建工具:maven

  • 单元测试:JUnit 4.12

  • 性能分析工具:JProfiler 14

  • 依赖jar包: junit4.12 & commons-io

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>

设计流程图及接口设计

一,流程图:

生成题目流程图:

判断题目答案流程图:


二,接口设计:

main文件夹:

text文件:

程序需要的文档:

程序实现

一,接口设计:

gitee代码

  • Main:主类,程序入口

  • FileIOUtil:读取文件和输出文件工具类

  • DecimalToFranctionUtil:将小数转化为真分数和带分数的工具类

  • CountUtil:生成题目和计算题目答案的工具类

1.1 FileIoUtil工具类

包含的方法:

核心:调用commons-io提供的方法

//    题目文件:将生成的题目存入文件中
    public static void topicToFile(ArrayList<String> topics) throws IOException {
        String exercises = "src/txt/Exercises.txt";
        //指定文件名
        //将题目存入集合中,集合中存放算数表达式的字符串
        FileUtils.writeLines(new File(exercises), topics);
    }

    //存放生成题目的答案
    public static void answerToFile(ArrayList<String> answers) throws IOException {
        final String answer = "src/txt/Answers.txt";
        FileUtils.writeLines(new File(answer), answers);
    }

    //存放结果文件
    public static void gradeToFile(ArrayList<String> list) throws IOException {
        final String grade = "src/txt/Grade.txt";
        FileUtils.writeLines(new File(grade), list);
    }

    //读取文件
    public static ArrayList<String> readFile(String fileName) throws IOException {
        //将内容存放在集合中,按行读取
        ArrayList<String> list = (ArrayList<String>) FileUtils.readLines(new File(fileName), "UTF-8");
        return list;
    }

1.2 CountUtil工具类(核心)

后缀表达式算法参考文献: 逆波兰表达式

包含的方法:

1.createTopic方法:生成一个四则运算(+,-,*,/,( ))

public static String createTopic(int r) {
        StringJoiner sj;
        while (true) {
            sj = new StringJoiner(" ");
//        存放数据
            ArrayList<String> number = new ArrayList<>();
            //生成的题目为字符串
            Random random = new Random();
            //获取随机个真分数(0,4)
            int franction = random.nextInt(0, 2);
            for (int i = 0; i < franction; i++) {
                int a = random.nextInt(1, r);
                int b = random.nextInt(1, r);
                String str;
                if (a > b) {
                    str = b + "/" + a;
                } else if (a < b) {
                    str = a + "/" + b;
                } else {
                    str = "1";
                }
                number.add(str);
            }
//        获取随机整数
            for (int i = 0; i < 4 - franction; i++) {
                number.add(String.valueOf(random.nextInt(0, r)));
            }
//            随机是否加()1:加 0:不加
            int flag = random.nextInt(0, 2);
            int end = -1;
            if (flag == 1) {
                //获取在第几位数字加()
                flag = random.nextInt(0, 3);
                end = flag + 1;
            } else {
                flag = -1;
            }
//       获取随机运算符并拼接
            Collections.shuffle(number);
            for (int i = 0; i < number.size(); i++) {
                if (flag == i) {
                    sj.add("(");
                }
                sj.add(number.get(i));
                if (end == i) {
                    sj.add(")");
                }
                if (i != number.size() - 1) {
                    sj.add(symbol[random.nextInt(0, 4)]);
                }
            }
            //判断题目是否符合要求
            if (checkTopic(sj.toString())) {
                break;
            }

        }
        return sj.toString();
    }

2.checkTopic方法:

  • 检查题目是否符合需求(真分数、结果和过程不产生负数、过程不产生假分数、0不作为除数)
public static boolean checkTopic(String topic) {
        //判断除数是否为0
        String[] split = topic.split("÷");
        for (int i = 1; i < split.length; i++) {
            if (String.valueOf(split[i].charAt(1)).equals("0")) {
                //System.out.println("丢弃");
                return false;
            }
        }
        String answer = getAnswer(topic);
        if ("-".equals(answer.charAt(0) + "") || answer.length() > 5)
            return false;
        return true;
    }

3.toPoLand方法:

  • 将中缀表达式转化为后缀表达式(利于答案的计算
  • 优点:转换后不用考虑运算符的优先次序,从左到右依次计算即可
public static String toPoLand(String topic) {
        ArrayList<String> nums = new ArrayList<>();
        LinkedList<String> symbols = new LinkedList<>();
        String[] split = topic.split(" ");
        String regex = "^\\d+(/\\d+)?$";
        Pattern p = Pattern.compile(regex);
        for (int i = 0; i < split.length; i++) {
            if (split[i].matches(regex)) {
                nums.add(split[i]);
            } else {
                if (symbols.size() == 0 || "(".equals(split[i])) {
                    symbols.addFirst(split[i]);
                } else if (")".equals(split[i])) {
                    String s = symbols.removeFirst();
                    nums.add(s);
                    symbols.removeFirst();
                } else {
                    String com = symbols.getFirst();
                    if ("(".equals(com))
                        symbols.addFirst(split[i]);
                    else if ("+".equals(split[i]) || "-".equals(split[i])) {
                        while (symbols.size() != 0) {
                            com = symbols.removeFirst();
                            nums.add(com);
                        }
                        symbols.addFirst(split[i]);
                    } else {
                        if ("*".equals(com) || "÷".equals(com)) {
                            com = symbols.removeFirst();
                            nums.add(com);
                        }
                        symbols.addFirst(split[i]);
                    }
                }
            }
        }
        while (symbols.size() != 0) {
            String com = symbols.removeFirst();
            nums.add(com);
        }
        StringJoiner sj = new StringJoiner(" ");
        for (String num : nums) {
            sj.add(num);
        }
        return sj.toString();
    }

4.getAnswer方法:计算后缀表达式的答案

public static String getAnswer(String topic) {
        double result = 0;
        //调用转化方法
        topic = toPoLand(topic);
        LinkedList<String> sum = new LinkedList<>();
        String[] split = topic.split(" ");
        String fractionPattern = "^\\d+/\\d+$";
        for (int i = 0; i < split.length; i++) {
            if (split[i].equals("+") || split[i].equals("-") || split[i].equals("*") || split[i].equals("÷")) {
                String str1 = sum.removeFirst();
                String str2 = sum.removeFirst();
                double num1, num2;
                if (Pattern.matches(fractionPattern, str1)) {
                    String[] split1 = str1.split("/");
                    num1 = Double.parseDouble(split1[0]) / Double.parseDouble(split1[1]);
                } else {
                    num1 = Double.parseDouble(str1);
                }
                if (Pattern.matches(fractionPattern, str2)) {
                    String[] split1 = str2.split("/");
                    num2 = Double.parseDouble(split1[0]) / Double.parseDouble(split1[1]);
                } else {
                    num2 = Double.parseDouble(str2);
                }

                switch (split[i]) {
                    case "+": {
                        sum.addFirst(String.valueOf(num1 + num2));
                        break;
                    }
                    case "-": {
                        sum.addFirst(String.valueOf(num2 - num1));
                        break;
                    }
                    case "*": {
                        sum.addFirst(String.valueOf(num2 * num1));
                        break;
                    }
                    case "÷": {
                        sum.addFirst(String.valueOf(num2 / num1));
                        break;
                    }
                }
            } else {
                sum.addFirst(split[i]);
            }
        }
        result = Double.parseDouble(sum.removeFirst());
        //小数转分数
        String str = DecimalToFranctionUtil.convertDecimalToFraction(result);
        return str;
    }

5.allTopic方法:多次调用createTopic和getAnswer方法

public static void allTopics(int n, int r) throws IOException {
        ArrayList<String> topics = new ArrayList<>();
//        for (int i = 0; i < n; i++) {
//
//            topics.add(createTopic(r));
//        }
        //计算每道题目的答案并存入list
        ArrayList<String> answers = new ArrayList<>();
        //调用计算答案的方法
        for (int i = 0; i < n; i++) {
            String topic = createTopic(r);
            answers.add((i + 1) + ". " + getAnswer(topic));
            topics.add((i + 1) + ". e = " + topic);
        }
        //将题目存入文件Exercises.txt
        FileIOUtil.topicToFile(topics);
        //将答案写入文件answers.txt
        FileIOUtil.answerToFile(answers);
    }

6.checkTopicAndAnswer方法:检查题目对应的答案是否正确

    /*
    判断文件的题目答案是否正确,并存入Grade.txt
    Correct: 5 (1, 3, 5, 7, 9)
    Wrong: 5 (2, 4, 6, 8, 10)
     */
    public static void checkTopicAndAnswer(String exercisefile, String answerfile) throws IOException {
        int correct = 0, wrong = 0;
        ArrayList<Integer> cor = new ArrayList<>();
        ArrayList<Integer> wro = new ArrayList<>();
        //读取文件
        ArrayList<String> topics = FileIOUtil.readFile(exercisefile);
        ArrayList<String> answers = FileIOUtil.readFile(answerfile);
        for (int i = 0; i < topics.size(); i++) {
            if (getAnswer(topics.get(i).split("= ")[1]).equals(answers.get(i).split(". ")[1])) {
                correct++;
                cor.add(i + 1);
            } else {
                wrong++;
                wro.add(i + 1);
            }
            ArrayList<String> list = new ArrayList<>();
            list.add("Correct: " + correct + cor.toString());
            list.add("Wrong: " + wrong + wro.toString());
            FileIOUtil.gradeToFile(list);
        }
    }

1.2 DecimalToFranctionUtil工具类(小数转换为真分数的类)

包含的方法:

1.Fraction类:存放分母和分子

class Fraction {
    private BigInteger numerator;
    private BigInteger denominator;

    public Fraction(BigInteger numerator, BigInteger denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public BigInteger getNumerator() {
        return numerator;
    }

    public BigInteger getDenominator() {
        return denominator;
    }
}

2.convertDecimalToFraction方法:

public static String convertDecimalToFraction(double decimal) {
        try {
            // 将输入的double值转换为字符串
            String str = String.valueOf(decimal);

            // 使用BigDecimal来处理浮点数精度问题
            BigDecimal bd = new BigDecimal(str);

            // 获取小数的位数
            int scale = bd.scale();

            // 计算分母,比如0.75的分母为100,0.125的分母为1000
            BigDecimal denominator = BigDecimal.TEN.pow(scale);

            // 将小数转化为分数
            BigDecimal numerator = bd.multiply(denominator);

            // 使用BigInteger来处理分子和分母
            BigInteger numeratorInt = numerator.toBigInteger();
            BigInteger denominatorInt = denominator.toBigInteger();

            // 使用最大公约数来简化分数
            BigInteger gcd = numeratorInt.gcd(denominatorInt);
            numeratorInt = numeratorInt.divide(gcd);
            denominatorInt = denominatorInt.divide(gcd);

            Fraction fraction = new Fraction(numeratorInt, denominatorInt);
            str = fraction.getNumerator() + "/" + fraction.getDenominator();
            if ("0".equals(String.valueOf(fraction.getNumerator())))
                return "0";
            if (fraction.getNumerator().equals(fraction.getDenominator()))
                return "1";
            if ("1".equals(String.valueOf(fraction.getDenominator())))
                return String.valueOf(fraction.getNumerator());

            // 假分数转带分数
            long num1 = Long.parseLong(String.valueOf(fraction.getNumerator()));
            long num2 = Long.parseLong(String.valueOf(fraction.getDenominator()));
            long num;
            if (num1 > num2) {
                num = (num1 - (num1 % num2)) / num2;
                str = num + "'" + (num1 % num2) + "/" + num2;
            }

            return str;
        } catch (NumberFormatException e) {
            // 处理输入格式错误的情况
            return "Error: Invalid input. Please enter a valid number.";
        } catch (ArithmeticException e) {
            // 处理除以零等算术异常
            return "Error: Division by zero or other arithmetic error";
        } catch (Exception e) {
            // 处理其他异常
            return "Error: An unexpected error occurred";
        }
    }

程序运行结果

生成题目和答案:

文件:

判断题目和答案:

文件:


Grade.txt:

性能分析

遥测

实时内存-所有对象

CPU调用树

单元测试

FilesIOUtilTest

CountUtilTest

DecimalToFrantionUtilTest

测试代码覆盖率

分工合作

陈泽瀚:负责运行代码的编写和博客园编写

林桂旭:负责测试代码编写和代码提交以及性能测试

项目小结

  • 结对项目中需要两人及时沟通,完成不同的分工任务。
  • 写完运行代码,另一个同伴写测试代码可以发现一些自己发现不到的问题。
  • 当工作中遇到困难时,要及时寻求解决方案,可以向其他成员寻求帮助,这样有助于提升效率。