| from __future__ import annotations |
|
|
| import math |
| import re |
| from typing import Optional, List |
|
|
| from models import SolverResult |
|
|
|
|
| def _nums(text: str) -> List[float]: |
| return [float(x) for x in re.findall(r"-?\d+(?:\.\d+)?", text)] |
|
|
|
|
| def _clean(text: str) -> str: |
| return re.sub(r"\s+", " ", (text or "").strip()).lower() |
|
|
|
|
| def _pct_to_decimal(p: float) -> float: |
| return p / 100.0 |
|
|
|
|
| def _safe_fmt(x: float) -> str: |
| return f"{x:g}" |
|
|
|
|
| def _build_result( |
| *, |
| topic: str, |
| internal_answer: Optional[float] = None, |
| ask: str, |
| given: str, |
| method: List[str], |
| ) -> SolverResult: |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic=topic, |
| answer_value=None, |
| internal_answer=_safe_fmt(internal_answer) if internal_answer is not None else None, |
| steps=[ |
| f"What the question is asking: {ask}", |
| f"What is given: {given}", |
| *method, |
| ], |
| ) |
|
|
|
|
| def _fallback_percent() -> SolverResult: |
| return SolverResult( |
| domain="quant", |
| solved=False, |
| topic="percent", |
| answer_value=None, |
| internal_answer=None, |
| steps=[ |
| "What the question is asking: identify whether you need the part, the whole, the percent, or the percent change.", |
| "Key relationships:", |
| "• part = percent × whole", |
| "• percent = part ÷ whole × 100", |
| "• percent change = change ÷ original × 100", |
| "Start by labeling the numbers in the problem as original, new, part, whole, rate, or time.", |
| ], |
| ) |
|
|
|
|
| def solve_percent(text: str) -> Optional[SolverResult]: |
| raw = text or "" |
| lower = _clean(raw) |
|
|
| percent_words = [ |
| "%", "percent", "percentage", "pct", |
| "increase", "decrease", "discount", "markup", |
| "profit", "loss", "interest", "percentile", |
| "tax", "tip", "sale" |
| ] |
| if not any(w in lower for w in percent_words): |
| return None |
|
|
| nums = _nums(lower) |
|
|
| |
| |
| |
| m = re.search( |
| r"what\s+is\s+(\d+(?:\.\d+)?)\s*(?:%|percent|pct)?\s+of\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| pct = float(m.group(1)) |
| whole = float(m.group(2)) |
| result = _pct_to_decimal(pct) * whole |
| return _build_result( |
| topic="percent", |
| internal_answer=result, |
| ask="Find the part when the percent and whole are known.", |
| given=f"percent = {pct}%, whole = {whole}", |
| method=[ |
| "Convert the percent to a decimal.", |
| "Use part = percent × whole.", |
| "Multiply the decimal percent by the whole.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| m = re.search( |
| r"(\d+(?:\.\d+)?)\s*(?:%|percent|pct)\s+of\s+(?:a\s+number|number|what\s+number|x|y|an?\s+amount|the\s+number).*?\bis\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| pct = float(m.group(1)) |
| part = float(m.group(2)) |
| dec = _pct_to_decimal(pct) |
| if dec == 0: |
| return _fallback_percent() |
| result = part / dec |
| return _build_result( |
| topic="percent", |
| internal_answer=result, |
| ask="Find the whole when the percent and part are known.", |
| given=f"percent = {pct}%, part = {part}", |
| method=[ |
| "Convert the percent to a decimal.", |
| "Use part = percent × whole.", |
| "Rearrange to whole = part ÷ percent.", |
| ], |
| ) |
|
|
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+is\s+(\d+(?:\.\d+)?)\s*(?:%|percent|pct)\s+of\s+(?:what|which)\s+(?:number|value|amount)", |
| lower |
| ) |
| if m: |
| part = float(m.group(1)) |
| pct = float(m.group(2)) |
| dec = _pct_to_decimal(pct) |
| if dec == 0: |
| return _fallback_percent() |
| result = part / dec |
| return _build_result( |
| topic="percent", |
| internal_answer=result, |
| ask="Find the original whole from a known part and percent.", |
| given=f"part = {part}, percent = {pct}%", |
| method=[ |
| "Convert the percent to decimal form.", |
| "Use part = percent × whole.", |
| "Solve for the whole by dividing the part by the decimal percent.", |
| ], |
| ) |
|
|
| |
| |
| |
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+is\s+what\s+(?:%|percent|percentage)\s+of\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| part = float(m.group(1)) |
| whole = float(m.group(2)) |
| if whole == 0: |
| return _fallback_percent() |
| result = (part / whole) * 100.0 |
| return _build_result( |
| topic="percent", |
| internal_answer=result, |
| ask="Find what percent one value is of another.", |
| given=f"part = {part}, whole = {whole}", |
| method=[ |
| "Use percent = part ÷ whole × 100.", |
| "Divide the part by the whole.", |
| "Convert the result to percent form.", |
| ], |
| ) |
|
|
| m = re.search( |
| r"what\s+(?:%|percent|percentage)\s+of\s+(-?\d+(?:\.\d+)?)\s+is\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| whole = float(m.group(1)) |
| part = float(m.group(2)) |
| if whole == 0: |
| return _fallback_percent() |
| result = (part / whole) * 100.0 |
| return _build_result( |
| topic="percent", |
| internal_answer=result, |
| ask="Find the percent represented by the part out of the whole.", |
| given=f"whole = {whole}, part = {part}", |
| method=[ |
| "Use percent = part ÷ whole × 100.", |
| "Divide the part by the whole.", |
| "Then convert to a percent.", |
| ], |
| ) |
|
|
| |
| |
| |
| m = re.search( |
| r"(?:what\s+(?:is\s+the\s+)?)?(?:percent\s+)?(increase|decrease|change).*?from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| direction = m.group(1) |
| old = float(m.group(2)) |
| new = float(m.group(3)) |
| if old == 0: |
| return _fallback_percent() |
| result = ((new - old) / old) * 100.0 |
| return _build_result( |
| topic="percent_change", |
| internal_answer=abs(result) if direction in ("increase", "decrease") else result, |
| ask="Find the percent change relative to the original value.", |
| given=f"original = {old}, new = {new}", |
| method=[ |
| "Find the change: new − original.", |
| "Use the original value in the denominator.", |
| "Compute change ÷ original × 100.", |
| "Match the sign or wording to increase vs decrease.", |
| ], |
| ) |
|
|
| m = re.search( |
| r"from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m and ("percent" in lower or "%" in lower): |
| old = float(m.group(1)) |
| new = float(m.group(2)) |
| if old == 0: |
| return _fallback_percent() |
| result = ((new - old) / old) * 100.0 |
| return _build_result( |
| topic="percent_change", |
| internal_answer=result, |
| ask="Find the percent change between the starting value and ending value.", |
| given=f"original = {old}, new = {new}", |
| method=[ |
| "Subtract to find the change.", |
| "Divide by the original value, not the new value.", |
| "Multiply by 100 to convert to percent form.", |
| ], |
| ) |
|
|
| |
| |
| |
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%", |
| lower |
| ) |
| if m: |
| original = float(m.group(1)) |
| direction = m.group(2) |
| pct = float(m.group(3)) |
| dec = _pct_to_decimal(pct) |
| multiplier = 1 + dec if direction == "increased" else 1 - dec |
| result = original * multiplier |
| return _build_result( |
| topic="percent_change", |
| internal_answer=result, |
| ask="Find the new value after a percent increase or decrease.", |
| given=f"original = {original}, change = {pct}% {direction}", |
| method=[ |
| "Convert the percent to a decimal.", |
| "Use a multiplier.", |
| "For increase, multiply by 1 + p.", |
| "For decrease, multiply by 1 − p.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| |
| m = re.search( |
| r"after\s+(?:an?\s+)?(\d+(?:\.\d+)?)\s*%\s*(increase|decrease|discount|markup|reduction|loss|gain).*?(?:is|becomes|equals|was|now\s+is)\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| pct = float(m.group(1)) |
| kind = m.group(2) |
| final_value = float(m.group(3)) |
| dec = _pct_to_decimal(pct) |
|
|
| if kind in ("increase", "markup", "gain"): |
| multiplier = 1 + dec |
| else: |
| multiplier = 1 - dec |
|
|
| if multiplier == 0: |
| return _fallback_percent() |
|
|
| result = final_value / multiplier |
| return _build_result( |
| topic="reverse_percent", |
| internal_answer=result, |
| ask="Find the original value before a percent change.", |
| given=f"final value = {final_value}, change = {pct}% {kind}", |
| method=[ |
| "Translate the percent change into a multiplier.", |
| "Increase/markup/gain → original × (1 + p).", |
| "Decrease/discount/loss → original × (1 − p).", |
| "Solve by dividing the final value by that multiplier.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| |
| m = re.search( |
| r"what\s+percent\s+(greater|less)\s+(?:is\s+)?(-?\d+(?:\.\d+)?)\s+than\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| relation = m.group(1) |
| a = float(m.group(2)) |
| b = float(m.group(3)) |
| if b == 0: |
| return _fallback_percent() |
| result = ((a - b) / b) * 100.0 |
| return _build_result( |
| topic="percent_comparison", |
| internal_answer=abs(result), |
| ask=f"Compare one value to another and express the difference as a percent of the reference value.", |
| given=f"compare {a} to reference {b}", |
| method=[ |
| "Find the difference between the two values.", |
| "Use the second value as the reference base.", |
| "Compute difference ÷ reference × 100.", |
| f"Interpret the result as percent {relation}.", |
| ], |
| ) |
|
|
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+is\s+what\s+percent\s+(greater|less)\s+than\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| a = float(m.group(1)) |
| relation = m.group(2) |
| b = float(m.group(3)) |
| if b == 0: |
| return _fallback_percent() |
| result = ((a - b) / b) * 100.0 |
| return _build_result( |
| topic="percent_comparison", |
| internal_answer=abs(result), |
| ask="Find by what percent one value is greater or less than another.", |
| given=f"value = {a}, reference = {b}", |
| method=[ |
| "Subtract to find the difference.", |
| "Divide by the reference value named after 'than'.", |
| "Multiply by 100 to get the percent comparison.", |
| f"Use the wording to label it as {relation}.", |
| ], |
| ) |
|
|
| |
| |
| |
| m = re.search( |
| r"(?:price|cost|bill|amount)\s+of\s+(-?\d+(?:\.\d+)?)\s+.*?(discount|markup|tax|tip)\s+of\s+(\d+(?:\.\d+)?)\s*%", |
| lower |
| ) |
| if m: |
| base = float(m.group(1)) |
| kind = m.group(2) |
| pct = float(m.group(3)) |
| dec = _pct_to_decimal(pct) |
|
|
| if kind == "discount": |
| result = base * (1 - dec) |
| else: |
| result = base * (1 + dec) |
|
|
| return _build_result( |
| topic="percent_application", |
| internal_answer=result, |
| ask=f"Apply a {kind} percentage to a base amount.", |
| given=f"base = {base}, rate = {pct}% {kind}", |
| method=[ |
| "Convert the percent to decimal form.", |
| "For discount, subtract the percent part from the base or multiply by 1 − p.", |
| "For markup, tax, or tip, add the percent part or multiply by 1 + p.", |
| ], |
| ) |
|
|
| |
| |
| |
| m = re.search( |
| r"(?:cost price|cp|cost)\s+(-?\d+(?:\.\d+)?)\s+.*?(?:selling price|sp|sold for)\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m and ("profit" in lower or "loss" in lower or "percent" in lower or "%" in lower): |
| cost = float(m.group(1)) |
| sell = float(m.group(2)) |
| if cost == 0: |
| return _fallback_percent() |
| result = ((sell - cost) / cost) * 100.0 |
| return _build_result( |
| topic="profit_loss", |
| internal_answer=result, |
| ask="Find the profit or loss percent relative to the cost price.", |
| given=f"cost price = {cost}, selling price = {sell}", |
| method=[ |
| "Find profit/loss = selling price − cost price.", |
| "Use cost price as the denominator.", |
| "Compute (profit or loss) ÷ cost price × 100.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+.*?(increase(?:d)?|decrease(?:d)?|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%.*?(increase(?:d)?|decrease(?:d)?|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%", |
| lower |
| ) |
| if m: |
| original = float(m.group(1)) |
| op1 = m.group(2) |
| p1 = _pct_to_decimal(float(m.group(3))) |
| op2 = m.group(4) |
| p2 = _pct_to_decimal(float(m.group(5))) |
|
|
| mult1 = 1 + p1 if "increase" in op1 else 1 - p1 |
| mult2 = 1 + p2 if "increase" in op2 else 1 - p2 |
| result = original * mult1 * mult2 |
|
|
| return _build_result( |
| topic="successive_percent_change", |
| internal_answer=result, |
| ask="Apply two percent changes in sequence.", |
| given=f"start = {original}, first change = {m.group(3)}%, second change = {m.group(5)}%", |
| method=[ |
| "Convert each percent to a decimal.", |
| "Turn each change into a multiplier.", |
| "Apply the first multiplier, then apply the second to the new value.", |
| "Do not add or subtract the percentages directly unless the problem specifically asks for net rate in a special setup.", |
| ], |
| ) |
|
|
| |
| |
| |
| if "percent difference" in lower: |
| m = re.search( |
| r"between\s+(-?\d+(?:\.\d+)?)\s+and\s+(-?\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| a = float(m.group(1)) |
| b = float(m.group(2)) |
| avg = (a + b) / 2.0 |
| if avg == 0: |
| return _fallback_percent() |
| result = abs(a - b) / avg * 100.0 |
| return _build_result( |
| topic="percent_difference", |
| internal_answer=result, |
| ask="Find the percent difference between two values.", |
| given=f"values = {a} and {b}", |
| method=[ |
| "Find the absolute difference between the two values.", |
| "Find the average of the two values.", |
| "Use percent difference = difference ÷ average × 100.", |
| "Keep this separate from percent change, which uses the original value as the denominator.", |
| ], |
| ) |
|
|
| |
| |
| |
| if "simple interest" in lower: |
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+.*?(\d+(?:\.\d+)?)\s*%\s+.*?(\d+(?:\.\d+)?)\s*(year|years|month|months)", |
| lower |
| ) |
| if m: |
| principal = float(m.group(1)) |
| rate = float(m.group(2)) |
| time = float(m.group(3)) |
| unit = m.group(4) |
|
|
| t_years = time / 12.0 if "month" in unit else time |
| result = principal * _pct_to_decimal(rate) * t_years |
|
|
| return _build_result( |
| topic="simple_interest", |
| internal_answer=result, |
| ask="Find interest earned using the simple interest formula.", |
| given=f"principal = {principal}, rate = {rate}%, time = {time} {unit}", |
| method=[ |
| "Use simple interest = principal × rate × time.", |
| "Convert the rate to decimal form.", |
| "Make sure time is in the same units as the rate.", |
| "If the rate is annual and time is in months, convert months to years first.", |
| ], |
| ) |
|
|
| |
| |
| |
| if "compound interest" in lower or "compounded" in lower: |
| m = re.search( |
| r"(-?\d+(?:\.\d+)?)\s+.*?(\d+(?:\.\d+)?)\s*%\s+.*?compounded\s+(annually|semiannually|quarterly|monthly).*?(\d+(?:\.\d+)?)\s+year", |
| lower |
| ) |
| if m: |
| principal = float(m.group(1)) |
| rate = float(m.group(2)) |
| comp = m.group(3) |
| years = float(m.group(4)) |
|
|
| c_map = { |
| "annually": 1, |
| "semiannually": 2, |
| "quarterly": 4, |
| "monthly": 12, |
| } |
| c = c_map[comp] |
| result = principal * ((1 + _pct_to_decimal(rate) / c) ** (c * years)) |
|
|
| return _build_result( |
| topic="compound_interest", |
| internal_answer=result, |
| ask="Find the final balance after compound interest.", |
| given=f"principal = {principal}, rate = {rate}%, compounded {comp}, time = {years} years", |
| method=[ |
| "Use the compound interest form A = P(1 + r/c)^(ct).", |
| "Convert the rate to decimal form.", |
| "Match the compounding frequency to the correct value of c.", |
| "Substitute carefully before evaluating.", |
| ], |
| ) |
|
|
| |
| |
| |
| if "percentile" in lower: |
| |
| |
| m = re.search( |
| r"(\d+(?:\.\d+)?)(?:st|nd|rd|th)\s+percentile\s+out\s+of\s+(\d+(?:\.\d+)?)", |
| lower |
| ) |
| if m: |
| pct = float(m.group(1)) |
| total = float(m.group(2)) |
| result = _pct_to_decimal(pct) * total |
| return _build_result( |
| topic="percentile", |
| internal_answer=result, |
| ask="Interpret a percentile as the proportion of the group below that score.", |
| given=f"{pct}th percentile out of {total}", |
| method=[ |
| "Convert the percentile to a decimal proportion.", |
| "Multiply by the total group size to estimate how many are below that position.", |
| "Check the wording carefully: percentile usually refers to the percentage of values below a score.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| if len(nums) >= 2 and any(k in lower for k in ["out of", "remaining", "sale", "discount", "tax", "tip", "profit", "loss"]): |
| return SolverResult( |
| domain="quant", |
| solved=False, |
| topic="percent", |
| answer_value=None, |
| internal_answer=None, |
| steps=[ |
| "This looks like a percent-style word problem.", |
| "What the question is asking: first decide whether the unknown is the part, whole, rate, original value, or changed value.", |
| "Useful checks:", |
| "• 'out of' often signals part/whole", |
| "• 'discount', 'tax', 'tip', 'markup' often signal multiplier use", |
| "• 'profit'/'loss' uses cost price as the base", |
| "• 'from ... to ...' often signals percent change", |
| ], |
| ) |
|
|
| return _fallback_percent() |