枫叶の碎碎念

Loading...

作业9.14:结对项目(四则运算)

这个作业属于哪个课程 班级链接
这个作业要求在哪里 作业要求链接
这个作业的目标 按需求设计四则运算算法;学会任务分工。

Github 链接:博客正文首行 github 链接

项目成员:

姓名 学号 Github链接 分工
凌枫 3121005661 https://github.com/Researcher-Feng/SoftwareEngineer_Calculator 负责博客编写、程序的完善和代码 Bug 修复
陈卓恒 3122004905 https://github.com/Jimmyczh231/SoftwareEngineer_Calculator 负责程序的基本实现和测试、博客检查

一、基本介绍

开发环境

  • 编程语言:python 3
  • IDE:PyCharm 2024.2.1 (Professional Edition)

项目介绍

这是一个结对项目,我们共同实现了一个自动生成小学四则运算题目的程序。

该程序具有两个基本功能:

  • 生成题目
  • 答案校验

题目可以通过两种方式运行:

  • 命令行方式
  • 图形化界面

目录组织

.
├── .idea
├── 3121005661
├── 3122004905
├── old						    // 存放旧的代码版本
│   ├── main1.py		
│   ├── main2.py
│   ├── main3.py
│   └── main4.py		
├── test						// 存放测试类
│   ├── graph.py	  // 代码调用关系图
│   ├── test.log	  // 空文件
│   ├── test_eval.py	  // 测试函数:字符串数学化
│   ├── test_evaluate.py	  // 测试函数:评估函数
│   ├── test_fraction_function.py	  // 测试函数:真分数字符串预处理
│   ├── test_generate_exercises.py		  // 测试函数:练习题生成
│   ├── test_generate_expression.py		  // 测试函数:表达式生成
│   ├── test_judge_function.py		  // 测试函数:答案校对
│   ├── test_logger.py		  // 测试函数:日志生成
│   └── test_valid.py			 // 测试函数:有效性验证
├── README.md					// Github readme 文档
├── requirements.txt				// 程序运行环境
└── main.py						// 程序入口

二、效能分析

我们让程序生成 1000 道题目,数值上界为 6 ,在执行过程中分析程序的性能。

程序初始性能

如上表所示,如果按照Time进行耗时的从大到小排序,可以发现,主程序main4的总用时为 15738 ms,用时是比较久的。除了核心主程序mainmainloop,一个核心的自定义方法on_generategenerate_exercises是所有自定义方法中耗时最多的,用时大约为 6000 ms 左右,函数的作用是产生满足要求的练习题以及生成对应的答案。其余方法例如generate_expressionevaluate_expressioneval_expr用时 2000 ms左右,这些方法都是需要优化的。

性能改进

基于上面的问题,我们对算法进行调整,对于输入的两个文本,我们直接使用轻量级的编辑距离计算相似度,如果相似度低于 50% ,为了确保查重的准确率,我们才调用自定义方法get_vocab、使用BLEU算法进行计算。性能优化后的情况如下:

程序优化后性能

我们对核心算法做了一定的改进,将性能提高了大约两倍。以前在生成题目的时候,我们的做法是:先随机生成一个表达式,然后遍历表达式中的符号,再逐个替换表达式中的符号(四种符号就能至少生成四个表达式),这样能够得到一组表达式,但是算法效率为 O(n2) ,效率很低;改进的做法是每次都随机生成,将算法效率降低为 O(n) ,因此上表所示各函数的执行时间均有所下降,其中main.py函数的时间从 15738 ms 降低到 8156 ms。

模块性能

我们引用代码行级分析以实现对函数内部各行所用时间的统计,下图为主要函数的行级代码性能情况:




三、设计实现过程

整体设计

函数接口设计

函数 功能
handle_cli_args() 命令行模式的入口函数,分析用户输入的各种命令,执行相应程序
color_logger() 彩色日志的初始化函数,用于命令行模式的输出
setup_gui() 图形化界面的入口函数,根据用户输入,执行相应程序
on_grade() 答案验证模块的入口函数,创建图形化界面,执行相应程序
on_generate() 题目生成模块的入口函数,创建图形化界面,执行相应程序
open_file_dialog(msg_string) 创建文件选择窗口的图形化界面,参数msg_string为提示信息
generate_exercises(n, r) 题目生成模块的功能处理函数,参数nr分别为题目数量、数值上届
is_valid_expression(expr) 表达式有效性验证函数,参数expr为表达式字符串,返回布尔类型
eval_expr(expr) 字符串数学化转换函数,参数expr为表达式字符串,返回计算结果
evaluate_expression(expr) 预处理字符串,提供eval_expr()函数的接口,返回布尔类型
generate_expression(r) 表达式生成函数,参数r为数值上届,返回表达式字符串

