Created
February 28, 2025 15:18
-
-
Save dkhenry/08d5cec4c2f50495012fc7c6dbc75c51 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import random | |
import os | |
import subprocess | |
from math import gcd | |
from fractions import Fraction | |
# LaTeX template components | |
LATEX_HEADER = r"""\documentclass[12pt]{article} | |
\usepackage[utf8]{inputenc} | |
\usepackage{geometry} | |
\geometry{a4paper, margin=1in} | |
\usepackage{array} | |
\usepackage{calc} | |
\usepackage{amsmath} | |
\usepackage{xcolor} | |
\title{} | |
\author{} | |
\date{} | |
\begin{document} | |
\begin{minipage}[t][0.2\textheight]{\textwidth} | |
\noindent | |
\colorbox{gray!30}{\parbox{\dimexpr\textwidth-2\fboxsep\relax}{\centering \large \textbf{FACTS PRACTICE TEST}}}\\[3mm] | |
\begin{flushright} | |
\begin{tabular}{r} | |
Name: \rule{2.5in}{0.4pt} \\ | |
Date: \rule{2.5in}{0.4pt} \\ | |
\end{tabular} | |
\end{flushright} | |
\vspace{2mm} | |
\centering | |
\small \textbf{Instructions:} Solve the problems. Write your answers below the lines. | |
\end{minipage} | |
\begin{minipage}[t][0.8\textheight]{\textwidth} | |
\vspace{5mm} | |
\newlength{\gridwidth} | |
\setlength{\gridwidth}{0.9\textwidth} | |
\newlength{\cellwidth} | |
\setlength{\cellwidth}{\gridwidth / 5} | |
\newlength{\gridheight} | |
\setlength{\gridheight}{0.75\textheight} | |
\newlength{\cellheight} | |
\setlength{\cellheight}{\gridheight / 6} | |
\newcommand{\vadd}[2]{% | |
\begin{minipage}[c][\cellheight][c]{\cellwidth} | |
\centering | |
\(\begin{array}{r} #1 \\ #2 \\ \hline \end{array}\) \\[1mm] | |
\vspace{3mm} | |
\end{minipage}% | |
} | |
\newcommand{\vfrac}[2]{% | |
\begin{minipage}[c][\cellheight][c]{\cellwidth} | |
\centering | |
\( #1 \, #2 \) \\[1mm] | |
\vspace{3mm} | |
\end{minipage}% | |
} | |
\newcommand{\vlongdiv}[2]{% | |
\begin{minipage}[c][\cellheight][c]{\cellwidth} | |
\centering | |
\( #1 \, \overline{) \, #2} \) \\[1mm] | |
\vspace{3mm} | |
\end{minipage}% | |
} | |
\begin{center} | |
\begin{tabular}{|c|c|c|c|c|} | |
\hline | |
""" | |
LATEX_FOOTER_NO_ANSWER = r""" \end{tabular} | |
\end{center} | |
\end{minipage} | |
\end{document} | |
""" | |
LATEX_FOOTER_WITH_ANSWER = r""" \end{tabular} | |
\end{center} | |
\end{minipage} | |
\newpage | |
\begin{center} | |
\large \textbf{Answer Key} (For Teachers Only) | |
\end{center} | |
\vspace{5mm} | |
\begin{center} | |
% No fixed-width boxes, just a flexible layout | |
\scriptsize | |
""" | |
LATEX_END = r""" | |
\end{center} | |
\end{document} | |
""" | |
def generate_integer_problem(operation, difficulty): | |
"""Generate an integer math problem based on operation and difficulty.""" | |
if difficulty == "easy": | |
min_val, max_val = 10, 99 | |
elif difficulty == "medium": | |
min_val, max_val = 100, 999 | |
else: # hard | |
min_val, max_val = 1000, 9999 | |
a = random.randint(min_val, max_val) | |
b = random.randint(min_val, max_val) | |
if operation == "addition": | |
result = a + b | |
symbol = "+" | |
elif operation == "subtraction": | |
a, b = max(a, b), min(a, b) | |
result = a - b | |
symbol = "-" | |
elif operation == "multiplication": | |
if difficulty == "easy": | |
a, b = random.randint(2, 9), random.randint(2, 9) | |
elif difficulty == "medium": | |
a, b = random.randint(10, 99), random.randint(2, 9) | |
else: | |
a, b = random.randint(10, 99), random.randint(10, 99) | |
result = a * b | |
symbol = " \\times " | |
else: # division | |
if difficulty == "easy": | |
quotient = random.randint(2, 9) | |
divisor = random.randint(2, 9) | |
elif difficulty == "medium": | |
quotient = random.randint(10, 99) | |
divisor = random.randint(2, 9) | |
else: | |
quotient = random.randint(10, 99) | |
divisor = random.randint(10, 99) | |
a = quotient * divisor | |
b = divisor | |
result = quotient | |
symbol = " \\div " | |
return a, symbol, b, result | |
def generate_fraction_problem(operation, difficulty): | |
"""Generate a fraction math problem with student-friendly properties.""" | |
if difficulty == "easy": | |
max_den = 10 | |
max_num = 9 | |
elif difficulty == "medium": | |
max_den = 20 | |
max_num = 19 | |
else: # hard | |
max_den = 50 | |
max_num = 49 | |
if operation in ["addition", "subtraction"]: | |
den = random.randint(2, max_den) | |
num1 = random.randint(1, min(max_num, den - 1)) | |
num2 = random.randint(1, min(max_num, den - 1)) | |
frac1 = Fraction(num1, den) | |
frac2 = Fraction(num2, den) | |
if operation == "addition": | |
result = frac1 + frac2 | |
symbol = "+" | |
else: | |
if frac1 < frac2: | |
frac1, frac2 = frac2, frac1 | |
result = frac1 - frac2 | |
symbol = "-" | |
else: # multiplication or division | |
den1 = random.randint(2, max_den) | |
num1 = random.randint(1, min(max_num, den1 - 1)) | |
den2 = random.randint(2, max_den) | |
num2 = random.randint(1, min(max_num, den2 - 1)) | |
frac1 = Fraction(num1, den1) | |
frac2 = Fraction(num2, den2) | |
if operation == "multiplication": | |
result = frac1 * frac2 | |
symbol = "\\times" | |
else: | |
result = frac1 / frac2 | |
symbol = "\\div" | |
result_num = result.numerator | |
result_den = result.denominator | |
frac1_str = f"\\frac{{{frac1.numerator}}}{{{frac1.denominator}}}" | |
frac2_str = f"\\frac{{{frac2.numerator}}}{{{frac2.denominator}}}" | |
result_str = f"\\frac{{{result_num}}}{{{result_den}}}" if result_den != 1 else str(result_num) | |
return frac1_str, symbol, frac2_str, result_str | |
def generate_longdivision_problem(difficulty): | |
"""Generate a long division problem with divisor and dividend.""" | |
if difficulty == "easy": | |
quotient_range = (2, 9) | |
divisor_range = (2, 9) | |
elif difficulty == "medium": | |
quotient_range = (10, 99) | |
divisor_range = (2, 9) | |
else: # hard | |
quotient_range = (10, 99) | |
divisor_range = (10, 99) | |
quotient = random.randint(*quotient_range) | |
divisor = random.randint(*divisor_range) | |
dividend = quotient * divisor # No remainder for simplicity | |
return divisor, dividend, quotient | |
def generate_worksheet(operation, difficulty, fractional=False, longdivision=False, answer_key=False): | |
"""Generate LaTeX content for one worksheet.""" | |
problems = [] | |
answers = [] | |
for _ in range(30): # 6 rows x 5 columns = 30 problems | |
if longdivision: | |
divisor, dividend, result = generate_longdivision_problem(difficulty) | |
problems.append(f"\\vlongdiv{{{divisor}}}{{{dividend}}}") | |
answers.append(f"${result}$ \\quad ${divisor} \\overline{{) {dividend}}}$") | |
elif fractional: | |
a, symbol, b, result = generate_fraction_problem(operation, difficulty) | |
problems.append(f"\\vfrac{{{a}}}{{{symbol} {b}}}") | |
answers.append(f"${a} {symbol} {b} = {result}$") | |
else: | |
a, symbol, b, result = generate_integer_problem(operation, difficulty) | |
problems.append(f"\\vadd{{{a}}}{{{symbol} {b}}}") | |
if operation in ["addition", "subtraction"]: | |
answers.append(f"$\\begin{{array}}{{r}} {a} \\\\ {symbol} {b} \\\\ \\hline {result} \\end{{array}}$") | |
else: | |
answers.append(f"${a} {symbol} {b} = {result}$") | |
# Format problem rows (with boxes) | |
problem_rows = [] | |
for i in range(0, 30, 5): | |
problem_rows.append(" & ".join(problems[i:i+5]) + " \\\\ \\hline") | |
# Format answer rows (no boxes, 6 rows of 5) | |
answer_rows = [] | |
if answer_key: | |
for i in range(0, 30, 5): | |
answer_row = " \\quad ".join(answers[i:i+5]) + " \\\\[3mm]" | |
answer_rows.append(answer_row) | |
if answer_key: | |
return "\n".join(problem_rows), "\n".join(answer_rows) | |
return "\n".join(problem_rows), None | |
def write_worksheet(filename, operation, difficulty, fractional=False, longdivision=False, answer_key=False): | |
"""Write a single worksheet to a .tex file.""" | |
problem_content, answer_content = generate_worksheet(operation, difficulty, fractional, longdivision, answer_key) | |
with open(filename, "w") as f: | |
f.write(LATEX_HEADER) | |
f.write(problem_content) | |
if answer_key and answer_content: | |
f.write(LATEX_FOOTER_WITH_ANSWER) | |
f.write(answer_content) | |
else: | |
f.write(LATEX_FOOTER_NO_ANSWER) | |
def main(): | |
parser = argparse.ArgumentParser(description="Generate math worksheets in LaTeX.") | |
parser.add_argument("--operation", choices=["addition", "subtraction", "multiplication", "division", "longdivision"], | |
required=True, help="Type of math operation") | |
parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], | |
default="medium", help="Difficulty level (easy: small numbers, medium: moderate, hard: larger)") | |
parser.add_argument("--num", type=int, default=1, help="Number of worksheets to generate") | |
parser.add_argument("--fractional", action="store_true", help="Generate fractional problems (not applicable for longdivision)") | |
parser.add_argument("--answer-key", action="store_true", help="Include answer key in the worksheet") | |
parser.add_argument("--build", action="store_true", help="Compile LaTeX files to PDF") | |
args = parser.parse_args() | |
# Determine problem type | |
fractional = args.fractional and args.operation != "longdivision" | |
longdivision = args.operation == "longdivision" | |
if args.fractional and args.operation == "longdivision": | |
print("Warning: --fractional is ignored for longdivision operation.") | |
# Generate worksheets | |
for i in range(args.num): | |
filename = f"worksheet_{args.operation}_{args.difficulty}_{'frac' if fractional else 'longdiv' if longdivision else 'int'}_{i+1}.tex" | |
write_worksheet(filename, args.operation, args.difficulty, fractional, longdivision, args.answer_key) | |
print(f"Generated {filename}") | |
# Compile to PDF if --build is specified | |
if args.build: | |
try: | |
subprocess.run(["latex", filename], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
subprocess.run(["dvipdfm", f"worksheet_{args.operation}_{args.difficulty}_{'frac' if fractional else 'longdiv' if longdivision else 'int'}_{i+1}.dvi"], | |
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
print(f"Compiled {filename} to PDF") | |
except subprocess.CalledProcessError as e: | |
print(f"Error compiling {filename}: {e}") | |
finally: | |
for ext in [".aux", ".log", ".dvi"]: | |
try: | |
os.remove(f"worksheet_{args.operation}_{args.difficulty}_{'frac' if fractional else 'longdiv' if longdivision else 'int'}_{i+1}{ext}") | |
except OSError: | |
pass | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment