结对项目

结对作业

项目 内容
这个作业属于哪个课程 < 软件工程 >
这个作业要求在哪里 < 作业要求 >
这个作业的目标 < 学会与搭档分工合作,完成一个项目 >

一、前言

github地址:点击进入

姓名 学号
刘晓霖 3118005374
任浩然 3118005381

二、PSP表格

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

三、项目设计

1.类的介绍

(1)运算数Number类

  由于生成的题目中,分数要以“a/b”的形式表示,同时为了方便运算,我们决定自定义一个Number来表示数字。
  该类中拥有分子和分母两个属性,并利用辗转相除法,在toString()方法中把数字化为最简的真分数形式,以方便
  形成题目的字符串。

public class Number {
    //分子
    private Integer numerator;
    //分母
    private Integer denominator;

      //getter & setter
      ...

    public String toString() {
        String s = null;

        Integer gcdNum = gcd(numerator,denominator);
        numerator = numerator / gcdNum;
        denominator = denominator / gcdNum;

        if (denominator == 1) {
            s = numerator.toString();
        } else if (numerator < denominator) {
            s = numerator + "/" + denominator;
        } else if (numerator > denominator) {
            Integer i = numerator / denominator;
            Integer m = numerator % denominator;
            if(m != 0){
                s = i + "'" + m + "/" + denominator;
            }else {
                s = i.toString();
            }

        }else if(numerator == denominator){
            s = "1";
        }

        return s;
    }

(以上为Number类部分代码)

(2)符号Symbol类

  为了实现在运算过程中,有括号的情况下按优先级进行运算,需要为四个运算符号设置优先级。
  同时为了实现后面的检测重复题目的实现,还需要为每个符号设置一个序号,以方便排序。

public abstract class Symbol {

    //符号优先级
    public int priotiry;
    //符号名称
    public String name;
    //符号序号
    public Float no;

    //getter & setter
    ...
}

  Symbol是一个抽象类,四个运算符(AddSyml,SubSyml,MulSyml,DivSyml)都继承于它。

(3)生成问题与计算CreateProblems类

 该类是项目的核心,题目生成、运算和排除重复功能都在此类。稍后详细介绍里面各个方法。

2.实现步骤

(1)实现思路

   生成3条 List,分别为用来存储运算数的 List numList,存储运算符的 List opList,存储括号里包含的运算符的位置序号 bracList。
  每次运算时,先在 bracList 中取出所有值,然后再在opList中找到以这些值为序号的符号,从中选出优先级最高的一个,再从
  numList中把该符号的左右两数取出,进行运算。如果 bracList 为空,则从 opList 第一个值开始,遍历出优先级最大的符号(优先级
  一样则选最先出现的),进行运算。运算完之后,还要对修改3条 List 的数据,然后重复以上步骤直至 opList 为空。
例如

  (1)从bracList得到括号包含的运算符位置为0,1.所以从opList中序号0到1范围找出优先级最大的一个,这里是"X",序号是1。再从numList中找到序号为1和1+1(即2)的数,进行一次运算。

  (2)经过一次运算后,把当前运算符的序号记为 pos,再把opList和bracList表中序号为 pos 的元素去除。把计算的新值代替 numList 中第 pos 个元素,删除 pos + 1 的元素,得到上图的表。

  (3)重复以上步骤,可得到以上表。此时 bracList 已为空,所以 opList 从第一个元素开始找优先级最高的一个,继续重复以上的运算步骤。得到最后结果 11'1/2 .

(2)问题生成