类与函数的调用关系

核心算法

本程序的一个核心函数是generate_exercises函数,下面是它的流程图:

该函数用于生成n道练习题,每个练习包含一个算式和答案。下面是具体的过程:

函数首先初始化用于存储算式和答案的列表。接着,函数使用generate_expression()函数生成n个算式,直到生成的算式数量等于n。该函数会不断地生成新的算式,直到生成的算式不符合要求(例如答案为负数)。生成的每个算式都会与evaluate_expression()函数一起评估,以确定答案是否合法。如果答案合法,则将其添加到答案集中。如果答案集中已经包含了n个合法的答案,则继续生成新的算式。如果答案集中包含了80000个或更多的合法答案,则停止生成新的算式。生成的每个算式都会被转换为不含空格的字符串,以避免生成的算式与已有的算式重复。当函数生成了所有需要的练习后,它会将练习和答案组合在一起,并返回它们。

功能 1 - 8 的『 需求-实现 』对照

需求1

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

Myapp.exe -n 10 将生成10个题目。

实现1

效果如下:

需求2

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

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

实现2

实现原理:如下面展示的代码所示,我们通过分析用户输入的参数来判断是否需要给出错误提示,使用r_existed变量来记录用户是否输入-r参数。如果用户没有给定该参数,程序会执行logger.error输出错误信息,然后通过help_msg()给出帮助信息。

# 检测参数输入错误
if (r_existed == 1 and n_existed == 0) or (r_existed == 0 and n_existed == 1) \
or (e_existed == 1 and a_existed == 0) or (e_existed == 0 and a_existed == 1) or \
(r_existed == 0 and n_existed == 0 and e_existed == 0 and a_existed == 0):
	logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
	help_msg()
	sys.exit()

效果如下:

需求3

3.生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。

实现3

实现原理:如下面展示的代码所示,我们为了确保不能产生负数,实现了“双保险”:不仅在计算每个减法子表达式(负数只可能在减法中产生)的时候,确保不能产生负数,还对最终结果进行验证,如果最终结果是负数,那么删除这道习题。

# 计算每个子表达式的时候确保不能产生负数
for _ in range(random.randint(1, 3)):
    op = random.choice(ops)
    next_num = generate_number()
    if random.choice([True, False]):
        expression = f"({expression} {op} {next_num})"
        added_parentheses = True  # Set flag to True
        if op == '-':
            if evaluate_expression(expression) < 0:
                return -1
    else:
        expression += f" {op} {next_num}"
        added_parentheses = False
# 最终结果确保不能产生负数
while len(exercises) < n:
    expr = generate_expression(r)
    if expr == -1:
        continue
    answer = evaluate_expression(expr)
    if answer < 0:
        continue

效果如下:

需求4

4.生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。

实现4

实现原理:如下面展示的代码所示,我们对于所有随机生成的分数和结果中的分数进行了下述处理方式,即使用Fraction库,对分数进行精确表示,对结果进行分类处理,分为:真分数、纯数字、带分数 三种情况。确保不会出现小数、假分数等非法情况。

# 分数和除法的逆向格式化(转带分数)
def convert_fraction_to_mixed(fraction_str):
    # 输入处理
    if '/' not in fraction_str:
        return fraction_str  # If it's not a fraction, return as is

    # 除法表达式拆分
    try:
        numerator, denominator = map(int, fraction_str.split('/'))
    except ValueError:
        return None  # 处理非整数

    if denominator == 0:
        return None  # 处理特殊情况

    whole = numerator // denominator
    remainder = abs(numerator) % denominator  # 用绝对值处理负数情况

    if whole == 0:
        return f"{remainder}/{denominator}"  # 真分数
    elif remainder == 0:
        return str(whole)  # 纯数字
    else:
        return f"{whole}'{remainder}/{denominator}"  # 带分数

