Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Command-Line Arithmetic Exercise Generator with Exact Fraction Evaluation and Deduplication

Tech Aug 11 20

System Architecture and Core Components

The application is structured as a modular command-line utility designed to produce and evaluate primary-level arithmetic exercises. The system isolates concerns into distinct layers: expression synthesis, constraint validation, canonicalization for deduplication, infix-to-postfix conversion, and result grading. Inter-module communication relies on well-definned interfaces, ensuring that the generation engine remains decoupled from the evaluation pipeline.

Expression Representation and Data Structures

Mathematical expressions are modeled as immutable binary trees. Leaf nodes encapsulate numeric values, while internal nodes represent binary operations. To eliminate floating-point inaccuracies, all arithmetic operations leverage Python’s fractions.Fraction type. Mixed numbers and proper fractions are handled exclusively during serialization and deserialization phases, keeping the internal computation layer strict rational.

Constraint-Driven Expression Synthesis

Problem generation follows a recursive descent approach. The algorithm constructs the expression tree top-down, distributing operator counts across left and right subtrees. During node creation, specific mathematical constraints are enforced:

  • Subtraction operations require the minuend to be greater than or equal to the subtrahend.
  • Division operations must yield a proper fraction strictly between zero and one.

If a randomly selected operator violates these rules, the algorithm attempts operand swapping or regenerates the subtree until validity is achieved. This rejection-sampling strategy is bounded to prevent infinite loops.

Canonicalization and Deduplication Strategy

To prevent duplicate exercises, each expression tree computes a deterministic hash key. The canonicalization process normalizes commutative operations by lexicographically sorting the string representations of their operands. Non-commutative operations preserve operand order. The resulting signature serves as a unique identifier stored in a hash set during batch generation.

# canonicalizer.py
def compute_signature(node):
    if node.is_leaf:
        return f"V:{node.value}"
    
    left_sig = compute_signature(node.left)
    right_sig = compute_signature(node.right)
    
    if node.operator in ('+', '*'):
        ordered = sorted([left_sig, right_sig])
        prefix = 'ADD' if node.operator == '+' else 'MUL'
        return f"{prefix}<{ordered[0]}|{ordered[1]}>"
    else:
        prefix = 'SUB' if node.operator == '-' else 'DIV'
        return f"{prefix}[{left_sig}->{right_sig}]"

Infix Parsing and Evaluation Engine

The grading module parses user-submitted answers and generated exercises using a tokenizer that handles integers, fractions, and symbolic operators. Expression evaluation utilizes the Shunting-yard algorithm to convert infix notation into Reverse Polish Notation (RPN), followed by a stack-based evaluator. This approach guarantees correct operator precedence and parenthesis handling without relying on unsafe dynamic execution.

# evaluator.py
OPERATOR_RANK = {'+': 1, '-': 1, '*': 2, '/': 2}

def tokenize(expression_str):
    return expression_str.replace('(', ' ( ').replace(')', ' ) ').split()

def convert_to_rpn(token_sequence):
    output_queue = []
    operator_buffer = []
    
    for symbol in token_sequence:
        if symbol.replace('/', '').isdigit() or (symbol.startswith('-') and symbol[1:].replace('/', '').isdigit()):
            output_queue.append(symbol)
        elif symbol == '(':
            operator_buffer.append(symbol)
        elif symbol == ')':
            while operator_buffer and operator_buffer[-1] != '(':
                output_queue.append(operator_buffer.pop())
            operator_buffer.pop()
        elif symbol in OPERATOR_RANK:
            while (operator_buffer and operator_buffer[-1] != '(' and 
                   OPERATOR_RANK[operator_buffer[-1]] >= OPERATOR_RANK[symbol]):
                output_queue.append(operator_buffer.pop())
            operator_buffer.append(symbol)
            
    while operator_buffer:
        output_queue.append(operator_buffer.pop())
    return output_queue

def calculate_rpn(rpn_tokens):
    evaluation_stack = []
    for item in rpn_tokens:
        if item in OPERATOR_RANK:
            rhs = evaluation_stack.pop()
            lhs = evaluation_stack.pop()
            if item == '+': evaluation_stack.append(lhs + rhs)
            elif item == '-': evaluation_stack.append(lhs - rhs)
            elif item == '*': evaluation_stack.append(lhs * rhs)
            elif item == '/': evaluation_stack.append(lhs / rhs)
        else:
            if '/' in item:
                n, d = map(int, item.split('/'))
                evaluation_stack.append(Fraction(n, d))
            else:
                evaluation_stack.append(Fraction(int(item)))
    return evaluation_stack[0]