 1.首先有一个 randomNum(int range) 的方法,该方法以 range 为范围,会生成一个随机数。
 2.在生成问题的 createProblem()方法中,首先利用以上函数,生成运算数的个数 num,然后运算符个数opNum = num - 1。括号内包含运算符的个数 bracNum 则由 randomNum(opNum) 得到。
 3.根据上一步骤得到的各个数,随机生成相对数量的数字、运算符、括号位置。
 4.由以上步骤,可得到3个List。再利用 StringBuilder 进行拼接,即可得到问题的字符串。

public String createProblem(Integer range) throws Exception {
           ...

 //随机生成参与运算的数
        for (int i = 0; i < num; i++) {

            Integer numerator = randomNum(range - 1) + 1;
            Integer denominator = randomNum(range - 1) + 1;
            Number number = new Number();
            number.setNumerator(numerator);
            number.setDenominator(denominator);
            numList.add(number);
            numList2.add(number);

        }
        //随机生成运算符
        for (int i = 0; i < opNum; i++) {
            Integer j = randomNum(4);
            Symbol symbol = null;
            switch (j) {
                case 0:
                    symbol = new AddSyml();
                    break;
                case 1:
                    symbol = new SubSyml();
                    break;
                case 2:
                    symbol = new MulSyml();
                    break;
                case 3:
                    symbol = new DivSyml();
                    break;
            }
            opList.add(symbol);
            opList2.add(symbol);
        }

 //生成问题字符串
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < num; i++) {
            //为方便拼接字符串,把所有的数字都替换为r
            if(i == num - 1){
                sb.append(" " + "r ");
            }else {
                sb.append(" " + "r");
            }
            try {
                sb.append(" " + opList.get(i).getName());
            } catch (Exception e) {

            }
        }

        //插入括号
        try{
            Integer bracFrom = bracList.get(0);
            Integer bracTo = null;
            if (bracNum == 2) {
                bracTo = bracList.get(1);
            }
            if (bracNum == 1) {
                sb.setCharAt((bracFrom * 4) + 3 - 3, '(');
                sb.setCharAt((bracFrom * 4) + 3 + 3, ')');
            } else if (bracNum == 2) {
                sb.setCharAt((bracFrom * 4) + 3 - 3, '(');
                sb.setCharAt((bracTo * 4) + 3 + 3, ')');
            }
        }catch (Exception e){

        }

        //从字符串的“r”分裂字符串成多个子串,再与数字一起拼接成完整的字符串
        String str = sb.toString();
        String[] rs = str.split("r");
        StringBuilder builder = new StringBuilder();
        for(int i = 0;i < rs.length;i++){
            if( i == rs.length - 1){
                builder.append(rs[i]);
            }else {
                builder.append(rs[i]).append(numList.get(i));
            }

        }
        builder.append("= ");

        //格式化字符串
        String result = builder.toString();
        result = result.replace("("," ( ");
        result = result.replace(")"," ) ").trim();


        return result;

           ...
}

(3)运算

 在一个问题生成之后,可利用那3条 List 表,按照上面的思路,即可实现运算。值得一提的是,由于不允许计算中出现负数,因此在每一次子运算中,如果结果是负数,就跳出该次循环。

while (opList.size() != 0) {
            sum = new Number();

            //括号起始位置
            Integer from = null;
            //括号结束位置
            Integer to = null;
            try {
                from = bracList.get(0);
                to = bracList.get(bracList.size() - 1);
            } catch (Exception e) {

            }
            //第一个运算数
            Number num1 = null;
            //第二个运算数
            Number num2 = null;

            //符号起始位置
            Integer opFrom;
            //符号结束位置
            Integer opTo;
            if (from != null) {
                //如果存在括号,那么符号要在括号内寻找优先级最高的
                opFrom = from;
                opTo = to;
            } else {
                //如果不存在括号,从头开始找优先级最高的符号
                opFrom = 0;
                opTo = opList.size() - 1;
            }

            //最大的优先级,默认括号内第一个符号
            Integer maxPriority = null;
            try{
                maxPriority = opList.get(opFrom).getPriotiry();
            }catch (Exception e){
                return null;
            }
            //优先级最大的符号的位置,默认括号内第一个符号
            Integer maxPos = opFrom;
            //目前运算符的优先级
            Integer priority = null;
            //目前运算符的位置
            Integer pos = null;
            //寻找当前运算中,优先级最高的符号以及其位置。
            for (int i = opFrom; i <= opTo; i++) {
                try{
                    priority = opList.get(i).getPriotiry();
                }catch (Exception e){
                    return null;
                }

                pos = i;
                if (priority > maxPriority) {
                    maxPriority = priority;
                    maxPos = pos;
                }
            }

            //分子
            Integer numerator = null;
            //分母
            Integer denominator = null;
            //把运算符左右两边的数取出来
            num1 = numList.get(maxPos);
            num2 = numList.get(maxPos + 1);
            String symbol = opList.get(maxPos + 0).getName();
            //计算过程
            if (symbol.equals("+")) {
                denominator = num1.getDenominator() * num2.getDenominator();
                Integer num1Numerator = num1.getNumerator() * num2.getDenominator();
                Integer num2Numerator = num2.getNumerator() * num1.getDenominator();
                numerator = num1Numerator + num2Numerator;
            } else if (symbol.equals("−")) {
                denominator = num1.getDenominator() * num2.getDenominator();
                Integer num1Numerator = num1.getNumerator() * num2.getDenominator();
                Integer num2Numerator = num2.getNumerator() * num1.getDenominator();
                numerator = num1Numerator - num2Numerator;
            } else if (symbol.equals("×")) {
                numerator = num1.getNumerator() * num2.getNumerator();
                denominator = num1.getDenominator() * num2.getDenominator();
            } else if (symbol.equals("÷")) {
                numerator = num1.getNumerator() * num2.getDenominator();
                denominator = num1.getDenominator() * num2.getNumerator();
            }
            sum.setNumerator(numerator);
            sum.setDenominator(denominator);

            if(symbol.equals("÷") && numerator <= 0 || denominator == 0){
                //除数不能为0
                return null;
            }

            if(numerator < 0){
                //计算过程出现负数,不符合要求
                return null;
            }

            if(opList.size() == 1){
                //获取计算的最后一步,以方便验证是否题目重复
                if(num1.getValue() > num2.getValue() && (symbol.equals("+")||symbol.equals("×"))){
                    resultStorage = num2.toString() + symbol + num1.toString();
                }else {
                    resultStorage = num1.toString() + symbol + num2.toString();
                }

            }

            //一次运算之后,修改各List数据
            numList.set(maxPos, sum);
            try {
                numList.remove(maxPos + 1);
                opList.remove(maxPos + 0);
                bracList.remove(maxPos);
            } catch (Exception e) {

            }

        }