效果如下:

需求5

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

实现5

实现原理:如下面展示的代码所示,我们通过random.randint(1, 3)确保生成的运算符在3个以内。

for _ in range(random.randint(1, 3)):
    op = random.choice(ops)

效果如下:

需求6

6.程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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. 四则运算题目1

  2. 四则运算题目2

    ...

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

实现6

实现原理:根据常识,如果题目能通过有限次交换+和×左右的算术表达式变换为另一道题目,那么他们结果一定是相同的;反之,如果两道题目的答案不同,他们不能通过有限次交换+和×左右的算术表达式变换为同一道题目。由此可见,他们互为充要条件。因此,只需要确保题目的答案不同,就能确保任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。为了方便起见,我们设置一个answer_set,每次生成答案都检查答案是否在answer_set中。虽然存在一定的性能问题,但是它能 100% 确保正确性。代码如下:

if answer in answer_set:
      continue
answer_set.add(answer)

需求7

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

  1. 答案1
  2. 答案2

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

实现7

效果如下:

需求8

8.程序应能支持一万道题目的生成。

实现8

对于一万道题目的生成,虽然可以正确执行且每道题均符合要求,但是可能会比较耗时的,时间可能取决于每个人电脑的配置(型号、核心数、主频)。我们测量了在两个不同的电脑配置下,生成不同数量的题目所需的时间,仅供参考:

型号 核心数 主频 要求生成题目数 数值上界 用时
Intel i7-8565U 8核 1.8GHz 10000 10 1分32秒
Intel i7-8565U 8核 1.8GHz 2000 7 1分02秒
apple m2 8g 8核 8GHz 10000 10 1分25秒
apple m2 8g 8核 8GHz 2000 7 17秒

效果如下:


需求9

9.程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:

Myapp.exe -e .txt -a .txt

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

Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)

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

实现9

效果如下:

四、代码说明

函数 handle_cli_args()

# 命令行模式-处理逻辑
@profile
def handle_cli_args():
    number = 0
    ranging = 0
    exercise_file = " "
    answer_file = " "
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hn:r:e:a:", ["help", "number=", "range=", "exercise_file=", "answer_file="])
    except:
        logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
        sys.exit()

    # 从左到右:是否包含生成题目数量、是否包含数值范围上届、是否包含题目路径、是否包含答案路径
    n_existed, r_existed, e_existed, a_existed = 0, 0, 0, 0
    for opt, arg in opts:
        if opt in ("-h", "--help"):  # 显示帮助
            help_msg()
            sys.exit()
        if opt in ("-n", "--number"):
            try:
                number = int(arg)
                if number <= 0:
                    logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
                    return
            except:
                logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
                sys.exit()
            if n_existed == 0:
                n_existed = 1
        elif opt in ("-r", "--ranging"):
            try:
                ranging = int(arg)
                if ranging > 10 or ranging <= 1:
                    logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
                    return
            except:
                logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
                sys.exit()
            if r_existed == 0:
                r_existed = 1
        elif opt in ("-e", "--exercise_file"):
            exercise_file = arg
            if e_existed == 0:
                e_existed = 1
        elif opt in ("-a", "--answer_file"):
            answer_file = arg
            if a_existed == 0:
                a_existed = 1
        else:
            help_msg()
            sys.exit()

    # 检测参数输入错误
    if (r_existed == 1 and n_existed == 0) or (r_existed == 0 and n_existed == 1) \
            or (e_existed == 1 and a_existed == 0) or (e_existed == 0 and a_existed == 1) or \
            (r_existed == 0 and n_existed == 0 and e_existed == 0 and a_existed == 0):
        logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
        help_msg()
        sys.exit()

    # 执行生成四则运算题目功能
    if n_existed and r_existed:
        exercises, answers = generate_exercises(number, ranging)
        with open("Exercises.txt", 'w', encoding='utf-8') as ef, open("Answers.txt", 'w', encoding='utf-8') as af:
            ef.write("\n".join(exercises))
            af.write("\n".join(answers))
        logger.info("\n已随机生成四则运算题目与对应答案,分别存入当前目录下的 Exercises.txt 文件和 Answers.txt 文件")

    # 执行判定答案对错功能
    if e_existed and a_existed:
        correct, wrong, invalid = judge_function(exercise_file, answer_file)
        print(f"判题结果:\nCorrect: {len(correct)} ({', '.join(map(str, correct))})")
        print(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})")
        if invalid:
            print(f"Invalid: {len(invalid)} ({', '.join(map(str, invalid))})")
        logger.info("\n判题完成,结果已保存到当前目录下的 Grade.txt")

    sys.exit()

