File size: 28,791 Bytes
121fd39
 
4d3cfba
121fd39
4d3cfba
121fd39
 
 
 
4d3cfba
 
 
121fd39
d72f959
 
4d3cfba
121fd39
4d3cfba
121fd39
 
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
 
 
 
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
 
4d3cfba
 
 
 
 
 
121fd39
 
 
 
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
4d3cfba
 
 
 
 
 
 
 
 
 
 
121fd39
 
 
 
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
4d3cfba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121fd39
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
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)

    # 1) Pure explainer / concept prompts
    explainer = _handle_explainer_prompt(raw, lower, help_mode)
    if explainer:
        return explainer

    # 2) Normalise abs(...) to |...| where useful
    expr = _normalize_abs_notation(raw)

    # 3) Try increasingly specific handlers
    handlers = [
        _solve_scaled_shifted_abs_equals_constant,   # a|x-b| + c = d
        _solve_sum_of_two_abs_equals_constant,       # |x-a| + |x-b| = k
        _solve_single_abs_inequality,               # |linear| < <= > >= k
        _solve_single_abs_equation,                 # |linear| = k
        _solve_abs_count_solutions,                 # “how many solutions” wrappers
        _solve_distance_interpretation_prompt,      # wording-based distance meaning
    ]

    for handler in handlers:
        out = handler(expr, raw, lower, compact, help_mode)
        if out is not None:
            return out

    # 4) Fallback: recognises topic but cannot fully parse
    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.",
            ],
        ),
    )


# ----------------------------
# Detection / helpers
# ----------------------------

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

    # abs(x-3) -> |x-3|
    s = re.sub(r'(?i)\babs\s*\(([^()]+)\)', r'|\1|', s)

    # absolute value of x-3 -> |x-3|
    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]]:
    # Returns left, op, right
    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

    # Normalize starting x / -x
    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]:
    # ax+b = a(x-h), so h = -b/a
    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


# ----------------------------
# Main solver blocks
# ----------------------------

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

    # k < 0 special cases
    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, ∞).",
                    ],
                ),
            )

    # Convert |a(x-center)| ? k  => |x-center| ? k/|a|
    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]:
    # Target forms like:
    # 2|x-3|+5=17
    # -3+4|x+2|=9
    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

    # Now solve |inner| = target
    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

    # forms: |x-a|+|x-b| = k
    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

    # remove wording and try to isolate a symbolic relation
    symbolic_match = re.search(r'(\|.+)', expr)
    if not symbolic_match:
        return None

    symbolic = symbolic_match.group(1)

    # try other solvers using answer mode internally
    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  # caller can still stringify if needed

    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)

    # single numeric
    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