(4) 检测重复

 首先,在每生成一道题目的时候,都会生成3条 List 来存储问题的信息。在这里我们再用2条 List(numList2 和 opList2)来复制 numList 和 opList ,来保存一个问题的原始数据(因为 numList 和 opList 会在运算过程中发生改变)。同时我们还需要存储运算的最后一步(即opList只剩一个值时),比如上面的例子中,要存储 “23 ÷ 2” 到叫 resultStorage 的字符串中。值得一提的是,如果最后一步是加法或乘法,需要对两位数字进行排序,因为 a+b 、axb 等价于 b+a、bxa,但 a-b 、a÷b不等价于 b-a b÷a。


 我们知道,如果两道题目是重复的,那么它们的 numList2 、 opList2 、 resultStorage 是一样的。但 List 里面值的顺序不一样是难以相比较的,因此我们利用两条 List 的值或序号(前面定义Symbol时提到过),对它们进行排序。最后把两条 List 和 resultStorage 的值拼接成一个字符串 problemInfo(比如上面的例子,problemInfo是“2 3 4 5 + x ÷ 23÷2”,再存入 Map<String,Boolean> storage 中。以后每生成一条题目时,进行一次 storage.get(problemInfo) 操作,如果返回值为 null,则没有重复。如果返回值不为空,则表明重复,丢弃该题。


 虽然该方法有一个小瑕疵,有可能出现不重复的题目判定为重复,但概率极低(需要数字一样,运算符一样,最后一步一样),而且不会出现重复的题目。

3.异常处理

1.如果输入的文件路劲不存在,则会提示文件不存在。

2.如果输入的取值范围不是正整数,则会提示重新输入。

4.使用测试



Grade.txt
(传入的答案文件,为了方便这里直接用了Answer.txt)


(再自己写一个txt,故意把第6、7、9题答案写错)

四、性能分析

五、个人总结

在初见项目要求时,觉得十分简单。但在具体设计的时候,还是发现有很多细节需要实现,并没那么简单。这次作业要求与另一位同学进行合作,也是我从来没试过的,一直以来都是自己做。在这过程中,我也收获了很多。在交流思路的过程中,双方的点子相碰撞,激发了我们更多的灵感。同时,在交流的过程中,如果自己的思路有漏洞,对方能即使反馈,能省下不少试错的时间。
--刘晓霖
这次作业,我们看了题目,商量了大概的框架之后,进行了明确的分工,也让我们培养了独自完成一个模块的能力,也锻炼了我们的团队合作意识,所以团队作业是一个很好锻炼自己能力的机会。
--任浩然

posted @ 2020-10-12 22:47  Oldlynn  阅读(134)  评论(0编辑  收藏  举报