该函数用于处理命令行参数的,使用了Python的getopt模块来解析命令行参数,并使用了profile装饰器来生成代码运行时性能分析数据。handle_cli_args()函数的主要功能是解析命令行参数,并根据不同的参数执行不同的操作。

  1. 首先,使用try/except块来捕获getopt模块可能出现的异常。
  2. 然后,使用for循环遍历opts列表,并获取每个选项的名称和值。
    如果选项是-h或--help,则调用help_msg()函数显示帮助信息并退出程序。
    如果选项是-n或--number,则将arg转换为整数并检查其是否有效。如果有效,则将其存储在number变量中。
    如果选项是-r或--ranging,则将arg转换为整数并检查其是否有效。如果有效,则将其存储在ranging变量中。
    如果选项是-e或--exercise_file,则将arg存储在exercise_file变量中。
    如果选项是-a或--answer_file,则将arg存储在answer_file变量中。
  3. 接下来,使用 if 语句检测参数输入是否有误。
    如果r_existed为1且n_existed为0,或者r_existed为0且n_existed为1,或者e_existed为1且a_existed为0,或者e_existed为0且a_existed为1,或者r_existed为0且n_existed为0且e_existed为0且a_existed为0,则说明参数输入有误,调用help_msg()函数显示帮助信息并退出程序。
    如果r_existed为1且n_existed为1,则执行generate_exercises()函数生成指定数量的四则运算题目和答案,并将题目和答案分别存储在exercises和answers变量中。接着,使用with语句打开两个文件,分别将题目和答案写入Exercises.txt和Answers.txt文件。最后,记录生成的题目和答案的数量并输出到控制台。
    如果e_existed为1且a_existed为1,则执行judge_function()函数判断题目和答案的对错,并将正确、错误和无效答案分别存储在correct、wrong和invalid变量中。接着,使用print()函数输出判题结果,包括正确答案、错误答案和无效答案的数量和内容。最后,记录判题结果并输出到控制台。

函数 generate_exercises()

@profile
def generate_exercises(n, r, bar=False):
    exercises = []
    answers = []
    seen_expressions = set()
    answer_set = set()
    progress = None
    root = None

    if bar:
        # 创建 Tkinter 窗口
        root = tk.Tk()
        root.title("加载中...")
        progress = ttk.Progressbar(root, orient="horizontal", length=300, mode="determinate")
        progress.pack(pady=20)
        progress['value'] = 0
        progress['maximum'] = n

    while len(exercises) < n:
        expr = generate_expression(r)
        if expr == -1:
            continue
        answer = evaluate_expression(expr)

        if answer < 0:
            continue
        answer = convert_fraction_to_mixed(str(answer))
        if answer in answer_set:
            continue
        answer_set.add(answer)

        if answer is not None:
            normalized_expr = expr.replace(" ", "")
            if normalized_expr not in seen_expressions:
                exercises.append(expr + " = ")
                answers.append(str(answer))
                seen_expressions.add(normalized_expr)
                if bar:
                    progress['value'] += 1  # 更新进度条
                    root.update()  # 更新窗口以反映进度条变化
    if bar:
        progress['value'] = n
        time.sleep(0.002)
        root.destroy()  # 关闭窗口

    # 行号
    numbered_exercises = [f"{i + 1}. {exercise}" for i, exercise in enumerate(exercises)]
    numbered_answers = [f"{i + 1}. {answer}" for i, answer in enumerate(answers)]

    return numbered_exercises, numbered_answers

