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

成员:3223004473詹艺珏  and   3223004301吴梦琪
📎Github链接:https://github.com/Jue610/Jue610/tree/main/ArithProbelm

这个作业属于哪个课程 23软件工程
这个作业要求在哪里 【作业3】结对项目
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序,培养团队协作和沟通交流能力

一、PSP表格

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

二、程序功能

  1. 目的
    • 主要用于生成数学表达式的练习题并计算答案,同时也能检查用户对这些练习题答案的正确性。
    • 它可以随机生成包含自然数、分数和带分数的四则运算表达式,将其规范化后写入练习题文件,计算答案并写入答案文件,最后根据用户提供的答案文件检查答案的正确性并生成成绩文件。
  2. 输入输出
  • 输入:通过命令行参数接受输入:
  • 生成练习题的数量-n和表达式中数字的范围-r来生成练习题;
python main.py -n 10000 -r 100
  • 练习题文件-e和答案文件-a来检查答案。
python main.py -e Exercises.txt -a Answers.txt
  • 输出:生成练习题文件Exercises.txt、答案文件Answers.txt以及成绩文件Grade.txt

三、代码结构与模块分析

  1. 类的定义
    • Node
      • 功能:用于构建表达式树。
      • 结构
        • type属性,表示节点类型(如numberoperator)。
        • value属性,当typenumber时存储分数值(以元组形式表示分子分母)。
        • operator属性,当typeoperator时存储操作符(如+-×÷)。
        • leftright属性,用于指向表达式树的左子树和右子树。
class Node:
    def __init__(self, type, value=None, operator=None, left=None, right=None):
        self.type = type
        self.value = value
        self.operator = operator
        self.left = left
        self.right = right
  1. 函数模块与接口设置
    • simplify_fraction函数
      • 功能:通过计算最大公约数来化简分数。
      • 使用递归的方式计算分子分母的最大公约数,然后将分数化简。
