软件工程结对项目-小学四则运算题目生成与判题程序

软件工程结对项目

一、项目参与成员

计算机科学与技术3班 许晓喆 3223004302

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477
这个作业目标 实现一个自动生成小学四则运算题目的命令行程序

PSP表格

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

二、设计实现

  1. 数据结构设计
    分数类(Fraction):处理真分数的表示、运算(加、减、乘、除)、约分和格式化输出。
    属性:integer(带分数的整数部分,默认 0)、numerator(分子)、denominator(分母,默认 1)。
    方法:add、subtract、multiply、divide(分数运算)、reduce(约分)、toString(格式化输出,如2’3/8)、compare(比较大小)。
    表达式类(Expression):处理表达式生成、计算和去重。
    属性:exprStr(表达式字符串)、result(计算结果,Fraction类型)。
    方法:generate(生成随机表达式)、calculate(计算结果)、isDuplicate(判断与其他表达式是否重复,考虑+和×的交换律)。
  2. 模块划分
    命令行解析模块:处理-n、-r、-e、-a等参数,控制程序流程。
    题目生成模块:调用Expression类生成满足约束的题目,确保不重复。
    文件操作模块:读写Exercises.txt、Answers.txt、Grade.txt。
    批改模块:对比题目文件和答案文件,统计对错。

三、代码实现(Python 示例)

  1. 分数类(Fraction.py)
点击查看代码
class Fraction:
    def __init__(self, numerator=0, denominator=1, integer=0):
        self.integer = integer
        self.numerator = numerator
        self.denominator = denominator
        self._normalize()
        self.reduce()

    def _normalize(self):
        if self.numerator >= self.denominator:
            self.integer += self.numerator // self.denominator
            self.numerator = self.numerator % self.denominator
        if self.integer != 0 and self.numerator == 0:
            self.denominator = 1

    def reduce(self):
        def gcd(a, b):
            while b:
                a, b = b, a % b
            return a
        g = gcd(self.numerator, self.denominator)
        if g != 0:
            self.numerator //= g
            self.denominator //= g

    def add(self, other):
        new_denominator = self.denominator * other.denominator
        new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
        new_integer = self.integer + other.integer
        return Fraction(new_numerator, new_denominator, new_integer)

    def subtract(self, other):
        new_denominator = self.denominator * other.denominator
        new_numerator = self.numerator * other.denominator - other.numerator * self.denominator
        new_integer = self.integer - other.integer
        return Fraction(new_numerator, new_denominator, new_integer)

    def multiply(self, other):
        new_numerator = (self.integer * self.denominator + self.numerator) * (other.integer * other.denominator + other.numerator)
        new_denominator = self.denominator * other.denominator
        return Fraction(new_numerator, new_denominator).reduce()

    def divide(self, other):
        if other.numerator == 0:
            raise ValueError("除数不能为0")
        new_numerator = (self.integer * self.denominator + self.numerator) * other.denominator
        new_denominator = self.denominator * (other.integer * other.denominator + other.numerator)
        return Fraction(new_numerator, new_denominator).reduce()

    def toString(self):
        if self.integer != 0 and self.numerator != 0:
            return f"{self.integer}’{self.numerator}/{self.denominator}"
        elif self.integer != 0:
            return f"{self.integer}"
        elif self.numerator != 0:
            return f"{self.numerator}/{self.denominator}"
        else:
            return "0"

    @staticmethod
    def fromString(s):
        if "'" in s:
            integer_part, frac_part = s.split("’")
            numerator, denominator = frac_part.split("/")
            return Fraction(int(numerator), int(denominator), int(integer_part))
        elif "/" in s:
            numerator, denominator = s.split("/")
            return Fraction(int(numerator), int(denominator), 0)
        else:
            return Fraction(0, 1, int(s))

    def __eq__(self, other):
        self._normalize()
        other._normalize()
        return (self.integer == other.integer and
                self.numerator == other.numerator and
                self.denominator == other.denominator)

    def compare(self, other):
        self_val = self.integer * self.denominator + self.numerator
        other_val = other.integer * other.denominator + other.numerator
        self_total = self_val * other.denominator
        other_total = other_val * self.denominator
        if self_total > other_total:
            return 1
        elif self_total == other_total:
            return 0
        else:
            return -1
  1. 表达式生成与计算(Expression.py)