函数generate_exercises()用于生成n道数学题,每个题目包含一个等式和一个答案。

  1. 函数首先创建两个列表,用于存储生成的题目和答案。然后,它使用generate_expression()函数生成n个等式,直到生成的题目数量等于n。
  2. 然后,该函数使用evaluate_expression()函数评估每个等式,并检查答案是否有效。如果答案有效,则将其添加到答案列表中。
  3. 接着,该函数使用convert_fraction_to_mixed()函数将答案转换为真分数和带分数的形式,以便更容易理解。如果答案已经在答案列表中,则不添加该等式,避免生成重复的等式。
  4. 最后,如果bar为True,则使用tkinter创建了一个窗口,并显示一个进度条,程序结束后会销毁窗口和进度条。

函数 judge_function()

@profile
def judge_function(exercise_file, answer_file):
    with open(exercise_file, 'r', encoding='utf-8') as ef, open(answer_file, 'r', encoding='utf-8') as af:
        exercises = ef.readlines()
        answers = af.readlines()

    correct = []
    wrong = []
    invalid = []

    for i, (e, a) in enumerate(zip(exercises, answers), 1):
        # Remove the numbering from exercises and answers
        expr = e.split('.', 1)[-1].split('=')[0].strip()  # Get expression without the number
        answer = a.split('.', 1)[-1].strip()  # Get answer without the number

        if not is_valid_expression(expr):
            invalid.append(i)
            continue

        if str(evaluate_expression(expr)) == convert_mixed_to_fraction(answer):
            correct.append(i)
        else:
            wrong.append(i)


    with open('Grade.txt', 'w', encoding='utf-8') as gf:
        gf.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
        gf.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
        if invalid:
            gf.write(f"Invalid: {len(invalid)} ({', '.join(map(str, invalid))})\n")
    return correct, wrong, invalid

该函数为答案校对函数。

  1. 函数judge_function接受两个文件路径作为输入,分别表示练习文件和答案文件。它将读取这两个文件,并将它们分隔为一系列的练习和答案对。
  2. 然后,遍历这些练习和答案对,并使用is_valid_expression函数检查每个表达式是否有效。
  3. 接着,执行评估操作,使用evaluate_expression函数评估每个表达式。如果答案正确,则将其添加到correct列表中;如果答案错误,则将其添加到wrong列表中。如果表达式无效,则将其添加到invalid列表中。
  4. 评估后,使用convert_mixed_to_fraction函数将答案转换为混合数。
  5. 最后,函数将打开Grade.txt文件,将正确、错误和无效的题目数量写入文件,并返回正确、错误和无效的题目列表。

五、测试运行

单元测试概览

测试功能 测试文件 测试内容 测试覆盖率
计算算式 test_eval.py 测试算术表达式是否正确处理,验证基本的加、减、乘、除运算 94%
算式处理 test_evaluate.py 测试基本算式的计算,检查对不同算式的处理 95%
分数转换 test_fraction_function.py 测试混合数分数转换的正确性,包括不同输入格式的验证和无效输入的处理 95%
算式生成 test_generate_expression.py 测试生成表达式的有效性,包括允许的运算符和括号的匹配 95%
生成完整练习 test_generate_exercise.py 测试生成的习题数量和答案的唯一性,确保没有重复题目 96%
批改算式和答案 test_judge_function.py 测试评估习题的功能,检查正确答案、错误答案和无效表达式的处理 98%
日志创建处理 test_logger.py 测试表达式有效性的检查,包括运算符连续出现和括号配对的验证 94%
检查算式 test_valid.py 测试表达式有效性的检查,包括运算符连续出现和括号配对的验证 94%

测试函数的设计

由于测试比较多,下面仅以judge_function 函数的测试函数为例,介绍测试函数的设计,其余测试大同小异。

针对 judge_function 函数,我们设计了两个主要的测试函数,以确保该函数能够准确评估输入的算术题和答案。以下是这两个测试函数的基本特点:

特征 说明
函数功能 评估给定的算术题及其对应的答案,返回正确、错误及无效答案的索引
输入 包含算术题的文本文件和包含答案的文本文件
输入的数据类型 均为文本文件,包含题目及答案的字符串
输出 三个列表,分别包含正确答案、错误答案和无效答案的索引

针对这一功能,我们设计了以下测试函数:

  1. test_judge_function:

    • 测试标准的算术题和答案,确保正确的答案被正确记录,错误的答案和无效的表达式得到合理处理。该函数涵盖了正常计算的情况,验证了正确、错误和无效索引的返回。
  2. test_invalid_expression:

    • 测试包含无效表达式的情况,确保无效表达式被准确识别并记录。该函数验证了 judge_function 对无效输入的处理能力。