def simplify_fraction(numerator, denominator):
    gcd = lambda a, b: a if b == 0 else gcd(b, a % b)
    common = gcd(abs(numerator), abs(denominator))
    return (numerator // common, denominator // common)
  • generate_number函数
    • 功能:随机生成自然数、分数或带分数。
    • 首先随机选择一种数字类型(自然数、分数、带分数),然后根据类型生成相应的数字,并将其包装成Node类型的对象。
def generate_number(r):
    choice = random.choices(['natural', 'fraction', 'mixed'], weights=[3, 2, 2], k=1)[0]
    if choice == 'natural':
        num = random.randint(0, r - 1)
        return Node('number', value=simplify_fraction(num, 1))
    elif choice == 'fraction':
        denominator = random.randint(2, r)
        numerator = random.randint(1, denominator - 1)
        return Node('number', value=simplify_fraction(numerator, denominator))
    else:
        integer = random.randint(1, r - 1)
        denominator = random.randint(2, r)
        numerator = random.randint(1, denominator - 1)
        total_numerator = integer * denominator + numerator
        return Node('number', value=simplify_fraction(total_numerator, denominator))
  • generate_expression函数
    • 功能:递归生成四则运算表达式树。
    • 如果最大操作符数量为0,则生成一个数字节点;否则选择一个操作符,递归生成左右子表达式树,最后构建一个操作符节点并返回。
def generate_expression(r, max_ops):
    if max_ops == 0:
        return generate_number(r)
    else:
        op = random.choice(['+', '-', '×', '÷'])
        left_ops = random.randint(0, max_ops - 1)
        right_ops = max_ops - 1 - left_ops
        left = generate_expression(r, left_ops)
        right = generate_expression(r, right_ops)
        return Node('operator', operator=op, left=left, right=right)
  • compute_value函数
    • 功能:计算表达式树的值。
    • 如果是数字节点直接返回其值;否则先计算左右子树的值,然后根据操作符进行相应的四则运算,并化简结果。
def compute_value(node):
    if node.type == 'number':
        return node.value
    left = compute_value(node.left)
    right = compute_value(node.right)
    ln, ld = left
    rn, rd = right
    if node.operator == '+':
        numerator = ln * rd + rn * ld
        denominator = ld * rd
    elif node.operator == '-':
        numerator = ln * rd - rn * ld
        denominator = ld * rd
        if numerator < 0:
            raise ValueError("Negative result")
    elif node.operator == '×':
        numerator = ln * rn
        denominator = ld * rd
    elif node.operator == '÷':
        if rn == 0:
            raise ValueError("Division by zero")
        numerator = ln * rd
        denominator = ld * rn
        if numerator / denominator >= 1:
            raise ValueError("Improper fraction")
    return simplify_fraction(numerator, denominator)
  • fraction_to_string函数
    • 功能:将分数转换为字符串形式。
    • 根据分子分母的关系判断并构建相应的字符串形式。
def fraction_to_string(numerator, denominator):
    if denominator == 1:
        return str(numerator)
    integer = numerator // denominator
    remainder = numerator % denominator
    if integer == 0:
        return f"{remainder}/{denominator}"
    else:
        return f"{integer}'{remainder}/{denominator}" if remainder != 0 else str(integer)

  • to_string函数
    • 功能:将表达式树转换为字符串形式。
    • 如果是数字节点,调用fraction_to_string函数转换为字符串;否则根据操作符的优先级和括号的需求,递归地将左右子树转换为字符串并构建表达式字符串。
def to_string(node, parent_priority=0):
    if node.type == 'number':
        return fraction_to_string(*node.value)
    current_priority = 2 if node.operator in ['×', '÷'] else 1
    left_str = to_string(node.left, current_priority)
    right_str = to_string(node.right, current_priority)
    expr_str = f"{left_str} {node.operator} {right_str}"
    if current_priority < parent_priority:
        expr_str = f"({expr_str})"
    return expr_str
  • normalize函数
    • 功能:对表达式树进行规范化处理。
    • 如果是数字节点直接返回;对于加法和乘法操作符,将表达式树展开为项列表,排序后重新构建表达式树;对于减法和除法操作符,递归地规范化左右子树。
def normalize(node):
    if node.type == 'number':
        return node
    if node.operator in ['+', '×']:
        terms = []
        stack = [node]
        while stack:
            current = stack.pop()
            if current.type == 'operator' and current.operator == node.operator:
                stack.append(current.right)
                stack.append(current.left)
            else:
                terms.append(current)
        terms = [normalize(term) for term in terms]
        terms.sort(key=lambda x: to_string(x))
        root = terms[0]
        for term in terms[1:]:
            root = Node('operator', operator=node.operator, left=root, right=term)
        return root
    else:
        return Node('operator', operator=node.operator, left=normalize(node.left), right=normalize(node.right))
  • generate_problems函数
    • 功能:生成指定数量的练习题并计算答案。
    • 通过循环随机生成表达式,规范化后将表达式和答案分别写入练习题文件和答案文件。
def generate_problems(n, r):
    generated = set()
    exercises = []
    answers = []
    while len(exercises) < n:
        expr = generate_expression(r, 3)
        normalized = normalize(expr)
        expr_str = to_string(normalized) + " ="
        if expr_str in generated:
            continue
        try:
            result = compute_value(expr)
            generated.add(expr_str)
            exercises.append(expr_str)
            answers.append(fraction_to_string(*result))
        except ValueError:
            pass

    # 写入题目文件
    with open('Exercises.txt', 'w') as f:
        for i, expr in enumerate(exercises, 1):
            f.write(f"{i}. {expr}\n")

    # 写入答案文件
    with open('Answers.txt', 'w') as f:
        for i, ans in enumerate(answers, 1):
            f.write(f"{i}. {ans}\n")
  • parse_answer函数
    • 功能:将答案字符串解析为Fraction对象。
    • 根据字符串中是否包含带分数的标识进行不同的解析操作。
def parse_answer(answer_str):
    answer_str = answer_str.strip()
    if "'" in answer_str:
        mixed, frac = answer_str.split("'", 1)
        whole = int(mixed)
        numerator, denominator = frac.split('/')
        return Fraction(whole * int(denominator) + int(numerator), int(denominator))
    elif '/' in answer_str:
        numerator, denominator = answer_str.split('/')
        return Fraction(int(numerator), int(denominator))
    else:
        return Fraction(int(answer_str), 1)
  • parse_expression函数
    • 功能:将表达式字符串解析为可用于eval函数计算的Python表达式字符串。
    • 进行一些字符替换(如将×替换为*÷替换为/),然后转换为相应的Python表达式字符串。
def parse_expression(expr_str):
    expr_str = expr_str.replace(' ', '').replace('×', '*').replace('÷', '/')
    tokens = re.findall(r"(\d+'\d+/\d+|\d+/\d+|\d+|\+|\-|\*|/|\(|\))", expr_str)
    converted = []
    for token in tokens:
        if token in '()+-*/':
            converted.append(token)
        else:
            converted.append(convert_number(token))
    return ''.join(converted)
  • convert_number函数
    • 功能:将数字字符串转换为Fraction对象的字符串表示形式。
    • 根据字符串是否为带分数或普通分数进行不同的转换操作。
def convert_number(s):
    if "'" in s:
        mixed, frac = s.split("'", 1)
        numerator, denominator = frac.split('/')
        return f"Fraction({int(mixed) * int(denominator) + int(numerator)}, {denominator})"
    elif '/' in s:
        numerator, denominator = s.split('/')
        return f"Fraction({numerator}, {denominator})"
    else:
        return f"Fraction({s}, 1)"
  • evaluate_expression函数
    • 功能:计算解析后的表达式字符串的值。
    • 使用eval函数在给定的Fraction对象的命名空间下计算表达式的值。
def evaluate_expression(py_expr):
    try:
        return eval(py_expr, {'Fraction': Fraction})
    except:
        return None
  • check_answers函数
    • 功能:检查答案文件中的答案是否正确。
    • 读取练习题文件和答案文件,对每个练习题解析表达式计算正确答案,解析用户答案,然后比较两者是否相等,最后将正确和错误的题目编号分别写入成绩文件。
def check_answers(exercise_file, answer_file):
    correct = []
    wrong = []

    # 读取题目文件
    with open(exercise_file, 'r') as f:
        exercises = [line.strip().split('. ', 1)[1].rstrip(' =') for line in f]  # 提取 "题目内容"

    # 读取答案文件
    with open(answer_file, 'r') as f:
        answers = [line.strip().split('. ', 1)[1] for line in f]  # 提取 "答案内容"

    if len(exercises) != len(answers):
        print("错误:题目与答案数量不匹配!")
        return

    for idx in range(len(exercises)):
        expr = exercises[idx]
        user_ans = answers[idx]

        # 解析表达式并计算正确答案
        py_expr = parse_expression(expr)
        correct_ans = evaluate_expression(py_expr)
        if correct_ans is None:
            wrong.append(idx + 1)
            continue

        # 解析答案
        try:
            user_frac = parse_answer(user_ans)
        except:
            wrong.append(idx + 1)
            continue

        # 比较答案
        if correct_ans == user_frac:
            correct.append(idx + 1)
        else:
            wrong.append(idx + 1)

    # 输出结果
    with open('Grade.txt', 'w') as f:
        f.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
        f.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
  • main函数
    • 功能:解析命令行参数并调用相应的函数(generate_problems或check_answers)。
    • 使用argparse模块解析命令行参数,根据参数调用不同的函数,如果参数不完整则输出错误信息并退出程序。
def main():
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("-n", type=int)
    group.add_argument("-e", type=str)
    parser.add_argument("-r", type=int)
    parser.add_argument("-a", type=str)
    args = parser.parse_args()

    if args.n is not None:
        if args.r is None:
            print("Error: -r required")
            sys.exit(1)
        generate_problems(args.n, args.r)
    elif args.e is not None:
        if args.a is None:
            print("Error: -a required")
            sys.exit(1)
        check_answers(args.e, args.a)
  1. 流程图

四、性能分析

对于生成模式的性能分析:

对于判断模式的性能分析:

可以看出,生成题目的函数消耗最大(因为分析时让程序生成10000道题目)。

五、单元测试用例

  • 主要分为TestMathGeneratorTestCLICommands两个测试用例类。
  • 单元测试覆盖率:
  1. TestMathGenerator类分析
    • setUptearDown方法
      • setUp中,创建临时目录用于测试文件的操作,备份了原始的命令行参数和标准输出。
      • tearDown方法用于清理临时目录、恢复原始的命令行参数和标准输出,并删除测试过程中可能生成的输出文件。
    • test_number_generation方法
      • 设置随机种子为42,调用generate_number函数生成一个数,然后对生成的数的格式进行验证,确保其是合法的分数形式。
    • test_expression_calculation方法
      • 针对表达式计算函数compute_value进行测试。对加除法乘法三种运算分别构建测试表达式,验证计算结果是否符合预期。
    • test_normalization方法
      • 构建一个复杂的表达式,调用normalize函数对表达式进行规范化,然后使用to_string函数将规范化后的表达式转换为字符串,验证转换后的字符串是否符合预期形式。
    • test_file_generation方法
      • 通过模拟命令行参数(-n-r)调用main函数,然后验证是否成功生成了Exercises.txtAnswers.txt文件,并且文件中的行数是否与预期的数量一致。
    • test_correct_answers方法
      • 创建测试用的题目文件和答案文件内容,调用create_test_files函数创建临时的测试文件,再调用check_answers函数对答案进行检查。最后验证Grade.txt文件是否存在,并检查文件内容中正确答案和错误答案的统计是否符合预期。
    • test_mixed_answers方法
      • 创建包含混合正确/错误答案的题目文件和答案文件内容,创建临时文件并调用check_answers函数。最后验证Grade.txt文件中的正确答案和错误答案统计符合预期。
    • test_cli_error_handling方法
      • 测试命令行缺少必要参数时的错误处理。
  2. TestCLICommands类分析
    • setUptearDown方法
      • 准备测试环境和清理测试后的环境。
    • test_generate_command方法
      • 通过模拟命令行参数(-n-r)调用main函数,验证是否成功生成了Exercises.txt文件,并且文件中的行数是否与-n指定的数量一致,以及每行是否包含=符号。
    • test_check_command方法
      • 先准备测试用的题目文件和答案文件,然后通过模拟命令行参数(-e-a)调用main函数,验证是否成功生成了Grade.txt文件,并且文件内容中正确答案的统计是否符合预期。
    • test_simple_generation方法
      • 创建一个测试文件,验证文件是否存在后将其删除。
    • test_basic_answer_check方法
      • 创建测试用的题目文件和答案文件,然后调用check_answers函数进行答案核对,最后验证是否成功生成了Grade.txt文件。

六、总结与反思

成功之处

  1. 功能实现方面
    实现了基本需求,包括按照指定数量和数值范围生成四则运算题目、确保题目计算过程中不产生负数、除法结果为真分数、题目不重复、生成答案文件、对给定的题目和答案文件进行对错判定等功能。
  2. 测试覆盖方面
    编写了多个测试用例,覆盖了程序的各种功能和边界情况,有效地保证了程序的正确性。

不足之处

  1. 性能优化方面
    虽然进行了一定的性能优化,但在生成大量题目时,程序的运行速度还有提升的空间。
  2. 异常处理方面
    在程序中,部分异常情况的处理还不够完善。比如在处理文件读写异常时,只是简单地给出了一些基本的错误提示,没有更详细的错误信息来帮助用户定位问题。

结对感受

  1. 闪光点
    💙 我们可以同时开展不同的任务:一个人负责设计和编写主要的逻辑代码,另一个人可以同时进行相关功能的测试用例编写。
    💚两个人可以及时互相审查代码。当一个人完成了一部分代码编写后,另一个人可以迅速进行审查,发现潜在的逻辑错误、代码规范问题或者性能瓶颈等。
    😎 我们通过互相学习,不仅提高了自己的技能,还增进了对整个项目的理解,并且可以互相监督对方的进度。
  2. 对彼此的看法
    😄zyj:我的搭档在项目开发的时候充满热情。即使在遇到困难和压力的时候,她也能保持积极的态度,这种热情也感染着我。
    😊 wmq:在这次的作业合作中,我深刻体会到我的搭档的出色之处,她思维缜密,逻辑清晰有条理,且对待工作细致严谨,真是难能可贵的合作伙伴!
  3. 建议
    在结对过程中,沟通的效率还有待提高。在今后的结对项目中,我们应该更加明确地表达自己的想法,并且在讨论之前先对问题进行更深入的思考,以提高沟通的效率。
posted @ 2025-03-15 21:47  AuroraJune  阅读(108)  评论(0)    收藏  举报