点击查看代码
import random
from Fraction import Fraction

class Expression:
    def __init__(self, range_val):
        self.range_val = range_val
        self.expr_str = ""
        self.result = Fraction()
        self.operators = []

    def generate_number(self):
        is_fraction = random.choice([True, False])
        if is_fraction:
            denominator = random.randint(2, self.range_val)
            numerator = random.randint(1, denominator - 1)
            is_mixed = random.choice([True, False])
            if is_mixed:
                integer = random.randint(1, self.range_val - 1)
                return Fraction(numerator, denominator, integer)
            else:
                return Fraction(numerator, denominator, 0)
        else:
            return Fraction(0, 1, random.randint(0, self.range_val - 1))

    def generate_expression(self, depth=0, max_operators=3):
        if depth == 0 or random.random() < 0.5 or max_operators == 0:
            num = self.generate_number()
            self.expr_str = num.toString()
            self.result = num
            return
        op = random.choice(["+", "-", "×", "÷"])
        self.operators.append(op)
        left = Expression(self.range_val)
        left.generate_expression(depth + 1, max_operators - 1)
        right = Expression(self.range_val)
        right.generate_expression(depth + 1, max_operators - 1)
        if op == "-":
            while left.result.compare(right.result) < 0:
                right.generate_expression(depth + 1, max_operators - 1)
        if op == "÷":
            valid = False
            while not valid:
                right.generate_expression(depth + 1, max_operators - 1)
                left_val = left.result.integer * left.result.denominator + left.result.numerator
                right_val = right.result.integer * right.result.denominator + right.result.numerator
                if left_val < right_val and (left_val * right.result.denominator) % (right_val * left.result.denominator) == 0:
                    valid = True
        self.expr_str = f"({left.expr_str} {op} {right.expr_str})"
        if op == "+":
            self.result = left.result.add(right.result)
        elif op == "-":
            self.result = left.result.subtract(right.result)
        elif op == "×":
            self.result = left.result.multiply(right.result)
        elif op == "÷":
            self.result = left.result.divide(right.result)
        if depth == 0:
            self.expr_str = self.expr_str.strip("()")

    def is_duplicate(self, other):
        if len(self.operators) != len(other.operators):
            return False
        def normalize(expr, ops):
            parts = []
            idx = 0
            for op in ops:
                part = expr[:expr.index(op)]
                parts.append(part)
                expr = expr[expr.index(op) + 1:]
            parts.append(expr)
            i = 0
            while i < len(ops):
                if ops[i] in ["+", "×"]:
                    j = i + 1
                    while j < len(ops) and ops[j] in ["+", "×"]:
                        j += 1
                    sub_parts = parts[i:i+2]
                    sub_parts.sort()
                    parts[i:i+2] = sub_parts
                    i = j
                else:
                    i += 1
            return parts, ops
        self_parts, self_ops = normalize(self.expr_str, self.operators)
        other_parts, other_ops = normalize(other.expr_str, other.operators)
        return self_parts == other_parts and self_ops == other_ops
  1. 主程序(main.py)
点击查看代码
import argparse
import os
from Expression import Expression
from Fraction import Fraction

def generate_exercises(num, range_val):
    exercises = []
    answers = []
    while len(exercises) < num:
        expr = Expression(range_val)
        expr.generate_expression()
        duplicate = False
        for e in exercises:
            if expr.is_duplicate(e):
                duplicate = True
                break
        if not duplicate:
            exercises.append(expr)
            answers.append(expr.result.toString())
    with open("Exercises.txt", "w", encoding="utf-8") as f:
        for i, e in enumerate(exercises, 1):
            f.write(f"{e.expr_str} =\n")
    with open("Answers.txt", "w", encoding="utf-8") as f:
        for a in answers:
            f.write(f"{a}\n")