设计小结

这两个测试函数通过涵盖不同类型的输入和预期输出,确保了 judge_function 在处理各种算术题及答案时的准确性和鲁棒性。这次项目中对代码模块化要求较高,针对不同模块功能的异常处理分散,测试函数和类比较不会有较多的测试。在实际开发中,应继续扩展测试以处理更多边界情况和异常输入。

测试代码

  • test_evaluate.py
import unittest
from fractions import Fraction
from main import eval_expr 


class TestEvalExpr(unittest.TestCase):

    def test_valid_expressions(self):
        """Test if valid expressions evaluate correctly"""
        valid_expressions = [
            "3 + 2",
            "4 - 1 + 1",
            "5 * 2",
            "(1/2 + 1/2)",
            "10 / 2"
        ]

        expected_results = [
            5,  # 3 + 2
            4,  # 4 - 1 + 1
            10,  # 5 × 2
            1,  # 1/2 + 1/2
            5  # 10 ÷ 2
        ]

        for expr, expected in zip(valid_expressions, expected_results):
            with self.subTest(expr=expr):
                self.assertEqual(eval_expr(expr), expected)



    def test_fraction_expressions(self):
        """Test if fraction expressions evaluate correctly"""
        fraction_expressions = [
            "1/2 + 1/2",
            "1/3 + 2/3",
            "(1/4 + 3/4) * 2"
        ]

        expected_results = [
            1,  # 1/2 + 1/2
            1,  # 1/3 + 2/3
            2  # (1/4 + 3/4) × 2
        ]

        for expr, expected in zip(fraction_expressions, expected_results):
            with self.subTest(expr=expr):
                self.assertEqual(eval_expr(expr), expected)



if __name__ == '__main__':
    unittest.main()
  • test_evaluate.py
import unittest
import re
from main import evaluate_expression 


class TestEvaluateExpression(unittest.TestCase):

    def test_valid_expressions(self):
        """Test if valid expressions evaluate correctly"""
        valid_expressions = [
            "3 + 5",
            "10 - 2 × 3",
            "8 ÷ 4 + 1",
            "1/2 + 1/2",
            "5 × (3 - 1)"
        ]

        expected_results = [
            8,  # 3 + 5
            4,  # 10 - 2 × 3
            3.0,  # 8 ÷ 4 + 1
            1.0,  # 1/2 + 1/2
            10  # 5 × (3 - 1)
        ]

        for expr, expected in zip(valid_expressions, expected_results):
            with self.subTest(expr=expr):
                self.assertEqual(evaluate_expression(expr), expected)

    def test_division_by_zero(self):
        """Test if division by zero returns None"""
        expr = "1 ÷ 0"
        self.assertIsNone(evaluate_expression(expr))

    def test_invalid_expressions(self):
        """Test if invalid expressions return None"""
        invalid_expressions = [
            "2 + ",
            "× 5",
            "10 ÷ a",
            "5 / (3 - 3)",
            "1/0 + 1",
            ""  # Test for empty string
        ]

        for expr in invalid_expressions:
            with self.subTest(expr=expr):
                self.assertIsNone(evaluate_expression(expr))


if __name__ == '__main__':
    unittest.main()
  • test_fraction_function.py
import unittest
from fractions import Fraction
from main import convert_mixed_to_fraction, convert_fraction_to_mixed