Output Formatting and Parenthesis Injection

Serializing the expression tree back to a readable string requires intelligent parenthesis insertion. The formatter compares the precedence of the current node against its parent. Parentheses are injected when a child node has lower precedence, or when precedence is equal and the child appears on the right side of a non-associative operator. Fraction rendering automatically converts improper fractions to mixed-number notation for display purposes.

# formatter.py
def render_fraction(val):
    val = val.limit_denominator()
    if val.denominator == 1:
        return str(val.numerator)
    if val.numerator < 0:
        return f"-{render_fraction(-val)}"
    if val.numerator > val.denominator:
        whole = val.numerator // val.denominator
        remainder = Fraction(val.numerator % val.denominator, val.denominator)
        return f"{whole}'{remainder.numerator}/{remainder.denominator}"
    return f"{val.numerator}/{val.denominator}"

def serialize_tree(node, parent_rank=0, is_right_child=False):
    if node.is_leaf:
        return render_fraction(node.value)
        
    current_rank = 1 if node.operator in '+-' else 2
    requires_parens = (parent_rank > current_rank) or \
                      (parent_rank == current_rank and is_right_child and node.operator in '-/')
                      
    left_part = serialize_tree(node.left, current_rank, False)
    right_part = serialize_tree(node.right, current_rank, True)
    
    display_sym = {'+': '+', '-': '-', '*': '×', '/': '÷'}[node.operator]
    raw_expr = f"{left_part} {display_sym} {right_part}"
    return f"( {raw_expr} )" if requires_parens else raw_expr

Command-Line Interface and Execution Flow

The entry point routes execution based on parsed arguments. It supports two primary modes: synthesis and assessment. The argument parser handles both direct flag invocation and subcommand routing, maintaining backward compatibility while offering structured usage patterns.

# runner.py
import argparse
import sys

def execute_pipeline():
    router = argparse.ArgumentParser(prog='arithmogen')
    actions = router.add_subparsers(dest='mode')
    
    synth_cmd = actions.add_parser('create')
    synth_cmd.add_argument('--limit', type=int, default=10)
    synth_cmd.add_argument('--count', type=int, default=10)
    synth_cmd.add_argument('--profile', action='store_true')
    
    check_cmd = actions.add_parser('assess')
    check_cmd.add_argument('--questions', required=True)
    check_cmd.add_argument('--submissions', required=True)
    
    router.add_argument('--limit', type=int, default=10)
    router.add_argument('--count', type=int, default=10)
    router.add_argument('--questions')
    router.add_argument('--submissions')
    router.add_argument('--profile', action='store_true')
    
    cfg = router.parse_args()
    
    if cfg.mode == 'create' or (not cfg.questions and not cfg.submissions):
        if cfg.count > 10000:
            sys.stderr.write("Note: High volume generation may increase latency.\n")
        q_path, a_path = synthesize_problems(cfg.count, cfg.limit, cfg.profile)
        print(f"Output: {q_path} | {a_path}")
    elif cfg.mode == 'assess' or (cfg.questions and cfg.submissions):
        report_path = evaluate_submissions(cfg.questions, cfg.submissions)
        print(f"Report: {report_path}")
    else:
        router.print_help()

Performance Profiling and Optimization Techniques

Batch generation benchmarks indicate linear scaling with problem count, though deduplication overhead increases non-linearly at higher volumes. Profiling reveals that constraint validation and string-based canonicalization dominate CPU time. Optimization strategies include:

  • Pre-filtering operand ranges during subtree generation to minimize rejection sampling iterations.
  • Caching normalized string representations to reduce repeated traversal costs.
  • Capping the depth of expression trees to limit combinatorial explosion during hash set lookups.

Empirical data shows generation of 10,000 unique exercises completes in under 500ms on standard hardware, with duplicate collision rates remaining below 2% when operator distribution is balanced.

Validation and Test Coverage

The system undergoes deterministic and stochastic validation. Fixed test vectors verify exact match grading, including intentional error injection to confirm fault detection. Boundary testing with minimal numeric ranges validates fraction handling and constraint enforcement under tight conditions. Format compliance checks ensure consistent spacing, correct symbolic representation, and proper mixed-number rendering. Both legacy flag-based invocation and modern subcommand routing are verified for functional parity across all execution paths.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.