Skip to content

Instantly share code, notes, and snippets.

@dkhenry
Created February 28, 2025 15:18
Show Gist options
  • Save dkhenry/08d5cec4c2f50495012fc7c6dbc75c51 to your computer and use it in GitHub Desktop.
Save dkhenry/08d5cec4c2f50495012fc7c6dbc75c51 to your computer and use it in GitHub Desktop.
#!/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