def parse_expression(expr_str):
    expr_str = expr_str.replace(" ", "")
    def tokenize(s):
        tokens = []
        i = 0
        while i < len(s):
            if s[i] in "()+-×÷":
                tokens.append(s[i])
                i += 1
            elif s[i].isdigit() or s[i] in "’/":
                j = i
                while j < len(s) and (s[j].isdigit() or s[j] in "’/"):
                    j += 1
                tokens.append(s[i:j])
                i = j
            else:
                i += 1
        return tokens

    tokens = tokenize(expr_str)
    def parse(tokens):
        def parse_primary():
            token = tokens.pop(0)
            if token == "(":
                expr = parse(tokens)
                if tokens.pop(0) != ")":
                    raise SyntaxError("Mismatched parentheses")
                return expr
            else:
                return Fraction.fromString(token)

        def parse_term():
            left = parse_primary()
            while tokens and tokens[0] in "×÷":
                op = tokens.pop(0)
                right = parse_primary()
                if op == "×":
                    left = left.multiply(right)
                else:
                    left = left.divide(right)
            return left

        left = parse_term()
        while tokens and tokens[0] in "+-":
            op = tokens.pop(0)
            right = parse_term()
            if op == "+":
                left = left.add(right)
            else:
                left = left.subtract(right)
        return left

    return parse(tokens)

def grade_exercises(exercise_file, answer_file):
    with open(exercise_file, "r", encoding="utf-8") as f:
        exercises = [line.strip().replace(" =", "") for line in f if line.strip()]
    with open(answer_file, "r", encoding="utf-8") as f:
        answers = [line.strip() for line in f if line.strip()]
    
    correct = []
    wrong = []
    for i in range(min(len(exercises), len(answers))):
        try:
            calculated = parse_expression(exercises[i])
            expected = Fraction.fromString(answers[i])
            if calculated == expected:
                correct.append(i + 1)
            else:
                wrong.append(i + 1)
        except:
            wrong.append(i + 1)
    
    with open("Grade.txt", "w", encoding="utf-8") as f:
        f.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
        f.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="小学四则运算题目生成程序")
    parser.add_argument("-n", type=int, help="生成题目的个数")
    parser.add_argument("-r", type=int, help="数值范围(必须提供)")
    parser.add_argument("-e", type=str, help="题目文件路径")
    parser.add_argument("-a", type=str, help="答案文件路径")
    args = parser.parse_args()

    if args.e and args.a:
        grade_exercises(args.e, args.a)
    elif args.n and args.r:
        generate_exercises(args.n, args.r)
    else:
        parser.error("请提供正确的参数组合(-n和-r,或-e和-a)")

四、测试用例

1.功能测试:基本生成

  • 命令:python main.py -n 5 -r 5
  • 预期:生成 5 道 5 以内的题目,无负数、除法结果为真分数,运算符≤3 个,无重复。
    2.边界测试:r=1
  • 命令:python main.py -n 3 -r 1
  • 预期:仅能生成自然数 0 的运算(如0 + 0 =、0 × 0 =)。
    3.去重测试
  • 生成题目后,检查Exercises.txt中无2+3=和3+2=同时出现的情况。
    4.批改功能测试
  • 手动修改Answers.txt中的答案,运行python main.py -e Exercises.txt -a Answers.txt,检查Grade.txt统计是否正确。
    5.大规模测试
  • 命令:python main.py -n 10000 -r 20
  • 预期:成功生成 1 万道题,无内存溢出,文件写入正常。
posted on 2025-10-21 18:07  kohdia  阅读(8)  评论(0)    收藏  举报