class TestConversionFunctions(unittest.TestCase):
    def test_convert_mixed_to_fraction(self):
        """Test the conversion of mixed numbers to fractions"""
        # Test conversion of mixed numbers to fractions
        self.assertEqual(convert_mixed_to_fraction("3'1/2"), "7/2")
        self.assertEqual(convert_mixed_to_fraction("2'3/4"), "11/4")
        self.assertEqual(convert_mixed_to_fraction("0'1/3"), "1/3")

        # Test conversion of multiple mixed numbers
        self.assertEqual(convert_mixed_to_fraction("3'1/2 + 1'1/4"), "7/2 + 5/4")

        # Test that expressions without mixed numbers remain unchanged
        self.assertEqual(convert_mixed_to_fraction("5/2 + 1/4"), "5/2 + 1/4")

    def test_convert_fraction_to_mixed(self):
        """Test the conversion of fractions to mixed numbers"""
        # Test conversion of fractions to mixed numbers
        self.assertEqual(convert_fraction_to_mixed("7/2"), "3'1/2")
        self.assertEqual(convert_fraction_to_mixed("11/4"), "2'3/4")
        self.assertEqual(convert_fraction_to_mixed("1/3"), "1/3")

        # Test conversion of fractions to whole numbers
        self.assertEqual(convert_fraction_to_mixed("6/3"), "2")

        # Test conversion of fractions to whole numbers
        self.assertEqual(convert_fraction_to_mixed("6"), "6")

        # Test invalid input
        self.assertIsNone(convert_fraction_to_mixed("5/a"))
        self.assertIsNone(convert_fraction_to_mixed("1/0"))  # Case where denominator is zero


if __name__ == '__main__':
    unittest.main()
  • test_generate_exercises.py
import unittest
from main import generate_exercises
class TestGenerateExercises(unittest.TestCase):

    def test_generate_exercises(self):
        """Test if generate_exercises produces the correct number of exercises and unique answers."""
        n = 10  # Number of exercises to generate
        r = 10  # Range for numbers
        exercises, answers = generate_exercises(n, r)

        # Check that we have the correct number of exercises
        self.assertEqual(len(exercises), n)
        self.assertEqual(len(answers), n)

        # Check for duplicates in exercises
        self.assertEqual(len(exercises), len(set(exercises)))

        # Check for duplicates in answers
        self.assertEqual(len(answers), len(set(answers)))

        # Check for negative answers
        for answer in answers:
            # 检查答案是否包含负号
            self.assertFalse('-' in answer, f"Invalid answer: {answer} is negative")

    def test_generate_exercises_with_progress_bar(self):
        """Test if generate_exercises with a progress bar works correctly."""
        n = 5  # Number of exercises to generate
        r = 5  # Range for numbers
        exercises, answers = generate_exercises(n, r, bar=True)

        # Check that we have the correct number of exercises
        self.assertEqual(len(exercises), n)
        self.assertEqual(len(answers), n)

        # Check for duplicates in answers
        self.assertEqual(len(answers), len(set(answers)))

if __name__ == '__main__':
    unittest.main()
  • test_generate_expression.py
import unittest
import random
from main import generate_expression, evaluate_expression


class TestGenerateExpression(unittest.TestCase):
    def setUp(self):
        self.r = 10  # Set the range
        self.iterations = 100  # Set the number of test iterations

    def test_generate_expression_basic(self):
        for _ in range(self.iterations):
            expression = generate_expression(self.r)
            if expression == -1:
                continue

            # Check if the expression contains numbers or fractions
            self.assertTrue(any(char.isdigit() for char in expression))
            self.assertTrue(any(char in ['/', ' ', '(', ')'] for char in expression))


    def test_generate_expression_format(self):
        for _ in range(self.iterations):
            expression = generate_expression(self.r)
            if expression == -1:
                continue

            # Check the format of the expression
            self.assertIsInstance(expression, str)
            self.assertTrue(len(expression) > 0)


if __name__ == '__main__':
    unittest.main()

  • test_judge_function.py
import unittest
import os
from main import judge_function, is_valid_expression, evaluate_expression, convert_mixed_to_fraction

