"""
Fractions Mastery (Console App)
-------------------------------
A long, educational Python program for learning fractions.
Features:
- Fraction basics (simplify, compare)
- Convert improper <-> mixed numbers
- Operations: +, -, *, /
- Random practice questions (levels)
- Option for mixed-number answers
- Score + time tracking
- Mistake review
- Save sessions to JSON
Educational purpose only.
"""
from __future__ import annotations
import random
import time
import json
from dataclasses import dataclass, asdict
from fractions import Fraction
from pathlib import Path
from typing import List, Optional, Tuple
import math
# -----------------------------
# Data Models
# -----------------------------
@dataclass
class Q:
prompt: str
correct_value: Fraction
user_value: Optional[Fraction] = None
correct: Optional[bool] = None
time_taken_s: Optional[float] = None
def finalize(self, user_value: Fraction, elapsed: float) -> None:
self.user_value = user_value
[Link] = (user_value == self.correct_value)
self.time_taken_s = elapsed
@dataclass
class Session:
mode: str
difficulty: str
started_at: float
ended_at: float
total_questions: int
correct: int
wrong: int
accuracy_percent: float
avg_time_s: float
questions: List[Q]
# -----------------------------
# Helpers
# -----------------------------
def fmt_time(s: float) -> str:
if s < 60:
return f"{s:.2f}s"
m = int(s // 60)
r = s % 60
return f"{m}m {r:.2f}s"
def read_choice(prompt: str, choices: List[str]) -> str:
m = {[Link](): c for c in choices}
while True:
raw = input(prompt).strip().lower()
if raw in m:
return m[raw]
print(f"Choose one of: {', '.join(choices)}")
def read_int(prompt: str, min_val: int | None = None, max_val: int | None = None) -> int:
while True:
raw = input(prompt).strip()
try:
v = int(raw)
except ValueError:
print("Enter a valid integer.")
continue
if min_val is not None and v < min_val:
print(f"Must be >= {min_val}.")
continue
if max_val is not None and v > max_val:
print(f"Must be <= {max_val}.")
continue
return v
def clear() -> None:
print("\n" * 60)
# -----------------------------
# Fraction formatting
# -----------------------------
def frac_to_mixed(f: Fraction) -> Tuple[int, Fraction]:
"""Return (whole, remainder_fraction_positive)."""
sign = -1 if f < 0 else 1
f = abs(f)
whole = [Link] // [Link]
rem = Fraction([Link] % [Link], [Link])
return sign * whole, rem
def format_fraction(f: Fraction, style: str = "simple") -> str:
"""
style:
- simple: 'a/b' or '0' or '3'
- mixed: 'w r/d' if improper
"""
if style == "mixed":
if [Link] == 1:
return str([Link])
w, r = frac_to_mixed(f)
if abs(f) < 1:
return f"{[Link]}/{[Link]}"
if r == 0:
return str(w)
# handle negative mixed numbers: -2 1/3
sign = "-" if f < 0 else ""
return f"{sign}{abs(w)} {[Link]}/{[Link]}"
else:
# simple
if [Link] == 1:
return str([Link])
return f"{[Link]}/{[Link]}"
# -----------------------------
# Parsing user answers
# -----------------------------
def parse_fraction_input(s: str) -> Fraction:
"""
Accepts:
- "a/b" (e.g., 3/4, -10/7)
- "w a/b" mixed (e.g., 2 1/3, -2 1/3)
- integer (e.g., 5, -1)
- decimal (e.g., 0.25) -> converted to Fraction exactly as float string via Fraction(s)
"""
s = [Link]()
if not s:
raise ValueError("Empty input")
# Mixed number: "w a/b"
parts = [Link]()
if len(parts) == 2 and "/" in parts[1]:
w = int(parts[0])
num, den = parts[1].split("/", 1)
num_i = int(num)
den_i = int(den)
if den_i == 0:
raise ValueError("Denominator cannot be 0")
rem = Fraction(num_i, den_i)
if rem < 0:
raise ValueError("Use positive remainder in mixed form (e.g., -2 1/3, not -2 -1/3).")
if w < 0:
return Fraction(w, 1) - rem
return Fraction(w, 1) + rem
# Simple fraction "a/b"
if "/" in s:
num, den = [Link]("/", 1)
num_i = int(num)
den_i = int(den)
if den_i == 0:
raise ValueError("Denominator cannot be 0")
return Fraction(num_i, den_i)
# Integer or decimal
try:
return Fraction(int(s), 1)
except ValueError:
# decimal string
return Fraction(s)
def ask_fraction(prompt: str) -> Fraction:
while True:
raw = input(prompt).strip()
try:
return parse_fraction_input(raw)
except Exception as e:
print(f"Invalid fraction input: {e}")
print("Examples: 3/4 | 2 1/3 | -5/2 | 7 | 0.25")
# -----------------------------
# Generators
# -----------------------------
def difficulty_bounds(level: str) -> Tuple[int, int]:
if level == "easy":
return 1, 9
if level == "medium":
return 1, 15
if level == "hard":
return 1, 30
if level == "insane":
return 1, 60
return 1, 12
def random_fraction(level: str, *, proper: bool = False, allow_negative: bool = False) ->
Fraction:
lo, hi = difficulty_bounds(level)
den = [Link](lo, hi)
num = [Link](lo, hi)
if proper:
num = [Link](0, den - 1) if den > 1 else 0
if allow_negative and [Link]() < 0.30:
num = -num
# Ensure non-zero denominator
return Fraction(num, den)
def make_operation_question(level: str, op: str) -> Tuple[str, Fraction]:
a = random_fraction(level, proper=False, allow_negative=False)
b = random_fraction(level, proper=False, allow_negative=False)
# Avoid division by 0
if op == "/":
while b == 0:
b = random_fraction(level, proper=False, allow_negative=False)
expr = f"{format_fraction(a)} {op} {format_fraction(b)}"
if op == "+":
ans = a + b
elif op == "-":
ans = a - b
elif op == "*":
ans = a * b
elif op == "/":
ans = a / b
else:
raise ValueError("Bad op")
return expr, ans
def make_compare_question(level: str) -> Tuple[str, Fraction]:
a = random_fraction(level, proper=False, allow_negative=False)
b = random_fraction(level, proper=False, allow_negative=False)
# We store answer as a Fraction but for compare we’ll ask user to type 1, 0, -1
# 1 => a > b
# 0 => a == b
# -1 => a < b
if a > b:
correct = Fraction(1, 1)
symbol = " ? "
elif a < b:
correct = Fraction(-1, 1)
symbol = " ? "
else:
correct = Fraction(0, 1)
symbol = " ? "
prompt = (
f"Compare: {format_fraction(a)} {symbol} {format_fraction(b)}\n"
"Type 1 if left>right, 0 if equal, -1 if left<right: "
)
return prompt, correct
def make_simplify_question(level: str) -> Tuple[str, Fraction]:
lo, hi = difficulty_bounds(level)
# Make a reducible fraction by multiplying numerator and denominator
base = random_fraction(level, proper=False, allow_negative=False)
k = [Link](2, max(2, min(10, hi)))
f = Fraction([Link] * k, [Link] * k)
prompt = f"Simplify: {[Link]}/{[Link]} = "
return prompt, base # simplified is base
# -----------------------------
# Quiz Runner
# -----------------------------
def run_quiz(mode: str, difficulty: str, n: int, answer_style: str) -> Session:
started = [Link]()
questions: List[Q] = []
correct = 0
wrong = 0
total_time = 0.0
clear()
print("Fractions Quiz")
print("==============")
print(f"Mode: {mode} | Difficulty: {difficulty} | Questions: {n} | Answer style:
{answer_style}\n")
for i in range(1, n + 1):
if mode == "operations":
op = [Link](["+", "-", "*", "/"])
expr, ans = make_operation_question(difficulty, op)
prompt = f"Q{i}/{n}: {expr} = "
q = Q(prompt=expr, correct_value=ans)
t0 = [Link]()
user = ask_fraction(prompt)
elapsed = [Link]() - t0
[Link](user, elapsed)
total_time += elapsed
✅
if [Link]:
print(" Correct!\n")
correct += 1
❌
else:
print(f" Wrong. Correct: {format_fraction(q.correct_value, answer_style)}\n")
wrong += 1
[Link](q)
elif mode == "simplify":
prompt, ans = make_simplify_question(difficulty)
q = Q(prompt=prompt, correct_value=ans)
t0 = [Link]()
user = ask_fraction(prompt)
elapsed = [Link]() - t0
[Link](user, elapsed)
total_time += elapsed
✅
if [Link]:
print(" Correct!\n")
correct += 1
❌
else:
print(f" Wrong. Correct: {format_fraction(q.correct_value, answer_style)}\n")
wrong += 1
[Link](q)
elif mode == "compare":
prompt, ans = make_compare_question(difficulty)
q = Q(prompt="compare", correct_value=ans)
t0 = [Link]()
val = read_int(prompt, min_val=-1, max_val=1)
elapsed = [Link]() - t0
user = Fraction(val, 1)
[Link](user, elapsed)
total_time += elapsed
✅
if [Link]:
print(" Correct!\n")
correct += 1
else:
# show actual relation
# reconstruct? simplest: compute from correct_value
if q.correct_value == 1:
rel = "left > right"
elif q.correct_value == -1:
rel = "left < right"
else:
❌
rel = "left == right"
print(f" Wrong. Correct relation: {rel}\n")
wrong += 1
[Link](q)
else:
raise ValueError("Unknown mode")
ended = [Link]()
accuracy = (correct / n) * 100.0 if n else 0.0
avg_time = (total_time / n) if n else 0.0
return Session(
mode=mode,
difficulty=difficulty,
started_at=started,
ended_at=ended,
total_questions=n,
correct=correct,
wrong=wrong,
accuracy_percent=accuracy,
avg_time_s=avg_time,
questions=questions,
)
# -----------------------------
# Reports + Saving
# -----------------------------
def print_summary(s: Session) -> None:
print("\nSession Summary")
print("===============")
print(f"Mode: {[Link]}")
print(f"Difficulty: {[Link]}")
print(f"Questions: {s.total_questions}")
print(f"Correct: {[Link]}")
print(f"Wrong: {[Link]}")
print(f"Accuracy: {s.accuracy_percent:.2f}%")
print(f"Avg time/question: {fmt_time(s.avg_time_s)}")
print(f"Total duration: {fmt_time(s.ended_at - s.started_at)}")
def print_mistakes(s: Session, answer_style: str) -> None:
mistakes = [q for q in [Link] if [Link] is False]
🔥
if not mistakes:
print("\nNo mistakes — you cooked ")
return
print("\nMistakes Review")
print("==============")
for i, q in enumerate(mistakes, start=1):
if [Link] == "compare":
# user_value is -1/0/1
uv = int(q.user_value) if q.user_value is not None else None
cv = int(q.correct_value)
print(f"{i:>2}. Compare question | you: {uv} | correct: {cv}")
else:
uv = format_fraction(q.user_value, answer_style) if q.user_value is not None else "?"
cv = format_fraction(q.correct_value, answer_style)
print(f"{i:>2}. {[Link]} | you: {uv} | correct: {cv} | time: {fmt_time(q.time_taken_s or
0)}")
def save_session(s: Session, filepath: str = "fractions_session.json") -> Path:
payload = {
"mode": [Link],
"difficulty": [Link],
"started_at": s.started_at,
"ended_at": s.ended_at,
"total_questions": s.total_questions,
"correct": [Link],
"wrong": [Link],
"accuracy_percent": s.accuracy_percent,
"avg_time_s": s.avg_time_s,
"questions": [
{
"prompt": [Link],
"correct_value": str(q.correct_value),
"user_value": str(q.user_value) if q.user_value is not None else None,
"correct": [Link],
"time_taken_s": q.time_taken_s,
}
for q in [Link]
],
}
path = Path(filepath)
path.write_text([Link](payload, indent=2), encoding="utf-8")
return path
# -----------------------------
# Learning Tools (Non-quiz)
# -----------------------------
def show_fraction_table(denominator: int, upto_numerator: int = 12) -> None:
print(f"\nFractions with denominator {denominator}")
print("-" * 35)
for n in range(1, upto_numerator + 1):
f = Fraction(n, denominator)
print(f"{n}/{denominator} = {float(f):.4f} (simplified: {format_fraction(f)})")
print("-" * 35)
def demo_operations() -> None:
print("\nQuick Demo (Fractions)")
print("----------------------")
a = Fraction(3, 4)
b = Fraction(5, 6)
print(f"a = {a}, b = {b}")
print(f"a + b = {a + b} = {float(a + b):.4f}")
print(f"a - b = {a - b} = {float(a - b):.4f}")
print(f"a * b = {a * b} = {float(a * b):.4f}")
print(f"a / b = {a / b} = {float(a / b):.4f}")
print()
# -----------------------------
# Menu
# -----------------------------
def menu() -> None:
while True:
print("\nFractions Mastery")
print("=================")
print("1) Fraction table (fixed denominator)")
print("2) Demo operations")
print("3) Quiz: operations (+ - * /)")
print("4) Quiz: simplify fractions")
print("5) Quiz: compare fractions")
print("6) Exit")
c = read_choice("Choose (1-6): ", ["1", "2", "3", "4", "5", "6"])
if c == "1":
d = read_int("Denominator (1-50): ", min_val=1, max_val=50)
upto = read_int("Max numerator (1-50): ", min_val=1, max_val=50)
clear()
show_fraction_table(d, upto_numerator=upto)
elif c == "2":
clear()
demo_operations()
elif c in ("3", "4", "5"):
difficulty = read_choice("Difficulty (easy/medium/hard/insane): ", ["easy", "medium",
"hard", "insane"])
n = read_int("Number of questions (1-100): ", min_val=1, max_val=100)
answer_style = read_choice("Answer format (simple/mixed): ", ["simple", "mixed"])
if c == "3":
s = run_quiz(mode="operations", difficulty=difficulty, n=n,
answer_style=answer_style)
elif c == "4":
s = run_quiz(mode="simplify", difficulty=difficulty, n=n, answer_style=answer_style)
else:
s = run_quiz(mode="compare", difficulty=difficulty, n=n,
answer_style=answer_style)
print_summary(s)
print_mistakes(s, answer_style)
if read_choice("Save session? (y/n): ", ["y", "n"]) == "y":
path = save_session(s)
print(f"Saved to: {[Link]()}")
👋")
else:
print("Bye
return
if __name__ == "__main__":
[Link]()
menu()