结对项目

结对项目

这个作业属于哪个课程 软件工程
作业要求 结对项目
作业目标 实现一个自动生成四则运算题目的程序,熟悉结对编程的流程和工作方式
GitHub链接 GitHub
成员组成 3219005445何婉莹+3219005451肖丽萍

一、PSP表格

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

二、效能分析

  1. 性能

  1. 类的内存占用情况

  • 由上图可以得知,主要因为字符处理时,数字和Fraction实体转换的较多,最后的输出和输入多用string的方式,所以char[] 占比较大。

  • 亮点:

    • 使用HashMap、ArrayList:
      HashTable、Vector等使用在多线程的场合,内部使用了同步机制,这个会降低程序的性能。

    • 当你要创建一个比较大的hashMap时,充分利用另一个构造函数

      public HashMap(int initialCapacity, float loadFactor)避免HashMap多次进行了hash重构。


三、接口的设计与实现过程

类名 功能
Generator 生成器:随机生成符合要求的运算表达式
FigureUtil 数字处理器:判断并处理表达式中的数字类型
Calculator 计算器:利用栈计算并处理运算式的结果
Corrector 批改器:批改答案并输出grade
  • 整体流程图

  • util包下类的UML图

四、代码说明

  1. Map():生成运算表达式
/**
* @param num 生成题目的个数
* @param range 题目中数值(自然数、真分数和真分数分母)的范围
*/
private static Map<String,String> createExp(int num, int range){
    //答案-表达式map
    Map<String,String> map = new HashMap<>();
    //生成表达式
    Random random = new Random();
    for (int i=0; i<num; ){
        StringBuilder sb = new StringBuilder();
        //随机生成运算符
        int opCount = random.nextInt(3)+1;
        String[] ops = new String[opCount];
        for (int j=0; j<opCount; j++){
            ops[j] = OPERATORS[random.nextInt(4)];
        }
        //随机生成运算数
        int figureCount = opCount + 1;
        String[] figures = new String[figureCount];
        for (int j=0; j<figureCount; j++){
            figures[j] = createFigure(range);
        }
        //随机决定是否生成括号,若是则决定左括号生成的位置
        boolean isBracket = false;
        int index = 0;
        if (opCount > 1){
            isBracket = random.nextBoolean();
        }
        if (isBracket){
            index = random.nextInt(figureCount - 1);
        }
        //组合表达式
        for (int j=0; j<figures.length-1; j++){
            if (isBracket && j==index){
                sb.append("( ");
            }
            sb.append(figures[j] + " ");
            if (isBracket && j==index+1){
                sb.append(") ");
            }
            sb.append(ops[j] + " ");
        }
        sb.append(figures[figures.length-1]);
        if (isBracket && figures.length-1==index+1){
            sb.append(" )");
        }
        String exp = sb.toString();
        //计算结果,检验生成的表达式是否合法,合法的才能存入map中
        try {
            String result = Calculator.calculate(exp);
            if (!result.equals("error") && map.get(result)==null){
                map.put(result, exp);
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return map;
}
  1. calculate():运算表达式求值
/**
* @param infixExp 运算表达式字符串
* @return 计算结果字符串
* @throws Exception
*/
public static String calculate(String infixExp) throws Exception {
    String res = "";
    Stack<String> numStack = new Stack<>();
    //先将中序表示的算式转为后序表达式(以list存放各个运算符和运算数)
    List<String> suffixExp = infixToSuffix(infixExp);
    //计算
    for (String str : suffixExp) {
        if (str.matches("[+\\-×÷]")) { //遇到符号则取出前两个数进行计算
                                      String next = numStack.pop();
                                      String pre = numStack.pop();
                                      res = calTwo(pre, next, str);
                                      //当运算过程中出现负数时,中断计算
                                      if (res.equals("error")) {
                                          return res;
                                      }
                                      numStack.push(res);
                                     } else { //遇到数字则入栈
                                             numStack.push(str);
                                            }
    }
    //将计算结果化至最简
    Fraction fraction = FigureUtil.parseFigure(res);
    int gcd = ArithmeticUtils.gcd(fraction.getNumerator(), fraction.getDenominator());
    fraction.setNumerator(fraction.getNumerator() / gcd);
    fraction.setDenominator(fraction.getDenominator() / gcd);
    //整理结果:假分数则化为真分数或整数
    if (fraction.getNumerator() >= fraction.getDenominator()) {
        fraction.setInteger(fraction.getNumerator() / fraction.getDenominator());
        fraction.setNumerator(fraction.getNumerator() % fraction.getDenominator());
    }
    if (fraction.getNumerator() == 0) {
        res = fraction.getInteger().toString();
    } else {
        res = FigureUtil.parseFraction(fraction);
    }
    return res;
}
  1. infixToSuffixr():中序表达式转后序表达式
/**
* @param infix 中序表达式字符串
* @return 1条list形式的后序表达式字符串
* @throws Exception
*/
private static List<String> infixToSuffix(String infix) throws Exception {
    if (StringUtils.isEmpty(infix.trim())) {
        return null;
    }
    ArrayList<String> suffix = new ArrayList<>();  // 保存后缀表达式
    Stack<String> opStack = new Stack<>();
    String[] split = infix.split(" ");
    for (String str : split) {
        if (str.matches("[+\\-×÷\\(\\)]")) {
            if (str.equals("(")) { //遇到"("入栈
                opStack.push(str);
            } else if (str.equals(")")) { //遇到")",出栈直到"("
                String topOperator;
                while (!(topOperator = opStack.pop()).equals("(")) {
                    suffix.add(topOperator);
                }
            } else { //遇到运算符
                //出栈,直到栈顶操作符的优先级大于当前操作符的优先级
                while (!opStack.isEmpty() && opPriority(str) <= opPriority(opStack.peek())) {
                    suffix.add(opStack.pop());
                }
                //当前操作符入栈
                opStack.push(str);
            }
        } else { //遇到数字直接输出
            suffix.add(str);
        }
    }
    if (!opStack.isEmpty()) {
        while (!opStack.isEmpty()) {
            suffix.add(opStack.pop());
        }
    }
    return suffix;
}

五、测试运行

  1. 使用 -n 参数控制生成题目的个数
  2. 使用 -r 参数控制题目中数值
  3. 生成的题目中计算过程不能产生负数,相关代码:
case "-":
    if ((a * y - b * x) < 0) {
        //计算结果出现非正数时,直接置标志位
        return "error";
    } else {
        result.setNumerator(a * y - b * x);
        result.setDenominator(x * y);
    }


//当运算过程中出现负数时,中断计算
if (res.equals("error")) {
    return res;
}
  • 计算过程中有负数怎么办:生成一条算式后马上计算出结果,如果中间的计算过程出现小于0的结果,直接抛弃这条算式生成新的。(计算过程中出现分母为0同理,直接抛弃)
  1. 生成的题目中如果存在形如e1÷ e2的子表达式,那么*其结果应是真分数*

  2. 每道题目中出现的运算符个数不超过3个。

//随机生成运算符
int opCount = random.nextInt(3)+1;
String[] ops = new String[opCount];
for (int j=0; j<opCount; j++){
ops[j] = OPERATORS[random.nextInt(4)];
}
  1. 程序一次运行生成的题目不能重复。
  • 观察到题目能通过有限次交换变成同一个题目时,其计算结果是相同的,根据计算结果查找map,对计算结果相同的算式进行比较,看是否“重复”,决定去留。
  1. 生成的题目存入执行程序的当前目录下的Exercises.txt文件,在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。
  2. 程序应能支持一万道题目的生成。
  3. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt


六、单元测试

本次单元测试设置了11个单元测试用例,具体设置如下:

特殊情况测试

  • 传入参数个数不正确
  • 输入非法参数
  • 传入的作答文件路径不存在
/** 传入参数个数不正确 */
@Test
public void test0_1(){
String[] args = {"-n","10"};
Main.main(args);
}

/** 输入非法参数 */
@Test
public void test0_2(){
String[] args = {"-n","10","?","?"};
Main.main(args);
}

/** 传入的作答文件路径不存在 */
@Test
public void test0_3(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/a.txt";
args[2] = "-a";
args[3] = PATH + "/b.txt";
Main.main(args);
}

功能测试

  • 测试生成题目功能
@Test
public void test1_1(){
String[] args = {"-n","10","-r","20"};
Main.main(args);
}

@Test
public void test1_2(){
String[] args = {"-n","100","-r","20"};
Main.main(args);
}

@Test
public void test1_3(){
String[] args = {"-n","1000","-r","10"};
Main.main(args);
}

@Test
public void test1_4(){
String[] args = {"-n","10000","-r","15"};
Main.main(args);
}
  • 测试批改答案功能
@Test
public void test2_1(){
    String[] args = new String[4];
    args[0] = "-e";
    args[1] = PATH + "/src/test/resources/Test1.txt";
    args[2] = "-a";
    args[3] = PATH + "/src/test/resources/Answers1.txt";
    Main.main(args);
}

@Test
public void test2_2(){
    String[] args = new String[4];
    args[0] = "-e";
    args[1] = PATH + "/src/test/resources/Test2.txt";
    args[2] = "-a";
    args[3] = PATH + "/src/test/resources/Answers2.txt";
    Main.main(args);
}

@Test
public void test2_3(){
    String[] args = new String[4];
    args[0] = "-e";
    args[1] = PATH + "/src/test/resources/Test3.txt";
    args[2] = "-a";
    args[3] = PATH + "/src/test/resources/Answers3.txt";
    Main.main(args);
}

@Test
public void test2_4(){
    String[] args = new String[4];
    args[0] = "-e";
    args[1] = PATH + "/src/test/resources/Test4.txt";
    args[2] = "-a";
    args[3] = PATH + "/src/test/resources/Answers4.txt";
    Main.main(args);
}
  • 测试覆盖率


七、项目小结

  • 何婉莹:
    • 此次作业的要求对我而言还是比较有挑战性的,但是在挑战的过程中很好地提升了自己的代码能力,让我意识到了自己容易眼高手低,还是要亲手写代码才能出真知。另外就是提升了自己对项目开发的整体规划能力,在大部分模块都设计好时才开始动手。
    • 此次作业需要结对完成,因此这次的开发经历让我注意到应如何与队友进行分工和沟通,在开发过程中也更加注重代码的可读性,学会了如何与队友进行对接。
  • 肖丽萍:
    • 两人合作时想法更加丰富,能针对问题提出更多元化的解决方案,计划的制定更完善,效率也更加地高,平均的工作量会降低,能互相学习对方的写代码优点,专注自己擅长的部分,弱势部分由队友补足。本次项目的实现整体并不难,但是有些地方要优化的话还是得多花时间精力。由于本学期课程与实验作业过多,本项目暂时在此告一段落,之后有时间的话会继续改进。
    • 在此郑重鸣谢队友老何带飞我这个fw。

八、参考博文

posted @ 2021-10-24 11:32  HeathenX  阅读(51)  评论(0编辑  收藏  举报