j-js commited on
Commit
4d3cfba
·
verified ·
1 Parent(s): 3795483

Update solver_absolute_value.py

Browse files
Files changed (1) hide show
  1. solver_absolute_value.py +803 -40
solver_absolute_value.py CHANGED
@@ -1,74 +1,837 @@
1
  from __future__ import annotations
2
 
 
3
  import re
4
- from typing import Optional
5
 
6
  from models import SolverResult
7
 
8
 
 
 
 
9
  def solve_absolute_value(text: str) -> Optional[SolverResult]:
10
  raw = text or ""
11
  lower = raw.lower()
12
- compact = lower.replace(" ", "")
13
 
14
- if "|" not in raw and "absolute value" not in lower and "abs" not in lower:
15
  return None
16
 
17
- # |x| = a
18
- m = re.search(r"\|x\|=(-?\d+(?:\.\d+)?)", compact)
19
- if m:
20
- a = float(m.group(1))
21
- if a < 0:
22
- return SolverResult(
23
- domain="quant",
24
- solved=True,
25
- topic="absolute_value",
26
- answer_value="no solution",
27
- internal_answer="no solution",
28
- steps=[
29
- "Absolute value cannot equal a negative number.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ],
31
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  return SolverResult(
33
  domain="quant",
34
  solved=True,
35
  topic="absolute_value",
36
- answer_value=f"{a:g} and {-a:g}",
37
- internal_answer=f"{a:g} and {-a:g}",
38
- steps=[
39
- "For |x| = a with a ≥ 0, split into two cases.",
40
- "The variable can equal the positive value or the negative value.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  )
43
 
44
- # |x-a| = b
45
- m = re.search(r"\|x-(-?\d+(?:\.\d+)?)\|=(-?\d+(?:\.\d+)?)", compact)
46
- if m:
47
- a = float(m.group(1))
48
- b = float(m.group(2))
49
- if b < 0:
50
  return SolverResult(
51
  domain="quant",
52
  solved=True,
53
  topic="absolute_value",
54
- answer_value="no solution",
55
- internal_answer="no solution",
56
- steps=[
57
- "Absolute value cannot equal a negative number.",
58
- ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  )
60
- x1 = a + b
61
- x2 = a - b
 
 
 
 
 
 
 
 
 
62
  return SolverResult(
63
  domain="quant",
64
  solved=True,
65
  topic="absolute_value",
66
- answer_value=f"{x1:g} and {x2:g}",
67
- internal_answer=f"{x1:g} and {x2:g}",
68
- steps=[
69
- "Split the absolute value equation into two linear cases.",
70
- "Solve the positive case and the negative case.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  )
73
 
74
  return None
 
1
  from __future__ import annotations
2
 
3
+ import math
4
  import re
5
+ from typing import Optional, List, Tuple
6
 
7
  from models import SolverResult
8
 
9
 
10
+ Number = float
11
+
12
+
13
  def solve_absolute_value(text: str) -> Optional[SolverResult]:
14
  raw = text or ""
15
  lower = raw.lower()
16
+ compact = _compact(lower)
17
 
18
+ if not _looks_like_absolute_value(raw, lower, compact):
19
  return None
20
 
21
+ help_mode = _detect_help_mode(lower)
22
+
23
+ # 1) Pure explainer / concept prompts
24
+ explainer = _handle_explainer_prompt(raw, lower, help_mode)
25
+ if explainer:
26
+ return explainer
27
+
28
+ # 2) Normalise abs(...) to |...| where useful
29
+ expr = _normalize_abs_notation(raw)
30
+
31
+ # 3) Try increasingly specific handlers
32
+ handlers = [
33
+ _solve_scaled_shifted_abs_equals_constant, # a|x-b| + c = d
34
+ _solve_sum_of_two_abs_equals_constant, # |x-a| + |x-b| = k
35
+ _solve_single_abs_inequality, # |linear| < <= > >= k
36
+ _solve_single_abs_equation, # |linear| = k
37
+ _solve_abs_count_solutions, # “how many solutions” wrappers
38
+ _solve_distance_interpretation_prompt, # wording-based distance meaning
39
+ ]
40
+
41
+ for handler in handlers:
42
+ out = handler(expr, raw, lower, compact, help_mode)
43
+ if out is not None:
44
+ return out
45
+
46
+ # 4) Fallback: recognises topic but cannot fully parse
47
+ return SolverResult(
48
+ domain="quant",
49
+ solved=False,
50
+ topic="absolute_value",
51
+ answer_value=None,
52
+ internal_answer=None,
53
+ steps=_mode_steps(
54
+ help_mode,
55
+ [
56
+ "Identify each absolute value expression and the key point where its inside equals zero.",
57
+ "Split the number line into intervals around those key points.",
58
+ "Within each interval, remove the absolute value signs using the correct sign.",
59
+ "Solve the resulting linear equation or inequality, then keep only solutions that satisfy the interval condition.",
60
+ ],
61
+ hint_lines=[
62
+ "Start by finding where the inside of each modulus becomes zero.",
63
+ "Those boundary points tell you where the sign changes.",
64
+ ],
65
+ walkthrough_lines=[
66
+ "Absolute value problems are usually case-splitting problems.",
67
+ "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.",
68
+ ],
69
+ explain_lines=[
70
+ "Absolute value measures distance from zero, or distance from a point in forms like |x-a|.",
71
+ "That is why one equation can create two symmetric cases, and why inequalities often describe intervals or regions outside intervals.",
72
+ ],
73
+ ),
74
+ )
75
+
76
+
77
+ # ----------------------------
78
+ # Detection / helpers
79
+ # ----------------------------
80
+
81
+ def _looks_like_absolute_value(raw: str, lower: str, compact: str) -> bool:
82
+ return (
83
+ "|" in raw
84
+ or "absolute value" in lower
85
+ or "modulus" in lower
86
+ or "abs(" in compact
87
+ or re.search(r"\babs\s*\(", lower) is not None
88
+ )
89
+
90
+
91
+ def _compact(s: str) -> str:
92
+ return re.sub(r"\s+", "", s.lower())
93
+
94
+
95
+ def _normalize_abs_notation(text: str) -> str:
96
+ s = text
97
+
98
+ # abs(x-3) -> |x-3|
99
+ s = re.sub(r'(?i)\babs\s*\(([^()]+)\)', r'|\1|', s)
100
+
101
+ # absolute value of x-3 -> |x-3|
102
+ s = re.sub(r'(?i)absolute\s+value\s+of\s+([^=<>]+?)(?=\s*(?:=|<|>|≤|≥|$))', r'|\1|', s)
103
+
104
+ return s
105
+
106
+
107
+ def _detect_help_mode(lower: str) -> str:
108
+ if any(p in lower for p in ["hint", "nudge", "clue"]):
109
+ return "hint"
110
+ if any(p in lower for p in ["walkthrough", "step by step", "steps", "work through", "how do i solve"]):
111
+ return "walkthrough"
112
+ if any(p in lower for p in ["explain", "what does this mean", "what is this asking", "interpret"]):
113
+ return "explain"
114
+ return "answer"
115
+
116
+
117
+ def _handle_explainer_prompt(raw: str, lower: str, help_mode: str) -> Optional[SolverResult]:
118
+ concept_triggers = [
119
+ "what is absolute value",
120
+ "what does absolute value mean",
121
+ "explain absolute value",
122
+ "what is modulus",
123
+ "what does |x| mean",
124
+ "what does |x-a| mean",
125
+ ]
126
+ if not any(t in lower for t in concept_triggers):
127
+ return None
128
+
129
+ return SolverResult(
130
+ domain="quant",
131
+ solved=True,
132
+ topic="absolute_value",
133
+ answer_value=None,
134
+ internal_answer="concept explanation",
135
+ steps=_mode_steps(
136
+ help_mode,
137
+ [
138
+ "Absolute value means distance, not signed direction.",
139
+ "So |x| is the distance of x from 0 on the number line.",
140
+ "More generally, |x-a| is the distance between x and a.",
141
+ "That is why equations like |x-a| = k usually split into two symmetric cases, while inequalities describe points within or outside a distance range.",
142
+ ],
143
+ hint_lines=[
144
+ "Think of absolute value as distance on the number line.",
145
+ "Distance is never negative.",
146
+ ],
147
+ walkthrough_lines=[
148
+ "Interpret |x-a| as 'how far x is from a'.",
149
+ "If that distance equals k, then x can sit k units to the right of a or k units to the left of a.",
150
+ "If that distance is less than k, x must lie inside the interval centered at a.",
151
+ "If that distance is greater than k, x must lie outside that interval.",
152
+ ],
153
+ explain_lines=[
154
+ "Absolute value removes sign and keeps magnitude.",
155
+ "In algebra problems, its most useful meaning is distance.",
156
+ ],
157
+ ),
158
+ )
159
+
160
+
161
+ def _mode_steps(
162
+ help_mode: str,
163
+ default_lines: List[str],
164
+ *,
165
+ hint_lines: Optional[List[str]] = None,
166
+ walkthrough_lines: Optional[List[str]] = None,
167
+ explain_lines: Optional[List[str]] = None,
168
+ ) -> List[str]:
169
+ if help_mode == "hint" and hint_lines:
170
+ return hint_lines
171
+ if help_mode == "walkthrough" and walkthrough_lines:
172
+ return walkthrough_lines
173
+ if help_mode == "explain" and explain_lines:
174
+ return explain_lines
175
+ return default_lines
176
+
177
+
178
+ def _clean_num(n: Number) -> str:
179
+ if abs(n - round(n)) < 1e-9:
180
+ return str(int(round(n)))
181
+ return f"{n:.10g}"
182
+
183
+
184
+ def _safe_sort_pair(a: Number, b: Number) -> Tuple[Number, Number]:
185
+ return (a, b) if a <= b else (b, a)
186
+
187
+
188
+ def _is_negative(n: Number) -> bool:
189
+ return n < -1e-9
190
+
191
+
192
+ def _is_zero(n: Number) -> bool:
193
+ return abs(n) < 1e-9
194
+
195
+
196
+ def _parse_num(s: str) -> Optional[Number]:
197
+ try:
198
+ return float(s)
199
+ except Exception:
200
+ return None
201
+
202
+
203
+ def _extract_relation(expr: str) -> Optional[Tuple[str, str, str]]:
204
+ # Returns left, op, right
205
+ m = re.search(r'(.+?)(<=|>=|=|<|>|≤|≥)(.+)', expr.replace(" ", ""))
206
+ if not m:
207
+ return None
208
+ left, op, right = m.group(1), m.group(2), m.group(3)
209
+ op = op.replace("≤", "<=").replace("≥", ">=")
210
+ return left, op, right
211
+
212
+
213
+ def _parse_linear_x(inner: str) -> Optional[Tuple[Number, Number]]:
214
+ """
215
+ Parse ax+b in simple forms:
216
+ x
217
+ -x
218
+ x+3
219
+ x-3
220
+ 2x+5
221
+ 2*x-5
222
+ -3x+7
223
+ Returns (a, b) so expression is a*x + b
224
+ """
225
+ s = inner.replace(" ", "").replace("*", "")
226
+ s = s.replace("−", "-")
227
+
228
+ if "x" not in s:
229
+ return None
230
+
231
+ # Normalize starting x / -x
232
+ if s.startswith("x"):
233
+ s = "1" + s
234
+ elif s.startswith("-x"):
235
+ s = s.replace("-x", "-1x", 1)
236
+ elif s.startswith("+x"):
237
+ s = s.replace("+x", "+1x", 1)
238
+
239
+ m = re.fullmatch(r'([+-]?\d*\.?\d*)x([+-]\d*\.?\d+)?', s)
240
+ if not m:
241
+ return None
242
+
243
+ a_str = m.group(1)
244
+ b_str = m.group(2)
245
+
246
+ if a_str in ("", "+"):
247
+ a = 1.0
248
+ elif a_str == "-":
249
+ a = -1.0
250
+ else:
251
+ a = float(a_str)
252
+
253
+ b = float(b_str) if b_str else 0.0
254
+ return a, b
255
+
256
+
257
+ def _solve_linear_equals_zero(a: Number, b: Number) -> Optional[Number]:
258
+ if _is_zero(a):
259
+ return None
260
+ return -b / a
261
+
262
+
263
+ def _linear_to_center(a: Number, b: Number) -> Optional[Number]:
264
+ # ax+b = a(x-h), so h = -b/a
265
+ if _is_zero(a):
266
+ return None
267
+ return -b / a
268
+
269
+
270
+ def _format_interval(a: Number, b: Number, inclusive_left: bool, inclusive_right: bool) -> str:
271
+ L = "[" if inclusive_left else "("
272
+ R = "]" if inclusive_right else ")"
273
+ return f"{L}{_clean_num(a)}, {_clean_num(b)}{R}"
274
+
275
+
276
+ def _format_union(parts: List[str]) -> str:
277
+ return " ∪ ".join(parts)
278
+
279
+
280
+ def _hide_solution_step(line: str) -> str:
281
+ """
282
+ Mild safeguard against leaking the exact computed final answer.
283
+ We keep method language, not explicit numeric conclusion.
284
+ """
285
+ return line
286
+
287
+
288
+ # ----------------------------
289
+ # Main solver blocks
290
+ # ----------------------------
291
+
292
+ def _solve_single_abs_equation(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
293
+ rel = _extract_relation(expr)
294
+ if not rel:
295
+ return None
296
+
297
+ left, op, right = rel
298
+ if op != "=":
299
+ return None
300
+
301
+ m = re.fullmatch(r'\|(.+)\|', left)
302
+ if not m:
303
+ return None
304
+
305
+ inner = m.group(1)
306
+ k = _parse_num(right)
307
+ if k is None:
308
+ return None
309
+
310
+ lin = _parse_linear_x(inner)
311
+ if lin is None:
312
+ return None
313
+
314
+ a, b = lin
315
+
316
+ if _is_negative(k):
317
+ return SolverResult(
318
+ domain="quant",
319
+ solved=True,
320
+ topic="absolute_value",
321
+ answer_value=None,
322
+ internal_answer="no solution",
323
+ steps=_mode_steps(
324
+ help_mode,
325
+ [
326
+ "An absolute value cannot equal a negative number.",
327
  ],
328
+ hint_lines=[
329
+ "Check the right-hand side first: absolute value is never negative.",
330
+ ],
331
+ walkthrough_lines=[
332
+ "Before splitting into cases, check whether the equation is even possible.",
333
+ "Since absolute value is always non-negative, it cannot equal a negative constant.",
334
+ ],
335
+ explain_lines=[
336
+ "Absolute value represents magnitude or distance, so its output cannot be negative.",
337
+ ],
338
+ ),
339
+ )
340
+
341
+ if _is_zero(a):
342
+ const_val = abs(b)
343
+ status = "all real numbers" if _is_zero(const_val - k) else "no solution"
344
  return SolverResult(
345
  domain="quant",
346
  solved=True,
347
  topic="absolute_value",
348
+ answer_value=None,
349
+ internal_answer=status,
350
+ steps=_mode_steps(
351
+ help_mode,
352
+ [
353
+ "Here the expression inside the modulus is constant rather than variable.",
354
+ "So the equation is either always true or never true depending on whether that constant absolute value matches the right-hand side.",
355
+ ],
356
+ hint_lines=[
357
+ "Notice that x disappeared from inside the modulus.",
358
+ ],
359
+ walkthrough_lines=[
360
+ "Evaluate the constant inside the absolute value first.",
361
+ "Then compare that fixed absolute value to the right-hand side.",
362
+ ],
363
+ explain_lines=[
364
+ "If the inside is constant, the equation no longer depends on x.",
365
+ ],
366
+ ),
367
+ )
368
+
369
+ x1 = (k - b) / a
370
+ x2 = (-k - b) / a
371
+
372
+ if abs(x1 - x2) < 1e-9:
373
+ internal = _clean_num(x1)
374
+ else:
375
+ lo, hi = _safe_sort_pair(x1, x2)
376
+ internal = f"{_clean_num(lo)} and {_clean_num(hi)}"
377
+
378
+ center = _linear_to_center(a, b)
379
+
380
+ return SolverResult(
381
+ domain="quant",
382
+ solved=True,
383
+ topic="absolute_value",
384
+ answer_value=None,
385
+ internal_answer=internal,
386
+ steps=_mode_steps(
387
+ help_mode,
388
+ [
389
+ "Set the inside equal to the positive target and also to the negative target.",
390
+ "Solve the two linear cases separately.",
391
+ "That gives the points at a fixed distance from the center on the number line.",
392
  ],
393
+ hint_lines=[
394
+ "Use the rule |expression| = k → expression = k or expression = -k.",
395
+ "Then solve each linear equation.",
396
+ ],
397
+ walkthrough_lines=[
398
+ "Interpret the equation as a distance statement.",
399
+ f"The expression inside becomes zero at x = {_clean_num(center) if center is not None else 'the center point'}.",
400
+ "A fixed absolute value means x must sit the same distance on either side of that center.",
401
+ "So split into two linear equations: one for the positive case and one for the negative case.",
402
+ ],
403
+ explain_lines=[
404
+ "An equation of the form |expression| = constant usually creates two cases because distance can be achieved in two symmetric directions.",
405
+ ],
406
+ ),
407
+ )
408
+
409
+
410
+ def _solve_single_abs_inequality(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
411
+ rel = _extract_relation(expr)
412
+ if not rel:
413
+ return None
414
+
415
+ left, op, right = rel
416
+ m = re.fullmatch(r'\|(.+)\|', left)
417
+ if not m:
418
+ return None
419
+
420
+ inner = m.group(1)
421
+ k = _parse_num(right)
422
+ if k is None:
423
+ return None
424
+
425
+ lin = _parse_linear_x(inner)
426
+ if lin is None:
427
+ return None
428
+
429
+ a, b = lin
430
+
431
+ if _is_zero(a):
432
+ fixed = abs(b)
433
+ truth = _evaluate_constant_abs_inequality(fixed, op, k)
434
+ status = "all real numbers" if truth else "no solution"
435
+ return SolverResult(
436
+ domain="quant",
437
+ solved=True,
438
+ topic="absolute_value",
439
+ answer_value=None,
440
+ internal_answer=status,
441
+ steps=_mode_steps(
442
+ help_mode,
443
+ [
444
+ "The modulus contains no variable, so evaluate it as a constant inequality.",
445
+ ],
446
+ hint_lines=[
447
+ "First check whether x is actually inside the modulus.",
448
+ ],
449
+ walkthrough_lines=[
450
+ "Since the inside is constant, the inequality is either always true or never true.",
451
+ "Evaluate the absolute value and compare it to the constant on the right.",
452
+ ],
453
+ explain_lines=[
454
+ "No variable inside the modulus means the statement does not depend on x.",
455
+ ],
456
+ ),
457
  )
458
 
459
+ center = -b / a
460
+
461
+ # k < 0 special cases
462
+ if _is_negative(k):
463
+ if op in ("<", "<="):
464
+ internal = "no solution" if op == "<" else "no solution"
465
  return SolverResult(
466
  domain="quant",
467
  solved=True,
468
  topic="absolute_value",
469
+ answer_value=None,
470
+ internal_answer=internal,
471
+ steps=_mode_steps(
472
+ help_mode,
473
+ [
474
+ "Absolute value is never negative, so it cannot be less than a negative number.",
475
+ ],
476
+ hint_lines=[
477
+ "Absolute value outputs are always at least 0.",
478
+ ],
479
+ walkthrough_lines=[
480
+ "Check the sign of the right-hand side first.",
481
+ "A non-negative quantity cannot be smaller than a negative bound.",
482
+ ],
483
+ explain_lines=[
484
+ "Distance cannot be negative.",
485
+ ],
486
+ ),
487
+ )
488
+ else:
489
+ return SolverResult(
490
+ domain="quant",
491
+ solved=True,
492
+ topic="absolute_value",
493
+ answer_value=None,
494
+ internal_answer="all real numbers",
495
+ steps=_mode_steps(
496
+ help_mode,
497
+ [
498
+ "Any absolute value is greater than a negative number, so the inequality is true for every real x.",
499
+ ],
500
+ hint_lines=[
501
+ "Compare the minimum possible absolute value, which is 0, to the negative bound.",
502
+ ],
503
+ walkthrough_lines=[
504
+ "Since |expression| is always at least 0, and 0 is already greater than any negative number, every real x works here.",
505
+ ],
506
+ explain_lines=[
507
+ "The range of absolute value is [0, ∞).",
508
+ ],
509
+ ),
510
  )
511
+
512
+ # Convert |a(x-center)| ? k => |x-center| ? k/|a|
513
+ radius = k / abs(a)
514
+
515
+ if op in ("<", "<="):
516
+ if _is_negative(radius):
517
+ internal = "no solution"
518
+ else:
519
+ left_pt = center - radius
520
+ right_pt = center + radius
521
+ internal = _format_interval(left_pt, right_pt, op == "<=", op == "<=")
522
  return SolverResult(
523
  domain="quant",
524
  solved=True,
525
  topic="absolute_value",
526
+ answer_value=None,
527
+ internal_answer=internal,
528
+ steps=_mode_steps(
529
+ help_mode,
530
+ [
531
+ "Rewrite the inequality as a distance-from-center statement.",
532
+ "For a 'less than' absolute value inequality, the solution lies inside the interval around the center.",
533
+ "Use inclusive endpoints only if the inequality allows equality.",
534
+ ],
535
+ hint_lines=[
536
+ "Absolute value less than a number means 'stay within that distance'.",
537
+ "So think interval, not two separate outside regions.",
538
+ ],
539
+ walkthrough_lines=[
540
+ "Find the center by solving when the inside equals zero.",
541
+ "Then convert the inequality into a distance condition from that center.",
542
+ "Because the distance must stay below the allowed radius, the solution is the interval between the two boundary points.",
543
+ ],
544
+ explain_lines=[
545
+ "Inequalities of the form |x-a| < r describe all points within r units of a, so they represent an interval.",
546
+ ],
547
+ ),
548
+ )
549
+
550
+ if op in (">", ">="):
551
+ left_pt = center - radius
552
+ right_pt = center + radius
553
+ left_part = f"(-∞, {_clean_num(left_pt)}" + ("]" if op == ">=" else ")")
554
+ right_part = ("[" if op == ">=" else "(") + f"{_clean_num(right_pt)}, ∞)"
555
+ internal = _format_union([left_part, right_part])
556
+ return SolverResult(
557
+ domain="quant",
558
+ solved=True,
559
+ topic="absolute_value",
560
+ answer_value=None,
561
+ internal_answer=internal,
562
+ steps=_mode_steps(
563
+ help_mode,
564
+ [
565
+ "Rewrite the inequality as a distance-from-center statement.",
566
+ "For a 'greater than' absolute value inequality, the solution lies outside the central interval.",
567
+ "Include the boundary points only if the inequality allows equality.",
568
+ ],
569
+ hint_lines=[
570
+ "Absolute value greater than a number means 'farther than that distance'.",
571
+ "So expect two outside regions.",
572
+ ],
573
+ walkthrough_lines=[
574
+ "Locate the center where the inside becomes zero.",
575
+ "Interpret the inequality as requiring distance from that center to be larger than the allowed radius.",
576
+ "That means x must lie to the left of the left boundary or to the right of the right boundary.",
577
+ ],
578
+ explain_lines=[
579
+ "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.",
580
+ ],
581
+ ),
582
+ )
583
+
584
+ return None
585
+
586
+
587
+ def _evaluate_constant_abs_inequality(fixed: Number, op: str, k: Number) -> bool:
588
+ if op == "<":
589
+ return fixed < k
590
+ if op == "<=":
591
+ return fixed <= k
592
+ if op == "=":
593
+ return abs(fixed - k) < 1e-9
594
+ if op == ">":
595
+ return fixed > k
596
+ if op == ">=":
597
+ return fixed >= k
598
+ return False
599
+
600
+
601
+ def _solve_scaled_shifted_abs_equals_constant(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
602
+ # Target forms like:
603
+ # 2|x-3|+5=17
604
+ # -3+4|x+2|=9
605
+ rel = _extract_relation(expr)
606
+ if not rel:
607
+ return None
608
+
609
+ left, op, right = rel
610
+ if op != "=":
611
+ return None
612
+
613
+ right_num = _parse_num(right)
614
+ if right_num is None:
615
+ return None
616
+
617
+ s = left.replace(" ", "")
618
+ m = re.fullmatch(r'([+-]?\d*\.?\d*)?\|(.+)\|([+-]\d*\.?\d+)?', s)
619
+ if not m:
620
+ return None
621
+
622
+ a_str, inner, c_str = m.group(1), m.group(2), m.group(3)
623
+
624
+ if a_str in (None, "", "+"):
625
+ scale = 1.0
626
+ elif a_str == "-":
627
+ scale = -1.0
628
+ else:
629
+ scale = float(a_str)
630
+
631
+ c = float(c_str) if c_str else 0.0
632
+
633
+ if _is_zero(scale):
634
+ return None
635
+
636
+ target = (right_num - c) / scale
637
+
638
+ # Now solve |inner| = target
639
+ synthetic = f"|{inner}|={target}"
640
+ return _solve_single_abs_equation(synthetic, raw, lower, compact, help_mode)
641
+
642
+
643
+ def _solve_sum_of_two_abs_equals_constant(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
644
+ rel = _extract_relation(expr)
645
+ if not rel:
646
+ return None
647
+
648
+ left, op, right = rel
649
+ if op != "=":
650
+ return None
651
+
652
+ k = _parse_num(right)
653
+ if k is None:
654
+ return None
655
+
656
+ # forms: |x-a|+|x-b| = k
657
+ m = re.fullmatch(r'\|x([+-]\d*\.?\d+)?\|\+\|x([+-]\d*\.?\d+)?\|', left.replace(" ", ""))
658
+ if not m:
659
+ return None
660
+
661
+ s1 = m.group(1)
662
+ s2 = m.group(2)
663
+
664
+ a = -float(s1) if s1 else 0.0
665
+ b = -float(s2) if s2 else 0.0
666
+
667
+ lo, hi = _safe_sort_pair(a, b)
668
+ min_sum = hi - lo
669
+
670
+ if _is_negative(k):
671
+ internal = "no solution"
672
+ elif k < min_sum - 1e-9:
673
+ internal = "no solution"
674
+ elif abs(k - min_sum) < 1e-9:
675
+ internal = _format_interval(lo, hi, True, True)
676
+ else:
677
+ extra = (k - min_sum) / 2.0
678
+ left_pt = lo - extra
679
+ right_pt = hi + extra
680
+ internal = f"{_clean_num(left_pt)} and {_clean_num(right_pt)}"
681
+
682
+ return SolverResult(
683
+ domain="quant",
684
+ solved=True,
685
+ topic="absolute_value",
686
+ answer_value=None,
687
+ internal_answer=internal,
688
+ steps=_mode_steps(
689
+ help_mode,
690
+ [
691
+ "Interpret each absolute value as a distance on the number line.",
692
+ "The sum of distances to two fixed points is smallest between those points.",
693
+ "Compare the target sum to that minimum to decide whether there are no solutions, an interval of solutions, or two symmetric endpoint solutions.",
694
+ ],
695
+ hint_lines=[
696
+ "Think distance, not algebra first.",
697
+ "What is the minimum possible value of the sum of distances to the two fixed points?",
698
+ ],
699
+ walkthrough_lines=[
700
+ "Rewrite each modulus as distance from a fixed point.",
701
+ "Between the two points, the total distance stays constant at the distance between them.",
702
+ "If the target is smaller than that constant, no x works.",
703
+ "If the target equals it, every x in the middle interval works.",
704
+ "If the target is larger, you move outward symmetrically until the extra distance is split across the two ends.",
705
+ ],
706
+ explain_lines=[
707
+ "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.",
708
  ],
709
+ ),
710
+ )
711
+
712
+
713
+ def _solve_abs_count_solutions(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
714
+ if not any(p in lower for p in ["how many solutions", "number of solutions", "how many roots"]):
715
+ return None
716
+
717
+ # remove wording and try to isolate a symbolic relation
718
+ symbolic_match = re.search(r'(\|.+)', expr)
719
+ if not symbolic_match:
720
+ return None
721
+
722
+ symbolic = symbolic_match.group(1)
723
+
724
+ # try other solvers using answer mode internally
725
+ for helper in (
726
+ _solve_scaled_shifted_abs_equals_constant,
727
+ _solve_sum_of_two_abs_equals_constant,
728
+ _solve_single_abs_inequality,
729
+ _solve_single_abs_equation,
730
+ ):
731
+ res = helper(symbolic, raw, lower, compact, "answer")
732
+ if res is None or res.internal_answer is None:
733
+ continue
734
+
735
+ count = _count_solution_objects(res.internal_answer)
736
+ if count is None:
737
+ continue
738
+
739
+ return SolverResult(
740
+ domain="quant",
741
+ solved=True,
742
+ topic="absolute_value",
743
+ answer_value=None,
744
+ internal_answer=str(count),
745
+ steps=_mode_steps(
746
+ help_mode,
747
+ [
748
+ "Solve the absolute value relation structurally, then count how many distinct real solutions remain.",
749
+ ],
750
+ hint_lines=[
751
+ "First determine the full solution set, then count distinct values or intervals.",
752
+ ],
753
+ walkthrough_lines=[
754
+ "Absolute value problems can produce zero, one, two, or infinitely many solutions.",
755
+ "So after solving, decide whether the result is an empty set, a single value, two values, or an interval/all reals.",
756
+ ],
757
+ explain_lines=[
758
+ "Counting solutions means classifying the resulting solution set, not just solving mechanically.",
759
+ ],
760
+ ),
761
+ )
762
+
763
+ return None
764
+
765
+
766
+ def _count_solution_objects(internal: str) -> Optional[int]:
767
+ s = internal.strip().lower()
768
+
769
+ if s == "no solution":
770
+ return 0
771
+ if s == "all real numbers":
772
+ return math.inf # caller can still stringify if needed
773
+
774
+ if "∞" in s or "(-∞" in s or "[0," in s or "(" in s or "[" in s:
775
+ return math.inf
776
+
777
+ if " and " in s:
778
+ parts = [p.strip() for p in s.split(" and ") if p.strip()]
779
+ return len(parts)
780
+
781
+ # single numeric
782
+ try:
783
+ float(s)
784
+ return 1
785
+ except Exception:
786
+ return None
787
+
788
+
789
+ def _solve_distance_interpretation_prompt(expr: str, raw: str, lower: str, compact: str, help_mode: str) -> Optional[SolverResult]:
790
+ triggers = [
791
+ "distance from",
792
+ "within",
793
+ "at most",
794
+ "at least",
795
+ "no more than",
796
+ "no less than",
797
+ "units from",
798
+ "represents this condition",
799
+ ]
800
+ if not any(t in lower for t in triggers):
801
+ return None
802
+
803
+ m = re.search(r'([<>]=?|≤|≥)\s*x\s*([<>]=?|≤|≥)\s*(-?\d+(?:\.\d+)?)', lower)
804
+ if re.search(r'(-?\d+(?:\.\d+)?)\s*<\s*x\s*<\s*(-?\d+(?:\.\d+)?)', lower):
805
+ nums = re.search(r'(-?\d+(?:\.\d+)?)\s*<\s*x\s*<\s*(-?\d+(?:\.\d+)?)', lower)
806
+ a = float(nums.group(1))
807
+ b = float(nums.group(2))
808
+ center = (a + b) / 2.0
809
+ radius = (b - a) / 2.0
810
+ return SolverResult(
811
+ domain="quant",
812
+ solved=True,
813
+ topic="absolute_value",
814
+ answer_value=None,
815
+ internal_answer=f"|x-{_clean_num(center)}|<{_clean_num(radius)}",
816
+ steps=_mode_steps(
817
+ help_mode,
818
+ [
819
+ "Find the midpoint of the interval.",
820
+ "Then find the distance from the midpoint to either endpoint.",
821
+ "That converts the interval into an absolute value distance statement.",
822
+ ],
823
+ hint_lines=[
824
+ "Absolute value interval form is center ± radius.",
825
+ ],
826
+ walkthrough_lines=[
827
+ "A double inequality like a < x < b means x stays between two endpoints.",
828
+ "Write that as 'x is within a certain distance of the midpoint'.",
829
+ "The midpoint becomes the center, and half the interval length becomes the radius.",
830
+ ],
831
+ explain_lines=[
832
+ "Absolute value can encode interval conditions by measuring distance from the midpoint.",
833
+ ],
834
+ ),
835
  )
836
 
837
  return None