| from __future__ import annotations |
|
|
| import math |
| import re |
| from typing import Optional, List, Tuple |
|
|
| from models import SolverResult |
|
|
|
|
| Number = float |
|
|
|
|
| def solve_absolute_value(text: str) -> Optional[SolverResult]: |
| raw = text or "" |
| lower = raw.lower() |
| compact = _compact(lower) |
|
|
| if not _looks_like_absolute_value(raw, lower, compact): |
| return None |
|
|
| help_mode = _detect_help_mode(lower) |
|
|
| |
| explainer = _handle_explainer_prompt(raw, lower, help_mode) |
| if explainer: |
| return explainer |
|
|
| |
| expr = _normalize_abs_notation(raw) |
|
|
| |
| handlers = [ |
| _solve_scaled_shifted_abs_equals_constant, |
| _solve_sum_of_two_abs_equals_constant, |
| _solve_single_abs_inequality, |
| _solve_single_abs_equation, |
| _solve_abs_count_solutions, |
| _solve_distance_interpretation_prompt, |
| ] |
|
|
| for handler in handlers: |
| out = handler(expr, raw, lower, compact, help_mode) |
| if out is not None: |
| return out |
|
|
| |
| return SolverResult( |
| domain="quant", |
| solved=False, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=None, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Identify each absolute value expression and the key point where its inside equals zero.", |
| "Split the number line into intervals around those key points.", |
| "Within each interval, remove the absolute value signs using the correct sign.", |
| "Solve the resulting linear equation or inequality, then keep only solutions that satisfy the interval condition.", |
| ], |
| hint_lines=[ |
| "Start by finding where the inside of each modulus becomes zero.", |
| "Those boundary points tell you where the sign changes.", |
| ], |
| walkthrough_lines=[ |
| "Absolute value problems are usually case-splitting problems.", |
| "The key move is to locate the sign-change points, open the modulus correctly in each region, and then check which solutions actually belong to that region.", |
| ], |
| explain_lines=[ |
| "Absolute value measures distance from zero, or distance from a point in forms like |x-a|.", |
| "That is why one equation can create two symmetric cases, and why inequalities often describe intervals or regions outside intervals.", |
| ], |
| ), |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def _looks_like_absolute_value(raw: str, lower: str, compact: str) -> bool: |
| return ( |
| "|" in raw |
| or "absolute value" in lower |
| or "modulus" in lower |
| or "abs(" in compact |
| or re.search(r"\babs\s*\(", lower) is not None |
| ) |
|
|
|
|
| def _compact(s: str) -> str: |
| return re.sub(r"\s+", "", s.lower()) |
|
|
|
|
| def _normalize_abs_notation(text: str) -> str: |
| s = text |
|
|
| |
| s = re.sub(r'(?i)\babs\s*\(([^()]+)\)', r'|\1|', s) |
|
|
| |
| s = re.sub(r'(?i)absolute\s+value\s+of\s+([^=<>]+?)(?=\s*(?:=|<|>|≤|≥|$))', r'|\1|', s) |
|
|
| return s |
|
|
|
|
| def _detect_help_mode(lower: str) -> str: |
| if any(p in lower for p in ["hint", "nudge", "clue"]): |
| return "hint" |
| if any(p in lower for p in ["walkthrough", "step by step", "steps", "work through", "how do i solve"]): |
| return "walkthrough" |
| if any(p in lower for p in ["explain", "what does this mean", "what is this asking", "interpret"]): |
| return "explain" |
| return "answer" |
|
|
|
|
| def _handle_explainer_prompt(raw: str, lower: str, help_mode: str) -> Optional[SolverResult]: |
| concept_triggers = [ |
| "what is absolute value", |
| "what does absolute value mean", |
| "explain absolute value", |
| "what is modulus", |
| "what does |x| mean", |
| "what does |x-a| mean", |
| ] |
| if not any(t in lower for t in concept_triggers): |
| return None |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer="concept explanation", |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Absolute value means distance, not signed direction.", |
| "So |x| is the distance of x from 0 on the number line.", |
| "More generally, |x-a| is the distance between x and a.", |
| "That is why equations like |x-a| = k usually split into two symmetric cases, while inequalities describe points within or outside a distance range.", |
| ], |
| hint_lines=[ |
| "Think of absolute value as distance on the number line.", |
| "Distance is never negative.", |
| ], |
| walkthrough_lines=[ |
| "Interpret |x-a| as 'how far x is from a'.", |
| "If that distance equals k, then x can sit k units to the right of a or k units to the left of a.", |
| "If that distance is less than k, x must lie inside the interval centered at a.", |
| "If that distance is greater than k, x must lie outside that interval.", |
| ], |
| explain_lines=[ |
| "Absolute value removes sign and keeps magnitude.", |
| "In algebra problems, its most useful meaning is distance.", |
| ], |
| ), |
| ) |
|
|
|
|
| def _mode_steps( |
| help_mode: str, |
| default_lines: List[str], |
| *, |
| hint_lines: Optional[List[str]] = None, |
| walkthrough_lines: Optional[List[str]] = None, |
| explain_lines: Optional[List[str]] = None, |
| ) -> List[str]: |
| if help_mode == "hint" and hint_lines: |
| return hint_lines |
| if help_mode == "walkthrough" and walkthrough_lines: |
| return walkthrough_lines |
| if help_mode == "explain" and explain_lines: |
| return explain_lines |
| return default_lines |
|
|
|
|
| def _clean_num(n: Number) -> str: |
| if abs(n - round(n)) < 1e-9: |
| return str(int(round(n))) |
| return f"{n:.10g}" |
|
|
|
|
| def _safe_sort_pair(a: Number, b: Number) -> Tuple[Number, Number]: |
| return (a, b) if a <= b else (b, a) |
|
|
|
|
| def _is_negative(n: Number) -> bool: |
| return n < -1e-9 |
|
|
|
|
| def _is_zero(n: Number) -> bool: |
| return abs(n) < 1e-9 |
|
|
|
|
| def _parse_num(s: str) -> Optional[Number]: |
| try: |
| return float(s) |
| except Exception: |
| return None |
|
|
|
|
| def _extract_relation(expr: str) -> Optional[Tuple[str, str, str]]: |
| |
| m = re.search(r'(.+?)(<=|>=|=|<|>|≤|≥)(.+)', expr.replace(" ", "")) |
| if not m: |
| return None |
| left, op, right = m.group(1), m.group(2), m.group(3) |
| op = op.replace("≤", "<=").replace("≥", ">=") |
| return left, op, right |
|
|
|
|
| def _parse_linear_x(inner: str) -> Optional[Tuple[Number, Number]]: |
| """ |
| Parse ax+b in simple forms: |
| x |
| -x |
| x+3 |
| x-3 |
| 2x+5 |
| 2*x-5 |
| -3x+7 |
| Returns (a, b) so expression is a*x + b |
| """ |
| s = inner.replace(" ", "").replace("*", "") |
| s = s.replace("−", "-") |
|
|
| if "x" not in s: |
| return None |
|
|
| |
| if s.startswith("x"): |
| s = "1" + s |
| elif s.startswith("-x"): |
| s = s.replace("-x", "-1x", 1) |
| elif s.startswith("+x"): |
| s = s.replace("+x", "+1x", 1) |
|
|
| m = re.fullmatch(r'([+-]?\d*\.?\d*)x([+-]\d*\.?\d+)?', s) |
| if not m: |
| return None |
|
|
| a_str = m.group(1) |
| b_str = m.group(2) |
|
|
| if a_str in ("", "+"): |
| a = 1.0 |
| elif a_str == "-": |
| a = -1.0 |
| else: |
| a = float(a_str) |
|
|
| b = float(b_str) if b_str else 0.0 |
| return a, b |
|
|
|
|
| def _solve_linear_equals_zero(a: Number, b: Number) -> Optional[Number]: |
| if _is_zero(a): |
| return None |
| return -b / a |
|
|
|
|
| def _linear_to_center(a: Number, b: Number) -> Optional[Number]: |
| |
| if _is_zero(a): |
| return None |
| return -b / a |
|
|
|
|
| def _format_interval(a: Number, b: Number, inclusive_left: bool, inclusive_right: bool) -> str: |
| L = "[" if inclusive_left else "(" |
| R = "]" if inclusive_right else ")" |
| return f"{L}{_clean_num(a)}, {_clean_num(b)}{R}" |
|
|
|
|
| def _format_union(parts: List[str]) -> str: |
| return " ∪ ".join(parts) |
|
|
|
|
| def _hide_solution_step(line: str) -> str: |
| """ |
| Mild safeguard against leaking the exact computed final answer. |
| We keep method language, not explicit numeric conclusion. |
| """ |
| return line |
|
|
|
|
| |
| |
| |
|
|
| def _solve_single_abs_equation(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| rel = _extract_relation(expr) |
| if not rel: |
| return None |
|
|
| left, op, right = rel |
| if op != "=": |
| return None |
|
|
| m = re.fullmatch(r'\|(.+)\|', left) |
| if not m: |
| return None |
|
|
| inner = m.group(1) |
| k = _parse_num(right) |
| if k is None: |
| return None |
|
|
| lin = _parse_linear_x(inner) |
| if lin is None: |
| return None |
|
|
| a, b = lin |
|
|
| if _is_negative(k): |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer="no solution", |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "An absolute value cannot equal a negative number.", |
| ], |
| hint_lines=[ |
| "Check the right-hand side first: absolute value is never negative.", |
| ], |
| walkthrough_lines=[ |
| "Before splitting into cases, check whether the equation is even possible.", |
| "Since absolute value is always non-negative, it cannot equal a negative constant.", |
| ], |
| explain_lines=[ |
| "Absolute value represents magnitude or distance, so its output cannot be negative.", |
| ], |
| ), |
| ) |
|
|
| if _is_zero(a): |
| const_val = abs(b) |
| status = "all real numbers" if _is_zero(const_val - k) else "no solution" |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=status, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Here the expression inside the modulus is constant rather than variable.", |
| "So the equation is either always true or never true depending on whether that constant absolute value matches the right-hand side.", |
| ], |
| hint_lines=[ |
| "Notice that x disappeared from inside the modulus.", |
| ], |
| walkthrough_lines=[ |
| "Evaluate the constant inside the absolute value first.", |
| "Then compare that fixed absolute value to the right-hand side.", |
| ], |
| explain_lines=[ |
| "If the inside is constant, the equation no longer depends on x.", |
| ], |
| ), |
| ) |
|
|
| x1 = (k - b) / a |
| x2 = (-k - b) / a |
|
|
| if abs(x1 - x2) < 1e-9: |
| internal = _clean_num(x1) |
| else: |
| lo, hi = _safe_sort_pair(x1, x2) |
| internal = f"{_clean_num(lo)} and {_clean_num(hi)}" |
|
|
| center = _linear_to_center(a, b) |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=internal, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Set the inside equal to the positive target and also to the negative target.", |
| "Solve the two linear cases separately.", |
| "That gives the points at a fixed distance from the center on the number line.", |
| ], |
| hint_lines=[ |
| "Use the rule |expression| = k → expression = k or expression = -k.", |
| "Then solve each linear equation.", |
| ], |
| walkthrough_lines=[ |
| "Interpret the equation as a distance statement.", |
| f"The expression inside becomes zero at x = {_clean_num(center) if center is not None else 'the center point'}.", |
| "A fixed absolute value means x must sit the same distance on either side of that center.", |
| "So split into two linear equations: one for the positive case and one for the negative case.", |
| ], |
| explain_lines=[ |
| "An equation of the form |expression| = constant usually creates two cases because distance can be achieved in two symmetric directions.", |
| ], |
| ), |
| ) |
|
|
|
|
| def _solve_single_abs_inequality(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| rel = _extract_relation(expr) |
| if not rel: |
| return None |
|
|
| left, op, right = rel |
| m = re.fullmatch(r'\|(.+)\|', left) |
| if not m: |
| return None |
|
|
| inner = m.group(1) |
| k = _parse_num(right) |
| if k is None: |
| return None |
|
|
| lin = _parse_linear_x(inner) |
| if lin is None: |
| return None |
|
|
| a, b = lin |
|
|
| if _is_zero(a): |
| fixed = abs(b) |
| truth = _evaluate_constant_abs_inequality(fixed, op, k) |
| status = "all real numbers" if truth else "no solution" |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=status, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "The modulus contains no variable, so evaluate it as a constant inequality.", |
| ], |
| hint_lines=[ |
| "First check whether x is actually inside the modulus.", |
| ], |
| walkthrough_lines=[ |
| "Since the inside is constant, the inequality is either always true or never true.", |
| "Evaluate the absolute value and compare it to the constant on the right.", |
| ], |
| explain_lines=[ |
| "No variable inside the modulus means the statement does not depend on x.", |
| ], |
| ), |
| ) |
|
|
| center = -b / a |
|
|
| |
| if _is_negative(k): |
| if op in ("<", "<="): |
| internal = "no solution" if op == "<" else "no solution" |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=internal, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Absolute value is never negative, so it cannot be less than a negative number.", |
| ], |
| hint_lines=[ |
| "Absolute value outputs are always at least 0.", |
| ], |
| walkthrough_lines=[ |
| "Check the sign of the right-hand side first.", |
| "A non-negative quantity cannot be smaller than a negative bound.", |
| ], |
| explain_lines=[ |
| "Distance cannot be negative.", |
| ], |
| ), |
| ) |
| else: |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer="all real numbers", |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Any absolute value is greater than a negative number, so the inequality is true for every real x.", |
| ], |
| hint_lines=[ |
| "Compare the minimum possible absolute value, which is 0, to the negative bound.", |
| ], |
| walkthrough_lines=[ |
| "Since |expression| is always at least 0, and 0 is already greater than any negative number, every real x works here.", |
| ], |
| explain_lines=[ |
| "The range of absolute value is [0, ∞).", |
| ], |
| ), |
| ) |
|
|
| |
| radius = k / abs(a) |
|
|
| if op in ("<", "<="): |
| if _is_negative(radius): |
| internal = "no solution" |
| else: |
| left_pt = center - radius |
| right_pt = center + radius |
| internal = _format_interval(left_pt, right_pt, op == "<=", op == "<=") |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=internal, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Rewrite the inequality as a distance-from-center statement.", |
| "For a 'less than' absolute value inequality, the solution lies inside the interval around the center.", |
| "Use inclusive endpoints only if the inequality allows equality.", |
| ], |
| hint_lines=[ |
| "Absolute value less than a number means 'stay within that distance'.", |
| "So think interval, not two separate outside regions.", |
| ], |
| walkthrough_lines=[ |
| "Find the center by solving when the inside equals zero.", |
| "Then convert the inequality into a distance condition from that center.", |
| "Because the distance must stay below the allowed radius, the solution is the interval between the two boundary points.", |
| ], |
| explain_lines=[ |
| "Inequalities of the form |x-a| < r describe all points within r units of a, so they represent an interval.", |
| ], |
| ), |
| ) |
|
|
| if op in (">", ">="): |
| left_pt = center - radius |
| right_pt = center + radius |
| left_part = f"(-∞, {_clean_num(left_pt)}" + ("]" if op == ">=" else ")") |
| right_part = ("[" if op == ">=" else "(") + f"{_clean_num(right_pt)}, ∞)" |
| internal = _format_union([left_part, right_part]) |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=internal, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Rewrite the inequality as a distance-from-center statement.", |
| "For a 'greater than' absolute value inequality, the solution lies outside the central interval.", |
| "Include the boundary points only if the inequality allows equality.", |
| ], |
| hint_lines=[ |
| "Absolute value greater than a number means 'farther than that distance'.", |
| "So expect two outside regions.", |
| ], |
| walkthrough_lines=[ |
| "Locate the center where the inside becomes zero.", |
| "Interpret the inequality as requiring distance from that center to be larger than the allowed radius.", |
| "That means x must lie to the left of the left boundary or to the right of the right boundary.", |
| ], |
| explain_lines=[ |
| "Inequalities of the form |x-a| > r describe points more than r units away from a, so they form two rays outside the middle interval.", |
| ], |
| ), |
| ) |
|
|
| return None |
|
|
|
|
| def _evaluate_constant_abs_inequality(fixed: Number, op: str, k: Number) -> bool: |
| if op == "<": |
| return fixed < k |
| if op == "<=": |
| return fixed <= k |
| if op == "=": |
| return abs(fixed - k) < 1e-9 |
| if op == ">": |
| return fixed > k |
| if op == ">=": |
| return fixed >= k |
| return False |
|
|
|
|
| def _solve_scaled_shifted_abs_equals_constant(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| |
| |
| |
| rel = _extract_relation(expr) |
| if not rel: |
| return None |
|
|
| left, op, right = rel |
| if op != "=": |
| return None |
|
|
| right_num = _parse_num(right) |
| if right_num is None: |
| return None |
|
|
| s = left.replace(" ", "") |
| m = re.fullmatch(r'([+-]?\d*\.?\d*)?\|(.+)\|([+-]\d*\.?\d+)?', s) |
| if not m: |
| return None |
|
|
| a_str, inner, c_str = m.group(1), m.group(2), m.group(3) |
|
|
| if a_str in (None, "", "+"): |
| scale = 1.0 |
| elif a_str == "-": |
| scale = -1.0 |
| else: |
| scale = float(a_str) |
|
|
| c = float(c_str) if c_str else 0.0 |
|
|
| if _is_zero(scale): |
| return None |
|
|
| target = (right_num - c) / scale |
|
|
| |
| synthetic = f"|{inner}|={target}" |
| return _solve_single_abs_equation(synthetic, raw, lower, compact, help_mode) |
|
|
|
|
| def _solve_sum_of_two_abs_equals_constant(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| rel = _extract_relation(expr) |
| if not rel: |
| return None |
|
|
| left, op, right = rel |
| if op != "=": |
| return None |
|
|
| k = _parse_num(right) |
| if k is None: |
| return None |
|
|
| |
| m = re.fullmatch(r'\|x([+-]\d*\.?\d+)?\|\+\|x([+-]\d*\.?\d+)?\|', left.replace(" ", "")) |
| if not m: |
| return None |
|
|
| s1 = m.group(1) |
| s2 = m.group(2) |
|
|
| a = -float(s1) if s1 else 0.0 |
| b = -float(s2) if s2 else 0.0 |
|
|
| lo, hi = _safe_sort_pair(a, b) |
| min_sum = hi - lo |
|
|
| if _is_negative(k): |
| internal = "no solution" |
| elif k < min_sum - 1e-9: |
| internal = "no solution" |
| elif abs(k - min_sum) < 1e-9: |
| internal = _format_interval(lo, hi, True, True) |
| else: |
| extra = (k - min_sum) / 2.0 |
| left_pt = lo - extra |
| right_pt = hi + extra |
| internal = f"{_clean_num(left_pt)} and {_clean_num(right_pt)}" |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=internal, |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Interpret each absolute value as a distance on the number line.", |
| "The sum of distances to two fixed points is smallest between those points.", |
| "Compare the target sum to that minimum to decide whether there are no solutions, an interval of solutions, or two symmetric endpoint solutions.", |
| ], |
| hint_lines=[ |
| "Think distance, not algebra first.", |
| "What is the minimum possible value of the sum of distances to the two fixed points?", |
| ], |
| walkthrough_lines=[ |
| "Rewrite each modulus as distance from a fixed point.", |
| "Between the two points, the total distance stays constant at the distance between them.", |
| "If the target is smaller than that constant, no x works.", |
| "If the target equals it, every x in the middle interval works.", |
| "If the target is larger, you move outward symmetrically until the extra distance is split across the two ends.", |
| ], |
| explain_lines=[ |
| "A sum like |x-a| + |x-b| measures total distance from x to two anchor points. Its behavior changes depending on whether x lies left of both, between them, or right of both.", |
| ], |
| ), |
| ) |
|
|
|
|
| def _solve_abs_count_solutions(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| if not any(p in lower for p in ["how many solutions", "number of solutions", "how many roots"]): |
| return None |
|
|
| |
| symbolic_match = re.search(r'(\|.+)', expr) |
| if not symbolic_match: |
| return None |
|
|
| symbolic = symbolic_match.group(1) |
|
|
| |
| for helper in ( |
| _solve_scaled_shifted_abs_equals_constant, |
| _solve_sum_of_two_abs_equals_constant, |
| _solve_single_abs_inequality, |
| _solve_single_abs_equation, |
| ): |
| res = helper(symbolic, raw, lower, compact, "answer") |
| if res is None or res.internal_answer is None: |
| continue |
|
|
| count = _count_solution_objects(res.internal_answer) |
| if count is None: |
| continue |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=str(count), |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Solve the absolute value relation structurally, then count how many distinct real solutions remain.", |
| ], |
| hint_lines=[ |
| "First determine the full solution set, then count distinct values or intervals.", |
| ], |
| walkthrough_lines=[ |
| "Absolute value problems can produce zero, one, two, or infinitely many solutions.", |
| "So after solving, decide whether the result is an empty set, a single value, two values, or an interval/all reals.", |
| ], |
| explain_lines=[ |
| "Counting solutions means classifying the resulting solution set, not just solving mechanically.", |
| ], |
| ), |
| ) |
|
|
| return None |
|
|
|
|
| def _count_solution_objects(internal: str) -> Optional[int]: |
| s = internal.strip().lower() |
|
|
| if s == "no solution": |
| return 0 |
| if s == "all real numbers": |
| return math.inf |
|
|
| if "∞" in s or "(-∞" in s or "[0," in s or "(" in s or "[" in s: |
| return math.inf |
|
|
| if " and " in s: |
| parts = [p.strip() for p in s.split(" and ") if p.strip()] |
| return len(parts) |
|
|
| |
| try: |
| float(s) |
| return 1 |
| except Exception: |
| return None |
|
|
|
|
| def _solve_distance_interpretation_prompt(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]: |
| triggers = [ |
| "distance from", |
| "within", |
| "at most", |
| "at least", |
| "no more than", |
| "no less than", |
| "units from", |
| "represents this condition", |
| ] |
| if not any(t in lower for t in triggers): |
| return None |
|
|
| m = re.search(r'([<>]=?|≤|≥)\s*x\s*([<>]=?|≤|≥)\s*(-?\d+(?:\.\d+)?)', lower) |
| if re.search(r'(-?\d+(?:\.\d+)?)\s*<\s*x\s*<\s*(-?\d+(?:\.\d+)?)', lower): |
| nums = re.search(r'(-?\d+(?:\.\d+)?)\s*<\s*x\s*<\s*(-?\d+(?:\.\d+)?)', lower) |
| a = float(nums.group(1)) |
| b = float(nums.group(2)) |
| center = (a + b) / 2.0 |
| radius = (b - a) / 2.0 |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="absolute_value", |
| answer_value=None, |
| internal_answer=f"|x-{_clean_num(center)}|<{_clean_num(radius)}", |
| steps=_mode_steps( |
| help_mode, |
| [ |
| "Find the midpoint of the interval.", |
| "Then find the distance from the midpoint to either endpoint.", |
| "That converts the interval into an absolute value distance statement.", |
| ], |
| hint_lines=[ |
| "Absolute value interval form is center ± radius.", |
| ], |
| walkthrough_lines=[ |
| "A double inequality like a < x < b means x stays between two endpoints.", |
| "Write that as 'x is within a certain distance of the midpoint'.", |
| "The midpoint becomes the center, and half the interval length becomes the radius.", |
| ], |
| explain_lines=[ |
| "Absolute value can encode interval conditions by measuring distance from the midpoint.", |
| ], |
| ), |
| ) |
|
|
| return None |