class TestJudgeFunction(unittest.TestCase):
    def setUp(self):
        # 创建临时的输入文件
        self.exercise_file = 'test_exercises.txt'
        self.answer_file = 'test_answers.txt'
        self.grade_file = 'Grade.txt'

        with open(self.exercise_file, 'w', encoding='utf-8') as ef:
            ef.write("1. 3 + 5 =\n")
            ef.write("2. 10 - 2 =\n")
            ef.write("3. 6 ÷ 0 =\n")
            ef.write("4. 8 × 2 =\n")
            ef.write("5. 1/2 + 1/2 =\n")

        with open(self.answer_file, 'w', encoding='utf-8') as af:
            af.write("1. 8\n")
            af.write("2. 8\n")
            af.write("3. undefined\n")  # Invalid case
            af.write("4. 16\n")
            af.write("5. 1\n")

    def tearDown(self):
        # 删除临时文件
        os.remove(self.exercise_file)
        os.remove(self.answer_file)
        if os.path.exists(self.grade_file):
            os.remove(self.grade_file)

    def test_judge_function(self):
        correct, wrong, invalid = judge_function(self.exercise_file, self.answer_file)

        # 检查正确和错误的索引
        self.assertEqual(correct, [1, 2, 4, 5])  # 期望的正确答案索引
        self.assertEqual(wrong, [])  # 无错误
        self.assertEqual(invalid, [3])  # 包含无效答案的索引

    def test_invalid_expression(self):
        # 测试无效表达式
        with open(self.exercise_file, 'w', encoding='utf-8') as ef:
            ef.write("1. 3 + 5 =\n")
            ef.write("2. invalid_expression\n")  # 无效表达式

        with open(self.answer_file, 'w', encoding='utf-8') as af:
            af.write("1. 8\n")
            af.write("2. 0\n")  # 任意答案

        correct, wrong, invalid = judge_function(self.exercise_file, self.answer_file)

        self.assertEqual(invalid, [2])  # 确保无效表达式被记录

if __name__ == '__main__':
    unittest.main()
  • test_logger.py
import logging
import unittest
from main import color_logger


class TestColorLogger(unittest.TestCase):

    def setUp(self):
        """Executed before each test, initializing the logger"""
        self.logger = color_logger()

    def test_logger_not_none(self):
        """Test if the logger is successfully created"""
        self.assertIsNotNone(self.logger)

    def test_logger_has_handlers(self):
        """Test if the logger has handlers"""
        self.assertGreater(len(self.logger.handlers), 0)

    def test_logger_level(self):
        """Test the logging level of the logger"""
        self.assertEqual(self.logger.level, logging.DEBUG)

    def test_console_handler_level(self):
        """Test the logging level of the console handler"""
        console_handler = self.logger.handlers[0]
        self.assertEqual(console_handler.level, logging.DEBUG)

    def test_file_handler_level(self):
        """Test the logging level of the file handler"""
        file_handler = self.logger.handlers[1]
        self.assertEqual(file_handler.level, logging.INFO)


if __name__ == '__main__':
    unittest.main()
  • test_valid.py
import unittest
from main import is_valid_expression


class TestIsValidExpression(unittest.TestCase):

    def test_valid_expressions(self):
        """Test if valid expressions return True"""
        valid_expressions = [
            "3 + 2",
            "4 - 1 + 1",
            "5 × 2",
            "(1/2 + 1/2)",
            "10 ÷ 2"
        ]

        for expr in valid_expressions:
            with self.subTest(expr=expr):
                self.assertTrue(is_valid_expression(expr))

    def test_invalid_expressions(self):
        """Test if invalid expressions return False"""
        invalid_expressions = [
            "2 + ",
            "3 ÷ 0",
            "(3 - 1",
            "× 5",
            "10 ÷ a",
            "3 + 5 ×",
            "3 ++ 5",
            "3 + (5 * 2"
        ]

        for expr in invalid_expressions:
            with self.subTest(expr=expr):
                self.assertFalse(is_valid_expression(expr))

    def test_empty_string(self):
        """Test if empty string returns False"""
        self.assertFalse(is_valid_expression(""))


if __name__ == '__main__':
    unittest.main()

测试覆盖率截图

六、PSP表格

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

七、结对感受

凌枫感受:在这次结对项目中,我主要负责代码 Bug 的修复、博客的编写,我认为最终的程序是完美的,因为它实现了全部的需求,而且既能通过命令行运行、又能通过图形化界面运行,这得益于开发过程中两个人的合作与交流。实际上,在代码开发中,我们都遇到了很多问题,而我的同伴发现了很多我没有发现的问题,这对项目帮助很大。

陈卓恒感受:在这次结对项目中,我主要负责程序的基本实现、博客的检查和基本的代码测试。最终的程序不仅达成了预期目标,成功实现了所有需求,还能够通过命令行和图形化界面运行。这一切得益于我与同伴之间的良好合作与沟通。在开发过程中,我们遇到了一些挑战,我的同伴提出了许多宝贵的建议,这对项目的进展起到了至关重要的作用。

commit 提交记录:

posted @ 2024-09-27 21:58  River-Flow  阅读(37)  评论(0编辑  收藏  举报