Skip to main content

hematite/tools/
math_util.rs

1// ─── Pure-Rust math utilities ─────────────────────────────────────────────────
2// Number theory, sequences, combinatorics — no Python sandbox, instant results.
3
4// Index-based loops are standard notation for DP tables, matrix ops, and
5// numeric algorithms. Iterator rewrites hurt readability in math code.
6#![allow(clippy::needless_range_loop)]
7
8use std::fmt::Write;
9
10// ── Primality and factorization ───────────────────────────────────────────────
11
12fn is_prime(n: u64) -> bool {
13    if n < 2 {
14        return false;
15    }
16    if n < 4 {
17        return true;
18    }
19    if n % 2 == 0 || n % 3 == 0 {
20        return false;
21    }
22    let mut i = 5u64;
23    while i * i <= n {
24        if n % i == 0 || n % (i + 2) == 0 {
25            return false;
26        }
27        i += 6;
28    }
29    true
30}
31
32fn factorize(mut n: u64) -> Vec<(u64, u32)> {
33    let mut factors: Vec<(u64, u32)> = Vec::new();
34    if n < 2 {
35        return factors;
36    }
37    for p in [2u64, 3] {
38        if n % p == 0 {
39            let mut exp = 0u32;
40            while n % p == 0 {
41                n /= p;
42                exp += 1;
43            }
44            factors.push((p, exp));
45        }
46    }
47    let mut i = 5u64;
48    while i * i <= n {
49        if n % i == 0 {
50            let mut exp = 0u32;
51            while n % i == 0 {
52                n /= i;
53                exp += 1;
54            }
55            factors.push((i, exp));
56        }
57        if n % (i + 2) == 0 {
58            let mut exp = 0u32;
59            while n % (i + 2) == 0 {
60                n /= i + 2;
61                exp += 1;
62            }
63            factors.push((i + 2, exp));
64        }
65        i += 6;
66    }
67    if n > 1 {
68        factors.push((n, 1));
69    }
70    factors
71}
72
73fn next_prime(n: u64) -> u64 {
74    let mut c = n + 1;
75    while !is_prime(c) {
76        c += 1;
77    }
78    c
79}
80
81fn prev_prime(n: u64) -> Option<u64> {
82    if n <= 2 {
83        return None;
84    }
85    let mut c = n - 1;
86    while c >= 2 {
87        if is_prime(c) {
88            return Some(c);
89        }
90        if c == 2 {
91            break;
92        }
93        c -= 1;
94    }
95    None
96}
97
98pub fn prime_info(n: u64) -> String {
99    let mut out = String::new();
100    let _ = writeln!(out, "Number: {}", n);
101    let _ = writeln!(out, "Prime:  {}", if is_prime(n) { "Yes" } else { "No" });
102
103    let factors = factorize(n);
104    if factors.is_empty() {
105        let _ = writeln!(out, "Factors: 1 (or 0)");
106    } else {
107        let expr: Vec<String> = factors
108            .iter()
109            .map(|(p, e)| {
110                if *e == 1 {
111                    format!("{}", p)
112                } else {
113                    format!("{}^{}", p, e)
114                }
115            })
116            .collect();
117        let _ = writeln!(out, "Factors: {}", expr.join(" × "));
118
119        // divisors from factors
120        let mut divisors = vec![1u64];
121        for (p, e) in &factors {
122            let len = divisors.len();
123            let mut pw = 1u64;
124            for _ in 0..*e {
125                pw *= p;
126                for i in 0..len {
127                    divisors.push(divisors[i] * pw);
128                }
129            }
130        }
131        divisors.sort_unstable();
132        let _ = writeln!(
133            out,
134            "Divisors ({} total): {}",
135            divisors.len(),
136            if divisors.len() <= 24 {
137                divisors
138                    .iter()
139                    .map(|d| d.to_string())
140                    .collect::<Vec<_>>()
141                    .join(", ")
142            } else {
143                format!(
144                    "{} ... {} [first 12 + last 12]",
145                    divisors[..12]
146                        .iter()
147                        .map(|d| d.to_string())
148                        .collect::<Vec<_>>()
149                        .join(", "),
150                    divisors[divisors.len() - 12..]
151                        .iter()
152                        .map(|d| d.to_string())
153                        .collect::<Vec<_>>()
154                        .join(", ")
155                )
156            }
157        );
158        // Euler's totient φ(n)
159        let phi = factors.iter().fold(n, |acc, (p, _)| acc / p * (p - 1));
160        let _ = writeln!(out, "φ(n):   {}", phi);
161        // sum of divisors σ(n)
162        let sigma: u64 = factors
163            .iter()
164            .map(|(p, e)| (p.pow(e + 1) - 1) / (p - 1))
165            .product();
166        let _ = writeln!(out, "σ(n):   {}", sigma);
167        // perfect number check
168        if sigma == 2 * n {
169            let _ = writeln!(out, "✓ Perfect number");
170        }
171    }
172
173    if let Some(pp) = prev_prime(n) {
174        let _ = writeln!(out, "Prev prime: {}", pp);
175    } else {
176        let _ = writeln!(out, "Prev prime: (none)");
177    }
178    let np = next_prime(n);
179    let _ = writeln!(out, "Next prime: {}", np);
180    out
181}
182
183// ── Sequences ─────────────────────────────────────────────────────────────────
184
185pub fn generate_sequence(kind: &str, count: usize, start: f64, step: f64) -> String {
186    let count = count.clamp(1, 10_000);
187    let kind = kind.trim().to_lowercase();
188    let mut out = String::new();
189
190    let nums: Vec<f64> = match kind.as_str() {
191        "arithmetic" | "arith" | "linear" => (0..count).map(|i| start + i as f64 * step).collect(),
192        "geometric" | "geo" | "geom" => {
193            let ratio = if step == 0.0 { 2.0 } else { step };
194            let mut v = start;
195            (0..count)
196                .map(|_| {
197                    let x = v;
198                    v *= ratio;
199                    x
200                })
201                .collect()
202        }
203        "fibonacci" | "fib" => {
204            let (mut a, mut b) = (start as u64, (start + step) as u64);
205            let mut seq = vec![a as f64, b as f64];
206            for _ in 2..count {
207                let c = a.saturating_add(b);
208                seq.push(c as f64);
209                a = b;
210                b = c;
211            }
212            seq.truncate(count);
213            seq
214        }
215        "prime" | "primes" => {
216            let mut seq = Vec::with_capacity(count);
217            let mut n: u64 = start.max(2.0) as u64;
218            if !is_prime(n) {
219                n = next_prime(n - 1);
220            }
221            while seq.len() < count {
222                seq.push(n as f64);
223                n = next_prime(n);
224            }
225            seq
226        }
227        "square" | "squares" => {
228            let s = start.max(0.0) as u64;
229            (s..s + count as u64).map(|i| (i * i) as f64).collect()
230        }
231        "triangular" | "triangle" => {
232            let s = start.max(0.0) as u64;
233            (s..s + count as u64)
234                .map(|i| (i * (i + 1) / 2) as f64)
235                .collect()
236        }
237        "cube" | "cubes" => {
238            let s = start.max(0.0) as u64;
239            (s..s + count as u64).map(|i| (i * i * i) as f64).collect()
240        }
241        "power2" | "powers-of-2" | "powers_of_2" => {
242            (0..count).map(|i| (1u64 << i.min(62)) as f64).collect()
243        }
244        _ => {
245            return format!(
246                "Unknown sequence type: '{}'\n\
247                 Available: arithmetic  geometric  fibonacci  prime  square  triangular  cube  power2\n\
248                 Defaults: --seq-start 1  --seq-step 1  --seq-count 10",
249                kind
250            );
251        }
252    };
253
254    let label = match kind.as_str() {
255        "arithmetic" | "arith" | "linear" => format!("Arithmetic (start={}, step={})", start, step),
256        "geometric" | "geo" | "geom" => format!(
257            "Geometric (start={}, ratio={})",
258            start,
259            if step == 0.0 { 2.0 } else { step }
260        ),
261        _ => kind[..1].to_uppercase() + &kind[1..],
262    };
263    let _ = writeln!(out, "{}: {} terms", label, nums.len());
264    let strs: Vec<String> = nums
265        .iter()
266        .map(|x| {
267            if x.fract() == 0.0 && x.abs() < 1e15 {
268                format!("{}", *x as i64)
269            } else {
270                let s = format!("{:.6e}", x);
271                // trim trailing zeros in mantissa
272                s
273            }
274        })
275        .collect();
276    // Wrap at 72 chars
277    let mut line = String::new();
278    for (i, s) in strs.iter().enumerate() {
279        let piece = if i == 0 {
280            s.clone()
281        } else {
282            format!(", {}", s)
283        };
284        if line.len() + piece.len() > 72 {
285            let _ = writeln!(out, "{}", line);
286            line = s.clone();
287        } else {
288            line.push_str(&piece);
289        }
290    }
291    if !line.is_empty() {
292        let _ = writeln!(out, "{}", line);
293    }
294    out
295}
296
297// ── Combinatorics ─────────────────────────────────────────────────────────────
298
299pub fn combinatorics(n: u64, k: u64) -> String {
300    let mut out = String::new();
301
302    // C(n,k) using multiplicative formula (avoid overflow for reasonable n)
303    let binom = if k > n {
304        0u128
305    } else {
306        let k = k.min(n - k);
307        (0..k).fold(1u128, |acc, i| acc * (n - i) as u128 / (i + 1) as u128)
308    };
309
310    // P(n,k) = n! / (n-k)!
311    let perm: u128 = if k > n {
312        0
313    } else {
314        (n - k + 1..=n).fold(1u128, |acc, i| acc.saturating_mul(i as u128))
315    };
316
317    let _ = writeln!(out, "n = {}  k = {}", n, k);
318    let _ = writeln!(
319        out,
320        "C(n,k) = n! / (k!(n-k)!) = {}  (combinations — order does not matter)",
321        binom
322    );
323    let _ = writeln!(
324        out,
325        "P(n,k) = n! / (n-k)!     = {}  (permutations — order matters)",
326        perm
327    );
328
329    // Pascal's triangle row
330    if n <= 20 {
331        let row: Vec<u128> = (0..=n)
332            .map(|j| {
333                let j = j.min(n - j);
334                (0..j).fold(1u128, |acc, i| acc * (n - i) as u128 / (i + 1) as u128)
335            })
336            .collect();
337        let _ = writeln!(
338            out,
339            "Pascal row {}: {}",
340            n,
341            row.iter()
342                .map(|x| x.to_string())
343                .collect::<Vec<_>>()
344                .join(", ")
345        );
346    }
347    out
348}
349
350// ── Boolean truth table ───────────────────────────────────────────────────────
351// Pure-Rust: parses and evaluates a Boolean expression over single-letter variables.
352
353pub fn truth_table(expr: &str) -> String {
354    // Collect variables (single uppercase or lowercase letters A-Z)
355    let mut vars: Vec<char> = expr
356        .chars()
357        .filter(|c| c.is_ascii_alphabetic())
358        .collect::<std::collections::HashSet<char>>()
359        .into_iter()
360        .collect();
361    vars.sort_unstable();
362
363    if vars.is_empty() {
364        return format!(
365            "No variables found in: {}\nUse single letters (A, B, C, ...) as variables.",
366            expr
367        );
368    }
369    if vars.len() > 6 {
370        return format!(
371            "Too many variables ({}). Limit: 6 (for 2^6 = 64 rows).",
372            vars.len()
373        );
374    }
375
376    let n_vars = vars.len();
377    let n_rows = 1usize << n_vars;
378    let mut out = String::new();
379    let expr_display = expr
380        .trim()
381        .replace("AND", "∧")
382        .replace("OR", "∨")
383        .replace("NOT", "¬")
384        .replace("XOR", "⊕")
385        .replace("NAND", "⊼")
386        .replace("NOR", "⊽");
387
388    // Header
389    for v in &vars {
390        let _ = write!(out, " {}  ", v);
391    }
392    let _ = writeln!(out, "| {}", expr_display);
393    let sep: String =
394        vars.iter().map(|_| "----").collect::<String>() + "+--" + &"-".repeat(expr_display.len());
395    let _ = writeln!(out, "{}", sep);
396
397    let mut true_rows = 0usize;
398    for row in 0..n_rows {
399        let vals: Vec<bool> = (0..n_vars)
400            .map(|i| (row >> (n_vars - 1 - i)) & 1 == 1)
401            .collect();
402        for &v in &vals {
403            let _ = write!(out, " {}  ", if v { 'T' } else { 'F' });
404        }
405        let result = eval_bool(expr, &vars, &vals);
406        match result {
407            Ok(r) => {
408                if r {
409                    true_rows += 1;
410                }
411                let _ = writeln!(out, "| {}", if r { 'T' } else { 'F' });
412            }
413            Err(e) => {
414                let _ = writeln!(out, "| Error: {}", e);
415            }
416        }
417    }
418    let _ = writeln!(out, "{}", sep);
419    let _ = writeln!(
420        out,
421        "True rows: {} / {}  ({}%)",
422        true_rows,
423        n_rows,
424        100 * true_rows / n_rows
425    );
426    if true_rows == 0 {
427        let _ = writeln!(out, "Classification: Contradiction (always false)");
428    } else if true_rows == n_rows {
429        let _ = writeln!(out, "Classification: Tautology (always true)");
430    } else {
431        let _ = writeln!(out, "Classification: Contingency");
432    }
433    out
434}
435
436fn eval_bool(expr: &str, vars: &[char], vals: &[bool]) -> Result<bool, String> {
437    let tokens = tokenize_bool(expr)?;
438    let (result, _) = parse_bool_or(&tokens, 0, vars, vals)?;
439    Ok(result)
440}
441
442#[derive(Debug, Clone, PartialEq)]
443enum BoolToken {
444    Var(char),
445    True,
446    False,
447    Not,
448    And,
449    Or,
450    Xor,
451    Nand,
452    Nor,
453    LParen,
454    RParen,
455}
456
457fn tokenize_bool(s: &str) -> Result<Vec<BoolToken>, String> {
458    let mut tokens = Vec::new();
459    let s = s
460        .replace("NAND", "⊼")
461        .replace("NOR", "⊽")
462        .replace("XOR", "⊕")
463        .replace("AND", "∧")
464        .replace("OR", "∨")
465        .replace("NOT", "¬")
466        .replace("&&", "∧")
467        .replace("||", "∨")
468        .replace("!", "¬")
469        .replace('&', "∧")
470        .replace('|', "∨")
471        .replace('^', "⊕");
472    let chars = s.chars().peekable();
473    for c in chars {
474        match c {
475            ' ' | '\t' | '\n' => {}
476            '(' => tokens.push(BoolToken::LParen),
477            ')' => tokens.push(BoolToken::RParen),
478            '¬' => tokens.push(BoolToken::Not),
479            '∧' => tokens.push(BoolToken::And),
480            '∨' => tokens.push(BoolToken::Or),
481            '⊕' => tokens.push(BoolToken::Xor),
482            '⊼' => tokens.push(BoolToken::Nand),
483            '⊽' => tokens.push(BoolToken::Nor),
484            '1' | 'T' => tokens.push(BoolToken::True),
485            '0' | 'F' => tokens.push(BoolToken::False),
486            c if c.is_ascii_alphabetic() => tokens.push(BoolToken::Var(c)),
487            other => return Err(format!("Unknown token: '{}'", other)),
488        }
489    }
490    Ok(tokens)
491}
492
493fn parse_bool_or(
494    tokens: &[BoolToken],
495    pos: usize,
496    vars: &[char],
497    vals: &[bool],
498) -> Result<(bool, usize), String> {
499    let (mut lhs, mut pos) = parse_bool_and(tokens, pos, vars, vals)?;
500    while pos < tokens.len() {
501        match &tokens[pos] {
502            BoolToken::Or => {
503                let (rhs, np) = parse_bool_and(tokens, pos + 1, vars, vals)?;
504                lhs = lhs || rhs;
505                pos = np;
506            }
507            BoolToken::Nor => {
508                let (rhs, np) = parse_bool_and(tokens, pos + 1, vars, vals)?;
509                lhs = !(lhs || rhs);
510                pos = np;
511            }
512            _ => break,
513        }
514    }
515    Ok((lhs, pos))
516}
517
518fn parse_bool_and(
519    tokens: &[BoolToken],
520    pos: usize,
521    vars: &[char],
522    vals: &[bool],
523) -> Result<(bool, usize), String> {
524    let (mut lhs, mut pos) = parse_bool_xor(tokens, pos, vars, vals)?;
525    while pos < tokens.len() {
526        match &tokens[pos] {
527            BoolToken::And => {
528                let (rhs, np) = parse_bool_xor(tokens, pos + 1, vars, vals)?;
529                lhs = lhs && rhs;
530                pos = np;
531            }
532            BoolToken::Nand => {
533                let (rhs, np) = parse_bool_xor(tokens, pos + 1, vars, vals)?;
534                lhs = !(lhs && rhs);
535                pos = np;
536            }
537            _ => break,
538        }
539    }
540    Ok((lhs, pos))
541}
542
543fn parse_bool_xor(
544    tokens: &[BoolToken],
545    pos: usize,
546    vars: &[char],
547    vals: &[bool],
548) -> Result<(bool, usize), String> {
549    let (mut lhs, mut pos) = parse_bool_not(tokens, pos, vars, vals)?;
550    while pos < tokens.len() && tokens[pos] == BoolToken::Xor {
551        let (rhs, np) = parse_bool_not(tokens, pos + 1, vars, vals)?;
552        lhs ^= rhs;
553        pos = np;
554    }
555    Ok((lhs, pos))
556}
557
558fn parse_bool_not(
559    tokens: &[BoolToken],
560    pos: usize,
561    vars: &[char],
562    vals: &[bool],
563) -> Result<(bool, usize), String> {
564    if pos < tokens.len() && tokens[pos] == BoolToken::Not {
565        let (inner, np) = parse_bool_not(tokens, pos + 1, vars, vals)?;
566        return Ok((!inner, np));
567    }
568    parse_bool_atom(tokens, pos, vars, vals)
569}
570
571fn parse_bool_atom(
572    tokens: &[BoolToken],
573    pos: usize,
574    vars: &[char],
575    vals: &[bool],
576) -> Result<(bool, usize), String> {
577    if pos >= tokens.len() {
578        return Err("Unexpected end of expression".into());
579    }
580    match &tokens[pos] {
581        BoolToken::LParen => {
582            let (inner, np) = parse_bool_or(tokens, pos + 1, vars, vals)?;
583            if np >= tokens.len() || tokens[np] != BoolToken::RParen {
584                return Err("Missing closing ')'".into());
585            }
586            Ok((inner, np + 1))
587        }
588        BoolToken::Var(c) => {
589            let idx = vars
590                .iter()
591                .position(|v| v == c)
592                .ok_or_else(|| format!("Unknown variable '{}'", c))?;
593            Ok((vals[idx], pos + 1))
594        }
595        BoolToken::True => Ok((true, pos + 1)),
596        BoolToken::False => Ok((false, pos + 1)),
597        other => Err(format!("Unexpected token: {:?}", other)),
598    }
599}
600
601// ── GCD / LCM ─────────────────────────────────────────────────────────────────
602
603fn gcd(a: u128, b: u128) -> u128 {
604    if b == 0 {
605        a
606    } else {
607        gcd(b, a % b)
608    }
609}
610
611pub fn gcd_lcm(a: u128, b: u128) -> String {
612    let g = gcd(a, b);
613    let l = if g == 0 { 0 } else { a / g * b };
614    format!("GCD({a}, {b}) = {g}\nLCM({a}, {b}) = {l}")
615}
616
617// ── Extended number theory ────────────────────────────────────────────────────
618// Query forms:
619//   "extgcd 35 15"         — extended Euclidean algorithm
620//   "crt 2 3 3 5"          — Chinese Remainder Theorem: x ≡ 2 (mod 3), x ≡ 3 (mod 5)
621//   "mobius 12"            — Möbius function μ(n)
622//   "modinv 7 13"          — modular inverse of 7 mod 13
623//   "modpow 3 10 1000"     — 3^10 mod 1000
624//   "cf 355/113"           — continued fraction expansion
625//   "goldbach 28"          — Goldbach conjecture: express as sum of two primes
626//   "totient 36"           — Euler's totient φ(n) (already in prime_info, but here as standalone)
627//   "jacobi 5 15"          — Jacobi symbol (a/n)
628//   "fermat 17"            — Fermat primality witness check
629
630pub fn number_theory(query: &str) -> String {
631    let q = query.trim();
632    let tokens: Vec<&str> = q.split_whitespace().collect();
633    if tokens.is_empty() {
634        return nt_usage();
635    }
636    match tokens[0].to_lowercase().as_str() {
637        "extgcd" | "xgcd" => {
638            if tokens.len() < 3 {
639                return "Usage: extgcd A B".into();
640            }
641            let a: i128 = match tokens[1].parse() {
642                Ok(v) => v,
643                Err(_) => return format!("Not a number: {}", tokens[1]),
644            };
645            let b: i128 = match tokens[2].parse() {
646                Ok(v) => v,
647                Err(_) => return format!("Not a number: {}", tokens[2]),
648            };
649            let (g, x, y) = ext_gcd(a, b);
650            format!(
651                "Extended GCD({}, {}):\n  GCD = {}\n  Bézout: {}×{} + {}×{} = {}\n  (verify: {}×{} + {}×{} = {})",
652                a, b, g, x, a, y, b, g, x, a, y, b, x*a + y*b
653            )
654        }
655        "crt" => {
656            // Interleaved: crt r1 m1 r2 m2 ...
657            if tokens.len() < 5 || tokens.len() % 2 == 0 {
658                return "Usage: crt r1 m1 r2 m2 [r3 m3 ...]\n  Example: crt 2 3 3 5  (x ≡ 2 mod 3 and x ≡ 3 mod 5)".into();
659            }
660            let pairs: Vec<(i128, i128)> = tokens[1..]
661                .chunks(2)
662                .filter_map(|c| {
663                    let r = c[0].parse::<i128>().ok()?;
664                    let m = c[1].parse::<i128>().ok()?;
665                    Some((r, m))
666                })
667                .collect();
668            if pairs.len() < 2 {
669                return "Need at least 2 remainder-modulus pairs.".into();
670            }
671            match crt(&pairs) {
672                Some((x, m)) => {
673                    let mut out = "Chinese Remainder Theorem:\n".to_string();
674                    for (r, mo) in &pairs {
675                        out.push_str(&format!("  x ≡ {} (mod {})\n", r, mo));
676                    }
677                    out.push_str(&format!(
678                        "Solution: x ≡ {} (mod {})  [smallest positive: {}]",
679                        x,
680                        m,
681                        ((x % m) + m) % m
682                    ));
683                    out
684                }
685                None => "No solution — moduli are not pairwise coprime.".into(),
686            }
687        }
688        "mobius" | "möbius" | "mu" => {
689            if tokens.len() < 2 {
690                return "Usage: mobius N".into();
691            }
692            let n: u64 = match tokens[1].parse() {
693                Ok(v) => v,
694                Err(_) => return format!("Not a number: {}", tokens[1]),
695            };
696            let mu = mobius(n);
697            let explanation = match mu {
698                0 => "n has a squared prime factor → μ(n) = 0",
699                1 => "n is squarefree with even number of prime factors → μ(n) = 1",
700                -1 => "n is squarefree with odd number of prime factors → μ(n) = -1",
701                _ => "",
702            };
703            format!("Möbius function μ({}) = {}\n  {}", n, mu, explanation)
704        }
705        "modinv" => {
706            if tokens.len() < 3 {
707                return "Usage: modinv A MOD".into();
708            }
709            let a: i128 = match tokens[1].parse() {
710                Ok(v) => v,
711                Err(_) => return format!("Not a number: {}", tokens[1]),
712            };
713            let m: i128 = match tokens[2].parse() {
714                Ok(v) => v,
715                Err(_) => return format!("Not a number: {}", tokens[2]),
716            };
717            match mod_inv(a, m) {
718                Some(inv) => format!(
719                    "Modular inverse: {}⁻¹ ≡ {} (mod {})\nVerify: {} × {} = {} ≡ 1 (mod {})",
720                    a,
721                    inv,
722                    m,
723                    a,
724                    inv,
725                    a * inv,
726                    m
727                ),
728                None => format!("No modular inverse: gcd({}, {}) ≠ 1 (not coprime)", a, m),
729            }
730        }
731        "modpow" | "powmod" => {
732            if tokens.len() < 4 {
733                return "Usage: modpow BASE EXP MOD".into();
734            }
735            let base: u128 = match tokens[1].parse() {
736                Ok(v) => v,
737                Err(_) => return format!("Not a number: {}", tokens[1]),
738            };
739            let exp: u128 = match tokens[2].parse() {
740                Ok(v) => v,
741                Err(_) => return format!("Not a number: {}", tokens[2]),
742            };
743            let modu: u128 = match tokens[3].parse() {
744                Ok(v) => v,
745                Err(_) => return format!("Not a number: {}", tokens[3]),
746            };
747            if modu == 0 {
748                return "Modulus cannot be zero.".into();
749            }
750            let result = mod_pow(base, exp, modu);
751            format!("{}^{} mod {} = {}", base, exp, modu, result)
752        }
753        "cf" | "cfrac" | "continued_fraction" => {
754            if tokens.len() < 2 {
755                return "Usage: cf N/D  or  cf DECIMAL".into();
756            }
757            let input = tokens[1];
758            let (num, den) = if input.contains('/') {
759                let parts: Vec<&str> = input.splitn(2, '/').collect();
760                let n: i64 = parts[0].parse().unwrap_or(0);
761                let d: i64 = parts[1].parse().unwrap_or(1);
762                (n, d)
763            } else if let Ok(f) = input.parse::<f64>() {
764                // Approximate as fraction with denominator up to 1e6
765                let scale = 1_000_000i64;
766                ((f * scale as f64).round() as i64, scale)
767            } else {
768                return format!("Cannot parse: {}", input);
769            };
770            if den == 0 {
771                return "Denominator cannot be zero.".into();
772            }
773            let coeffs = cf_expansion(num, den, 20);
774            let convergents = cf_convergents(&coeffs);
775            let mut out = format!(
776                "Continued fraction of {}/{} = {}:\n",
777                num,
778                den,
779                num as f64 / den as f64
780            );
781            out.push_str(&format!(
782                "  CF = [{}]\n",
783                coeffs
784                    .iter()
785                    .map(|x| x.to_string())
786                    .collect::<Vec<_>>()
787                    .join("; ")
788            ));
789            out.push_str("  Convergents:\n");
790            for (p, q) in &convergents {
791                out.push_str(&format!("    {}/{} = {:.8}\n", p, q, *p as f64 / *q as f64));
792            }
793            out
794        }
795        "goldbach" => {
796            if tokens.len() < 2 {
797                return "Usage: goldbach N (must be even, > 2)".into();
798            }
799            let n: u64 = match tokens[1].parse() {
800                Ok(v) => v,
801                Err(_) => return format!("Not a number: {}", tokens[1]),
802            };
803            if n <= 2 || n % 2 != 0 {
804                return format!("{} must be even and > 2 for Goldbach's conjecture.", n);
805            }
806            let pairs: Vec<(u64, u64)> = (2..=n / 2)
807                .filter(|&p| is_prime(p) && is_prime(n - p))
808                .map(|p| (p, n - p))
809                .collect();
810            let mut out = format!("Goldbach decompositions of {}:\n", n);
811            if pairs.is_empty() {
812                out.push_str("  No decompositions found (unexpected for n > 2).\n");
813            } else {
814                for (p, q) in pairs.iter().take(10) {
815                    out.push_str(&format!("  {} = {} + {}\n", n, p, q));
816                }
817                if pairs.len() > 10 {
818                    out.push_str(&format!("  ... ({} total decompositions)\n", pairs.len()));
819                }
820            }
821            out
822        }
823        "totient" | "phi" | "euler" => {
824            if tokens.len() < 2 {
825                return "Usage: totient N".into();
826            }
827            let n: u64 = match tokens[1].parse() {
828                Ok(v) => v,
829                Err(_) => return format!("Not a number: {}", tokens[1]),
830            };
831            let phi = euler_totient(n);
832            format!(
833                "Euler's totient φ({}) = {}\n  (count of integers 1..{} coprime to {})",
834                n, phi, n, n
835            )
836        }
837        "jacobi" => {
838            if tokens.len() < 3 {
839                return "Usage: jacobi A N (N must be odd)".into();
840            }
841            let a: i64 = match tokens[1].parse() {
842                Ok(v) => v,
843                Err(_) => return format!("Not a number: {}", tokens[1]),
844            };
845            let n: i64 = match tokens[2].parse() {
846                Ok(v) => v,
847                Err(_) => return format!("Not a number: {}", tokens[2]),
848            };
849            if n <= 0 || n % 2 == 0 {
850                return "N must be a positive odd integer.".into();
851            }
852            let j = jacobi_symbol(a, n);
853            let meaning = match j {
854                0 => "a is not coprime to n",
855                1 => "a is a quadratic residue mod n (or n is composite)",
856                -1 => "a is a quadratic non-residue mod n",
857                _ => "",
858            };
859            format!("Jacobi symbol ({}/{}) = {}\n  {}", a, n, j, meaning)
860        }
861        _ => {
862            // Try to interpret as a single number for a complete number theory report
863            if let Ok(n) = tokens[0].parse::<u64>() {
864                nt_report(n)
865            } else {
866                nt_usage()
867            }
868        }
869    }
870}
871
872fn nt_usage() -> String {
873    "Number theory operations:\n\
874     hematite --number-theory 'extgcd 35 15'\n\
875     hematite --number-theory 'crt 2 3 3 5'\n\
876     hematite --number-theory 'mobius 30'\n\
877     hematite --number-theory 'modinv 7 13'\n\
878     hematite --number-theory 'modpow 3 10 1000'\n\
879     hematite --number-theory 'cf 355/113'\n\
880     hematite --number-theory 'goldbach 28'\n\
881     hematite --number-theory 'totient 36'\n\
882     hematite --number-theory 'jacobi 5 15'\n\
883     hematite --number-theory '42'    (full report for a number)"
884        .into()
885}
886
887fn nt_report(n: u64) -> String {
888    let mut out = String::new();
889    let _ = writeln!(out, "Number theory report for {}", n);
890    let _ = writeln!(out, "  Euler's totient φ(n) = {}", euler_totient(n));
891    let _ = writeln!(out, "  Möbius μ(n) = {}", mobius(n));
892    if n < 1_000_000 {
893        let sigma: u64 = (1..=n).filter(|d| n % d == 0).sum();
894        let _ = writeln!(out, "  Sum of divisors σ(n) = {}", sigma);
895        if sigma == 2 * n {
896            let _ = writeln!(out, "  → Perfect number!");
897        } else if sigma > 2 * n {
898            let _ = writeln!(out, "  → Abundant number");
899        } else {
900            let _ = writeln!(out, "  → Deficient number");
901        }
902    }
903    if n >= 4 && n % 2 == 0 {
904        if let Some((p, q)) = (2..=n / 2)
905            .filter(|&p| is_prime(p) && is_prime(n - p))
906            .map(|p| (p, n - p))
907            .next()
908        {
909            let _ = writeln!(out, "  Goldbach: {} = {} + {}", n, p, q);
910        }
911    }
912    out
913}
914
915fn ext_gcd(a: i128, b: i128) -> (i128, i128, i128) {
916    if b == 0 {
917        return (a, 1, 0);
918    }
919    let (g, x1, y1) = ext_gcd(b, a % b);
920    (g, y1, x1 - (a / b) * y1)
921}
922
923fn mod_inv(a: i128, m: i128) -> Option<i128> {
924    let (g, x, _) = ext_gcd(a.rem_euclid(m), m);
925    if g != 1 {
926        return None;
927    }
928    Some(x.rem_euclid(m))
929}
930
931fn mod_pow(mut base: u128, mut exp: u128, modu: u128) -> u128 {
932    if modu == 1 {
933        return 0;
934    }
935    let mut result = 1u128;
936    base %= modu;
937    while exp > 0 {
938        if exp % 2 == 1 {
939            result = result.wrapping_mul(base) % modu;
940        }
941        exp /= 2;
942        base = base.wrapping_mul(base) % modu;
943    }
944    result
945}
946
947fn crt(pairs: &[(i128, i128)]) -> Option<(i128, i128)> {
948    let mut x = pairs[0].0;
949    let mut m = pairs[0].1;
950    for &(r, mi) in &pairs[1..] {
951        let g = gcd(m as u128, mi.unsigned_abs()) as i128;
952        if (r - x) % g != 0 {
953            return None;
954        }
955        let lcm = m / g * mi;
956        let inv = mod_inv(m / g, mi / g)?;
957        x = x + m * ((r - x) / g % (mi / g) * inv % (mi / g));
958        m = lcm;
959        x = x.rem_euclid(m);
960    }
961    Some((x, m))
962}
963
964fn mobius(n: u64) -> i32 {
965    if n == 1 {
966        return 1;
967    }
968    let factors = factorize(n);
969    for (_, exp) in &factors {
970        if *exp > 1 {
971            return 0;
972        }
973    }
974    if factors.len() % 2 == 0 {
975        1
976    } else {
977        -1
978    }
979}
980
981fn euler_totient(n: u64) -> u64 {
982    if n == 0 {
983        return 0;
984    }
985    let factors = factorize(n);
986    let mut phi = n;
987    for (p, _) in factors {
988        phi = phi / p * (p - 1);
989    }
990    phi
991}
992
993fn cf_expansion(mut num: i64, mut den: i64, max_terms: usize) -> Vec<i64> {
994    let mut coeffs = Vec::new();
995    for _ in 0..max_terms {
996        coeffs.push(num / den);
997        let rem = num % den;
998        if rem == 0 {
999            break;
1000        }
1001        num = den;
1002        den = rem;
1003    }
1004    coeffs
1005}
1006
1007fn cf_convergents(coeffs: &[i64]) -> Vec<(i64, i64)> {
1008    let mut result = Vec::new();
1009    let (mut p_prev, mut q_prev) = (1i64, 0i64);
1010    let (mut p_curr, mut q_curr) = (coeffs[0], 1i64);
1011    result.push((p_curr, q_curr));
1012    for &a in &coeffs[1..] {
1013        let p_next = a * p_curr + p_prev;
1014        let q_next = a * q_curr + q_prev;
1015        result.push((p_next, q_next));
1016        p_prev = p_curr;
1017        q_prev = q_curr;
1018        p_curr = p_next;
1019        q_curr = q_next;
1020    }
1021    result
1022}
1023
1024fn jacobi_symbol(mut a: i64, mut n: i64) -> i32 {
1025    if n <= 0 || n % 2 == 0 {
1026        return 0;
1027    }
1028    let mut result = 1i32;
1029    a = a.rem_euclid(n);
1030    while a != 0 {
1031        while a % 2 == 0 {
1032            a /= 2;
1033            if n % 8 == 3 || n % 8 == 5 {
1034                result = -result;
1035            }
1036        }
1037        std::mem::swap(&mut a, &mut n);
1038        if a % 4 == 3 && n % 4 == 3 {
1039            result = -result;
1040        }
1041        a %= n;
1042    }
1043    if n == 1 {
1044        result
1045    } else {
1046        0
1047    }
1048}
1049
1050// ── Roman numerals ────────────────────────────────────────────────────────────
1051
1052pub fn to_roman(mut n: u64) -> String {
1053    if n == 0 {
1054        return "Roman numerals start at 1.".into();
1055    }
1056    if n > 3_999_999 {
1057        return format!("{n} is too large for standard Roman numerals (max 3,999,999).");
1058    }
1059    const TABLE: &[(u64, &str)] = &[
1060        (1_000_000, "M̄"),
1061        (900_000, "C̄M̄"),
1062        (500_000, "D̄"),
1063        (400_000, "C̄D̄"),
1064        (100_000, "C̄"),
1065        (90_000, "X̄C̄"),
1066        (50_000, "L̄"),
1067        (40_000, "X̄L̄"),
1068        (10_000, "X̄"),
1069        (9_000, "MX̄"),
1070        (5_000, "V̄"),
1071        (4_000, "MV̄"),
1072        (1000, "M"),
1073        (900, "CM"),
1074        (500, "D"),
1075        (400, "CD"),
1076        (100, "C"),
1077        (90, "XC"),
1078        (50, "L"),
1079        (40, "XL"),
1080        (10, "X"),
1081        (9, "IX"),
1082        (5, "V"),
1083        (4, "IV"),
1084        (1, "I"),
1085    ];
1086    let mut out = String::new();
1087    for &(val, sym) in TABLE {
1088        while n >= val {
1089            out.push_str(sym);
1090            n -= val;
1091        }
1092    }
1093    out
1094}
1095
1096pub fn from_roman(s: &str) -> String {
1097    let s = s.trim().to_uppercase();
1098    let map = [
1099        ("M̄", 1_000_000u64),
1100        ("C̄M̄", 900_000),
1101        ("D̄", 500_000),
1102        ("C̄D̄", 400_000),
1103        ("C̄", 100_000),
1104        ("X̄C̄", 90_000),
1105        ("L̄", 50_000),
1106        ("X̄L̄", 40_000),
1107        ("X̄", 10_000),
1108        ("MX̄", 9_000),
1109        ("V̄", 5_000),
1110        ("MV̄", 4_000),
1111        ("M", 1000),
1112        ("CM", 900),
1113        ("D", 500),
1114        ("CD", 400),
1115        ("C", 100),
1116        ("XC", 90),
1117        ("L", 50),
1118        ("XL", 40),
1119        ("X", 10),
1120        ("IX", 9),
1121        ("V", 5),
1122        ("IV", 4),
1123        ("I", 1),
1124    ];
1125    let mut pos = 0usize;
1126    let chars: Vec<char> = s.chars().collect();
1127    let mut total = 0u64;
1128    'outer: while pos < chars.len() {
1129        for &(sym, val) in &map {
1130            let sc: Vec<char> = sym.chars().collect();
1131            if chars[pos..].starts_with(&sc) {
1132                total += val;
1133                pos += sc.len();
1134                continue 'outer;
1135            }
1136        }
1137        return format!(
1138            "Unrecognized Roman numeral character at position {}: '{}'",
1139            pos, chars[pos]
1140        );
1141    }
1142    format!("{} = {}", s, total)
1143}
1144
1145pub fn roman_info(input: &str) -> String {
1146    let t = input.trim();
1147    if let Ok(n) = t.parse::<u64>() {
1148        let r = to_roman(n);
1149        format!("{n} = {r}")
1150    } else {
1151        from_roman(t)
1152    }
1153}
1154
1155// ── Number base conversion ────────────────────────────────────────────────────
1156
1157pub fn base_convert(input: &str, from_base: u32, to_base: u32) -> String {
1158    if !(2..=36).contains(&from_base) || !(2..=36).contains(&to_base) {
1159        return "Base must be between 2 and 36.".into();
1160    }
1161    let s = input.trim().to_ascii_uppercase();
1162    let value = u128::from_str_radix(&s, from_base).unwrap_or(0);
1163    // check parse success separately
1164    if u128::from_str_radix(&s, from_base).is_err() {
1165        return format!("'{}' is not a valid base-{} number.", s, from_base);
1166    }
1167    let to_str = |mut v: u128, base: u32| -> String {
1168        if v == 0 {
1169            return "0".into();
1170        }
1171        let digits: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1172        let mut result = Vec::new();
1173        while v > 0 {
1174            result.push(digits[(v % base as u128) as usize] as char);
1175            v /= base as u128;
1176        }
1177        result.into_iter().rev().collect()
1178    };
1179    let _ = value; // used via closure
1180    let value2 = u128::from_str_radix(&s, from_base).unwrap();
1181    let mut out = String::new();
1182    let _ = writeln!(out, "Input (base {}): {}", from_base, s);
1183    let _ = writeln!(out, "Decimal: {}", value2);
1184    let _ = writeln!(
1185        out,
1186        "Output (base {}): {}",
1187        to_base,
1188        to_str(value2, to_base)
1189    );
1190    if to_base != 2 && from_base != 2 {
1191        let _ = writeln!(out, "Binary:  {}", to_str(value2, 2));
1192    }
1193    if to_base != 16 && from_base != 16 {
1194        let _ = writeln!(out, "Hex:     {}", to_str(value2, 16));
1195    }
1196    if to_base != 8 && from_base != 8 {
1197        let _ = writeln!(out, "Octal:   {}", to_str(value2, 8));
1198    }
1199    out
1200}
1201
1202// ── Date arithmetic ───────────────────────────────────────────────────────────
1203
1204fn days_in_month(year: i32, month: u32) -> u32 {
1205    match month {
1206        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1207        4 | 6 | 9 | 11 => 30,
1208        2 => {
1209            if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
1210                29
1211            } else {
1212                28
1213            }
1214        }
1215        _ => 30,
1216    }
1217}
1218
1219fn to_jdn(y: i32, m: u32, d: u32) -> i64 {
1220    let a = (14 - m as i32) / 12;
1221    let yr = y + 4800 - a;
1222    let mo = m as i32 + 12 * a - 3;
1223    d as i64 + (153 * mo + 2) as i64 / 5 + 365 * yr as i64 + yr as i64 / 4 - yr as i64 / 100
1224        + yr as i64 / 400
1225        - 32045
1226}
1227
1228fn from_jdn(jdn: i64) -> (i32, u32, u32) {
1229    let l = jdn + 68569;
1230    let n = 4 * l / 146097;
1231    let l = l - (146097 * n + 3) / 4;
1232    let i = 4000 * (l + 1) / 1461001;
1233    let l = l - 1461 * i / 4 + 31;
1234    let j = 80 * l / 2447;
1235    let d = l - 2447 * j / 80;
1236    let l = j / 11;
1237    let m = j + 2 - 12 * l;
1238    let y = 100 * (n - 49) + i + l;
1239    (y as i32, m as u32, d as u32)
1240}
1241
1242fn parse_date(s: &str) -> Option<(i32, u32, u32)> {
1243    let s = s.trim();
1244    // Try YYYY-MM-DD
1245    let parts: Vec<&str> = s
1246        .splitn(3, |c: char| !c.is_ascii_digit())
1247        .filter(|p| !p.is_empty())
1248        .collect();
1249    if parts.len() == 3 {
1250        let y = parts[0].parse::<i32>().ok()?;
1251        let m = parts[1].parse::<u32>().ok()?;
1252        let d = parts[2].parse::<u32>().ok()?;
1253        if (1..=12).contains(&m) && d >= 1 && d <= days_in_month(y, m) {
1254            return Some((y, m, d));
1255        }
1256    }
1257    None
1258}
1259
1260const WEEKDAYS: &[&str] = &[
1261    "Monday",
1262    "Tuesday",
1263    "Wednesday",
1264    "Thursday",
1265    "Friday",
1266    "Saturday",
1267    "Sunday",
1268];
1269
1270pub fn date_calc(input: &str) -> String {
1271    let s = input.trim();
1272    let mut out = String::new();
1273
1274    // "DATE1 to DATE2" or "DATE1, DATE2" → days between
1275    // "DATE +N" or "DATE -N" → add/subtract days
1276    // "DATE" alone → info about that date
1277    // "unix TIMESTAMP" → convert Unix timestamp
1278    // "timestamp DATE" → date to Unix timestamp
1279
1280    if s.to_lowercase().starts_with("unix ") {
1281        let ts: i64 = match s[5..].trim().parse() {
1282            Ok(v) => v,
1283            Err(_) => return "Usage: --date 'unix 1700000000'".into(),
1284        };
1285        let jdn = ts / 86400 + 2440588;
1286        let (y, m, d) = from_jdn(jdn);
1287        let dow = ((jdn + 1) % 7) as usize;
1288        let _ = writeln!(out, "Unix: {}", ts);
1289        let _ = writeln!(out, "Date: {}-{:02}-{:02} ({})", y, m, d, WEEKDAYS[dow]);
1290        return out;
1291    }
1292
1293    if s.to_lowercase().starts_with("timestamp ") {
1294        let date_str = &s["timestamp ".len()..];
1295        if let Some((y, m, d)) = parse_date(date_str) {
1296            let jdn = to_jdn(y, m, d);
1297            let ts = (jdn - 2440588) * 86400;
1298            let _ = writeln!(out, "Date: {}-{:02}-{:02}", y, m, d);
1299            let _ = writeln!(out, "Unix timestamp (midnight UTC): {}", ts);
1300        } else {
1301            out.push_str("Could not parse date. Use YYYY-MM-DD format.");
1302        }
1303        return out;
1304    }
1305
1306    // Split on "to" or ","
1307    let (a_str, b_str) = if let Some(pos) = s.to_lowercase().find(" to ") {
1308        (s[..pos].trim(), Some(s[pos + 4..].trim()))
1309    } else if s.contains(',') {
1310        let mut it = s.splitn(2, ',');
1311        (
1312            it.next().unwrap_or("").trim(),
1313            Some(it.next().unwrap_or("").trim()),
1314        )
1315    } else {
1316        (s, None)
1317    };
1318
1319    // Check for "+N" or "-N" at the end (date arithmetic)
1320    let plus_re = {
1321        let a = a_str.trim_end();
1322        if let Some(idx) = a.rfind(['+', '-']) {
1323            let (date_part, offset_part) = a.split_at(idx);
1324            if let Ok(n) = offset_part.trim().parse::<i64>() {
1325                Some((date_part.trim(), n))
1326            } else {
1327                None
1328            }
1329        } else {
1330            None
1331        }
1332    };
1333
1334    if let Some((date_part, offset)) = plus_re {
1335        if let Some((y, m, d)) = parse_date(date_part) {
1336            let jdn = to_jdn(y, m, d) + offset;
1337            let (y2, m2, d2) = from_jdn(jdn);
1338            let dow = ((jdn + 1) % 7) as usize;
1339            let _ = writeln!(
1340                out,
1341                "{}-{:02}-{:02}  {} {} days  →  {}-{:02}-{:02} ({})",
1342                y,
1343                m,
1344                d,
1345                if offset >= 0 { "+" } else { "" },
1346                offset,
1347                y2,
1348                m2,
1349                d2,
1350                WEEKDAYS[dow]
1351            );
1352        } else {
1353            out.push_str(
1354                "Could not parse date. Use: --date '2024-03-15 +90' or '2024-01-01 to 2024-12-31'",
1355            );
1356        }
1357        return out;
1358    }
1359
1360    if let (Some((y1, m1, d1)), Some(b)) = (parse_date(a_str), b_str) {
1361        if let Some((y2, m2, d2)) = parse_date(b) {
1362            let jdn1 = to_jdn(y1, m1, d1);
1363            let jdn2 = to_jdn(y2, m2, d2);
1364            let diff = jdn2 - jdn1;
1365            let dow1 = ((jdn1 + 1) % 7) as usize;
1366            let dow2 = ((jdn2 + 1) % 7) as usize;
1367            let _ = writeln!(out, "From: {}-{:02}-{:02} ({})", y1, m1, d1, WEEKDAYS[dow1]);
1368            let _ = writeln!(out, "To:   {}-{:02}-{:02} ({})", y2, m2, d2, WEEKDAYS[dow2]);
1369            let _ = writeln!(
1370                out,
1371                "Difference: {} days  ({} weeks {} days)",
1372                diff.abs(),
1373                diff.abs() / 7,
1374                diff.abs() % 7
1375            );
1376            let _ = writeln!(
1377                out,
1378                "           ≈ {:.2} months  ≈ {:.3} years",
1379                diff.abs() as f64 / 30.4375,
1380                diff.abs() as f64 / 365.25
1381            );
1382            if diff < 0 {
1383                let _ = writeln!(out, "(B is before A — {} days ago)", diff.abs());
1384            }
1385        } else {
1386            out.push_str("Could not parse second date.");
1387        }
1388        return out;
1389    }
1390
1391    // Single date info
1392    if let Some((y, m, d)) = parse_date(a_str) {
1393        let jdn = to_jdn(y, m, d);
1394        let dow = ((jdn + 1) % 7) as usize;
1395        let ts = (jdn - 2440588) * 86400;
1396        let day_of_year: u32 = (1..m).map(|mo| days_in_month(y, mo)).sum::<u32>() + d;
1397        let is_leap = y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);
1398        let days_left = (if is_leap { 366 } else { 365 }) - day_of_year;
1399        let _ = writeln!(out, "Date:       {}-{:02}-{:02}", y, m, d);
1400        let _ = writeln!(
1401            out,
1402            "Day:        {} (day {} of {}, {} remaining)",
1403            WEEKDAYS[dow],
1404            day_of_year,
1405            if is_leap { 366 } else { 365 },
1406            days_left
1407        );
1408        let _ = writeln!(out, "Leap year:  {}", if is_leap { "Yes" } else { "No" });
1409        let _ = writeln!(out, "Unix stamp: {} (midnight UTC)", ts);
1410        let _ = writeln!(out, "Julian day: {}", jdn);
1411    } else {
1412        out.push_str("Could not parse date. Examples:\n  --date '2024-06-15'\n  --date '2024-01-01 to 2024-12-31'\n  --date '2024-03-15 +90'\n  --date 'unix 1700000000'");
1413    }
1414    out
1415}
1416
1417// ── IPv4 subnet calculator ────────────────────────────────────────────────────
1418
1419pub fn subnet_calc(cidr: &str) -> String {
1420    let cidr = cidr.trim();
1421    let mut out = String::new();
1422
1423    // Parse "A.B.C.D/prefix" or "A.B.C.D mask M.M.M.M"
1424    let (ip_str, prefix) = if let Some(idx) = cidr.find('/') {
1425        let prefix: u8 = match cidr[idx + 1..].trim().parse() {
1426            Ok(v) => v,
1427            Err(_) => return "Invalid prefix length. Use CIDR format: 192.168.1.0/24".into(),
1428        };
1429        (&cidr[..idx], prefix)
1430    } else {
1431        return "Use CIDR notation: 192.168.1.0/24".into();
1432    };
1433
1434    let parse_ip = |s: &str| -> Option<u32> {
1435        let parts: Vec<u8> = s.trim().split('.').filter_map(|x| x.parse().ok()).collect();
1436        if parts.len() == 4 {
1437            Some(
1438                ((parts[0] as u32) << 24)
1439                    | ((parts[1] as u32) << 16)
1440                    | ((parts[2] as u32) << 8)
1441                    | parts[3] as u32,
1442            )
1443        } else {
1444            None
1445        }
1446    };
1447
1448    let ip = match parse_ip(ip_str) {
1449        Some(v) => v,
1450        None => return format!("Invalid IP address: '{}'", ip_str),
1451    };
1452
1453    if prefix > 32 {
1454        return "Prefix must be 0–32.".into();
1455    }
1456
1457    let mask: u32 = if prefix == 0 {
1458        0
1459    } else {
1460        !0u32 << (32 - prefix)
1461    };
1462    let network = ip & mask;
1463    let broadcast = network | !mask;
1464    let first_host = if prefix >= 31 { network } else { network + 1 };
1465    let last_host = if prefix >= 31 {
1466        broadcast
1467    } else {
1468        broadcast - 1
1469    };
1470    let host_count: u64 = if prefix >= 32 {
1471        1
1472    } else if prefix == 31 {
1473        2
1474    } else {
1475        (1u64 << (32 - prefix)) - 2
1476    };
1477
1478    let fmt_ip = |v: u32| {
1479        format!(
1480            "{}.{}.{}.{}",
1481            v >> 24,
1482            (v >> 16) & 0xff,
1483            (v >> 8) & 0xff,
1484            v & 0xff
1485        )
1486    };
1487
1488    let class = match ip >> 24 {
1489        0..=127 => "A",
1490        128..=191 => "B",
1491        192..=223 => "C",
1492        224..=239 => "D (Multicast)",
1493        _ => "E (Reserved)",
1494    };
1495    let private = (ip >> 24) == 10
1496        || ((ip >> 24) == 172 && ((ip >> 20) & 0xf) == 1)
1497        || ((ip >> 24) == 192 && ((ip >> 16) & 0xff) == 168);
1498
1499    let _ = writeln!(out, "CIDR:       {}/{}", fmt_ip(ip), prefix);
1500    let _ = writeln!(out, "Network:    {}/{}", fmt_ip(network), prefix);
1501    let _ = writeln!(out, "Broadcast:  {}", fmt_ip(broadcast));
1502    let _ = writeln!(out, "Subnet mask:{}", fmt_ip(mask));
1503    let _ = writeln!(out, "First host: {}", fmt_ip(first_host));
1504    let _ = writeln!(out, "Last host:  {}", fmt_ip(last_host));
1505    let _ = writeln!(out, "Hosts:      {}", host_count);
1506    let _ = writeln!(
1507        out,
1508        "Class:      {}  |  Private: {}",
1509        class,
1510        if private { "Yes" } else { "No" }
1511    );
1512    out
1513}
1514
1515// ── Color space conversion ────────────────────────────────────────────────────
1516
1517pub fn color_convert(input: &str) -> String {
1518    let s = input.trim().to_ascii_lowercase();
1519    let mut out = String::new();
1520
1521    // Parse hex: #RRGGBB or RRGGBB or #RGB
1522    let hex_input = s.trim_start_matches('#');
1523    let (r8, g8, b8) = if hex_input.len() == 6 {
1524        if let (Ok(r), Ok(g), Ok(b)) = (
1525            u8::from_str_radix(&hex_input[0..2], 16),
1526            u8::from_str_radix(&hex_input[2..4], 16),
1527            u8::from_str_radix(&hex_input[4..6], 16),
1528        ) {
1529            (r, g, b)
1530        } else {
1531            return format!("Invalid hex: '{}'", input);
1532        }
1533    } else if hex_input.len() == 3 {
1534        if let (Ok(r), Ok(g), Ok(b)) = (
1535            u8::from_str_radix(&hex_input[0..1].repeat(2), 16),
1536            u8::from_str_radix(&hex_input[1..2].repeat(2), 16),
1537            u8::from_str_radix(&hex_input[2..3].repeat(2), 16),
1538        ) {
1539            (r, g, b)
1540        } else {
1541            return format!("Invalid hex: '{}'", input);
1542        }
1543    } else if s.starts_with("rgb(") || s.starts_with("rgb ") {
1544        let nums: Vec<u8> = s
1545            .chars()
1546            .filter(|c| c.is_ascii_digit() || *c == ' ' || *c == ',')
1547            .collect::<String>()
1548            .split(|c: char| !c.is_ascii_digit())
1549            .filter_map(|x| x.parse().ok())
1550            .collect();
1551        if nums.len() >= 3 {
1552            (nums[0], nums[1], nums[2])
1553        } else {
1554            return "Usage: --color '#ff8800' or --color 'rgb(255,136,0)'".into();
1555        }
1556    } else {
1557        return "Usage: --color '#ff8800' or --color 'rgb(255,136,0)' or --color '3f8'".into();
1558    };
1559
1560    let rf = r8 as f64 / 255.0;
1561    let gf = g8 as f64 / 255.0;
1562    let bf = b8 as f64 / 255.0;
1563
1564    // RGB → HSL
1565    let cmax = rf.max(gf).max(bf);
1566    let cmin = rf.min(gf).min(bf);
1567    let delta = cmax - cmin;
1568    let l = (cmax + cmin) / 2.0;
1569    let s_hsl = if delta == 0.0 {
1570        0.0
1571    } else {
1572        delta / (1.0 - (2.0 * l - 1.0).abs())
1573    };
1574    let h_hsl = if delta == 0.0 {
1575        0.0
1576    } else if cmax == rf {
1577        60.0 * (((gf - bf) / delta) % 6.0)
1578    } else if cmax == gf {
1579        60.0 * ((bf - rf) / delta + 2.0)
1580    } else {
1581        60.0 * ((rf - gf) / delta + 4.0)
1582    };
1583    let h_hsl = if h_hsl < 0.0 { h_hsl + 360.0 } else { h_hsl };
1584
1585    // RGB → HSV
1586    let v_hsv = cmax;
1587    let s_hsv = if cmax == 0.0 { 0.0 } else { delta / cmax };
1588
1589    // RGB → CMYK
1590    let k_cmyk = 1.0 - cmax;
1591    let (c_cmyk, m_cmyk, y_cmyk) = if k_cmyk == 1.0 {
1592        (0.0, 0.0, 0.0)
1593    } else {
1594        (
1595            (1.0 - rf - k_cmyk) / (1.0 - k_cmyk),
1596            (1.0 - gf - k_cmyk) / (1.0 - k_cmyk),
1597            (1.0 - bf - k_cmyk) / (1.0 - k_cmyk),
1598        )
1599    };
1600
1601    let _ = writeln!(out, "Hex:   #{:02X}{:02X}{:02X}", r8, g8, b8);
1602    let _ = writeln!(out, "RGB:   rgb({}, {}, {})", r8, g8, b8);
1603    let _ = writeln!(
1604        out,
1605        "HSL:   hsl({:.1}°, {:.1}%, {:.1}%)",
1606        h_hsl,
1607        s_hsl * 100.0,
1608        l * 100.0
1609    );
1610    let _ = writeln!(
1611        out,
1612        "HSV:   hsv({:.1}°, {:.1}%, {:.1}%)",
1613        h_hsl,
1614        s_hsv * 100.0,
1615        v_hsv * 100.0
1616    );
1617    let _ = writeln!(
1618        out,
1619        "CMYK:  cmyk({:.0}%, {:.0}%, {:.0}%, {:.0}%)",
1620        c_cmyk * 100.0,
1621        m_cmyk * 100.0,
1622        y_cmyk * 100.0,
1623        k_cmyk * 100.0
1624    );
1625    // Luminance (WCAG)
1626    let lum = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
1627    let _ = writeln!(
1628        out,
1629        "Luminance: {:.4}  (WCAG relative, 0=black 1=white)",
1630        lum
1631    );
1632    let contrast_white = (1.0 + 0.05) / (lum + 0.05);
1633    let _ = writeln!(
1634        out,
1635        "Contrast vs white: {:.2}:1  (WCAG AA needs 4.5:1)",
1636        contrast_white
1637    );
1638    out
1639}
1640
1641// ── Molecular weight calculator ───────────────────────────────────────────────
1642// Parses chemical formulas like H2O, C6H12O6, Ca(NO3)2, (NH4)2SO4
1643
1644// Symbol → atomic mass (standard atomic weights, IUPAC 2021)
1645fn atomic_masses() -> &'static [(&'static str, f64)] {
1646    &[
1647        ("H", 1.008),
1648        ("He", 4.0026),
1649        ("Li", 6.94),
1650        ("Be", 9.0122),
1651        ("B", 10.81),
1652        ("C", 12.011),
1653        ("N", 14.007),
1654        ("O", 15.999),
1655        ("F", 18.998),
1656        ("Ne", 20.180),
1657        ("Na", 22.990),
1658        ("Mg", 24.305),
1659        ("Al", 26.982),
1660        ("Si", 28.085),
1661        ("P", 30.974),
1662        ("S", 32.06),
1663        ("Cl", 35.45),
1664        ("Ar", 39.948),
1665        ("K", 39.098),
1666        ("Ca", 40.078),
1667        ("Sc", 44.956),
1668        ("Ti", 47.867),
1669        ("V", 50.942),
1670        ("Cr", 51.996),
1671        ("Mn", 54.938),
1672        ("Fe", 55.845),
1673        ("Co", 58.933),
1674        ("Ni", 58.693),
1675        ("Cu", 63.546),
1676        ("Zn", 65.38),
1677        ("Ga", 69.723),
1678        ("Ge", 72.630),
1679        ("As", 74.922),
1680        ("Se", 78.971),
1681        ("Br", 79.904),
1682        ("Kr", 83.798),
1683        ("Rb", 85.468),
1684        ("Sr", 87.62),
1685        ("Y", 88.906),
1686        ("Zr", 91.224),
1687        ("Nb", 92.906),
1688        ("Mo", 95.95),
1689        ("Tc", 98.0),
1690        ("Ru", 101.07),
1691        ("Rh", 102.906),
1692        ("Pd", 106.42),
1693        ("Ag", 107.868),
1694        ("Cd", 112.414),
1695        ("In", 114.818),
1696        ("Sn", 118.710),
1697        ("Sb", 121.760),
1698        ("Te", 127.60),
1699        ("I", 126.904),
1700        ("Xe", 131.293),
1701        ("Cs", 132.905),
1702        ("Ba", 137.327),
1703        ("La", 138.905),
1704        ("Ce", 140.116),
1705        ("Pr", 140.908),
1706        ("Nd", 144.242),
1707        ("Pm", 145.0),
1708        ("Sm", 150.36),
1709        ("Eu", 151.964),
1710        ("Gd", 157.25),
1711        ("Tb", 158.925),
1712        ("Dy", 162.500),
1713        ("Ho", 164.930),
1714        ("Er", 167.259),
1715        ("Tm", 168.934),
1716        ("Yb", 173.045),
1717        ("Lu", 174.967),
1718        ("Hf", 178.49),
1719        ("Ta", 180.948),
1720        ("W", 183.84),
1721        ("Re", 186.207),
1722        ("Os", 190.23),
1723        ("Ir", 192.217),
1724        ("Pt", 195.084),
1725        ("Au", 196.967),
1726        ("Hg", 200.592),
1727        ("Tl", 204.38),
1728        ("Pb", 207.2),
1729        ("Bi", 208.980),
1730        ("Po", 209.0),
1731        ("At", 210.0),
1732        ("Rn", 222.0),
1733        ("Fr", 223.0),
1734        ("Ra", 226.0),
1735        ("Ac", 227.0),
1736        ("Th", 232.038),
1737        ("Pa", 231.036),
1738        ("U", 238.029),
1739        ("Np", 237.0),
1740        ("Pu", 244.0),
1741        ("Am", 243.0),
1742        ("Cm", 247.0),
1743        ("Bk", 247.0),
1744        ("Cf", 251.0),
1745        ("Es", 252.0),
1746        ("Fm", 257.0),
1747        ("Md", 258.0),
1748        ("No", 259.0),
1749        ("Lr", 266.0),
1750        ("Rf", 267.0),
1751        ("Db", 268.0),
1752        ("Sg", 271.0),
1753        ("Bh", 270.0),
1754        ("Hs", 277.0),
1755        ("Mt", 278.0),
1756        ("Ds", 281.0),
1757        ("Rg", 282.0),
1758        ("Cn", 285.0),
1759        ("Nh", 286.0),
1760        ("Fl", 289.0),
1761        ("Mc", 290.0),
1762        ("Lv", 293.0),
1763        ("Ts", 294.0),
1764        ("Og", 294.0),
1765    ]
1766}
1767
1768fn lookup_mass(sym: &str) -> Option<f64> {
1769    atomic_masses()
1770        .iter()
1771        .find(|(s, _)| *s == sym)
1772        .map(|(_, m)| *m)
1773}
1774
1775fn parse_formula(chars: &[char], pos: &mut usize) -> Result<Vec<(String, u32)>, String> {
1776    let mut items: Vec<(String, u32)> = Vec::new();
1777    while *pos < chars.len() {
1778        match chars[*pos] {
1779            '(' => {
1780                *pos += 1;
1781                let inner = parse_formula(chars, pos)?;
1782                if *pos >= chars.len() || chars[*pos] != ')' {
1783                    return Err("Missing closing ')'".into());
1784                }
1785                *pos += 1;
1786                let count = read_number(chars, pos).unwrap_or(1);
1787                for (sym, n) in inner {
1788                    items.push((sym, n * count));
1789                }
1790            }
1791            '[' => {
1792                *pos += 1;
1793                let inner = parse_formula(chars, pos)?;
1794                if *pos >= chars.len() || chars[*pos] != ']' {
1795                    return Err("Missing closing ']'".into());
1796                }
1797                *pos += 1;
1798                let count = read_number(chars, pos).unwrap_or(1);
1799                for (sym, n) in inner {
1800                    items.push((sym, n * count));
1801                }
1802            }
1803            ')' | ']' => break,
1804            c if c.is_ascii_uppercase() => {
1805                let mut sym = c.to_string();
1806                *pos += 1;
1807                while *pos < chars.len() && chars[*pos].is_ascii_lowercase() {
1808                    sym.push(chars[*pos]);
1809                    *pos += 1;
1810                }
1811                let count = read_number(chars, pos).unwrap_or(1);
1812                items.push((sym, count));
1813            }
1814            '·' | '•' | '.' => {
1815                *pos += 1;
1816            } // hydrate dot
1817            ' ' | '\t' => {
1818                *pos += 1;
1819            }
1820            other => return Err(format!("Unexpected character: '{}'", other)),
1821        }
1822    }
1823    Ok(items)
1824}
1825
1826fn read_number(chars: &[char], pos: &mut usize) -> Option<u32> {
1827    let start = *pos;
1828    while *pos < chars.len() && chars[*pos].is_ascii_digit() {
1829        *pos += 1;
1830    }
1831    if *pos == start {
1832        None
1833    } else {
1834        chars[start..*pos].iter().collect::<String>().parse().ok()
1835    }
1836}
1837
1838pub fn molecular_weight(formula: &str) -> String {
1839    let chars: Vec<char> = formula.chars().collect();
1840    let mut pos = 0usize;
1841    let items = match parse_formula(&chars, &mut pos) {
1842        Ok(v) => v,
1843        Err(e) => return format!("Parse error: {}. Example: H2O, C6H12O6, Ca(NO3)2", e),
1844    };
1845
1846    // Aggregate counts by element
1847    let mut counts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
1848    for (sym, n) in &items {
1849        *counts.entry(sym.clone()).or_insert(0) += n;
1850    }
1851
1852    let mut mw = 0.0f64;
1853    let mut breakdown: Vec<(String, u32, f64)> = Vec::new();
1854    let mut unknown: Vec<String> = Vec::new();
1855    let mut syms: Vec<String> = counts.keys().cloned().collect();
1856    syms.sort();
1857    for sym in &syms {
1858        let n = counts[sym];
1859        if let Some(mass) = lookup_mass(sym) {
1860            let contrib = mass * n as f64;
1861            mw += contrib;
1862            breakdown.push((sym.clone(), n, contrib));
1863        } else {
1864            unknown.push(sym.clone());
1865        }
1866    }
1867
1868    if !unknown.is_empty() {
1869        return format!(
1870            "Unknown element(s): {}. Check your formula.",
1871            unknown.join(", ")
1872        );
1873    }
1874
1875    let mut out = String::new();
1876    let _ = writeln!(out, "Formula: {}", formula.trim());
1877    let _ = writeln!(out, "Molecular weight: {:.4} g/mol", mw);
1878    let _ = writeln!(out);
1879    let _ = writeln!(out, "Composition:");
1880    for (sym, n, contrib) in &breakdown {
1881        let pct = 100.0 * contrib / mw;
1882        let _ = writeln!(
1883            out,
1884            "  {:2} × {:2}  {:8.4} g/mol  ({:.2}%)",
1885            n, sym, contrib, pct
1886        );
1887    }
1888    // Common derived values
1889    let _ = writeln!(out);
1890    let _ = writeln!(out, "1 mole = {:.4} g", mw);
1891    let _ = writeln!(out, "1 g    = {:.6} mol", 1.0 / mw);
1892    out
1893}
1894
1895// ── Physical constants ────────────────────────────────────────────────────────
1896
1897const CONSTANTS: &[(&str, &str, &str, &str)] = &[
1898    // (name, value, unit, aliases)
1899    (
1900        "Speed of light",
1901        "299792458",
1902        "m/s",
1903        "c,light,speed of light,c0",
1904    ),
1905    (
1906        "Planck constant",
1907        "6.62607015e-34",
1908        "J·s",
1909        "h,planck,planck constant",
1910    ),
1911    (
1912        "Reduced Planck (ℏ)",
1913        "1.054571817e-34",
1914        "J·s",
1915        "hbar,h-bar,reduced planck,hbar",
1916    ),
1917    (
1918        "Gravitational constant",
1919        "6.67430e-11",
1920        "N·m²/kg²",
1921        "G,gravity,gravitational,newton gravity",
1922    ),
1923    (
1924        "Elementary charge",
1925        "1.602176634e-19",
1926        "C",
1927        "e,electron charge,elementary charge,charge",
1928    ),
1929    (
1930        "Electron mass",
1931        "9.1093837015e-31",
1932        "kg",
1933        "me,electron mass,m_e",
1934    ),
1935    (
1936        "Proton mass",
1937        "1.67262192369e-27",
1938        "kg",
1939        "mp,proton mass,m_p",
1940    ),
1941    (
1942        "Neutron mass",
1943        "1.67492749804e-27",
1944        "kg",
1945        "mn,neutron mass,m_n",
1946    ),
1947    (
1948        "Avogadro constant",
1949        "6.02214076e23",
1950        "mol⁻¹",
1951        "NA,avogadro,avogadro constant,N_A",
1952    ),
1953    (
1954        "Boltzmann constant",
1955        "1.380649e-23",
1956        "J/K",
1957        "k,kb,boltzmann,boltzmann constant,k_B",
1958    ),
1959    (
1960        "Gas constant",
1961        "8.314462618",
1962        "J/(mol·K)",
1963        "R,gas constant,universal gas constant,molar gas",
1964    ),
1965    (
1966        "Stefan-Boltzmann",
1967        "5.670374419e-8",
1968        "W/(m²·K⁴)",
1969        "sigma,stefan,stefan-boltzmann,σ",
1970    ),
1971    (
1972        "Vacuum permittivity",
1973        "8.8541878128e-12",
1974        "F/m",
1975        "eps0,epsilon0,vacuum permittivity,ε₀",
1976    ),
1977    (
1978        "Vacuum permeability",
1979        "1.25663706212e-6",
1980        "N/A²",
1981        "mu0,mu_0,vacuum permeability,μ₀",
1982    ),
1983    (
1984        "Bohr radius",
1985        "5.29177210903e-11",
1986        "m",
1987        "a0,bohr,bohr radius,a_0",
1988    ),
1989    (
1990        "Fine structure constant",
1991        "7.2973525693e-3",
1992        "dimensionless",
1993        "alpha,fine structure,α",
1994    ),
1995    (
1996        "Rydberg constant",
1997        "10973731.568160",
1998        "m⁻¹",
1999        "Ry,rydberg,rydberg constant",
2000    ),
2001    (
2002        "Faraday constant",
2003        "96485.33212",
2004        "C/mol",
2005        "F,faraday,faraday constant",
2006    ),
2007    (
2008        "Standard gravity",
2009        "9.80665",
2010        "m/s²",
2011        "g,grav,standard gravity,g0,g_n",
2012    ),
2013    (
2014        "Atomic mass unit",
2015        "1.66053906660e-27",
2016        "kg",
2017        "amu,u,dalton,atomic mass unit",
2018    ),
2019    (
2020        "Standard atmosphere",
2021        "101325",
2022        "Pa",
2023        "atm,atmosphere,standard atmosphere",
2024    ),
2025    (
2026        "Electron volt",
2027        "1.602176634e-19",
2028        "J",
2029        "eV,electronvolt,electron volt",
2030    ),
2031    (
2032        "Speed of sound (20°C)",
2033        "343",
2034        "m/s",
2035        "sound,speed of sound,vsound",
2036    ),
2037    (
2038        "Molar volume (STP)",
2039        "22.414",
2040        "L/mol",
2041        "molar volume,vm,STP volume",
2042    ),
2043    (
2044        "Wien displacement",
2045        "2.897771955e-3",
2046        "m·K",
2047        "wien,wien displacement,b",
2048    ),
2049];
2050
2051pub fn physical_const(query: &str) -> String {
2052    let q = query.trim().to_lowercase();
2053    let mut out = String::new();
2054
2055    if q.is_empty() || q == "list" || q == "all" {
2056        let _ = writeln!(out, "Physical Constants  (use --const NAME to look up)");
2057        let _ = writeln!(out, "{}", "─".repeat(60));
2058        for (name, val, unit, _) in CONSTANTS {
2059            let _ = writeln!(out, "  {:<30} {} {}", name, val, unit);
2060        }
2061        return out;
2062    }
2063
2064    let matches: Vec<_> = CONSTANTS
2065        .iter()
2066        .filter(|(name, _, _, aliases)| {
2067            name.to_lowercase().contains(&q) || aliases.to_lowercase().contains(&q)
2068        })
2069        .collect();
2070
2071    if matches.is_empty() {
2072        let _ = writeln!(
2073            out,
2074            "No constant found for '{}'. Use --const list to see all.",
2075            query.trim()
2076        );
2077        return out;
2078    }
2079
2080    for (name, val, unit, aliases) in matches {
2081        let _ = writeln!(out, "{}", name);
2082        let _ = writeln!(out, "  Value:   {}", val);
2083        let _ = writeln!(out, "  Unit:    {}", unit);
2084        let _ = writeln!(out, "  Aliases: {}", aliases);
2085        // Parse as f64 for display
2086        if let Ok(v) = val.parse::<f64>() {
2087            if v.abs() > 1e6 || v.abs() < 1e-4 {
2088                let _ = writeln!(out, "  ≈        {:.6e}", v);
2089            }
2090        }
2091        let _ = writeln!(out);
2092    }
2093    out
2094}
2095
2096// ── Normal distribution (statistics) ─────────────────────────────────────────
2097// CDF, PDF, and inverse CDF (quantile) using rational approximations.
2098
2099fn erf_approx(x: f64) -> f64 {
2100    // Abramowitz & Stegun 7.1.26 — max error 1.5e-7
2101    let sign = if x < 0.0 { -1.0 } else { 1.0 };
2102    let x = x.abs();
2103    let t = 1.0 / (1.0 + 0.3275911 * x);
2104    let y = 1.0
2105        - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t
2106            + 0.254829592)
2107            * t
2108            * (-x * x).exp();
2109    sign * y
2110}
2111
2112fn normal_cdf(x: f64, mu: f64, sigma: f64) -> f64 {
2113    0.5 * (1.0 + erf_approx((x - mu) / (sigma * 2.0f64.sqrt())))
2114}
2115
2116fn normal_pdf(x: f64, mu: f64, sigma: f64) -> f64 {
2117    let z = (x - mu) / sigma;
2118    (-0.5 * z * z).exp() / (sigma * (2.0 * std::f64::consts::PI).sqrt())
2119}
2120
2121fn normal_inv_cdf(p: f64) -> f64 {
2122    // Rational approximation (Peter Acklam)
2123    let p = p.clamp(1e-10, 1.0 - 1e-10);
2124    let (a, b) = if p < 0.5 {
2125        let t = (-2.0 * p.ln()).sqrt();
2126        let c = [2.515517, 0.802853, 0.010328];
2127        let d = [1.432788, 0.189269, 0.001308];
2128        let num = c[0] + c[1] * t + c[2] * t * t;
2129        let den = 1.0 + d[0] * t + d[1] * t * t + d[2] * t * t * t;
2130        (-(t - num / den), p)
2131    } else {
2132        let t = (-2.0 * (1.0 - p).ln()).sqrt();
2133        let c = [2.515517, 0.802853, 0.010328];
2134        let d = [1.432788, 0.189269, 0.001308];
2135        let num = c[0] + c[1] * t + c[2] * t * t;
2136        let den = 1.0 + d[0] * t + d[1] * t * t + d[2] * t * t * t;
2137        (t - num / den, p)
2138    };
2139    let _ = b;
2140    a
2141}
2142
2143pub fn stat_normal(query: &str) -> String {
2144    let q = query.trim().to_lowercase();
2145    let mut out = String::new();
2146
2147    // Parse: "cdf X", "cdf X mu sigma", "pdf X", "inv P", "z-score X mu sigma", "p-value Z"
2148    let parts: Vec<&str> = q.split_whitespace().collect();
2149    if parts.is_empty() {
2150        out.push_str("Usage:\n  --normal 'cdf 1.96'             P(Z ≤ 1.96) for standard normal\n  --normal 'cdf 70 60 10'        P(X ≤ 70) for N(60, 10)\n  --normal 'pdf 1.96'            Standard normal PDF at x=1.96\n  --normal 'inv 0.975'           z-score for 97.5th percentile\n  --normal 'between -1.96 1.96'  P(-1.96 ≤ Z ≤ 1.96)");
2151        return out;
2152    }
2153
2154    let parse_f = |s: &str| s.parse::<f64>().ok();
2155
2156    match parts[0] {
2157        "cdf" if parts.len() >= 2 => {
2158            let x = parse_f(parts[1]).unwrap_or(0.0);
2159            let mu = if parts.len() > 2 {
2160                parse_f(parts[2]).unwrap_or(0.0)
2161            } else {
2162                0.0
2163            };
2164            let sg = if parts.len() > 3 {
2165                parse_f(parts[3]).unwrap_or(1.0)
2166            } else {
2167                1.0
2168            };
2169            let p = normal_cdf(x, mu, sg);
2170            let z = (x - mu) / sg;
2171            let _ = writeln!(out, "Distribution: N({}, {})", mu, sg);
2172            let _ = writeln!(out, "x = {}", x);
2173            let _ = writeln!(out, "z-score = {:.6}", z);
2174            let _ = writeln!(out, "P(X ≤ {}) = {:.8}  ({:.4}%)", x, p, p * 100.0);
2175            let _ = writeln!(
2176                out,
2177                "P(X > {}) = {:.8}  ({:.4}%)",
2178                x,
2179                1.0 - p,
2180                (1.0 - p) * 100.0
2181            );
2182        }
2183        "pdf" if parts.len() >= 2 => {
2184            let x = parse_f(parts[1]).unwrap_or(0.0);
2185            let mu = if parts.len() > 2 {
2186                parse_f(parts[2]).unwrap_or(0.0)
2187            } else {
2188                0.0
2189            };
2190            let sg = if parts.len() > 3 {
2191                parse_f(parts[3]).unwrap_or(1.0)
2192            } else {
2193                1.0
2194            };
2195            let p = normal_pdf(x, mu, sg);
2196            let _ = writeln!(out, "PDF at x={}: {:.8}", x, p);
2197        }
2198        "inv" | "quantile" | "ppf" if parts.len() >= 2 => {
2199            let p = parse_f(parts[1]).unwrap_or(0.5);
2200            let mu = if parts.len() > 2 {
2201                parse_f(parts[2]).unwrap_or(0.0)
2202            } else {
2203                0.0
2204            };
2205            let sg = if parts.len() > 3 {
2206                parse_f(parts[3]).unwrap_or(1.0)
2207            } else {
2208                1.0
2209            };
2210            let z = normal_inv_cdf(p);
2211            let x = mu + sg * z;
2212            let _ = writeln!(out, "P = {} → z-score = {:.6} → x = {:.6}", p, z, x);
2213            let _ = writeln!(out, "Interpretation: P(X ≤ {:.6}) = {:.4}%", x, p * 100.0);
2214        }
2215        "between" | "interval" if parts.len() >= 3 => {
2216            let a = parse_f(parts[1]).unwrap_or(-1.96);
2217            let b = parse_f(parts[2]).unwrap_or(1.96);
2218            let mu = if parts.len() > 3 {
2219                parse_f(parts[3]).unwrap_or(0.0)
2220            } else {
2221                0.0
2222            };
2223            let sg = if parts.len() > 4 {
2224                parse_f(parts[4]).unwrap_or(1.0)
2225            } else {
2226                1.0
2227            };
2228            let p = normal_cdf(b, mu, sg) - normal_cdf(a, mu, sg);
2229            let _ = writeln!(out, "P({} ≤ X ≤ {}) = {:.8}  ({:.4}%)", a, b, p, p * 100.0);
2230        }
2231        "table" | "z-table" => {
2232            let _ = writeln!(out, "Standard Normal CDF  P(Z ≤ z)");
2233            let _ = writeln!(out, "  z     P(Z ≤ z)   P(Z > z)");
2234            let _ = writeln!(out, "  ─────────────────────────────");
2235            for &z in &[
2236                -3.0f64, -2.576, -2.326, -1.960, -1.645, -1.282, -0.842, 0.0, 0.842, 1.282, 1.645,
2237                1.960, 2.326, 2.576, 3.0,
2238            ] {
2239                let p = normal_cdf(z, 0.0, 1.0);
2240                let _ = writeln!(out, "  {:6.3}   {:.6}   {:.6}", z, p, 1.0 - p);
2241            }
2242        }
2243        _ => {
2244            // Try to parse as a plain z-score
2245            if let Some(z) = parse_f(parts[0]) {
2246                let p = normal_cdf(z, 0.0, 1.0);
2247                let _ = writeln!(out, "Standard normal CDF at z={}: {:.8}", z, p);
2248            } else {
2249                out.push_str("Usage: --normal 'cdf 1.96'  --normal 'inv 0.975'  --normal 'table'\n       --normal 'between -1.96 1.96'  --normal 'pdf 0'");
2250            }
2251        }
2252    }
2253    out
2254}
2255
2256// ── Multi-distribution probability calculator ─────────────────────────────────
2257// Distributions: normal, binomial, poisson, t, chi2, exponential, uniform, geometric
2258// Operations: pdf/pmf, cdf, quantile/inv
2259//
2260// Syntax: DIST [op] PARAMS
2261//   normal cdf 1.96              P(Z ≤ 1.96)
2262//   normal pdf 0 mu=2 sd=1       f(0) for N(2,1)
2263//   binomial pmf 3 n=10 p=0.4    P(X=3) for Bin(10, 0.4)
2264//   binomial cdf 5 n=10 p=0.4    P(X ≤ 5)
2265//   poisson pmf 2 lam=3          P(X=2) for Poi(3)
2266//   t cdf 2.0 df=9               P(T ≤ 2.0) for t(9)
2267//   chi2 cdf 5.99 df=2           P(X ≤ 5.99) for chi²(2)
2268//   exponential cdf 1.0 lam=0.5  P(X ≤ 1.0) for Exp(0.5)
2269//   uniform cdf 0.7 a=0 b=1      P(X ≤ 0.7) for Uniform(0,1)
2270//   geometric pmf 3 p=0.5        P(X=3) for Geo(0.5)
2271pub fn prob_calc(query: &str) -> String {
2272    use std::f64::consts::PI;
2273    let q = query.trim().to_lowercase();
2274    if q.is_empty() || q == "help" || q == "?" {
2275        return prob_usage();
2276    }
2277
2278    let parts: Vec<&str> = q.split_whitespace().collect();
2279    if parts.is_empty() {
2280        return prob_usage();
2281    }
2282
2283    // Parse named params like mu=2.5, sd=1.0
2284    let get_param = |parts: &[&str], name: &str, default: f64| -> f64 {
2285        parts
2286            .iter()
2287            .find(|s| s.starts_with(name))
2288            .and_then(|s| s.split_once('=').map(|x| x.1))
2289            .and_then(|v| v.parse().ok())
2290            .unwrap_or(default)
2291    };
2292    let get_positional = |parts: &[&str], idx: usize| -> Option<f64> {
2293        parts
2294            .iter()
2295            .filter(|s| !s.contains('='))
2296            .nth(idx)
2297            .and_then(|s| s.parse().ok())
2298    };
2299
2300    let dist = parts[0];
2301    // Determine op: second token if it's a known op, else "all"
2302    let op_candidate = parts.get(1).copied().unwrap_or("all");
2303    let (op, data_start) = if matches!(
2304        op_candidate,
2305        "cdf" | "pdf" | "pmf" | "inv" | "quantile" | "between" | "all"
2306    ) {
2307        (op_candidate, 2usize)
2308    } else {
2309        ("all", 1usize)
2310    };
2311    let data: &[&str] = &parts[data_start..];
2312
2313    let mut out = String::new();
2314    let sep = "=".repeat(64);
2315
2316    match dist {
2317        "normal" | "norm" | "gaussian" => {
2318            let x = get_positional(data, 0).unwrap_or(0.0);
2319            let mu = get_param(data, "mu", get_param(data, "mean", 0.0));
2320            let sd = get_param(
2321                data,
2322                "sd",
2323                get_param(data, "sigma", get_param(data, "std", 1.0)),
2324            );
2325            let _ = writeln!(out, "{}", sep);
2326            let _ = writeln!(out, "  Normal Distribution  N(μ={}, σ={})", mu, sd);
2327            let _ = writeln!(out, "{}", sep);
2328            if op == "pdf" || op == "all" {
2329                let _ = writeln!(out, "  PDF f({}) = {:.8}", x, normal_pdf(x, mu, sd));
2330            }
2331            if op == "cdf" || op == "all" {
2332                let p = normal_cdf(x, mu, sd);
2333                let _ = writeln!(out, "  CDF P(X ≤ {}) = {:.8}", x, p);
2334                let _ = writeln!(out, "  P(X > {})     = {:.8}", x, 1.0 - p);
2335                let z = (x - mu) / sd;
2336                let _ = writeln!(out, "  z-score       = {:.4}", z);
2337            }
2338            if op == "inv" || op == "quantile" {
2339                let p = get_positional(data, 0).unwrap_or(0.975);
2340                let z = normal_inv_cdf(p);
2341                let _ = writeln!(out, "  Quantile p={:.4}  →  x = {:.6}", p, mu + sd * z);
2342            }
2343            if op == "between" {
2344                let a = get_positional(data, 0).unwrap_or(-1.0);
2345                let b = get_positional(data, 1).unwrap_or(1.0);
2346                let p = normal_cdf(b, mu, sd) - normal_cdf(a, mu, sd);
2347                let _ = writeln!(out, "  P({} ≤ X ≤ {}) = {:.8}", a, b, p);
2348            }
2349            if op == "all" {
2350                // Common quantiles
2351                let _ = writeln!(
2352                    out,
2353                    "  Quantiles: p=.025 → {:.4}  p=.975 → {:.4}  p=.5 → {:.4}",
2354                    mu + sd * normal_inv_cdf(0.025),
2355                    mu + sd * normal_inv_cdf(0.975),
2356                    mu
2357                );
2358            }
2359        }
2360        "binomial" | "binom" | "bin" => {
2361            let k = get_positional(data, 0).unwrap_or(0.0) as i64;
2362            let n = get_param(data, "n", 10.0) as u64;
2363            let p = get_param(data, "p", 0.5);
2364            let _ = writeln!(out, "{}", sep);
2365            let _ = writeln!(out, "  Binomial Distribution  Bin(n={}, p={})", n, p);
2366            let _ = writeln!(out, "{}", sep);
2367            let mean = n as f64 * p;
2368            let var = n as f64 * p * (1.0 - p);
2369            let _ = writeln!(
2370                out,
2371                "  Mean={:.4}  Var={:.4}  StdDev={:.4}",
2372                mean,
2373                var,
2374                var.sqrt()
2375            );
2376            let binom_pmf = |k: i64, n: u64, p: f64| -> f64 {
2377                if k < 0 || k > n as i64 {
2378                    return 0.0;
2379                }
2380                let k = k as u64;
2381                // Log-space computation to avoid overflow
2382                let log_binom: f64 = log_factorial(n) - log_factorial(k) - log_factorial(n - k);
2383                (log_binom + k as f64 * p.ln() + (n - k) as f64 * (1.0 - p).ln()).exp()
2384            };
2385            if op == "pmf" || op == "all" {
2386                let pmf = binom_pmf(k, n, p);
2387                let _ = writeln!(out, "  PMF P(X={}) = {:.8}", k, pmf);
2388            }
2389            if op == "cdf" || op == "all" {
2390                let cdf: f64 = (0..=k).map(|i| binom_pmf(i, n, p)).sum();
2391                let _ = writeln!(out, "  CDF P(X≤{}) = {:.8}", k, cdf);
2392                let _ = writeln!(out, "  P(X>{})     = {:.8}", k, 1.0 - cdf);
2393            }
2394            if op == "all" {
2395                // Show distribution for small n
2396                if n <= 20 {
2397                    let _ = writeln!(out, "  PMF table:");
2398                    for i in 0..=n {
2399                        let pmf = binom_pmf(i as i64, n, p);
2400                        let bar = (pmf * 50.0) as usize;
2401                        let _ = writeln!(out, "    k={:>3}  {:.6}  {}", i, pmf, "#".repeat(bar));
2402                    }
2403                }
2404            }
2405        }
2406        "poisson" | "pois" => {
2407            let k = get_positional(data, 0).unwrap_or(0.0) as i64;
2408            let lam = get_param(data, "lam", get_param(data, "lambda", 1.0));
2409            let _ = writeln!(out, "{}", sep);
2410            let _ = writeln!(out, "  Poisson Distribution  Poi(λ={})", lam);
2411            let _ = writeln!(out, "{}", sep);
2412            let _ = writeln!(
2413                out,
2414                "  Mean={:.4}  Var={:.4}  StdDev={:.4}",
2415                lam,
2416                lam,
2417                lam.sqrt()
2418            );
2419            let pois_pmf = |k: i64, lam: f64| -> f64 {
2420                if k < 0 {
2421                    return 0.0;
2422                }
2423                (-lam + k as f64 * lam.ln() - log_factorial(k as u64)).exp()
2424            };
2425            if op == "pmf" || op == "all" {
2426                let _ = writeln!(out, "  PMF P(X={}) = {:.8}", k, pois_pmf(k, lam));
2427            }
2428            if op == "cdf" || op == "all" {
2429                let cdf: f64 = (0..=k).map(|i| pois_pmf(i, lam)).sum();
2430                let _ = writeln!(out, "  CDF P(X≤{}) = {:.8}", k, cdf);
2431                let _ = writeln!(out, "  P(X>{})     = {:.8}", k, 1.0 - cdf);
2432            }
2433            if op == "all" && lam <= 30.0 {
2434                let hi = (lam + 4.0 * lam.sqrt()).ceil() as i64;
2435                let _ = writeln!(out, "  PMF table (up to k={}):", hi);
2436                for i in 0..=hi.min(40) {
2437                    let pmf = pois_pmf(i, lam);
2438                    let bar = (pmf * 60.0) as usize;
2439                    let _ = writeln!(out, "    k={:>3}  {:.6}  {}", i, pmf, "#".repeat(bar));
2440                }
2441            }
2442        }
2443        "t" | "student" | "t-dist" => {
2444            let x = get_positional(data, 0).unwrap_or(0.0);
2445            let df = get_param(data, "df", get_param(data, "dof", 1.0));
2446            let _ = writeln!(out, "{}", sep);
2447            let _ = writeln!(out, "  Student's t Distribution  t(df={})", df);
2448            let _ = writeln!(out, "{}", sep);
2449            let t_pdf = |x: f64, df: f64| -> f64 {
2450                let lg_n = lgamma((df + 1.0) / 2.0);
2451                let lg_d = lgamma(df / 2.0);
2452                let coef = (lg_n - lg_d - 0.5 * (df * PI).ln()).exp();
2453                coef * (1.0 + x * x / df).powf(-(df + 1.0) / 2.0)
2454            };
2455            // t CDF via regularized incomplete beta
2456            let t_cdf = |x: f64, df: f64| -> f64 {
2457                let t2 = x * x;
2458                let z = df / (df + t2);
2459                let ib = reg_inc_beta(df / 2.0, 0.5, z);
2460                if x >= 0.0 {
2461                    1.0 - 0.5 * ib
2462                } else {
2463                    0.5 * ib
2464                }
2465            };
2466            if op == "pdf" || op == "all" {
2467                let _ = writeln!(out, "  PDF f({:.4}) = {:.8}", x, t_pdf(x, df));
2468            }
2469            if op == "cdf" || op == "all" {
2470                let p = t_cdf(x, df);
2471                let _ = writeln!(out, "  CDF P(T≤{:.4}) = {:.8}", x, p);
2472                let _ = writeln!(
2473                    out,
2474                    "  Two-tailed P  = {:.8}  (p-value for |T|≥{:.4})",
2475                    2.0 * (1.0 - t_cdf(x.abs(), df)),
2476                    x.abs()
2477                );
2478            }
2479            if op == "all" {
2480                // Common critical values
2481                let _ = writeln!(out, "  Critical values (two-tailed):");
2482                for alpha in [0.10, 0.05, 0.01, 0.001] {
2483                    // Bisect to find t s.t. 2*(1-cdf(t)) = alpha
2484                    let target = 1.0 - alpha / 2.0;
2485                    let cv = bisect(|t| t_cdf(t, df) - target, 0.0, 100.0, 60);
2486                    let _ = writeln!(out, "    α={:.3}  t* = {:.4}", alpha, cv);
2487                }
2488            }
2489        }
2490        "chi2" | "chisquare" | "chi-square" | "chi_sq" => {
2491            let x = get_positional(data, 0).unwrap_or(1.0);
2492            let df = get_param(data, "df", get_param(data, "dof", 1.0));
2493            let _ = writeln!(out, "{}", sep);
2494            let _ = writeln!(out, "  Chi-Square Distribution  χ²(df={})", df);
2495            let _ = writeln!(out, "{}", sep);
2496            let _ = writeln!(
2497                out,
2498                "  Mean={:.4}  Var={:.4}  StdDev={:.4}",
2499                df,
2500                2.0 * df,
2501                (2.0 * df).sqrt()
2502            );
2503            let chi2_pdf = |x: f64, k: f64| -> f64 {
2504                if x <= 0.0 {
2505                    return 0.0;
2506                }
2507                let lg = lgamma(k / 2.0);
2508                ((k / 2.0 - 1.0) * x.ln() - x / 2.0 - (k / 2.0) * 2.0f64.ln() - lg).exp()
2509            };
2510            // Chi2 CDF via regularized incomplete gamma
2511            let chi2_cdf = |x: f64, k: f64| -> f64 {
2512                if x <= 0.0 {
2513                    return 0.0;
2514                }
2515                reg_inc_gamma(k / 2.0, x / 2.0)
2516            };
2517            if op == "pdf" || op == "all" {
2518                let _ = writeln!(out, "  PDF f({:.4}) = {:.8}", x, chi2_pdf(x, df));
2519            }
2520            if op == "cdf" || op == "all" {
2521                let p = chi2_cdf(x, df);
2522                let _ = writeln!(out, "  CDF P(X≤{:.4}) = {:.8}", x, p);
2523                let _ = writeln!(out, "  P-value (upper) = {:.8}", 1.0 - p);
2524            }
2525            if op == "all" {
2526                let _ = writeln!(out, "  Critical values (upper tail):");
2527                for alpha in [0.10, 0.05, 0.025, 0.01] {
2528                    let cv = bisect(|t| chi2_cdf(t, df) - (1.0 - alpha), 0.0, df + 100.0, 80);
2529                    let _ = writeln!(out, "    α={:.3}  χ² = {:.4}", alpha, cv);
2530                }
2531            }
2532        }
2533        "exponential" | "exp" | "expon" => {
2534            let x = get_positional(data, 0).unwrap_or(1.0);
2535            let lam = get_param(
2536                data,
2537                "lam",
2538                get_param(data, "lambda", get_param(data, "rate", 1.0)),
2539            );
2540            let _ = writeln!(out, "{}", sep);
2541            let _ = writeln!(out, "  Exponential Distribution  Exp(λ={})", lam);
2542            let _ = writeln!(out, "{}", sep);
2543            let _ = writeln!(
2544                out,
2545                "  Mean={:.4}  StdDev={:.4}  Median={:.4}",
2546                1.0 / lam,
2547                1.0 / lam,
2548                2.0f64.ln() / lam
2549            );
2550            if op == "pdf" || op == "all" {
2551                let pdf = if x >= 0.0 {
2552                    lam * (-lam * x).exp()
2553                } else {
2554                    0.0
2555                };
2556                let _ = writeln!(out, "  PDF f({:.4}) = {:.8}", x, pdf);
2557            }
2558            if op == "cdf" || op == "all" {
2559                let p = if x >= 0.0 {
2560                    1.0 - (-lam * x).exp()
2561                } else {
2562                    0.0
2563                };
2564                let _ = writeln!(out, "  CDF P(X≤{:.4}) = {:.8}", x, p);
2565                let _ = writeln!(out, "  P(X>{:.4})    = {:.8}", x, 1.0 - p);
2566                let _ = writeln!(out, "  Quantile p=.5  → {:.6}  (median)", 2.0f64.ln() / lam);
2567            }
2568        }
2569        "uniform" | "unif" => {
2570            let x = get_positional(data, 0).unwrap_or(0.5);
2571            let a = get_param(data, "a", get_param(data, "lo", 0.0));
2572            let b = get_param(data, "b", get_param(data, "hi", 1.0));
2573            let _ = writeln!(out, "{}", sep);
2574            let _ = writeln!(out, "  Uniform Distribution  U({}, {})", a, b);
2575            let _ = writeln!(out, "{}", sep);
2576            let rng = b - a;
2577            let _ = writeln!(
2578                out,
2579                "  Mean={:.4}  Var={:.6}  StdDev={:.4}",
2580                (a + b) / 2.0,
2581                rng * rng / 12.0,
2582                (rng * rng / 12.0).sqrt()
2583            );
2584            if op == "pdf" || op == "all" {
2585                let pdf = if x >= a && x <= b { 1.0 / rng } else { 0.0 };
2586                let _ = writeln!(out, "  PDF f({:.4}) = {:.8}", x, pdf);
2587            }
2588            if op == "cdf" || op == "all" {
2589                let p = if x < a {
2590                    0.0
2591                } else if x > b {
2592                    1.0
2593                } else {
2594                    (x - a) / rng
2595                };
2596                let _ = writeln!(out, "  CDF P(X≤{:.4}) = {:.8}", x, p);
2597            }
2598        }
2599        "geometric" | "geo" | "geom" => {
2600            let k = get_positional(data, 0).unwrap_or(1.0) as i64;
2601            let p = get_param(data, "p", 0.5);
2602            let _ = writeln!(out, "{}", sep);
2603            let _ = writeln!(
2604                out,
2605                "  Geometric Distribution  Geo(p={})  [trials until first success]",
2606                p
2607            );
2608            let _ = writeln!(out, "{}", sep);
2609            let _ = writeln!(
2610                out,
2611                "  Mean={:.4}  Var={:.4}  StdDev={:.4}",
2612                1.0 / p,
2613                (1.0 - p) / (p * p),
2614                ((1.0 - p) / (p * p)).sqrt()
2615            );
2616            let geo_pmf = |k: i64, p: f64| -> f64 {
2617                if k < 1 {
2618                    return 0.0;
2619                }
2620                (1.0 - p).powi((k - 1) as i32) * p
2621            };
2622            if op == "pmf" || op == "all" {
2623                let _ = writeln!(out, "  PMF P(X={}) = {:.8}", k, geo_pmf(k, p));
2624            }
2625            if op == "cdf" || op == "all" {
2626                let cdf = 1.0 - (1.0 - p).powi(k as i32);
2627                let _ = writeln!(out, "  CDF P(X≤{}) = {:.8}", k, cdf);
2628                let _ = writeln!(out, "  P(X>{})    = {:.8}", k, 1.0 - cdf);
2629            }
2630            if op == "all" {
2631                let _ = writeln!(out, "  PMF table:");
2632                for i in 1..=10i64.min((10.0 / p) as i64) {
2633                    let pmf = geo_pmf(i, p);
2634                    let bar = (pmf * 50.0) as usize;
2635                    let _ = writeln!(out, "    k={:>3}  {:.6}  {}", i, pmf, "#".repeat(bar));
2636                }
2637            }
2638        }
2639        _ => {
2640            out.push_str(&prob_usage());
2641        }
2642    }
2643
2644    if out.trim().is_empty() || out.starts_with("Probability") {
2645        return out;
2646    }
2647    let _ = writeln!(out, "{}", sep);
2648    out
2649}
2650
2651fn log_factorial(n: u64) -> f64 {
2652    (1..=n).map(|i| (i as f64).ln()).sum::<f64>()
2653}
2654
2655fn lgamma(x: f64) -> f64 {
2656    // Lanczos approximation
2657    let g = 7.0;
2658    let c: [f64; 9] = [
2659        0.999_999_999_999_809_9,
2660        676.5203681218851,
2661        -1259.1392167224028,
2662        771.323_428_777_653_1,
2663        -176.615_029_162_140_6,
2664        12.507343278686905,
2665        -0.13857109526572012,
2666        9.984_369_578_019_572e-6,
2667        1.5056327351493116e-7,
2668    ];
2669    if x < 0.5 {
2670        PI.ln() - (PI * x).sin().ln() - lgamma(1.0 - x)
2671    } else {
2672        let x = x - 1.0;
2673        let mut a = c[0];
2674        for i in 1..9 {
2675            a += c[i] / (x + i as f64);
2676        }
2677        let t = x + g + 0.5;
2678        0.5 * (2.0 * PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
2679    }
2680}
2681
2682fn reg_inc_beta(a: f64, b: f64, x: f64) -> f64 {
2683    // Regularized incomplete beta via continued fraction (Lentz's method)
2684    if x <= 0.0 {
2685        return 0.0;
2686    }
2687    if x >= 1.0 {
2688        return 1.0;
2689    }
2690    // Use symmetry for stability
2691    if x > (a + 1.0) / (a + b + 2.0) {
2692        return 1.0 - reg_inc_beta(b, a, 1.0 - x);
2693    }
2694    let lbeta = lgamma(a) + lgamma(b) - lgamma(a + b);
2695    let front = (a * x.ln() + b * (1.0 - x).ln() - lbeta).exp() / a;
2696    // Lentz continued fraction
2697    let mut c_cf = 1.0;
2698    let mut d_cf = 1.0 - (a + b) * x / (a + 1.0);
2699    if d_cf.abs() < 1e-30 {
2700        d_cf = 1e-30;
2701    }
2702    d_cf = 1.0 / d_cf;
2703    let mut f = d_cf;
2704    for m in 1..200usize {
2705        let mf = m as f64;
2706        // Even step
2707        let nm = mf * (b - mf) * x / ((a + 2.0 * mf - 1.0) * (a + 2.0 * mf));
2708        d_cf = 1.0 + nm * d_cf;
2709        if d_cf.abs() < 1e-30 {
2710            d_cf = 1e-30;
2711        }
2712        c_cf = 1.0 + nm / c_cf;
2713        if c_cf.abs() < 1e-30 {
2714            c_cf = 1e-30;
2715        }
2716        d_cf = 1.0 / d_cf;
2717        f *= d_cf * c_cf;
2718        // Odd step
2719        let nm2 = -(a + mf) * (a + b + mf) * x / ((a + 2.0 * mf) * (a + 2.0 * mf + 1.0));
2720        d_cf = 1.0 + nm2 * d_cf;
2721        if d_cf.abs() < 1e-30 {
2722            d_cf = 1e-30;
2723        }
2724        c_cf = 1.0 + nm2 / c_cf;
2725        if c_cf.abs() < 1e-30 {
2726            c_cf = 1e-30;
2727        }
2728        d_cf = 1.0 / d_cf;
2729        let delta = d_cf * c_cf;
2730        f *= delta;
2731        if (delta - 1.0).abs() < 3e-7 {
2732            break;
2733        }
2734    }
2735    front * f
2736}
2737
2738fn reg_inc_gamma(a: f64, x: f64) -> f64 {
2739    // Regularized lower incomplete gamma via series expansion
2740    if x <= 0.0 {
2741        return 0.0;
2742    }
2743    if x > a + 1.0 {
2744        // Use continued fraction for large x
2745        return 1.0 - reg_inc_gamma_upper(a, x);
2746    }
2747    let mut sum = 1.0 / a;
2748    let mut term = 1.0 / a;
2749    for n in 1..200u64 {
2750        term *= x / (a + n as f64);
2751        sum += term;
2752        if term.abs() < 1e-10 * sum.abs() {
2753            break;
2754        }
2755    }
2756    sum * (-x + a * x.ln() - lgamma(a)).exp()
2757}
2758
2759fn reg_inc_gamma_upper(a: f64, x: f64) -> f64 {
2760    // Regularized upper incomplete gamma via Lentz CF
2761    let mut c_cf = 1.0;
2762    let b0 = x + 1.0 - a;
2763    let mut d_cf = if b0.abs() < 1e-30 { 1e30 } else { 1.0 / b0 };
2764    let mut f = d_cf;
2765    for i in 1..200u64 {
2766        let ai = -(i as f64) * (i as f64 - a);
2767        let bi = x + (2 * i + 1) as f64 - a;
2768        d_cf = bi + ai * d_cf;
2769        if d_cf.abs() < 1e-30 {
2770            d_cf = 1e-30;
2771        }
2772        c_cf = bi + ai / c_cf;
2773        if c_cf.abs() < 1e-30 {
2774            c_cf = 1e-30;
2775        }
2776        d_cf = 1.0 / d_cf;
2777        let delta = d_cf * c_cf;
2778        f *= delta;
2779        if (delta - 1.0).abs() < 3e-7 {
2780            break;
2781        }
2782    }
2783    f * (-x + a * x.ln() - lgamma(a)).exp()
2784}
2785
2786fn bisect<F: Fn(f64) -> f64>(f: F, mut lo: f64, mut hi: f64, iters: usize) -> f64 {
2787    for _ in 0..iters {
2788        let mid = (lo + hi) / 2.0;
2789        if f(mid) < 0.0 {
2790            lo = mid;
2791        } else {
2792            hi = mid;
2793        }
2794    }
2795    (lo + hi) / 2.0
2796}
2797
2798fn prob_usage() -> String {
2799    "Probability distributions — instant, no model, no cloud:\n\
2800     \n\
2801     hematite --probability 'normal cdf 1.96'              P(Z ≤ 1.96) std normal\n\
2802     hematite --probability 'normal cdf 70 mu=60 sd=10'    P(X ≤ 70) for N(60,10)\n\
2803     hematite --probability 'binomial pmf 3 n=10 p=0.4'    P(X=3) Bin(10,0.4)\n\
2804     hematite --probability 'binomial cdf 5 n=10 p=0.4'    P(X≤5) Bin(10,0.4)\n\
2805     hematite --probability 'poisson pmf 2 lam=3'          P(X=2) Poi(3)\n\
2806     hematite --probability 't cdf 2.0 df=9'               P(T≤2.0) t(9)\n\
2807     hematite --probability 'chi2 cdf 5.99 df=2'           P(X≤5.99) chi²(2)\n\
2808     hematite --probability 'exponential cdf 1.0 lam=0.5'  P(X≤1) Exp(0.5)\n\
2809     hematite --probability 'uniform cdf 0.7 a=0 b=1'      P(X≤0.7) U(0,1)\n\
2810     hematite --probability 'geometric pmf 3 p=0.5'        P(X=3) Geo(0.5)\n\
2811     \n\
2812     Operations: pdf/pmf  cdf  inv/quantile  between  (default: show all)\n\
2813     Distributions: normal  binomial  poisson  t  chi2  exponential  uniform  geometric"
2814        .into()
2815}
2816
2817// ── Unit conversion ───────────────────────────────────────────────────────────
2818// Query forms:
2819//   "5 km to miles"   "32 F to C"   "1 atm to Pa"   "100 mph to km/h"
2820//   "list" or "units" — show all supported categories and units
2821
2822pub fn unit_convert(query: &str) -> String {
2823    let q = query.trim();
2824    if q.eq_ignore_ascii_case("list")
2825        || q.eq_ignore_ascii_case("units")
2826        || q.eq_ignore_ascii_case("help")
2827    {
2828        return unit_convert_list();
2829    }
2830
2831    // Parse: "<value> <from_unit> to <to_unit>"
2832    // Also accept: "<value> <from_unit> in <to_unit>"
2833    let lower = q.to_lowercase();
2834    let sep = if lower.contains(" to ") {
2835        " to "
2836    } else if lower.contains(" in ") {
2837        " in "
2838    } else {
2839        ""
2840    };
2841
2842    if sep.is_empty() {
2843        return "Usage: hematite --convert '5 km to miles'\n\
2844             Common examples:\n\
2845             hematite --convert '100 f to c'\n\
2846             hematite --convert '1 atm to Pa'\n\
2847             hematite --convert '60 mph to km/h'\n\
2848             hematite --convert '1 GiB to MB'\n\
2849             hematite --convert '1 cal to J'\n\
2850             hematite --convert 'list'    (show all units)"
2851            .to_string();
2852    }
2853
2854    let parts: Vec<&str> = q
2855        .splitn(2, &sep.to_uppercase().as_str().to_string())
2856        .collect();
2857    let parts: Vec<&str> = if parts.len() < 2 {
2858        q.splitn(2, sep).collect()
2859    } else {
2860        parts
2861    };
2862
2863    if parts.len() < 2 {
2864        return format!("Could not parse: '{}'. Try: '5 km to miles'", q);
2865    }
2866
2867    let lhs = parts[0].trim();
2868    let to_unit = parts[1].trim();
2869
2870    // Split lhs into numeric value and unit
2871    let (value_str, from_unit) = split_value_unit(lhs);
2872    let value: f64 = match value_str.parse() {
2873        Ok(v) => v,
2874        Err(_) => return format!("Cannot parse value: '{}'", value_str),
2875    };
2876
2877    match convert(value, from_unit.trim(), to_unit.trim()) {
2878        Ok((result, category)) => {
2879            let result_str = if result.abs() >= 1e-3 && result.abs() < 1e7 {
2880                format!("{:.8}", result)
2881                    .trim_end_matches('0')
2882                    .trim_end_matches('.')
2883                    .to_string()
2884            } else {
2885                format!("{:.6e}", result)
2886            };
2887            format!(
2888                "{} {} = {} {}\n({})",
2889                value_str.trim(),
2890                from_unit.trim(),
2891                result_str,
2892                to_unit.trim(),
2893                category
2894            )
2895        }
2896        Err(e) => e,
2897    }
2898}
2899
2900fn split_value_unit(s: &str) -> (&str, &str) {
2901    // Find the boundary between the numeric part and the unit part
2902    let s = s.trim();
2903    let mut end = 0;
2904    for (i, c) in s.char_indices() {
2905        if c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E' {
2906            end = i + c.len_utf8();
2907        } else if i == 0 && (c == '-' || c == '+') {
2908            end = 1;
2909        } else if i > 0 {
2910            break;
2911        }
2912    }
2913    if end == 0 {
2914        end = s.len();
2915    }
2916    (&s[..end], s[end..].trim())
2917}
2918
2919// Returns (converted_value, category_name) or Err(message)
2920fn convert(value: f64, from: &str, to: &str) -> Result<(f64, &'static str), String> {
2921    // Normalize unit names
2922    let from_n = norm_unit(from);
2923    let to_n = norm_unit(to);
2924
2925    // Walk categories
2926    for (cat_name, units) in UNIT_CATEGORIES {
2927        // Find from_unit base factor
2928        let from_entry = units
2929            .iter()
2930            .find(|(names, _)| names.contains(&from_n.as_str()));
2931        let to_entry = units
2932            .iter()
2933            .find(|(names, _)| names.contains(&to_n.as_str()));
2934
2935        if let (Some(fe), Some(te)) = (from_entry, to_entry) {
2936            // Temperature handled specially
2937            if *cat_name == "Temperature" {
2938                return Ok((
2939                    convert_temperature(value, from_n.as_str(), to_n.as_str()),
2940                    "Temperature",
2941                ));
2942            }
2943            // For all other categories: value * from_factor = SI base; SI base / to_factor = result
2944            let si = value * fe.1;
2945            let result = si / te.1;
2946            return Ok((result, cat_name));
2947        }
2948    }
2949
2950    // Try to give a helpful error
2951    let known: Vec<&str> = UNIT_CATEGORIES
2952        .iter()
2953        .flat_map(|(_, units)| units.iter().flat_map(|(names, _)| names.iter().copied()))
2954        .collect();
2955    let mut close: Vec<&str> = known
2956        .iter()
2957        .filter(|n| levenshtein(n, from_n.as_str()) <= 2)
2958        .copied()
2959        .collect();
2960    close.extend(
2961        known
2962            .iter()
2963            .filter(|n| levenshtein(n, to_n.as_str()) <= 2)
2964            .copied(),
2965    );
2966    close.dedup();
2967
2968    if close.is_empty() {
2969        Err(format!(
2970            "Unknown units: '{}' or '{}'. Run 'hematite --convert list' to see all.",
2971            from, to
2972        ))
2973    } else {
2974        Err(format!("Unknown unit(s). Did you mean: {}?\nRun 'hematite --convert list' for all supported units.", close.join(", ")))
2975    }
2976}
2977
2978fn norm_unit(s: &str) -> String {
2979    s.trim()
2980        .to_lowercase()
2981        .replace("°", "")
2982        .replace("²", "2")
2983        .replace("³", "3")
2984        .replace("/s", "_per_s")
2985        .replace("per second", "_per_s")
2986}
2987
2988fn convert_temperature(value: f64, from: &str, to: &str) -> f64 {
2989    // Convert to Kelvin first
2990    let kelvin = match from {
2991        "c" | "celsius" => value + 273.15,
2992        "f" | "fahrenheit" => (value - 32.0) * 5.0 / 9.0 + 273.15,
2993        "k" | "kelvin" => value,
2994        "r" | "rankine" => value * 5.0 / 9.0,
2995        _ => value,
2996    };
2997    match to {
2998        "c" | "celsius" => kelvin - 273.15,
2999        "f" | "fahrenheit" => (kelvin - 273.15) * 9.0 / 5.0 + 32.0,
3000        "k" | "kelvin" => kelvin,
3001        "r" | "rankine" => kelvin * 9.0 / 5.0,
3002        _ => kelvin,
3003    }
3004}
3005
3006fn levenshtein(a: &str, b: &str) -> usize {
3007    let a: Vec<char> = a.chars().collect();
3008    let b: Vec<char> = b.chars().collect();
3009    let (m, n) = (a.len(), b.len());
3010    let mut dp = vec![vec![0usize; n + 1]; m + 1];
3011    for i in 0..=m {
3012        dp[i][0] = i;
3013    }
3014    for j in 0..=n {
3015        dp[0][j] = j;
3016    }
3017    for i in 1..=m {
3018        for j in 1..=n {
3019            dp[i][j] = if a[i - 1] == b[j - 1] {
3020                dp[i - 1][j - 1]
3021            } else {
3022                1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
3023            };
3024        }
3025    }
3026    dp[m][n]
3027}
3028
3029// Each entry: (&[unit_aliases], factor_to_SI_base)
3030// Temperature is handled separately (non-linear)
3031type UnitEntry = (&'static [&'static str], f64);
3032type UnitCategory = (&'static str, &'static [UnitEntry]);
3033
3034static UNIT_CATEGORIES: &[UnitCategory] = &[
3035    (
3036        "Length",
3037        &[
3038            (&["m", "meter", "meters", "metre", "metres"], 1.0),
3039            (
3040                &["km", "kilometer", "kilometers", "kilometre", "kilometres"],
3041                1000.0,
3042            ),
3043            (
3044                &[
3045                    "cm",
3046                    "centimeter",
3047                    "centimeters",
3048                    "centimetre",
3049                    "centimetres",
3050                ],
3051                0.01,
3052            ),
3053            (
3054                &[
3055                    "mm",
3056                    "millimeter",
3057                    "millimeters",
3058                    "millimetre",
3059                    "millimetres",
3060                ],
3061                0.001,
3062            ),
3063            (
3064                &["um", "micrometer", "micrometers", "micron", "microns"],
3065                1e-6,
3066            ),
3067            (
3068                &["nm", "nanometer", "nanometers", "nanometre", "nanometres"],
3069                1e-9,
3070            ),
3071            (&["mi", "mile", "miles"], 1609.344),
3072            (&["yd", "yard", "yards"], 0.9144),
3073            (&["ft", "foot", "feet"], 0.3048),
3074            (&["in", "inch", "inches"], 0.0254),
3075            (&["nmi", "nautical_mile", "nautical_miles"], 1852.0),
3076            (
3077                &["ly", "light_year", "light_years", "lightyear", "lightyears"],
3078                9.460_730_472_580_8e15,
3079            ),
3080            (
3081                &["au", "astronomical_unit", "astronomical_units"],
3082                1.495978707e11,
3083            ),
3084            (&["pc", "parsec", "parsecs"], 3.085677581e16),
3085            (&["ang", "angstrom", "angstroms"], 1e-10),
3086        ],
3087    ),
3088    (
3089        "Mass",
3090        &[
3091            (&["kg", "kilogram", "kilograms", "kilogramme"], 1.0),
3092            (&["g", "gram", "grams", "gramme"], 0.001),
3093            (&["mg", "milligram", "milligrams", "milligramme"], 1e-6),
3094            (&["ug", "microgram", "micrograms"], 1e-9),
3095            (
3096                &["t", "tonne", "tonnes", "metric_ton", "metric_tons"],
3097                1000.0,
3098            ),
3099            (&["lb", "lbs", "pound", "pounds"], 0.45359237),
3100            (&["oz", "ounce", "ounces"], 0.028349523125),
3101            (&["st", "stone", "stones"], 6.35029318),
3102            (&["ton", "short_ton", "short_tons"], 907.18474),
3103            (&["long_ton", "long_tons"], 1016.0469088),
3104            (&["gr", "grain", "grains"], 6.479891e-5),
3105            (&["u", "amu", "dalton", "daltons", "da"], 1.66053906660e-27),
3106        ],
3107    ),
3108    (
3109        "Temperature",
3110        &[
3111            (&["c", "celsius"], 1.0), // factors unused
3112            (&["f", "fahrenheit"], 1.0),
3113            (&["k", "kelvin"], 1.0),
3114            (&["r", "rankine"], 1.0),
3115        ],
3116    ),
3117    (
3118        "Time",
3119        &[
3120            (&["s", "sec", "second", "seconds"], 1.0),
3121            (&["ms", "millisecond", "milliseconds"], 0.001),
3122            (&["us", "microsecond", "microseconds"], 1e-6),
3123            (&["ns", "nanosecond", "nanoseconds"], 1e-9),
3124            (&["min", "minute", "minutes"], 60.0),
3125            (&["h", "hr", "hour", "hours"], 3600.0),
3126            (&["d", "day", "days"], 86400.0),
3127            (&["wk", "week", "weeks"], 604800.0),
3128            (&["mo", "month", "months"], 2629800.0),
3129            (&["yr", "year", "years"], 31557600.0),
3130        ],
3131    ),
3132    (
3133        "Area",
3134        &[
3135            (
3136                &["m2", "sqm", "square_meter", "square_meters", "square_metre"],
3137                1.0,
3138            ),
3139            (&["km2", "sqkm", "square_kilometer", "square_km"], 1e6),
3140            (
3141                &["cm2", "sqcm", "square_centimeter", "square_centimeters"],
3142                1e-4,
3143            ),
3144            (
3145                &["mm2", "sqmm", "square_millimeter", "square_millimeters"],
3146                1e-6,
3147            ),
3148            (&["ha", "hectare", "hectares"], 1e4),
3149            (&["ac", "acre", "acres"], 4046.8564224),
3150            (&["sqft", "sq_ft", "square_foot", "square_feet"], 0.09290304),
3151            (
3152                &["sqin", "sq_in", "square_inch", "square_inches"],
3153                6.4516e-4,
3154            ),
3155            (
3156                &["sqmi", "sq_mi", "square_mile", "square_miles"],
3157                2589988.110336,
3158            ),
3159            (
3160                &["sqyd", "sq_yd", "square_yard", "square_yards"],
3161                0.83612736,
3162            ),
3163        ],
3164    ),
3165    (
3166        "Volume",
3167        &[
3168            (&["m3", "cubic_meter", "cubic_meters", "cubic_metre"], 1.0),
3169            (&["l", "liter", "liters", "litre", "litres"], 0.001),
3170            (&["ml", "milliliter", "milliliters", "millilitre"], 1e-6),
3171            (&["cl", "centiliter", "centiliters"], 1e-5),
3172            (&["dl", "deciliter", "deciliters"], 1e-4),
3173            (&["ul", "microliter", "microliters"], 1e-9),
3174            (
3175                &["cm3", "cc", "cubic_centimeter", "cubic_centimeters"],
3176                1e-6,
3177            ),
3178            (&["mm3", "cubic_millimeter", "cubic_millimeters"], 1e-9),
3179            (&["km3", "cubic_kilometer", "cubic_kilometers"], 1e9),
3180            (&["ft3", "cubic_foot", "cubic_feet"], 0.0283168466),
3181            (&["in3", "cubic_inch", "cubic_inches"], 1.6387064e-5),
3182            (&["yd3", "cubic_yard", "cubic_yards"], 0.764554858),
3183            (&["gal", "gallon", "gallons"], 0.003785411784),
3184            (&["qt", "quart", "quarts"], 9.46352946e-4),
3185            (&["pt", "pint", "pints"], 4.73176473e-4),
3186            (&["cup", "cups"], 2.36588237e-4),
3187            (
3188                &["floz", "fl_oz", "fluid_ounce", "fluid_ounces"],
3189                2.95735296e-5,
3190            ),
3191            (&["tbsp", "tablespoon", "tablespoons"], 1.47867648e-5),
3192            (&["tsp", "teaspoon", "teaspoons"], 4.92892159e-6),
3193            (&["bbl", "barrel", "barrels"], 0.158987295),
3194            (
3195                &["gal_uk", "uk_gallon", "imperial_gallon", "imperial_gallons"],
3196                0.00454609,
3197            ),
3198        ],
3199    ),
3200    (
3201        "Speed",
3202        &[
3203            (&["m_per_s", "m/s", "mps"], 1.0),
3204            (&["km_per_s", "km/s", "kmps"], 1000.0),
3205            (
3206                &["km/h", "kmh", "kph", "km_per_h", "km_per_hour"],
3207                1.0 / 3.6,
3208            ),
3209            (&["mph", "mi/h", "mi_per_h", "miles_per_hour"], 0.44704),
3210            (&["knot", "knots", "kn"], 0.514444),
3211            (&["ft_per_s", "ft/s", "fps"], 0.3048),
3212            (&["c_speed", "speed_of_light"], 299792458.0),
3213            (&["mach"], 340.29),
3214        ],
3215    ),
3216    (
3217        "Force",
3218        &[
3219            (&["n", "newton", "newtons"], 1.0),
3220            (&["kn", "kilonewton", "kilonewtons"], 1000.0),
3221            (&["mn", "meganewton", "meganewtons"], 1e6),
3222            (&["lbf", "pound_force", "pound-force"], 4.44822162),
3223            (&["kgf", "kilogram_force", "kilogram-force"], 9.80665),
3224            (&["dyn", "dyne", "dynes"], 1e-5),
3225            (&["ozf", "ounce_force"], 0.278013851),
3226        ],
3227    ),
3228    (
3229        "Pressure",
3230        &[
3231            (&["pa", "pascal", "pascals"], 1.0),
3232            (&["kpa", "kilopascal", "kilopascals"], 1000.0),
3233            (&["mpa", "megapascal", "megapascals"], 1e6),
3234            (&["gpa", "gigapascal", "gigapascals"], 1e9),
3235            (
3236                &[
3237                    "hpa",
3238                    "hectopascal",
3239                    "hectopascals",
3240                    "mbar",
3241                    "millibar",
3242                    "millibars",
3243                ],
3244                100.0,
3245            ),
3246            (&["bar", "bars"], 1e5),
3247            (&["atm", "atmosphere", "atmospheres"], 101325.0),
3248            (&["torr"], 133.322368),
3249            (&["mmhg", "mm_hg", "millimeter_of_mercury"], 133.322368),
3250            (&["psi", "pound_per_square_inch"], 6894.75729),
3251            (&["inhg", "in_hg", "inch_of_mercury"], 3386.389),
3252        ],
3253    ),
3254    (
3255        "Energy",
3256        &[
3257            (&["j", "joule", "joules"], 1.0),
3258            (&["kj", "kilojoule", "kilojoules"], 1000.0),
3259            (&["mj", "megajoule", "megajoules"], 1e6),
3260            (&["gj", "gigajoule", "gigajoules"], 1e9),
3261            (
3262                &["cal", "calorie", "calories", "thermochemical_calorie"],
3263                4.184,
3264            ),
3265            (
3266                &["kcal", "kilocalorie", "kilocalories", "food_calorie"],
3267                4184.0,
3268            ),
3269            (&["wh", "watt_hour", "watt_hours"], 3600.0),
3270            (&["kwh", "kilowatt_hour", "kilowatt_hours"], 3.6e6),
3271            (&["mwh", "megawatt_hour", "megawatt_hours"], 3.6e9),
3272            (&["ev", "electronvolt", "electronvolts"], 1.602176634e-19),
3273            (
3274                &["kev", "kiloelectronvolt", "kiloelectronvolts"],
3275                1.602176634e-16,
3276            ),
3277            (
3278                &["mev", "megaelectronvolt", "megaelectronvolts"],
3279                1.602176634e-13,
3280            ),
3281            (
3282                &["gev", "gigaelectronvolt", "gigaelectronvolts"],
3283                1.602176634e-10,
3284            ),
3285            (
3286                &["tev", "teraelectronvolt", "teraelectronvolts"],
3287                1.602176634e-7,
3288            ),
3289            (&["btu", "british_thermal_unit"], 1055.05585),
3290            (&["erg", "ergs"], 1e-7),
3291            (&["ft_lb", "foot_pound", "foot_pounds"], 1.35581795),
3292            (&["therm", "therms"], 1.05480400e8),
3293        ],
3294    ),
3295    (
3296        "Power",
3297        &[
3298            (&["w", "watt", "watts"], 1.0),
3299            (&["kw", "kilowatt", "kilowatts"], 1000.0),
3300            (&["mw", "megawatt", "megawatts"], 1e6),
3301            (&["gw", "gigawatt", "gigawatts"], 1e9),
3302            (&["tw", "terawatt", "terawatts"], 1e12),
3303            (&["mw_milli", "milliwatt", "milliwatts"], 0.001),
3304            (&["hp", "horsepower"], 745.69987),
3305            (&["ps", "metric_horsepower"], 735.49875),
3306            (&["btu_h", "btu/h", "btu_per_hour"], 0.29307107),
3307            (&["erg_s", "erg/s", "erg_per_second"], 1e-7),
3308            (&["ft_lb_s", "ft_lb/s"], 1.35581795),
3309        ],
3310    ),
3311    (
3312        "Data",
3313        &[
3314            (&["bit", "bits"], 1.0),
3315            (&["byte", "bytes", "b"], 8.0),
3316            (&["kb", "kilobit", "kilobits"], 1e3),
3317            (&["kib", "kibibit", "kibibits"], 1024.0),
3318            (&["mb", "megabit", "megabits"], 1e6),
3319            (&["mib", "mebibit", "mebibits"], 1024.0 * 1024.0),
3320            (&["gb", "gigabit", "gigabits"], 1e9),
3321            (&["gib", "gibibit", "gibibits"], 1024.0 * 1024.0 * 1024.0),
3322            (&["tb", "terabit", "terabits"], 1e12),
3323            (
3324                &["tib", "tebibit", "tebibits"],
3325                1024.0 * 1024.0 * 1024.0 * 1024.0,
3326            ),
3327            (&["pb", "petabit", "petabits"], 1e15),
3328            (&["kb_byte", "kilobyte", "kilobytes"], 8e3),
3329            (&["kib_byte", "kibibyte", "kibibytes"], 8.0 * 1024.0),
3330            (&["mb_byte", "megabyte", "megabytes"], 8e6),
3331            (
3332                &["mib_byte", "mebibyte", "mebibytes"],
3333                8.0 * 1024.0 * 1024.0,
3334            ),
3335            (&["gb_byte", "gigabyte", "gigabytes"], 8e9),
3336            (
3337                &["gib_byte", "gibibyte", "gibibytes"],
3338                8.0 * 1024.0 * 1024.0 * 1024.0,
3339            ),
3340            (&["tb_byte", "terabyte", "terabytes"], 8e12),
3341            (
3342                &["tib_byte", "tebibyte", "tebibytes"],
3343                8.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0,
3344            ),
3345        ],
3346    ),
3347    (
3348        "Angle",
3349        &[
3350            (&["rad", "radian", "radians"], 1.0),
3351            (&["deg", "degree", "degrees"], std::f64::consts::PI / 180.0),
3352            (
3353                &["grad", "gradian", "gradians", "gon", "gons"],
3354                std::f64::consts::PI / 200.0,
3355            ),
3356            (
3357                &["arcmin", "arc_minute", "arc_minutes", "minute_of_arc"],
3358                std::f64::consts::PI / 10800.0,
3359            ),
3360            (
3361                &["arcsec", "arc_second", "arc_seconds", "second_of_arc"],
3362                std::f64::consts::PI / 648000.0,
3363            ),
3364            (
3365                &["rev", "revolution", "revolutions", "turn", "turns"],
3366                2.0 * std::f64::consts::PI,
3367            ),
3368        ],
3369    ),
3370    (
3371        "Frequency",
3372        &[
3373            (&["hz", "hertz"], 1.0),
3374            (&["khz", "kilohertz"], 1e3),
3375            (&["mhz", "megahertz"], 1e6),
3376            (&["ghz", "gigahertz"], 1e9),
3377            (&["thz", "terahertz"], 1e12),
3378            (&["rpm", "revolutions_per_minute"], 1.0 / 60.0),
3379            (&["rad_per_s", "rad/s"], 1.0 / (2.0 * std::f64::consts::PI)),
3380        ],
3381    ),
3382    (
3383        "Illuminance",
3384        &[
3385            (&["lx", "lux"], 1.0),
3386            (&["fc", "footcandle", "footcandles"], 10.7639),
3387            (&["phot", "phots"], 1e4),
3388        ],
3389    ),
3390    (
3391        "Fuel Economy",
3392        &[
3393            (&["mpg", "miles_per_gallon"], 1.0),
3394            (&["mpg_uk", "miles_per_gallon_uk", "imperial_mpg"], 1.20095),
3395            (&["l_per_100km", "l/100km", "liters_per_100km"], 235.214583),
3396            (&["km_per_l", "km/l", "km_per_liter"], 2.35214583),
3397        ],
3398    ),
3399];
3400
3401fn unit_convert_list() -> String {
3402    let mut out = String::new();
3403    let _ = writeln!(out, "Supported unit categories and aliases:");
3404    let _ = writeln!(out, "Usage: hematite --convert '<value> <from> to <to>'");
3405    let _ = writeln!(out);
3406    for (cat, units) in UNIT_CATEGORIES {
3407        let _ = writeln!(out, "  {}:", cat);
3408        for (names, _) in *units {
3409            let _ = writeln!(out, "    {}", names.join(", "));
3410        }
3411        let _ = writeln!(out);
3412    }
3413    out
3414}
3415
3416// ── Vector / linear-algebra calculator ───────────────────────────────────────
3417// Pure-Rust — no sandbox, instant results.
3418// Supports 2D and 3D vectors, arbitrary-dimension for dot/mag/normalize.
3419//
3420// Query forms:
3421//   "[1,2,3] dot [4,5,6]"
3422//   "[1,2,3] cross [4,5,6]"
3423//   "[1,2,3] + [4,5,6]"    (add)
3424//   "[1,2,3] - [4,5,6]"    (subtract)
3425//   "3 * [1,2,3]"          (scalar multiply)
3426//   "mag [3,4]"            (magnitude)
3427//   "norm [1,2,3]"         (normalize)
3428//   "angle [1,0] [0,1]"    (angle in degrees)
3429//   "proj [1,2] onto [3,4]" (vector projection)
3430//   "[1,2,3]"              (info about a single vector)
3431
3432pub fn vector_calc(query: &str) -> String {
3433    let q = query.trim();
3434
3435    // ── unary ops ────────────────────────────────────────────────────────────
3436    if let Some(rest) = strip_prefix_ci(q, "mag") {
3437        if let Some(v) = parse_vec(rest.trim()) {
3438            return format_vec_result("Magnitude", &[], vec_mag(&v));
3439        }
3440    }
3441    if let Some(rest) = strip_prefix_ci(q, "magnitude") {
3442        if let Some(v) = parse_vec(rest.trim()) {
3443            return format_vec_result("Magnitude", &[], vec_mag(&v));
3444        }
3445    }
3446    if let Some(rest) = strip_prefix_ci(q, "norm") {
3447        if let Some(v) = parse_vec(rest.trim()) {
3448            let mag = vec_mag(&v);
3449            if mag == 0.0 {
3450                return "Zero vector has no unit direction.".into();
3451            }
3452            let n: Vec<f64> = v.iter().map(|x| x / mag).collect();
3453            return format_vec_display("Unit vector (normalized)", &n);
3454        }
3455    }
3456    if let Some(rest) = strip_prefix_ci(q, "normalize") {
3457        if let Some(v) = parse_vec(rest.trim()) {
3458            let mag = vec_mag(&v);
3459            if mag == 0.0 {
3460                return "Zero vector has no unit direction.".into();
3461            }
3462            let n: Vec<f64> = v.iter().map(|x| x / mag).collect();
3463            return format_vec_display("Unit vector (normalized)", &n);
3464        }
3465    }
3466
3467    // ── angle between two vectors ─────────────────────────────────────────────
3468    if let Some(rest) = strip_prefix_ci(q, "angle") {
3469        let vecs = find_all_vecs(rest.trim());
3470        if vecs.len() >= 2 {
3471            let a = &vecs[0];
3472            let b = &vecs[1];
3473            if a.len() != b.len() {
3474                return "Vectors must have the same dimension for angle.".into();
3475            }
3476            let dot = vec_dot(a, b);
3477            let ma = vec_mag(a);
3478            let mb = vec_mag(b);
3479            if ma == 0.0 || mb == 0.0 {
3480                return "Cannot compute angle involving a zero vector.".into();
3481            }
3482            let cos_theta = (dot / (ma * mb)).clamp(-1.0, 1.0);
3483            let deg = cos_theta.acos().to_degrees();
3484            let rad = cos_theta.acos();
3485            return format!(
3486                "Angle between {} and {}:\n  {:.6}°  ({:.6} radians)\n  cos θ = {:.6}",
3487                fmt_vec(a),
3488                fmt_vec(b),
3489                deg,
3490                rad,
3491                cos_theta
3492            );
3493        }
3494    }
3495
3496    // ── projection ────────────────────────────────────────────────────────────
3497    if q.to_lowercase().contains("proj") && q.to_lowercase().contains("onto") {
3498        let vecs = find_all_vecs(q);
3499        if vecs.len() >= 2 {
3500            let a = &vecs[0];
3501            let b = &vecs[1];
3502            if a.len() != b.len() {
3503                return "Vectors must have the same dimension for projection.".into();
3504            }
3505            let b_mag2: f64 = b.iter().map(|x| x * x).sum();
3506            if b_mag2 == 0.0 {
3507                return "Cannot project onto a zero vector.".into();
3508            }
3509            let scalar = vec_dot(a, b) / b_mag2;
3510            let proj: Vec<f64> = b.iter().map(|x| x * scalar).collect();
3511            let mut out = String::new();
3512            let _ = writeln!(out, "Projection of {} onto {}:", fmt_vec(a), fmt_vec(b));
3513            let _ = writeln!(out, "  proj = {}", fmt_vec(&proj));
3514            let _ = writeln!(out, "  scalar factor = {:.6}", scalar);
3515            return out;
3516        }
3517    }
3518
3519    // ── binary ops: look for keyword between two vectors ──────────────────────
3520    let lower = q.to_lowercase();
3521
3522    // dot product
3523    if lower.contains(" dot ") {
3524        if let Some(idx) = lower.find(" dot ") {
3525            let left = &q[..idx];
3526            let right = &q[idx + 5..];
3527            if let (Some(a), Some(b)) = (parse_vec(left.trim()), parse_vec(right.trim())) {
3528                if a.len() != b.len() {
3529                    return format!("Dimension mismatch: {} vs {}", a.len(), b.len());
3530                }
3531                let d = vec_dot(&a, &b);
3532                return format!("{} · {} = {}", fmt_vec(&a), fmt_vec(&b), fmt_scalar(d));
3533            }
3534        }
3535    }
3536
3537    // cross product
3538    if lower.contains(" cross ") {
3539        if let Some(idx) = lower.find(" cross ") {
3540            let left = &q[..idx];
3541            let right = &q[idx + 7..];
3542            if let (Some(a), Some(b)) = (parse_vec(left.trim()), parse_vec(right.trim())) {
3543                if a.len() != 3 || b.len() != 3 {
3544                    return "Cross product requires two 3D vectors.".into();
3545                }
3546                let c = vec_cross(&a, &b);
3547                return format!(
3548                    "{} × {} = {}\n  |result| = {}",
3549                    fmt_vec(&a),
3550                    fmt_vec(&b),
3551                    fmt_vec(&c),
3552                    fmt_scalar(vec_mag(&c))
3553                );
3554            }
3555        }
3556    }
3557
3558    // scalar × vector: "3 * [1,2,3]" or "[1,2,3] * 3"
3559    if lower.contains(" * ") {
3560        if let Some(idx) = q.find(" * ") {
3561            let left = q[..idx].trim();
3562            let right = q[idx + 3..].trim();
3563            // scalar * vec
3564            if let (Ok(s), Some(v)) = (left.parse::<f64>(), parse_vec(right)) {
3565                let result: Vec<f64> = v.iter().map(|x| x * s).collect();
3566                return format!("{} × {} = {}", s, fmt_vec(&v), fmt_vec(&result));
3567            }
3568            // vec * scalar
3569            if let (Some(v), Ok(s)) = (parse_vec(left), right.parse::<f64>()) {
3570                let result: Vec<f64> = v.iter().map(|x| x * s).collect();
3571                return format!("{} × {} = {}", fmt_vec(&v), s, fmt_vec(&result));
3572            }
3573        }
3574    }
3575
3576    // vector + vector
3577    if let Some(idx) = q.find(" + ") {
3578        let left = q[..idx].trim();
3579        let right = q[idx + 3..].trim();
3580        if let (Some(a), Some(b)) = (parse_vec(left), parse_vec(right)) {
3581            if a.len() != b.len() {
3582                return format!("Dimension mismatch: {} vs {}", a.len(), b.len());
3583            }
3584            let c: Vec<f64> = a.iter().zip(b.iter()).map(|(x, y)| x + y).collect();
3585            return format!("{} + {} = {}", fmt_vec(&a), fmt_vec(&b), fmt_vec(&c));
3586        }
3587    }
3588
3589    // vector - vector
3590    if let Some(idx) = q.rfind(" - ") {
3591        let left = q[..idx].trim();
3592        let right = q[idx + 3..].trim();
3593        if let (Some(a), Some(b)) = (parse_vec(left), parse_vec(right)) {
3594            if a.len() != b.len() {
3595                return format!("Dimension mismatch: {} vs {}", a.len(), b.len());
3596            }
3597            let c: Vec<f64> = a.iter().zip(b.iter()).map(|(x, y)| x - y).collect();
3598            return format!("{} - {} = {}", fmt_vec(&a), fmt_vec(&b), fmt_vec(&c));
3599        }
3600    }
3601
3602    // single vector — info card
3603    if let Some(v) = parse_vec(q) {
3604        let mut out = String::new();
3605        let mag = vec_mag(&v);
3606        let _ = writeln!(out, "Vector:    {}", fmt_vec(&v));
3607        let _ = writeln!(out, "Dimension: {}", v.len());
3608        let _ = writeln!(out, "Magnitude: {}", fmt_scalar(mag));
3609        if mag > 0.0 {
3610            let unit: Vec<f64> = v.iter().map(|x| x / mag).collect();
3611            let _ = writeln!(out, "Unit vec:  {}", fmt_vec(&unit));
3612        }
3613        if v.len() == 2 {
3614            let angle = v[1].atan2(v[0]).to_degrees();
3615            let _ = writeln!(out, "Angle (from +x): {:.4}°", angle);
3616        }
3617        return out;
3618    }
3619
3620    format!(
3621        "Could not parse: '{}'\n\
3622         Examples:\n\
3623           hematite --vectors '[1,2,3] dot [4,5,6]'\n\
3624           hematite --vectors '[1,2,3] cross [4,5,6]'\n\
3625           hematite --vectors '[1,2,3] + [4,5,6]'\n\
3626           hematite --vectors 'mag [3,4]'\n\
3627           hematite --vectors 'norm [1,2,3]'\n\
3628           hematite --vectors 'angle [1,0] [0,1]'\n\
3629           hematite --vectors 'proj [1,2] onto [3,4]'\n\
3630           hematite --vectors '3 * [1,2,3]'",
3631        q
3632    )
3633}
3634
3635fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
3636    if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
3637        Some(&s[prefix.len()..])
3638    } else {
3639        None
3640    }
3641}
3642
3643fn parse_vec(s: &str) -> Option<Vec<f64>> {
3644    // Accepts: [1,2,3]  (1,2,3)  1,2,3  1 2 3
3645    let s = s
3646        .trim()
3647        .trim_start_matches(['[', '('])
3648        .trim_end_matches([']', ')']);
3649    let parts: Vec<&str> = if s.contains(',') {
3650        s.split(',').collect()
3651    } else {
3652        s.split_whitespace().collect()
3653    };
3654    if parts.is_empty() {
3655        return None;
3656    }
3657    let nums: Vec<f64> = parts
3658        .iter()
3659        .filter_map(|p| p.trim().parse::<f64>().ok())
3660        .collect();
3661    if nums.len() == parts.len() && !nums.is_empty() {
3662        Some(nums)
3663    } else {
3664        None
3665    }
3666}
3667
3668fn find_all_vecs(s: &str) -> Vec<Vec<f64>> {
3669    // Find all bracket-delimited vectors in a string
3670    let mut result = Vec::new();
3671    let mut i = 0;
3672    let chars: Vec<char> = s.chars().collect();
3673    while i < chars.len() {
3674        if chars[i] == '[' || chars[i] == '(' {
3675            let close = if chars[i] == '[' { ']' } else { ')' };
3676            if let Some(j) = chars[i + 1..].iter().position(|&c| c == close) {
3677                let inner: String = chars[i + 1..i + 1 + j].iter().collect();
3678                if let Some(v) = parse_vec(&inner) {
3679                    result.push(v);
3680                }
3681                i += j + 2;
3682                continue;
3683            }
3684        }
3685        i += 1;
3686    }
3687    result
3688}
3689
3690fn vec_dot(a: &[f64], b: &[f64]) -> f64 {
3691    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
3692}
3693
3694fn vec_mag(v: &[f64]) -> f64 {
3695    v.iter().map(|x| x * x).sum::<f64>().sqrt()
3696}
3697
3698fn vec_cross(a: &[f64], b: &[f64]) -> Vec<f64> {
3699    vec![
3700        a[1] * b[2] - a[2] * b[1],
3701        a[2] * b[0] - a[0] * b[2],
3702        a[0] * b[1] - a[1] * b[0],
3703    ]
3704}
3705
3706fn fmt_vec(v: &[f64]) -> String {
3707    let inner: Vec<String> = v.iter().map(|x| fmt_scalar(*x)).collect();
3708    format!("[{}]", inner.join(", "))
3709}
3710
3711fn fmt_scalar(x: f64) -> String {
3712    if x.fract() == 0.0 && x.abs() < 1e12 {
3713        format!("{}", x as i64)
3714    } else if x.abs() >= 1e-3 && x.abs() < 1e7 {
3715        format!("{:.6}", x)
3716            .trim_end_matches('0')
3717            .trim_end_matches('.')
3718            .to_string()
3719    } else {
3720        format!("{:.6e}", x)
3721    }
3722}
3723
3724fn format_vec_result(label: &str, _v: &[f64], val: f64) -> String {
3725    format!("{}: {}", label, fmt_scalar(val))
3726}
3727
3728fn format_vec_display(label: &str, v: &[f64]) -> String {
3729    format!("{}: {}", label, fmt_vec(v))
3730}
3731
3732// ── Monte Carlo simulation ────────────────────────────────────────────────────
3733// Pure Rust — no Python sandbox, instant results.
3734// Query forms:
3735//   "pi N"             — estimate π by random darts (N trials, default 1e6)
3736//   "dice NdM [+K] R"  — roll N d-M dice R times, show distribution
3737//   "birthday N"       — birthday problem: probability ≥2 share a birthday in room of N
3738//   "ruin P A B N"     — gambler's ruin: win prob P, start $A, goal $B, N simulations
3739//   "ci N MEAN STD"    — 95%/99% confidence interval via bootstrap-style simulation
3740//   "walk N STEPS"     — N random walks of STEPS steps, report stats
3741
3742pub fn simulate(query: &str) -> String {
3743    let q = query.trim();
3744    let tokens: Vec<&str> = q.split_whitespace().collect();
3745    if tokens.is_empty() {
3746        return simulate_usage();
3747    }
3748
3749    match tokens[0].to_lowercase().as_str() {
3750        "pi" => {
3751            let n: u64 = tokens
3752                .get(1)
3753                .and_then(|s| s.parse().ok())
3754                .unwrap_or(1_000_000);
3755            let n = n.min(100_000_000);
3756            let mut inside = 0u64;
3757            let mut rng = Lcg64::new(0xdeadbeef_12345678);
3758            for _ in 0..n {
3759                let x = rng.next_f64() * 2.0 - 1.0;
3760                let y = rng.next_f64() * 2.0 - 1.0;
3761                if x * x + y * y <= 1.0 {
3762                    inside += 1;
3763                }
3764            }
3765            let pi_est = 4.0 * inside as f64 / n as f64;
3766            let error = (pi_est - std::f64::consts::PI).abs();
3767            format!(
3768                "Monte Carlo π estimate ({} trials):\n  π ≈ {:.8}\n  True π = {:.8}\n  Error: {:.6e}\n  Inside circle: {} / {}",
3769                n, pi_est, std::f64::consts::PI, error, inside, n
3770            )
3771        }
3772        "birthday" => {
3773            let n: u32 = tokens.get(1).and_then(|s| s.parse().ok()).unwrap_or(23);
3774            // Exact probability via inclusion-exclusion
3775            let p_no_match = (0..n as u64).fold(1.0f64, |acc, i| acc * (365 - i) as f64 / 365.0);
3776            let p_match = 1.0 - p_no_match;
3777            let mut out = format!("Birthday problem — room of {} people:\n", n);
3778            out.push_str(&format!(
3779                "  P(at least 2 share a birthday) = {:.6} ({:.2}%)\n",
3780                p_match,
3781                p_match * 100.0
3782            ));
3783            out.push_str(&format!(
3784                "  P(all different birthdays)      = {:.6} ({:.2}%)\n",
3785                p_no_match,
3786                p_no_match * 100.0
3787            ));
3788            // Find 50% threshold
3789            let n50 = (1..366u32)
3790                .find(|&k| {
3791                    let p = 1.0 - (0..k as u64).fold(1.0f64, |a, i| a * (365 - i) as f64 / 365.0);
3792                    p >= 0.5
3793                })
3794                .unwrap_or(23);
3795            out.push_str(&format!(
3796                "  Minimum group for ≥50% chance: {} people\n",
3797                n50
3798            ));
3799            out
3800        }
3801        "dice" => {
3802            // dice 2d6 1000   or   dice 1d20+3 500
3803            let spec = tokens.get(1).copied().unwrap_or("1d6");
3804            let rolls: u64 = tokens.get(2).and_then(|s| s.parse().ok()).unwrap_or(1000);
3805            let rolls = rolls.min(1_000_000);
3806            // Parse NdM+K
3807            let (n_dice, sides, bonus) = parse_dice_spec(spec);
3808            let mut counts: std::collections::HashMap<i64, u64> = std::collections::HashMap::new();
3809            let mut rng = Lcg64::new(0xcafe_babe_dead_beef);
3810            for _ in 0..rolls {
3811                let total: i64 = (0..n_dice)
3812                    .map(|_| (rng.next_u64() % sides as u64) as i64 + 1)
3813                    .sum::<i64>()
3814                    + bonus;
3815                *counts.entry(total).or_insert(0) += 1;
3816            }
3817            let mut sorted_keys: Vec<i64> = counts.keys().copied().collect();
3818            sorted_keys.sort();
3819            let mean: f64 = sorted_keys
3820                .iter()
3821                .map(|&k| k as f64 * counts[&k] as f64)
3822                .sum::<f64>()
3823                / rolls as f64;
3824            let mut out = format!("Dice simulation: {} × {} rolls\n", rolls, spec);
3825            let _ = writeln!(
3826                out,
3827                "  Mean: {:.3}   Range: {}–{}",
3828                mean,
3829                sorted_keys.first().unwrap_or(&0),
3830                sorted_keys.last().unwrap_or(&0)
3831            );
3832            out.push_str("  Distribution:\n");
3833            let max_count = counts.values().copied().max().unwrap_or(1);
3834            for k in &sorted_keys {
3835                let c = counts[k];
3836                let pct = 100.0 * c as f64 / rolls as f64;
3837                let bar_len = (c as f64 / max_count as f64 * 30.0) as usize;
3838                let _ = writeln!(out, "    {:4}  {:6.2}%  {}", k, pct, "█".repeat(bar_len));
3839            }
3840            out
3841        }
3842        "ruin" | "gambler" => {
3843            let p: f64 = tokens.get(1).and_then(|s| s.parse().ok()).unwrap_or(0.5);
3844            let a: i64 = tokens.get(2).and_then(|s| s.parse().ok()).unwrap_or(10);
3845            let b: i64 = tokens.get(3).and_then(|s| s.parse().ok()).unwrap_or(20);
3846            let n: u64 = tokens.get(4).and_then(|s| s.parse().ok()).unwrap_or(10_000);
3847            let n = n.min(100_000);
3848            if a <= 0 || b <= a {
3849                return "Usage: ruin PROB START GOAL N_SIM (GOAL > START > 0)".into();
3850            }
3851
3852            let mut wins = 0u64;
3853            let mut steps_total = 0u64;
3854            let mut rng = Lcg64::new(0x1234_5678_9abc_def0);
3855            for _ in 0..n {
3856                let mut money = a;
3857                let mut steps = 0u64;
3858                while money > 0 && money < b {
3859                    let r = rng.next_f64();
3860                    money += if r < p { 1 } else { -1 };
3861                    steps += 1;
3862                    if steps > 100_000 {
3863                        break;
3864                    }
3865                }
3866                if money >= b {
3867                    wins += 1;
3868                }
3869                steps_total += steps;
3870            }
3871            let win_rate = wins as f64 / n as f64;
3872            let avg_steps = steps_total as f64 / n as f64;
3873            // Exact formula for fair/unfair game
3874            let exact = if (p - 0.5).abs() < 1e-10 {
3875                a as f64 / b as f64
3876            } else {
3877                let q = 1.0 - p;
3878                let r = q / p;
3879                (1.0 - r.powi(a as i32)) / (1.0 - r.powi(b as i32))
3880            };
3881            format!(
3882                "Gambler's Ruin ({} simulations):\n  Win prob p={:.4}  Start=${} → Goal=${}\n\
3883                 \n  Simulated win rate:  {:.4} ({:.2}%)\n  Exact formula:       {:.4} ({:.2}%)\n\
3884                 \n  Average steps to finish: {:.1}",
3885                n,
3886                p,
3887                a,
3888                b,
3889                win_rate,
3890                win_rate * 100.0,
3891                exact,
3892                exact * 100.0,
3893                avg_steps
3894            )
3895        }
3896        "walk" | "random_walk" => {
3897            let n_walks: u64 = tokens.get(1).and_then(|s| s.parse().ok()).unwrap_or(1000);
3898            let steps: u64 = tokens.get(2).and_then(|s| s.parse().ok()).unwrap_or(100);
3899            let n_walks = n_walks.min(100_000);
3900            let steps = steps.min(100_000);
3901            let mut final_positions: Vec<f64> = Vec::with_capacity(n_walks as usize);
3902            let mut max_deviation: f64 = 0.0;
3903            let mut rng = Lcg64::new(0xabcdef01_23456789);
3904            for _ in 0..n_walks {
3905                let mut pos = 0.0f64;
3906                for _ in 0..steps {
3907                    pos += if rng.next_f64() < 0.5 { 1.0 } else { -1.0 };
3908                }
3909                final_positions.push(pos);
3910                if pos.abs() > max_deviation {
3911                    max_deviation = pos.abs();
3912                }
3913            }
3914            let mean = final_positions.iter().sum::<f64>() / n_walks as f64;
3915            let variance: f64 = final_positions
3916                .iter()
3917                .map(|x| (x - mean).powi(2))
3918                .sum::<f64>()
3919                / n_walks as f64;
3920            let std_dev = variance.sqrt();
3921            let theoretical_std = (steps as f64).sqrt();
3922            format!(
3923                "Random Walk simulation ({} walks × {} steps):\n  Mean final position: {:.4}\n  Std deviation:       {:.4}  (theoretical √N = {:.4})\n  Max |deviation|:     {:.0}\n  Expected: walk ends within ±{:.1} of origin with 95% probability",
3924                n_walks, steps, mean, std_dev, theoretical_std, max_deviation, 1.96 * theoretical_std
3925            )
3926        }
3927        _ => {
3928            // Try to parse as "pi N" with the number as first token
3929            if let Ok(n) = tokens[0].parse::<u64>() {
3930                // Assume it's an N for pi estimation
3931                return simulate(&format!("pi {}", n));
3932            }
3933            simulate_usage()
3934        }
3935    }
3936}
3937
3938fn simulate_usage() -> String {
3939    "Monte Carlo simulation:\n\
3940     hematite --simulate 'pi 1000000'           estimate π with N darts\n\
3941     hematite --simulate 'birthday 23'          birthday problem\n\
3942     hematite --simulate 'dice 2d6 10000'       roll 2d6 × 10000\n\
3943     hematite --simulate 'ruin 0.48 10 20 5000' gambler's ruin\n\
3944     hematite --simulate 'walk 1000 200'        random walk simulation"
3945        .into()
3946}
3947
3948fn parse_dice_spec(spec: &str) -> (i64, i64, i64) {
3949    // NdM+K or NdM-K
3950    let lower = spec.to_lowercase();
3951    let (dice_part, bonus) = if let Some(idx) = lower.rfind('+') {
3952        let b: i64 = spec[idx + 1..].parse().unwrap_or(0);
3953        (&spec[..idx], b)
3954    } else if let Some(idx) = lower[1..].rfind('-').map(|i| i + 1) {
3955        let b: i64 = spec[idx + 1..].parse().unwrap_or(0);
3956        (&spec[..idx], -b)
3957    } else {
3958        (spec, 0i64)
3959    };
3960    if let Some(d_pos) = dice_part.to_lowercase().find('d') {
3961        let n: i64 = dice_part[..d_pos].parse().unwrap_or(1).max(1);
3962        let s: i64 = dice_part[d_pos + 1..].parse().unwrap_or(6).max(2);
3963        (n, s, bonus)
3964    } else {
3965        (1, 6, 0)
3966    }
3967}
3968
3969// Minimal 64-bit LCG PRNG — no stdlib rng needed
3970struct Lcg64 {
3971    state: u64,
3972}
3973impl Lcg64 {
3974    fn new(seed: u64) -> Self {
3975        Self {
3976            state: seed.wrapping_add(1),
3977        }
3978    }
3979    fn next_u64(&mut self) -> u64 {
3980        self.state = self
3981            .state
3982            .wrapping_mul(6_364_136_223_846_793_005)
3983            .wrapping_add(1_442_695_040_888_963_407);
3984        self.state
3985    }
3986    fn next_f64(&mut self) -> f64 {
3987        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
3988    }
3989}
3990
3991// ── Propositional logic / Boolean algebra ────────────────────────────────────
3992// Parse Boolean expression → truth table, CNF/DNF, SAT/TAUT check, simplify.
3993// Operators: AND (&& / & / and / *)  OR (|| / | / or / +)
3994//            NOT (! / ~ / not)  XOR (^ / xor)  NAND XNOR NOR
3995//            IMPLIES (-> / => / implies)  IFF (<-> / <=> / iff)
3996//
3997// Modes (first token):
3998//   table EXPR         truth table
3999//   sat   EXPR         satisfiability check
4000//   taut  EXPR         tautology check
4001//   cnf   EXPR         conjunctive normal form
4002//   dnf   EXPR         disjunctive normal form
4003//   equiv EXPR1 ; EXPR2  check logical equivalence
4004//   simplify EXPR      rule-based simplification
4005//   (default)          table + sat + taut
4006
4007// ── Boolean expression AST ────────────────────────────────────────────────
4008
4009#[derive(Clone, Debug, PartialEq)]
4010#[allow(dead_code)]
4011enum BExpr {
4012    Var(String),
4013    Not(Box<BExpr>),
4014    And(Box<BExpr>, Box<BExpr>),
4015    Or(Box<BExpr>, Box<BExpr>),
4016    Xor(Box<BExpr>, Box<BExpr>),
4017    Implies(Box<BExpr>, Box<BExpr>),
4018    Iff(Box<BExpr>, Box<BExpr>),
4019    Nand(Box<BExpr>, Box<BExpr>),
4020    Nor(Box<BExpr>, Box<BExpr>),
4021    Xnor(Box<BExpr>, Box<BExpr>),
4022    Const(bool),
4023}
4024
4025struct BParser<'a> {
4026    chars: &'a [char],
4027    pos: usize,
4028}
4029
4030impl<'a> BParser<'a> {
4031    fn new(chars: &'a [char]) -> Self {
4032        Self { chars, pos: 0 }
4033    }
4034    fn peek(&self) -> Option<char> {
4035        self.chars.get(self.pos).copied()
4036    }
4037    fn consume(&mut self) -> Option<char> {
4038        let c = self.peek();
4039        self.pos += 1;
4040        c
4041    }
4042    fn skip_ws(&mut self) {
4043        while matches!(self.peek(), Some(' ') | Some('\t')) {
4044            self.pos += 1;
4045        }
4046    }
4047
4048    fn parse_iff(&mut self) -> Result<BExpr, String> {
4049        let mut left = self.parse_implies()?;
4050        loop {
4051            self.skip_ws();
4052            if self.try_keyword("iff") || self.try_str("<->") || self.try_str("<=>") {
4053                let right = self.parse_implies()?;
4054                left = BExpr::Iff(Box::new(left), Box::new(right));
4055            } else {
4056                break;
4057            }
4058        }
4059        Ok(left)
4060    }
4061
4062    fn parse_implies(&mut self) -> Result<BExpr, String> {
4063        let left = self.parse_or()?;
4064        self.skip_ws();
4065        if self.try_str("->") || self.try_str("=>") || self.try_keyword("implies") {
4066            let right = self.parse_implies()?;
4067            return Ok(BExpr::Implies(Box::new(left), Box::new(right)));
4068        }
4069        Ok(left)
4070    }
4071
4072    fn parse_or(&mut self) -> Result<BExpr, String> {
4073        let mut left = self.parse_xor()?;
4074        loop {
4075            self.skip_ws();
4076            if self.try_str("||")
4077                || self.try_str("|")
4078                || self.try_keyword("or")
4079                || self.try_keyword("nor")
4080            {
4081                let right = self.parse_xor()?;
4082                left = BExpr::Or(Box::new(left), Box::new(right));
4083            } else {
4084                break;
4085            }
4086        }
4087        Ok(left)
4088    }
4089
4090    fn parse_xor(&mut self) -> Result<BExpr, String> {
4091        let mut left = self.parse_and()?;
4092        loop {
4093            self.skip_ws();
4094            if self.try_keyword("xor") || self.try_keyword("xnor") || self.try_str("^") {
4095                let right = self.parse_and()?;
4096                left = BExpr::Xor(Box::new(left), Box::new(right));
4097            } else {
4098                break;
4099            }
4100        }
4101        Ok(left)
4102    }
4103
4104    fn parse_and(&mut self) -> Result<BExpr, String> {
4105        let mut left = self.parse_not()?;
4106        loop {
4107            self.skip_ws();
4108            if self.try_str("&&")
4109                || self.try_str("&")
4110                || self.try_keyword("and")
4111                || self.try_keyword("nand")
4112                || self.try_str("*")
4113            {
4114                let right = self.parse_not()?;
4115                left = BExpr::And(Box::new(left), Box::new(right));
4116            } else {
4117                break;
4118            }
4119        }
4120        Ok(left)
4121    }
4122
4123    fn parse_not(&mut self) -> Result<BExpr, String> {
4124        self.skip_ws();
4125        if self.peek() == Some('!') || self.peek() == Some('~') {
4126            self.consume();
4127            let inner = self.parse_not()?;
4128            return Ok(BExpr::Not(Box::new(inner)));
4129        }
4130        if self.try_keyword("not") {
4131            let inner = self.parse_not()?;
4132            return Ok(BExpr::Not(Box::new(inner)));
4133        }
4134        self.parse_atom()
4135    }
4136
4137    fn parse_atom(&mut self) -> Result<BExpr, String> {
4138        self.skip_ws();
4139        if self.peek() == Some('(') {
4140            self.consume();
4141            let inner = self.parse_iff()?;
4142            self.skip_ws();
4143            if self.peek() == Some(')') {
4144                self.consume();
4145            }
4146            return Ok(inner);
4147        }
4148        // Keyword literals
4149        if self.try_keyword("true") || self.try_keyword("1") {
4150            return Ok(BExpr::Const(true));
4151        }
4152        if self.try_keyword("false") || self.try_keyword("0") {
4153            return Ok(BExpr::Const(false));
4154        }
4155        // Variable name
4156        if matches!(self.peek(), Some(c) if c.is_alphabetic() || c == '_') {
4157            let start = self.pos;
4158            while matches!(self.peek(), Some(c) if c.is_alphanumeric() || c == '_') {
4159                self.pos += 1;
4160            }
4161            let name: String = self.chars[start..self.pos].iter().collect();
4162            return Ok(BExpr::Var(name));
4163        }
4164        Err(format!(
4165            "unexpected char '{}'",
4166            self.peek().map(|c| c.to_string()).unwrap_or("EOF".into())
4167        ))
4168    }
4169
4170    fn try_str(&mut self, s: &str) -> bool {
4171        let chars: Vec<char> = s.chars().collect();
4172        let remaining = &self.chars[self.pos..];
4173        if remaining.len() >= chars.len() && remaining[..chars.len()] == chars[..] {
4174            self.pos += chars.len();
4175            return true;
4176        }
4177        false
4178    }
4179
4180    fn try_keyword(&mut self, kw: &str) -> bool {
4181        let saved = self.pos;
4182        self.skip_ws();
4183        let chars: Vec<char> = kw.chars().collect();
4184        let remaining = &self.chars[self.pos..];
4185        if remaining.len() >= chars.len()
4186            && remaining[..chars.len()]
4187                .iter()
4188                .map(|c| c.to_lowercase().next().unwrap())
4189                .collect::<Vec<_>>()
4190                == chars
4191            && !matches!(remaining.get(chars.len()), Some(c) if c.is_alphanumeric() || *c == '_')
4192        {
4193            self.pos += chars.len();
4194            return true;
4195        }
4196        self.pos = saved;
4197        false
4198    }
4199}
4200
4201fn parse_bexpr(s: &str) -> Result<BExpr, String> {
4202    let chars: Vec<char> = s.chars().collect();
4203    let mut p = BParser::new(&chars);
4204    let e = p.parse_iff()?;
4205    p.skip_ws();
4206    if p.pos < p.chars.len() {
4207        let rest: String = p.chars[p.pos..].iter().collect();
4208        if !rest.trim().is_empty() {
4209            return Err(format!("unexpected trailing: '{}'", rest.trim()));
4210        }
4211    }
4212    Ok(e)
4213}
4214
4215// Collect variables in order of first appearance
4216fn collect_vars(e: &BExpr, vars: &mut Vec<String>) {
4217    match e {
4218        BExpr::Var(v) => {
4219            if !vars.contains(v) {
4220                vars.push(v.clone());
4221            }
4222        }
4223        BExpr::Not(a) => collect_vars(a, vars),
4224        BExpr::And(a, b)
4225        | BExpr::Or(a, b)
4226        | BExpr::Xor(a, b)
4227        | BExpr::Implies(a, b)
4228        | BExpr::Iff(a, b)
4229        | BExpr::Nand(a, b)
4230        | BExpr::Nor(a, b)
4231        | BExpr::Xnor(a, b) => {
4232            collect_vars(a, vars);
4233            collect_vars(b, vars);
4234        }
4235        BExpr::Const(_) => {}
4236    }
4237}
4238
4239fn eval_bexpr(e: &BExpr, assignment: &[(&str, bool)]) -> bool {
4240    match e {
4241        BExpr::Const(b) => *b,
4242        BExpr::Var(v) => assignment
4243            .iter()
4244            .find(|(n, _)| n == v)
4245            .map(|(_, b)| *b)
4246            .unwrap_or(false),
4247        BExpr::Not(a) => !eval_bexpr(a, assignment),
4248        BExpr::And(a, b) => eval_bexpr(a, assignment) && eval_bexpr(b, assignment),
4249        BExpr::Or(a, b) => eval_bexpr(a, assignment) || eval_bexpr(b, assignment),
4250        BExpr::Xor(a, b) => eval_bexpr(a, assignment) ^ eval_bexpr(b, assignment),
4251        BExpr::Xnor(a, b) => !(eval_bexpr(a, assignment) ^ eval_bexpr(b, assignment)),
4252        BExpr::Nand(a, b) => !(eval_bexpr(a, assignment) && eval_bexpr(b, assignment)),
4253        BExpr::Nor(a, b) => !(eval_bexpr(a, assignment) || eval_bexpr(b, assignment)),
4254        BExpr::Implies(a, b) => !eval_bexpr(a, assignment) || eval_bexpr(b, assignment),
4255        BExpr::Iff(a, b) => eval_bexpr(a, assignment) == eval_bexpr(b, assignment),
4256    }
4257}
4258
4259fn bexpr_to_str(e: &BExpr) -> String {
4260    match e {
4261        BExpr::Const(true) => "true".into(),
4262        BExpr::Const(false) => "false".into(),
4263        BExpr::Var(v) => v.clone(),
4264        BExpr::Not(a) => format!("¬{}", bexpr_atom_str(a)),
4265        BExpr::And(a, b) => format!("({} ∧ {})", bexpr_to_str(a), bexpr_to_str(b)),
4266        BExpr::Or(a, b) => format!("({} ∨ {})", bexpr_to_str(a), bexpr_to_str(b)),
4267        BExpr::Xor(a, b) => format!("({} ⊕ {})", bexpr_to_str(a), bexpr_to_str(b)),
4268        BExpr::Implies(a, b) => format!("({} → {})", bexpr_to_str(a), bexpr_to_str(b)),
4269        BExpr::Iff(a, b) => format!("({} ↔ {})", bexpr_to_str(a), bexpr_to_str(b)),
4270        BExpr::Nand(a, b) => format!("({}↑{})", bexpr_to_str(a), bexpr_to_str(b)),
4271        BExpr::Nor(a, b) => format!("({}↓{})", bexpr_to_str(a), bexpr_to_str(b)),
4272        BExpr::Xnor(a, b) => format!("({}⊙{})", bexpr_to_str(a), bexpr_to_str(b)),
4273    }
4274}
4275
4276fn bexpr_atom_str(e: &BExpr) -> String {
4277    match e {
4278        BExpr::Var(v) => v.clone(),
4279        BExpr::Const(b) => b.to_string(),
4280        _ => format!("({})", bexpr_to_str(e)),
4281    }
4282}
4283
4284fn simplify_bexpr(e: BExpr) -> BExpr {
4285    match e {
4286        BExpr::Not(a) => {
4287            let a = simplify_bexpr(*a);
4288            match a {
4289                BExpr::Const(b) => BExpr::Const(!b),
4290                BExpr::Not(inner) => *inner,
4291                _ => BExpr::Not(Box::new(a)),
4292            }
4293        }
4294        BExpr::And(a, b) => {
4295            let a = simplify_bexpr(*a);
4296            let b = simplify_bexpr(*b);
4297            match (&a, &b) {
4298                (BExpr::Const(false), _) | (_, BExpr::Const(false)) => BExpr::Const(false),
4299                (BExpr::Const(true), _) => b,
4300                (_, BExpr::Const(true)) => a,
4301                _ if a == b => a,
4302                _ => BExpr::And(Box::new(a), Box::new(b)),
4303            }
4304        }
4305        BExpr::Or(a, b) => {
4306            let a = simplify_bexpr(*a);
4307            let b = simplify_bexpr(*b);
4308            match (&a, &b) {
4309                (BExpr::Const(true), _) | (_, BExpr::Const(true)) => BExpr::Const(true),
4310                (BExpr::Const(false), _) => b,
4311                (_, BExpr::Const(false)) => a,
4312                _ if a == b => a,
4313                _ => BExpr::Or(Box::new(a), Box::new(b)),
4314            }
4315        }
4316        BExpr::Xor(a, b) => {
4317            let a = simplify_bexpr(*a);
4318            let b = simplify_bexpr(*b);
4319            match (&a, &b) {
4320                (BExpr::Const(false), _) => b,
4321                (_, BExpr::Const(false)) => a,
4322                (BExpr::Const(true), _) => BExpr::Not(Box::new(b)),
4323                (_, BExpr::Const(true)) => BExpr::Not(Box::new(a)),
4324                _ if a == b => BExpr::Const(false),
4325                _ => BExpr::Xor(Box::new(a), Box::new(b)),
4326            }
4327        }
4328        BExpr::Implies(a, b) => {
4329            let a = simplify_bexpr(*a);
4330            let b = simplify_bexpr(*b);
4331            match (&a, &b) {
4332                (BExpr::Const(false), _) => BExpr::Const(true),
4333                (BExpr::Const(true), _) => b,
4334                (_, BExpr::Const(true)) => BExpr::Const(true),
4335                _ if a == b => BExpr::Const(true),
4336                _ => BExpr::Implies(Box::new(a), Box::new(b)),
4337            }
4338        }
4339        BExpr::Iff(a, b) => {
4340            let a = simplify_bexpr(*a);
4341            let b = simplify_bexpr(*b);
4342            match (&a, &b) {
4343                _ if a == b => BExpr::Const(true),
4344                _ => BExpr::Iff(Box::new(a), Box::new(b)),
4345            }
4346        }
4347        other => other,
4348    }
4349}
4350
4351pub fn logic_calc(query: &str) -> String {
4352    let q = query.trim();
4353    let q_lower = q.to_lowercase();
4354
4355    // Detect mode
4356    let (mode, expr_str, expr2_str) =
4357        if q_lower.starts_with("table ") || q_lower.starts_with("truth ") {
4358            (
4359                "table",
4360                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4361                "",
4362            )
4363        } else if q_lower.starts_with("sat ") {
4364            (
4365                "sat",
4366                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4367                "",
4368            )
4369        } else if q_lower.starts_with("taut ") {
4370            (
4371                "taut",
4372                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4373                "",
4374            )
4375        } else if q_lower.starts_with("cnf ") {
4376            (
4377                "cnf",
4378                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4379                "",
4380            )
4381        } else if q_lower.starts_with("dnf ") {
4382            (
4383                "dnf",
4384                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4385                "",
4386            )
4387        } else if q_lower.starts_with("simplify ") {
4388            (
4389                "simplify",
4390                q.split_once(' ').map(|x| x.1).unwrap_or("").trim(),
4391                "",
4392            )
4393        } else if q_lower.starts_with("equiv ") {
4394            let rest = q.split_once(' ').map(|x| x.1).unwrap_or("").trim();
4395            if let Some(semi) = rest.find(';') {
4396                ("equiv", rest[..semi].trim(), rest[semi + 1..].trim())
4397            } else {
4398                ("equiv", rest, "")
4399            }
4400        } else {
4401            ("info", q, "")
4402        };
4403
4404    let mut out = String::new();
4405    let w = 64usize;
4406    let _ = writeln!(out, "{}", "=".repeat(w));
4407
4408    let expr = match parse_bexpr(expr_str) {
4409        Ok(e) => e,
4410        Err(e) => {
4411            let _ = writeln!(out, "  Logic — parse error: {}", e);
4412            let _ = writeln!(
4413                out,
4414                "  Input: {}",
4415                expr_str.chars().take(60).collect::<String>()
4416            );
4417            let _ = writeln!(out, "  Usage: hematite --logic 'A and (B or C)'");
4418            let _ = writeln!(out, "{}", "=".repeat(w));
4419            return out;
4420        }
4421    };
4422
4423    let mut vars: Vec<String> = Vec::new();
4424    collect_vars(&expr, &mut vars);
4425
4426    if vars.is_empty() {
4427        let result = eval_bexpr(&expr, &[]);
4428        let _ = writeln!(out, "  Logic  |  Constant expression: {}", result);
4429        let _ = writeln!(out, "{}", "=".repeat(w));
4430        return out;
4431    }
4432
4433    if vars.len() > 20 {
4434        let _ = writeln!(out, "  Logic — too many variables ({}), max 20", vars.len());
4435        let _ = writeln!(out, "{}", "=".repeat(w));
4436        return out;
4437    }
4438
4439    let n = vars.len();
4440    let rows = 1usize << n;
4441
4442    // Evaluate all rows
4443    let results: Vec<bool> = (0..rows)
4444        .map(|mask| {
4445            let assignment: Vec<(&str, bool)> = vars
4446                .iter()
4447                .enumerate()
4448                .map(|(i, v)| (v.as_str(), (mask >> (n - 1 - i)) & 1 == 1))
4449                .collect();
4450            eval_bexpr(&expr, &assignment)
4451        })
4452        .collect();
4453
4454    let sat_count = results.iter().filter(|&&b| b).count();
4455    let is_taut = sat_count == rows;
4456    let is_sat = sat_count > 0;
4457    let is_contra = sat_count == 0;
4458
4459    let _ = writeln!(out, "  Boolean Logic Analysis");
4460    let _ = writeln!(out, "  Expression: {}", bexpr_to_str(&expr));
4461    let _ = writeln!(out, "  Variables : {}", vars.join(", "));
4462    let _ = writeln!(
4463        out,
4464        "  {} satisfying assignments of {} ({}%)",
4465        sat_count,
4466        rows,
4467        sat_count * 100 / rows
4468    );
4469    let _ = writeln!(
4470        out,
4471        "  Status: {}",
4472        if is_taut {
4473            "TAUTOLOGY (always true)"
4474        } else if is_contra {
4475            "CONTRADICTION (always false)"
4476        } else {
4477            "CONTINGENT (sometimes true)"
4478        }
4479    );
4480
4481    match mode {
4482        "sat" => {
4483            if is_sat {
4484                let first_sat = (0..rows).find(|&mask| results[mask]).unwrap();
4485                let assignment: Vec<String> = vars
4486                    .iter()
4487                    .enumerate()
4488                    .map(|(i, v)| format!("{}={}", v, (first_sat >> (n - 1 - i)) & 1 == 1))
4489                    .collect();
4490                let _ = writeln!(
4491                    out,
4492                    "  SAT: YES — satisfying assignment: {}",
4493                    assignment.join(", ")
4494                );
4495            } else {
4496                let _ = writeln!(out, "  SAT: NO — contradiction");
4497            }
4498        }
4499        "taut" => {
4500            let _ = writeln!(out, "  TAUTOLOGY: {}", if is_taut { "YES" } else { "NO" });
4501            if !is_taut {
4502                let first_false = (0..rows).find(|&mask| !results[mask]).unwrap();
4503                let assignment: Vec<String> = vars
4504                    .iter()
4505                    .enumerate()
4506                    .map(|(i, v)| format!("{}={}", v, (first_false >> (n - 1 - i)) & 1 == 1))
4507                    .collect();
4508                let _ = writeln!(out, "  Counterexample: {}", assignment.join(", "));
4509            }
4510        }
4511        "cnf" => {
4512            // Build CNF from false rows
4513            let false_rows: Vec<usize> = (0..rows).filter(|&m| !results[m]).collect();
4514            if false_rows.is_empty() {
4515                let _ = writeln!(out, "  CNF: true (tautology)");
4516            } else {
4517                let _ = writeln!(out, "  CNF (maxterms):");
4518                for mask in &false_rows[..false_rows.len().min(8)] {
4519                    let clause: Vec<String> = vars
4520                        .iter()
4521                        .enumerate()
4522                        .map(|(i, v)| {
4523                            if (mask >> (n - 1 - i)) & 1 == 0 {
4524                                v.clone()
4525                            } else {
4526                                format!("¬{}", v)
4527                            }
4528                        })
4529                        .collect();
4530                    let _ = writeln!(out, "  ({})", clause.join(" ∨ "));
4531                }
4532                if false_rows.len() > 8 {
4533                    let _ = writeln!(out, "  ... ({} more clauses)", false_rows.len() - 8);
4534                }
4535            }
4536        }
4537        "dnf" => {
4538            // Build DNF from true rows
4539            let true_rows: Vec<usize> = (0..rows).filter(|&m| results[m]).collect();
4540            if true_rows.is_empty() {
4541                let _ = writeln!(out, "  DNF: false (contradiction)");
4542            } else {
4543                let _ = writeln!(out, "  DNF (minterms):");
4544                for mask in &true_rows[..true_rows.len().min(8)] {
4545                    let term: Vec<String> = vars
4546                        .iter()
4547                        .enumerate()
4548                        .map(|(i, v)| {
4549                            if (mask >> (n - 1 - i)) & 1 == 1 {
4550                                v.clone()
4551                            } else {
4552                                format!("¬{}", v)
4553                            }
4554                        })
4555                        .collect();
4556                    let _ = writeln!(out, "  ({})", term.join(" ∧ "));
4557                }
4558                if true_rows.len() > 8 {
4559                    let _ = writeln!(out, "  ... ({} more terms)", true_rows.len() - 8);
4560                }
4561            }
4562        }
4563        "simplify" => {
4564            let simp = simplify_bexpr(expr.clone());
4565            let _ = writeln!(out, "  Simplified: {}", bexpr_to_str(&simp));
4566        }
4567        "equiv" => {
4568            let expr2 = match parse_bexpr(expr2_str) {
4569                Ok(e) => e,
4570                Err(e) => {
4571                    let _ = writeln!(out, "  Parse error (expr2): {}", e);
4572                    let _ = writeln!(out, "{}", "=".repeat(w));
4573                    return out;
4574                }
4575            };
4576            let mut vars2 = vars.clone();
4577            collect_vars(&expr2, &mut vars2);
4578            vars2.sort();
4579            vars2.dedup();
4580            let n2 = vars2.len();
4581            let rows2 = 1usize << n2;
4582            let equiv = (0..rows2).all(|mask| {
4583                let assignment: Vec<(&str, bool)> = vars2
4584                    .iter()
4585                    .enumerate()
4586                    .map(|(i, v)| (v.as_str(), (mask >> (n2 - 1 - i)) & 1 == 1))
4587                    .collect();
4588                eval_bexpr(&expr, &assignment) == eval_bexpr(&expr2, &assignment)
4589            });
4590            let _ = writeln!(out, "  Expr1: {}", bexpr_to_str(&expr));
4591            let _ = writeln!(out, "  Expr2: {}", bexpr_to_str(&expr2));
4592            let _ = writeln!(
4593                out,
4594                "  Logically equivalent: {}",
4595                if equiv { "YES" } else { "NO" }
4596            );
4597        }
4598        _ => {
4599            // "info" or "table" — show full truth table
4600            let max_table_rows = if n <= 4 { rows } else { rows.min(32) };
4601            let _ = writeln!(out, "\n  Truth Table:");
4602            // Header
4603            let var_header: String = vars
4604                .iter()
4605                .map(|v| format!("  {:>3}", v))
4606                .collect::<Vec<_>>()
4607                .join("");
4608            let _ = writeln!(out, "{}  │  Result", var_header);
4609            let _ = writeln!(out, "  {}", "-".repeat(vars.len() * 5 + 10));
4610            for mask in 0..max_table_rows {
4611                let row_vals: String = (0..n)
4612                    .map(|i| {
4613                        format!(
4614                            "  {:>3}",
4615                            if (mask >> (n - 1 - i)) & 1 == 1 {
4616                                "T"
4617                            } else {
4618                                "F"
4619                            }
4620                        )
4621                    })
4622                    .collect::<Vec<_>>()
4623                    .join("");
4624                let _ = writeln!(
4625                    out,
4626                    "{}  │  {}",
4627                    row_vals,
4628                    if results[mask] { "T" } else { "F" }
4629                );
4630            }
4631            if max_table_rows < rows {
4632                let _ = writeln!(out, "  ... ({} rows omitted — use --logic 'table EXPR' for full table with ≤4 vars)", rows - max_table_rows);
4633            }
4634
4635            // SAT summary
4636            if is_sat {
4637                let first_sat = (0..rows).find(|&m| results[m]).unwrap();
4638                let sat_ex: Vec<String> = vars
4639                    .iter()
4640                    .enumerate()
4641                    .map(|(i, v)| {
4642                        format!(
4643                            "{}={}",
4644                            v,
4645                            if (first_sat >> (n - 1 - i)) & 1 == 1 {
4646                                "T"
4647                            } else {
4648                                "F"
4649                            }
4650                        )
4651                    })
4652                    .collect();
4653                let _ = writeln!(out, "\n  SAT witness: {}", sat_ex.join(", "));
4654            }
4655        }
4656    }
4657
4658    let _ = writeln!(out, "{}", "=".repeat(w));
4659    out
4660}
4661
4662// ── Linear algebra / matrix operations ───────────────────────────────────────
4663// det / inv / solve / mul / transpose / eigenvalues / rank — pure-Rust
4664//
4665// Matrix input formats (any mix):
4666//   [[1,2,3],[4,5,6],[7,8,9]]   JSON-style
4667//   1 2 3; 4 5 6; 7 8 9         semicolon rows
4668//   1 2 3\n4 5 6\n7 8 9         newline rows
4669//
4670// Modes (first token of query):
4671//   det A         determinant
4672//   inv A         inverse
4673//   solve A b     Ax = b  (b is an extra row/column vector)
4674//   mul A B       A × B
4675//   transpose A   transpose
4676//   eigen A       eigenvalues & eigenvectors (up to 8×8)
4677//   rank A        matrix rank
4678//   lu A          LU decomposition
4679//   info A        all basic info (default)
4680
4681type Matrix = Vec<Vec<f64>>;
4682
4683fn mat_rows(m: &Matrix) -> usize {
4684    m.len()
4685}
4686fn mat_cols(m: &Matrix) -> usize {
4687    m.first().map(|r| r.len()).unwrap_or(0)
4688}
4689
4690fn parse_matrix(s: &str) -> Result<Matrix, String> {
4691    let s = s.trim();
4692    // Try [[...],[...]] JSON-like
4693    if s.starts_with('[') {
4694        return parse_matrix_json(s);
4695    }
4696    // Semicolon or newline rows
4697    let row_strs: Vec<&str> = s
4698        .split([';', '\n'])
4699        .map(str::trim)
4700        .filter(|r| !r.is_empty())
4701        .collect();
4702    if row_strs.is_empty() {
4703        return Err("empty matrix".into());
4704    }
4705    let mut mat: Matrix = Vec::new();
4706    for row_str in &row_strs {
4707        let row: Vec<f64> = row_str
4708            .split([',', ' ', '\t'])
4709            .map(str::trim)
4710            .filter(|s| !s.is_empty())
4711            .map(|tok| {
4712                tok.parse::<f64>()
4713                    .map_err(|_| format!("bad number: {}", tok))
4714            })
4715            .collect::<Result<Vec<_>, _>>()?;
4716        mat.push(row);
4717    }
4718    let ncols = mat[0].len();
4719    for (i, row) in mat.iter().enumerate() {
4720        if row.len() != ncols {
4721            return Err(format!(
4722                "row {} has {} columns, expected {}",
4723                i,
4724                row.len(),
4725                ncols
4726            ));
4727        }
4728    }
4729    Ok(mat)
4730}
4731
4732fn parse_matrix_json(s: &str) -> Result<Matrix, String> {
4733    // Simple recursive bracket parser — no serde needed
4734    let chars: Vec<char> = s.chars().collect();
4735    let mut pos = 0;
4736    fn skip(chars: &[char], pos: &mut usize) {
4737        while *pos < chars.len() && chars[*pos].is_whitespace() {
4738            *pos += 1;
4739        }
4740    }
4741    fn parse_num(chars: &[char], pos: &mut usize) -> Result<f64, String> {
4742        skip(chars, pos);
4743        let start = *pos;
4744        while *pos < chars.len()
4745            && (chars[*pos].is_ascii_digit() || matches!(chars[*pos], '.' | '-' | '+' | 'e' | 'E'))
4746        {
4747            *pos += 1;
4748        }
4749        let s: String = chars[start..*pos].iter().collect();
4750        s.trim()
4751            .parse::<f64>()
4752            .map_err(|_| format!("bad number: '{}'", s))
4753    }
4754    fn parse_row(chars: &[char], pos: &mut usize) -> Result<Vec<f64>, String> {
4755        skip(chars, pos);
4756        if chars.get(*pos) != Some(&'[') {
4757            return Err("expected '[' for row".into());
4758        }
4759        *pos += 1;
4760        let mut row = Vec::new();
4761        loop {
4762            skip(chars, pos);
4763            if chars.get(*pos) == Some(&']') {
4764                *pos += 1;
4765                break;
4766            }
4767            if !row.is_empty() {
4768                if chars.get(*pos) == Some(&',') {
4769                    *pos += 1;
4770                } else {
4771                    return Err("expected ','".into());
4772                }
4773            }
4774            row.push(parse_num(chars, pos)?);
4775        }
4776        Ok(row)
4777    }
4778    skip(&chars, &mut pos);
4779    if chars.get(pos) != Some(&'[') {
4780        return Err("expected outer '['".into());
4781    }
4782    pos += 1;
4783    let mut mat: Matrix = Vec::new();
4784    loop {
4785        skip(&chars, &mut pos);
4786        if chars.get(pos) == Some(&']') {
4787            break;
4788        }
4789        if !mat.is_empty() {
4790            if chars.get(pos) == Some(&',') {
4791                pos += 1;
4792            } else {
4793                return Err("expected ','".into());
4794            }
4795        }
4796        skip(&chars, &mut pos);
4797        // Check if this is a number (1-D vector case) or another row
4798        if chars.get(pos) == Some(&'[') {
4799            mat.push(parse_row(&chars, &mut pos)?);
4800        } else {
4801            // flat 1-D vector — wrap as single row
4802            let n = parse_num(&chars, &mut pos)?;
4803            mat.push(vec![n]);
4804        }
4805    }
4806    if mat.is_empty() {
4807        return Err("empty matrix".into());
4808    }
4809    let ncols = mat[0].len();
4810    for (i, row) in mat.iter().enumerate() {
4811        if row.len() != ncols {
4812            return Err(format!(
4813                "row {} has {} cols, expected {}",
4814                i,
4815                row.len(),
4816                ncols
4817            ));
4818        }
4819    }
4820    Ok(mat)
4821}
4822
4823fn mat_clone(m: &Matrix) -> Matrix {
4824    m.clone()
4825}
4826
4827fn mat_identity(n: usize) -> Matrix {
4828    (0..n)
4829        .map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
4830        .collect()
4831}
4832
4833fn mat_fmt(m: &Matrix) -> String {
4834    let rows = mat_rows(m);
4835    let cols = mat_cols(m);
4836    let cells: Vec<String> = m
4837        .iter()
4838        .flat_map(|row| {
4839            row.iter().map(|v| {
4840                if v.abs() < 1e-12 {
4841                    "0".to_string()
4842                } else if v.fract() == 0.0 && v.abs() < 1e9 {
4843                    format!("{}", *v as i64)
4844                } else {
4845                    format!("{:.6}", v)
4846                        .trim_end_matches('0')
4847                        .trim_end_matches('.')
4848                        .to_string()
4849                }
4850            })
4851        })
4852        .collect();
4853    // Align columns
4854    let mut col_widths: Vec<usize> = vec![0; cols];
4855    for r in 0..rows {
4856        for c in 0..cols {
4857            col_widths[c] = col_widths[c].max(cells[r * cols + c].len());
4858        }
4859    }
4860    let mut out = String::new();
4861    for r in 0..rows {
4862        out.push_str("  [ ");
4863        for c in 0..cols {
4864            let s = &cells[r * cols + c];
4865            out.push_str(&format!("{:>w$}", s, w = col_widths[c]));
4866            if c < cols - 1 {
4867                out.push_str("  ");
4868            }
4869        }
4870        out.push_str(" ]\n");
4871    }
4872    out
4873}
4874
4875// Returns (L, U, P, sign) where P*A = L*U and sign is the permutation sign
4876fn lu_decompose(a: &Matrix) -> Result<(Matrix, Matrix, Vec<usize>, i32), String> {
4877    let n = mat_rows(a);
4878    if mat_cols(a) != n {
4879        return Err("LU requires square matrix".into());
4880    }
4881    let mut u = mat_clone(a);
4882    let mut l = mat_identity(n);
4883    let mut perm: Vec<usize> = (0..n).collect();
4884    let mut sign = 1i32;
4885
4886    for col in 0..n {
4887        // Partial pivoting
4888        let mut max_row = col;
4889        let mut max_val = u[col][col].abs();
4890        for row in (col + 1)..n {
4891            if u[row][col].abs() > max_val {
4892                max_val = u[row][col].abs();
4893                max_row = row;
4894            }
4895        }
4896        if max_val < 1e-14 {
4897            return Err("matrix is singular (or near-singular)".into());
4898        }
4899        if max_row != col {
4900            u.swap(col, max_row);
4901            perm.swap(col, max_row);
4902            sign = -sign;
4903            // Also swap l columns already filled
4904            for j in 0..col {
4905                let tmp = l[col][j];
4906                l[col][j] = l[max_row][j];
4907                l[max_row][j] = tmp;
4908            }
4909        }
4910        for row in (col + 1)..n {
4911            let factor = u[row][col] / u[col][col];
4912            l[row][col] = factor;
4913            for k in col..n {
4914                u[row][k] -= factor * u[col][k];
4915            }
4916        }
4917    }
4918    Ok((l, u, perm, sign))
4919}
4920
4921fn mat_det(a: &Matrix) -> Result<f64, String> {
4922    match lu_decompose(a) {
4923        Ok((_, u, _, sign)) => {
4924            let d: f64 = (0..mat_rows(a)).map(|i| u[i][i]).product();
4925            Ok(d * sign as f64)
4926        }
4927        Err(_) => Ok(0.0), // singular
4928    }
4929}
4930
4931fn mat_solve_lu(l: &Matrix, u: &Matrix, perm: &[usize], b: &[f64]) -> Vec<f64> {
4932    let n = l.len();
4933    // Apply permutation to b
4934    let pb: Vec<f64> = (0..n).map(|i| b[perm[i]]).collect();
4935    // Forward substitution Ly = Pb
4936    let mut y = vec![0.0f64; n];
4937    for i in 0..n {
4938        y[i] = pb[i] - (0..i).map(|j| l[i][j] * y[j]).sum::<f64>();
4939    }
4940    // Back substitution Ux = y
4941    let mut x = vec![0.0f64; n];
4942    for i in (0..n).rev() {
4943        x[i] = (y[i] - (i + 1..n).map(|j| u[i][j] * x[j]).sum::<f64>()) / u[i][i];
4944    }
4945    x
4946}
4947
4948fn mat_inv(a: &Matrix) -> Result<Matrix, String> {
4949    let n = mat_rows(a);
4950    let (l, u, perm, _) = lu_decompose(a)?;
4951    let mut inv = mat_identity(n);
4952    for col in 0..n {
4953        let b: Vec<f64> = (0..n).map(|i| if i == col { 1.0 } else { 0.0 }).collect();
4954        let x = mat_solve_lu(&l, &u, &perm, &b);
4955        for row in 0..n {
4956            inv[row][col] = x[row];
4957        }
4958    }
4959    Ok(inv)
4960}
4961
4962fn mat_mul(a: &Matrix, b: &Matrix) -> Result<Matrix, String> {
4963    let (ar, ac) = (mat_rows(a), mat_cols(a));
4964    let (br, bc) = (mat_rows(b), mat_cols(b));
4965    if ac != br {
4966        return Err(format!(
4967            "incompatible dimensions {}×{} × {}×{}",
4968            ar, ac, br, bc
4969        ));
4970    }
4971    let mut c = vec![vec![0.0f64; bc]; ar];
4972    for i in 0..ar {
4973        for j in 0..bc {
4974            for k in 0..ac {
4975                c[i][j] += a[i][k] * b[k][j];
4976            }
4977        }
4978    }
4979    Ok(c)
4980}
4981
4982fn mat_transpose(a: &Matrix) -> Matrix {
4983    let (r, c) = (mat_rows(a), mat_cols(a));
4984    (0..c).map(|j| (0..r).map(|i| a[i][j]).collect()).collect()
4985}
4986
4987fn mat_rank(a: &Matrix) -> usize {
4988    let mut m = mat_clone(a);
4989    let rows = mat_rows(&m);
4990    let cols = mat_cols(&m);
4991    let mut rank = 0usize;
4992    let mut row_cursor = 0usize;
4993    for col in 0..cols {
4994        let pivot = (row_cursor..rows).find(|&r| m[r][col].abs() > 1e-10);
4995        if let Some(pr) = pivot {
4996            m.swap(row_cursor, pr);
4997            let pivot_val = m[row_cursor][col];
4998            for j in col..cols {
4999                m[row_cursor][j] /= pivot_val;
5000            }
5001            for r in 0..rows {
5002                if r != row_cursor && m[r][col].abs() > 1e-10 {
5003                    let factor = m[r][col];
5004                    for j in col..cols {
5005                        m[r][j] -= factor * m[row_cursor][j];
5006                    }
5007                }
5008            }
5009            rank += 1;
5010            row_cursor += 1;
5011        }
5012    }
5013    rank
5014}
5015
5016// Power iteration eigenvalue (largest eigenvalue only)
5017fn mat_eigen_power(a: &Matrix, max_iter: usize) -> Option<(f64, Vec<f64>)> {
5018    let n = mat_rows(a);
5019    if n == 0 {
5020        return None;
5021    }
5022    let mut v: Vec<f64> = (0..n).map(|i| if i == 0 { 1.0 } else { 0.1 }).collect();
5023    let mut lam = 0.0f64;
5024    for _ in 0..max_iter {
5025        // Av
5026        let av: Vec<f64> = (0..n)
5027            .map(|i| (0..n).map(|j| a[i][j] * v[j]).sum::<f64>())
5028            .collect();
5029        let norm = av.iter().map(|x| x * x).sum::<f64>().sqrt();
5030        if norm < 1e-14 {
5031            break;
5032        }
5033        lam = av.iter().zip(&v).map(|(a, b)| a * b).sum::<f64>();
5034        v = av.iter().map(|x| x / norm).collect();
5035    }
5036    Some((lam, v))
5037}
5038
5039// QR decomposition via modified Gram-Schmidt
5040fn mat_qr(a: &Matrix) -> (Matrix, Matrix) {
5041    let m = mat_rows(a);
5042    let n = mat_cols(a);
5043    let cols_a: Vec<Vec<f64>> = (0..n).map(|j| (0..m).map(|i| a[i][j]).collect()).collect();
5044    let mut q_cols: Vec<Vec<f64>> = Vec::new();
5045    let mut r: Matrix = vec![vec![0.0; n]; n.min(m)];
5046    for j in 0..n {
5047        let mut v: Vec<f64> = cols_a[j].clone();
5048        for (k, qk) in q_cols.iter().enumerate() {
5049            let proj: f64 = v.iter().zip(qk).map(|(a, b)| a * b).sum();
5050            r[k][j] = proj;
5051            for i in 0..m {
5052                v[i] -= proj * qk[i];
5053            }
5054        }
5055        let norm = v.iter().map(|x| x * x).sum::<f64>().sqrt();
5056        if norm > 1e-12 {
5057            let qj: Vec<f64> = v.iter().map(|x| x / norm).collect();
5058            r[q_cols.len()][j] = norm;
5059            q_cols.push(qj);
5060        }
5061    }
5062    let q: Matrix = (0..m)
5063        .map(|i| {
5064            q_cols
5065                .iter()
5066                .map(|col| *col.get(i).unwrap_or(&0.0))
5067                .collect()
5068        })
5069        .collect();
5070    (q, r)
5071}
5072
5073// Singular values via eigenvalues of A^T A (Jacobi-style for small matrices)
5074fn mat_svd_values(a: &Matrix) -> (Vec<f64>, Matrix) {
5075    let n = mat_cols(a);
5076    // Build A^T A
5077    let at = mat_transpose(a);
5078    let ata = mat_mul(&at, a).unwrap_or_else(|_| vec![vec![0.0; n]; n]);
5079    // Power iteration for eigenvalues/vectors of A^T A
5080    let mut a_copy = ata.clone();
5081    let mut sigma_vals: Vec<f64> = Vec::new();
5082    let mut v_vecs: Vec<Vec<f64>> = Vec::new();
5083    for _ in 0..n {
5084        match mat_eigen_power(&a_copy, 1000) {
5085            Some((lam, v)) => {
5086                let sv = lam.abs().sqrt();
5087                sigma_vals.push(sv);
5088                v_vecs.push(v.clone());
5089                // Deflate A^T A
5090                for i in 0..n {
5091                    for j in 0..n {
5092                        a_copy[i][j] -= lam * v[i] * v[j];
5093                    }
5094                }
5095            }
5096            None => break,
5097        }
5098    }
5099    // Sort descending
5100    let mut pairs: Vec<(f64, Vec<f64>)> = sigma_vals.into_iter().zip(v_vecs).collect();
5101    pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
5102    let svs: Vec<f64> = pairs.iter().map(|p| p.0).collect();
5103    let v_mat: Matrix = (0..n)
5104        .map(|i| pairs.iter().map(|p| *p.1.get(i).unwrap_or(&0.0)).collect())
5105        .collect();
5106    (svs, v_mat)
5107}
5108
5109// Cholesky decomposition (lower-triangular L s.t. A = L Lᵀ)
5110fn mat_cholesky(a: &Matrix) -> Result<Matrix, String> {
5111    let n = mat_rows(a);
5112    let mut l: Matrix = vec![vec![0.0; n]; n];
5113    for i in 0..n {
5114        for j in 0..=i {
5115            let sum: f64 = (0..j).map(|k| l[i][k] * l[j][k]).sum();
5116            if i == j {
5117                let d = a[i][i] - sum;
5118                if d < -1e-10 {
5119                    return Err(format!("not positive definite (d[{}] = {:.4e})", i, d));
5120                }
5121                l[i][j] = d.max(0.0).sqrt();
5122            } else {
5123                if l[j][j].abs() < 1e-14 {
5124                    return Err("zero pivot".into());
5125                }
5126                l[i][j] = (a[i][j] - sum) / l[j][j];
5127            }
5128        }
5129    }
5130    Ok(l)
5131}
5132
5133// Moore-Penrose pseudoinverse via normal equations (A⁺ = (AᵀA)⁻¹Aᵀ for full column rank)
5134fn mat_pinv(a: &Matrix) -> Result<Matrix, String> {
5135    let at = mat_transpose(a);
5136    let ata = mat_mul(&at, a)?;
5137    let ata_inv = mat_inv(&ata)?;
5138    mat_mul(&ata_inv, &at)
5139}
5140
5141pub fn matrix_calc(query: &str) -> String {
5142    let q = query.trim();
5143
5144    // Detect mode
5145    let (mode, rest) = {
5146        let words: Vec<&str> = q.splitn(2, char::is_whitespace).collect();
5147        let m = words[0].to_lowercase();
5148        let rest = words.get(1).copied().unwrap_or("").trim();
5149        match m.as_str() {
5150            "det" | "determinant" => ("det", rest.to_string()),
5151            "inv" | "inverse" => ("inv", rest.to_string()),
5152            "solve" => ("solve", rest.to_string()),
5153            "mul" | "multiply" => ("mul", rest.to_string()),
5154            "transpose" | "trans" => ("transpose", rest.to_string()),
5155            "eigen" | "eigenvalues" | "eig" => ("eigen", rest.to_string()),
5156            "rank" => ("rank", rest.to_string()),
5157            "lu" => ("lu", rest.to_string()),
5158            "qr" => ("qr", rest.to_string()),
5159            "svd" => ("svd", rest.to_string()),
5160            "chol" | "cholesky" => ("chol", rest.to_string()),
5161            "pinv" | "pseudoinverse" | "pseudo" => ("pinv", rest.to_string()),
5162            _ => ("info", q.to_string()),
5163        }
5164    };
5165
5166    let mut out = String::new();
5167    let w = 64usize;
5168    let _ = writeln!(out, "{}", "=".repeat(w));
5169
5170    // For "solve" we need two matrices: A and b
5171    if mode == "solve" {
5172        // rest should be "A_matrix b_vector" — split on " / " or last matrix
5173        // Try to split at ']  [' or ']; [' or just split the two bracket groups
5174        let parts = split_two_matrices(&rest);
5175        if parts.len() < 2 {
5176            let _ = writeln!(out, "  Matrix — Solve Ax = b");
5177            let _ = writeln!(out, "  Error: provide matrix A and vector b, e.g.:");
5178            let _ = writeln!(out, "  --matrix 'solve [[1,2],[3,4]] [[5],[6]]'");
5179            let _ = writeln!(out, "{}", "=".repeat(w));
5180            return out;
5181        }
5182        let a_str = &parts[0];
5183        let b_str = &parts[1];
5184        let a = match parse_matrix(a_str) {
5185            Ok(m) => m,
5186            Err(e) => {
5187                let _ = writeln!(out, "  Parse error (A): {}", e);
5188                let _ = writeln!(out, "{}", "=".repeat(w));
5189                return out;
5190            }
5191        };
5192        let b_mat = match parse_matrix(b_str) {
5193            Ok(m) => m,
5194            Err(e) => {
5195                let _ = writeln!(out, "  Parse error (b): {}", e);
5196                let _ = writeln!(out, "{}", "=".repeat(w));
5197                return out;
5198            }
5199        };
5200        let n = mat_rows(&a);
5201        let b_vec: Vec<f64> = if mat_cols(&b_mat) == 1 {
5202            b_mat.iter().map(|r| r[0]).collect()
5203        } else if mat_rows(&b_mat) == 1 {
5204            b_mat[0].clone()
5205        } else {
5206            let _ = writeln!(out, "  Error: b must be a column or row vector");
5207            let _ = writeln!(out, "{}", "=".repeat(w));
5208            return out;
5209        };
5210        if b_vec.len() != n {
5211            let _ = writeln!(
5212                out,
5213                "  Error: A is {}×{} but b has {} elements",
5214                n,
5215                mat_cols(&a),
5216                b_vec.len()
5217            );
5218            let _ = writeln!(out, "{}", "=".repeat(w));
5219            return out;
5220        }
5221        let _ = writeln!(out, "  Matrix — Solve Ax = b");
5222        let _ = writeln!(out, "  A ({}×{}):", n, mat_cols(&a));
5223        out.push_str(&mat_fmt(&a));
5224        let _ = writeln!(out, "  b:");
5225        let b_col: Matrix = b_vec.iter().map(|&v| vec![v]).collect();
5226        out.push_str(&mat_fmt(&b_col));
5227        match lu_decompose(&a) {
5228            Ok((l, u, perm, _)) => {
5229                let x = mat_solve_lu(&l, &u, &perm, &b_vec);
5230                let _ = writeln!(out, "  Solution x:");
5231                for (i, &xi) in x.iter().enumerate() {
5232                    let _ = writeln!(out, "    x[{}] = {:.8}", i, xi);
5233                }
5234                // Verify: Ax - b residual
5235                let residual: f64 = (0..n)
5236                    .map(|i| {
5237                        let ax_i: f64 = (0..n).map(|j| a[i][j] * x[j]).sum();
5238                        (ax_i - b_vec[i]).powi(2)
5239                    })
5240                    .sum::<f64>()
5241                    .sqrt();
5242                let _ = writeln!(out, "  Residual |Ax - b| = {:.2e}", residual);
5243            }
5244            Err(e) => {
5245                let _ = writeln!(out, "  Error: {}", e);
5246            }
5247        }
5248        let _ = writeln!(out, "{}", "=".repeat(w));
5249        return out;
5250    }
5251
5252    // For mul we need two matrices
5253    if mode == "mul" {
5254        let parts = split_two_matrices(&rest);
5255        if parts.len() < 2 {
5256            let _ = writeln!(out, "  Matrix multiply: provide two matrices, e.g.:");
5257            let _ = writeln!(out, "  --matrix 'mul [[1,2],[3,4]] [[5,6],[7,8]]'");
5258            let _ = writeln!(out, "{}", "=".repeat(w));
5259            return out;
5260        }
5261        let a = match parse_matrix(&parts[0]) {
5262            Ok(m) => m,
5263            Err(e) => {
5264                let _ = writeln!(out, "  Parse error (A): {}", e);
5265                let _ = writeln!(out, "{}", "=".repeat(w));
5266                return out;
5267            }
5268        };
5269        let b = match parse_matrix(&parts[1]) {
5270            Ok(m) => m,
5271            Err(e) => {
5272                let _ = writeln!(out, "  Parse error (B): {}", e);
5273                let _ = writeln!(out, "{}", "=".repeat(w));
5274                return out;
5275            }
5276        };
5277        let _ = writeln!(out, "  Matrix Multiply  A × B");
5278        let _ = writeln!(out, "  A ({}×{}):", mat_rows(&a), mat_cols(&a));
5279        out.push_str(&mat_fmt(&a));
5280        let _ = writeln!(out, "  B ({}×{}):", mat_rows(&b), mat_cols(&b));
5281        out.push_str(&mat_fmt(&b));
5282        match mat_mul(&a, &b) {
5283            Ok(c) => {
5284                let _ = writeln!(out, "  A × B ({}×{}):", mat_rows(&c), mat_cols(&c));
5285                out.push_str(&mat_fmt(&c));
5286            }
5287            Err(e) => {
5288                let _ = writeln!(out, "  Error: {}", e);
5289            }
5290        }
5291        let _ = writeln!(out, "{}", "=".repeat(w));
5292        return out;
5293    }
5294
5295    // Single-matrix operations
5296    let mat_str = &rest;
5297    let a = match parse_matrix(mat_str) {
5298        Ok(m) => m,
5299        Err(e) => {
5300            let _ = writeln!(out, "  Parse error: {}", e);
5301            let _ = writeln!(
5302                out,
5303                "  Input: {}",
5304                mat_str.chars().take(80).collect::<String>()
5305            );
5306            let _ = writeln!(out, "  Formats: [[1,2],[3,4]]  or  1 2; 3 4");
5307            let _ = writeln!(out, "{}", "=".repeat(w));
5308            return out;
5309        }
5310    };
5311
5312    let (rows, cols) = (mat_rows(&a), mat_cols(&a));
5313    let _ = writeln!(out, "  Matrix Operations  ({}×{})", rows, cols);
5314    out.push_str(&mat_fmt(&a));
5315
5316    match mode {
5317        "det" => {
5318            if rows != cols {
5319                let _ = writeln!(out, "  Error: det requires square matrix");
5320            } else {
5321                match mat_det(&a) {
5322                    Ok(d) => {
5323                        let _ = writeln!(out, "  det(A) = {:.8}", d);
5324                    }
5325                    Err(e) => {
5326                        let _ = writeln!(out, "  Error: {}", e);
5327                    }
5328                }
5329            }
5330        }
5331        "inv" => {
5332            if rows != cols {
5333                let _ = writeln!(out, "  Error: inv requires square matrix");
5334            } else {
5335                match mat_inv(&a) {
5336                    Ok(inv) => {
5337                        let _ = writeln!(out, "  A⁻¹:");
5338                        out.push_str(&mat_fmt(&inv));
5339                    }
5340                    Err(e) => {
5341                        let _ = writeln!(out, "  Error: {}", e);
5342                    }
5343                }
5344            }
5345        }
5346        "transpose" => {
5347            let t = mat_transpose(&a);
5348            let _ = writeln!(out, "  Aᵀ ({}×{}):", mat_cols(&a), rows);
5349            out.push_str(&mat_fmt(&t));
5350        }
5351        "rank" => {
5352            let r = mat_rank(&a);
5353            let _ = writeln!(out, "  rank(A) = {}", r);
5354            if rows == cols {
5355                let _ = writeln!(
5356                    out,
5357                    "  {} ({}×{} square, rank {})",
5358                    if r == rows {
5359                        "Full rank"
5360                    } else {
5361                        "Rank-deficient"
5362                    },
5363                    rows,
5364                    cols,
5365                    r
5366                );
5367            }
5368        }
5369        "lu" => {
5370            if rows != cols {
5371                let _ = writeln!(out, "  Error: LU requires square matrix");
5372            } else {
5373                match lu_decompose(&a) {
5374                    Ok((l, u, perm, _)) => {
5375                        let _ = writeln!(out, "  L (lower triangular):");
5376                        out.push_str(&mat_fmt(&l));
5377                        let _ = writeln!(out, "  U (upper triangular):");
5378                        out.push_str(&mat_fmt(&u));
5379                        let perm_str = perm
5380                            .iter()
5381                            .map(|&p| p.to_string())
5382                            .collect::<Vec<_>>()
5383                            .join(", ");
5384                        let _ = writeln!(out, "  Pivot permutation: [{}]", perm_str);
5385                    }
5386                    Err(e) => {
5387                        let _ = writeln!(out, "  Error: {}", e);
5388                    }
5389                }
5390            }
5391        }
5392        "eigen" => {
5393            if rows != cols {
5394                let _ = writeln!(out, "  Error: eigenvalues require square matrix");
5395            } else if rows > 8 {
5396                let _ = writeln!(
5397                    out,
5398                    "  Error: power iteration limited to 8×8 (matrix is {}×{})",
5399                    rows, cols
5400                );
5401            } else {
5402                let _ = writeln!(out, "  Eigenvalues (power iteration + deflation):");
5403                let mut a_copy = mat_clone(&a);
5404                for k in 0..rows {
5405                    match mat_eigen_power(&a_copy, 500) {
5406                        Some((lam, v)) => {
5407                            let v_str = v
5408                                .iter()
5409                                .map(|x| format!("{:.4}", x))
5410                                .collect::<Vec<_>>()
5411                                .join(", ");
5412                            let _ = writeln!(
5413                                out,
5414                                "  λ{} = {:.6}  eigenvector ≈ [{}]",
5415                                k + 1,
5416                                lam,
5417                                v_str
5418                            );
5419                            // Deflate
5420                            for i in 0..rows {
5421                                for j in 0..rows {
5422                                    a_copy[i][j] -= lam * v[i] * v[j];
5423                                }
5424                            }
5425                        }
5426                        None => break,
5427                    }
5428                }
5429                let trace: f64 = (0..rows).map(|i| a[i][i]).sum();
5430                let _ = writeln!(out, "  Trace = {:.6}", trace);
5431                if let Ok(d) = mat_det(&a) {
5432                    let _ = writeln!(out, "  Det   = {:.6}", d);
5433                }
5434            }
5435        }
5436        "qr" => {
5437            // Gram-Schmidt QR decomposition
5438            let (q_mat, r_mat) = mat_qr(&a);
5439            let _ = writeln!(out, "  QR Decomposition  (A = Q · R)");
5440            let _ = writeln!(out, "  Q (orthonormal columns):");
5441            out.push_str(&mat_fmt(&q_mat));
5442            let _ = writeln!(out, "  R (upper-triangular):");
5443            out.push_str(&mat_fmt(&r_mat));
5444        }
5445        "svd" => {
5446            // SVD via Jacobi iterations (symmetric A^T A → eigendecomposition)
5447            // Returns singular values and V matrix; U approximated for small matrices
5448            if rows > 8 || cols > 8 {
5449                let _ = writeln!(
5450                    out,
5451                    "  Error: SVD limited to 8×8 matrices ({}×{})",
5452                    rows, cols
5453                );
5454            } else {
5455                let (s_vals, v_mat) = mat_svd_values(&a);
5456                let _ = writeln!(out, "  SVD Singular Values:");
5457                for (i, sv) in s_vals.iter().enumerate() {
5458                    let bar_len = if s_vals[0].abs() > 1e-12 {
5459                        (sv / s_vals[0] * 20.0) as usize
5460                    } else {
5461                        0
5462                    };
5463                    let _ = writeln!(out, "  σ{} = {:.6}  {}", i + 1, sv, "#".repeat(bar_len));
5464                }
5465                let rank: usize = s_vals.iter().filter(|&&v| v.abs() > 1e-9).count();
5466                let cond = if s_vals.last().map(|v| v.abs()).unwrap_or(0.0) > 1e-12 {
5467                    format!(
5468                        "{:.4}",
5469                        s_vals[0] / s_vals.iter().cloned().fold(f64::INFINITY, f64::min)
5470                    )
5471                } else {
5472                    "∞ (singular)".to_string()
5473                };
5474                let _ = writeln!(out, "  Rank: {}  |  Condition number: {}", rank, cond);
5475                let _ = writeln!(out, "  V (right singular vectors):");
5476                out.push_str(&mat_fmt(&v_mat));
5477            }
5478        }
5479        "chol" => {
5480            if rows != cols {
5481                let _ = writeln!(out, "  Error: Cholesky requires square matrix");
5482            } else {
5483                match mat_cholesky(&a) {
5484                    Ok(l) => {
5485                        let _ = writeln!(out, "  Cholesky Decomposition  (A = L · Lᵀ)");
5486                        let _ = writeln!(out, "  L (lower-triangular):");
5487                        out.push_str(&mat_fmt(&l));
5488                    }
5489                    Err(e) => {
5490                        let _ = writeln!(
5491                            out,
5492                            "  Error: {}  (matrix must be symmetric positive-definite)",
5493                            e
5494                        );
5495                    }
5496                }
5497            }
5498        }
5499        "pinv" => {
5500            // Moore-Penrose pseudoinverse via SVD-based approach (normal equations for full-rank)
5501            match mat_pinv(&a) {
5502                Ok(p) => {
5503                    let _ = writeln!(out, "  Moore-Penrose Pseudoinverse (A⁺):");
5504                    out.push_str(&mat_fmt(&p));
5505                    // Verify: A A⁺ A ≈ A
5506                    if let Ok(aa_p) = mat_mul(&a, &p) {
5507                        if let Ok(aapa) = mat_mul(&aa_p, &a) {
5508                            let mut err = 0.0f64;
5509                            for i in 0..aapa.len() {
5510                                for j in 0..aapa[i].len() {
5511                                    let d = (aapa[i][j] - a[i][j]).abs();
5512                                    if d > err {
5513                                        err = d;
5514                                    }
5515                                }
5516                            }
5517                            let _ = writeln!(out, "  Verify ||A*A+*A - A||_inf = {:.2e}", err);
5518                        }
5519                    }
5520                }
5521                Err(e) => {
5522                    let _ = writeln!(out, "  Error: {}", e);
5523                }
5524            }
5525        }
5526        _ => {
5527            // "info" — show all applicable results
5528            let _ = writeln!(out, "  Rank: {}", mat_rank(&a));
5529            if rows == cols {
5530                if let Ok(d) = mat_det(&a) {
5531                    let _ = writeln!(out, "  Det:  {:.6}", d);
5532                }
5533                match mat_inv(&a) {
5534                    Ok(inv) => {
5535                        let _ = writeln!(out, "  Inverse:");
5536                        out.push_str(&mat_fmt(&inv));
5537                    }
5538                    Err(_) => {
5539                        let _ = writeln!(out, "  Inverse: N/A (singular)");
5540                    }
5541                }
5542                let trace: f64 = (0..rows).map(|i| a[i][i]).sum();
5543                let _ = writeln!(out, "  Trace: {:.6}", trace);
5544                let frobenius: f64 = a
5545                    .iter()
5546                    .flat_map(|r| r.iter())
5547                    .map(|v| v * v)
5548                    .sum::<f64>()
5549                    .sqrt();
5550                let _ = writeln!(out, "  Frobenius norm: {:.6}", frobenius);
5551            }
5552            let t = mat_transpose(&a);
5553            let _ = writeln!(out, "  Transpose:");
5554            out.push_str(&mat_fmt(&t));
5555        }
5556    }
5557
5558    let _ = writeln!(out, "{}", "=".repeat(w));
5559    out
5560}
5561
5562fn split_two_matrices(s: &str) -> Vec<String> {
5563    // Split "A B" where A and B are either [[...]] or "row; row" groups
5564    let s = s.trim();
5565    if s.starts_with('[') {
5566        // Find the end of the first [[...]] group
5567        let mut depth = 0;
5568        let mut end = 0;
5569        for (i, c) in s.chars().enumerate() {
5570            if c == '[' {
5571                depth += 1;
5572            } else if c == ']' {
5573                depth -= 1;
5574                if depth == 0 {
5575                    end = i + 1;
5576                    break;
5577                }
5578            }
5579        }
5580        if end == 0 || end >= s.len() {
5581            return vec![s.to_string()];
5582        }
5583        let a_str = s[..end].trim().to_string();
5584        let b_str = s[end..].trim().trim_start_matches([',', ' ']).to_string();
5585        if b_str.is_empty() {
5586            return vec![a_str];
5587        }
5588        return vec![a_str, b_str];
5589    }
5590    // For semicolon format: split on " / " or "| " or just try natural language split
5591    if let Some(pos) = s.find(" / ") {
5592        return vec![s[..pos].trim().to_string(), s[pos + 3..].trim().to_string()];
5593    }
5594    vec![s.to_string()]
5595}
5596
5597// ── Financial math ───────────────────────────────────────────────────────────
5598// NPV / IRR / loan amortization / compound interest / bond pricing / Black-Scholes
5599// Pure-Rust, instant, no sandbox.
5600//
5601// Query syntax:
5602//   npv RATE CF0 CF1 CF2 ...          net present value
5603//   irr CF0 CF1 CF2 ...               internal rate of return (bisection)
5604//   loan PRINCIPAL RATE_PCT YEARS     loan amortization schedule summary
5605//   compound PRINCIPAL RATE_PCT YEARS [PERIODS_PER_YEAR]
5606//   bond FACE COUPON_PCT YIELD_PCT YEARS [PERIODS_PER_YEAR]
5607//   bs SPOT STRIKE RATE_PCT SIGMA_PCT YEARS [call|put]   Black-Scholes
5608
5609pub fn finance_calc(query: &str) -> String {
5610    let q = query.trim();
5611    let tokens: Vec<&str> = q.split_whitespace().collect();
5612    if tokens.is_empty() {
5613        return finance_usage();
5614    }
5615
5616    let mut out = String::new();
5617    let w = 64usize;
5618    let _ = writeln!(out, "{}", "=".repeat(w));
5619
5620    match tokens[0].to_lowercase().as_str() {
5621        "npv" => {
5622            if tokens.len() < 3 {
5623                let _ = writeln!(out, "  Usage: npv RATE CF0 CF1 CF2 ...");
5624                let _ = writeln!(out, "{}", "=".repeat(w));
5625                return out;
5626            }
5627            let rate: f64 = match tokens[1].trim_end_matches('%').parse() {
5628                Ok(v) => {
5629                    if tokens[1].contains('%') {
5630                        v / 100.0
5631                    } else {
5632                        v
5633                    }
5634                }
5635                Err(_) => {
5636                    let _ = writeln!(out, "  Error: bad rate '{}'", tokens[1]);
5637                    let _ = writeln!(out, "{}", "=".repeat(w));
5638                    return out;
5639                }
5640            };
5641            let cfs: Vec<f64> = tokens[2..]
5642                .iter()
5643                .filter_map(|s| s.replace(',', "").parse::<f64>().ok())
5644                .collect();
5645            if cfs.is_empty() {
5646                let _ = writeln!(out, "  Error: no cash flows found");
5647                let _ = writeln!(out, "{}", "=".repeat(w));
5648                return out;
5649            }
5650            let npv: f64 = cfs
5651                .iter()
5652                .enumerate()
5653                .map(|(t, &cf)| cf / (1.0 + rate).powi(t as i32))
5654                .sum();
5655            let _ = writeln!(out, "  NPV Analysis");
5656            let _ = writeln!(out, "  Discount rate : {:.4}%", rate * 100.0);
5657            let _ = writeln!(
5658                out,
5659                "  Cash flows    : {}",
5660                cfs.iter()
5661                    .map(|cf| format!("{:.2}", cf))
5662                    .collect::<Vec<_>>()
5663                    .join("  ")
5664            );
5665            let _ = writeln!(out, "  NPV           : {:.4}", npv);
5666            let _ = writeln!(
5667                out,
5668                "  Decision      : {}",
5669                if npv > 0.0 {
5670                    "Accept (NPV > 0)"
5671                } else if npv < 0.0 {
5672                    "Reject (NPV < 0)"
5673                } else {
5674                    "Indifferent"
5675                }
5676            );
5677        }
5678        "irr" => {
5679            if tokens.len() < 3 {
5680                let _ = writeln!(out, "  Usage: irr CF0 CF1 CF2 ...");
5681                let _ = writeln!(out, "{}", "=".repeat(w));
5682                return out;
5683            }
5684            let cfs: Vec<f64> = tokens[1..]
5685                .iter()
5686                .filter_map(|s| s.replace(',', "").parse::<f64>().ok())
5687                .collect();
5688            if cfs.is_empty() {
5689                let _ = writeln!(out, "  Error: no cash flows");
5690                let _ = writeln!(out, "{}", "=".repeat(w));
5691                return out;
5692            }
5693            fn npv_at(rate: f64, cfs: &[f64]) -> f64 {
5694                cfs.iter()
5695                    .enumerate()
5696                    .map(|(t, &cf)| cf / (1.0 + rate).powi(t as i32))
5697                    .sum()
5698            }
5699            // Bisection search for IRR in (-0.9999, 10.0)
5700            let mut lo = -0.9999f64;
5701            let mut hi = 10.0f64;
5702            let npv_lo = npv_at(lo, &cfs);
5703            let npv_hi = npv_at(hi, &cfs);
5704            let _ = writeln!(out, "  IRR Analysis");
5705            if npv_lo * npv_hi > 0.0 {
5706                let _ = writeln!(out, "  IRR: no unique root found in (-99.99%, 1000%) — check sign changes in cash flows");
5707            } else {
5708                for _ in 0..200 {
5709                    let mid = (lo + hi) / 2.0;
5710                    if npv_at(mid, &cfs) * npv_at(lo, &cfs) < 0.0 {
5711                        hi = mid;
5712                    } else {
5713                        lo = mid;
5714                    }
5715                    if (hi - lo).abs() < 1e-10 {
5716                        break;
5717                    }
5718                }
5719                let irr = (lo + hi) / 2.0;
5720                let _ = writeln!(
5721                    out,
5722                    "  Cash flows : {}",
5723                    cfs.iter()
5724                        .map(|cf| format!("{:.2}", cf))
5725                        .collect::<Vec<_>>()
5726                        .join("  ")
5727                );
5728                let _ = writeln!(out, "  IRR        : {:.6}%", irr * 100.0);
5729                let npv_check = npv_at(irr, &cfs);
5730                let _ = writeln!(out, "  NPV @ IRR  : {:.8} (should be ~0)", npv_check);
5731            }
5732        }
5733        "loan" => {
5734            if tokens.len() < 4 {
5735                let _ = writeln!(out, "  Usage: loan PRINCIPAL RATE_PCT YEARS");
5736                let _ = writeln!(out, "{}", "=".repeat(w));
5737                return out;
5738            }
5739            let principal: f64 = tokens[1].replace(',', "").parse().unwrap_or(0.0);
5740            let annual_rate: f64 = tokens[2]
5741                .trim_end_matches('%')
5742                .parse::<f64>()
5743                .unwrap_or(0.0)
5744                / 100.0;
5745            let years: f64 = tokens[3].parse().unwrap_or(0.0);
5746            let n = (years * 12.0).round() as u32;
5747            let r = annual_rate / 12.0;
5748            let payment = if r.abs() < 1e-12 {
5749                principal / n as f64
5750            } else {
5751                principal * r * (1.0 + r).powi(n as i32) / ((1.0 + r).powi(n as i32) - 1.0)
5752            };
5753            let total_paid = payment * n as f64;
5754            let total_interest = total_paid - principal;
5755            let _ = writeln!(out, "  Loan Amortization");
5756            let _ = writeln!(out, "  Principal      : {:>12.2}", principal);
5757            let _ = writeln!(out, "  Annual rate    : {:>12.4}%", annual_rate * 100.0);
5758            let _ = writeln!(out, "  Term           : {:>12} months ({} years)", n, years);
5759            let _ = writeln!(out, "  Monthly payment: {:>12.2}", payment);
5760            let _ = writeln!(out, "  Total paid     : {:>12.2}", total_paid);
5761            let _ = writeln!(out, "  Total interest : {:>12.2}", total_interest);
5762            let _ = writeln!(
5763                out,
5764                "  Interest ratio : {:>12.2}%",
5765                total_interest / total_paid * 100.0
5766            );
5767            // Show amortization table for first/last few months if reasonable
5768            if n <= 60 || n <= 360 {
5769                let show_rows = 6usize.min(n as usize);
5770                let _ = writeln!(
5771                    out,
5772                    "\n  {:<6}  {:>12}  {:>12}  {:>12}  {:>12}",
5773                    "Month", "Payment", "Principal", "Interest", "Balance"
5774                );
5775                let _ = writeln!(out, "  {}", "-".repeat(58));
5776                let mut balance = principal;
5777                for mo in 1..=n {
5778                    let interest_part = balance * r;
5779                    let principal_part = payment - interest_part;
5780                    balance -= principal_part;
5781                    if balance < 0.0 {
5782                        balance = 0.0;
5783                    }
5784                    if mo as usize <= show_rows || mo as usize > n as usize - show_rows {
5785                        let _ = writeln!(
5786                            out,
5787                            "  {:<6}  {:>12.2}  {:>12.2}  {:>12.2}  {:>12.2}",
5788                            mo, payment, principal_part, interest_part, balance
5789                        );
5790                    } else if mo as usize == show_rows + 1 {
5791                        let _ = writeln!(out, "  {:^58}", "...");
5792                    }
5793                }
5794            }
5795        }
5796        "compound" => {
5797            if tokens.len() < 4 {
5798                let _ = writeln!(out, "  Usage: compound PRINCIPAL RATE_PCT YEARS [PERIODS]");
5799                let _ = writeln!(out, "{}", "=".repeat(w));
5800                return out;
5801            }
5802            let p: f64 = tokens[1].replace(',', "").parse().unwrap_or(0.0);
5803            let r: f64 = tokens[2]
5804                .trim_end_matches('%')
5805                .parse::<f64>()
5806                .unwrap_or(0.0)
5807                / 100.0;
5808            let t: f64 = tokens[3].parse().unwrap_or(1.0);
5809            let n: f64 = tokens.get(4).and_then(|s| s.parse().ok()).unwrap_or(1.0);
5810            let fv = p * (1.0 + r / n).powf(n * t);
5811            let fv_cont = p * (r * t).exp();
5812            let _ = writeln!(out, "  Compound Interest");
5813            let _ = writeln!(out, "  Principal     : {:>12.2}", p);
5814            let _ = writeln!(out, "  Annual rate   : {:>12.4}%", r * 100.0);
5815            let _ = writeln!(out, "  Years         : {:>12}", t);
5816            let _ = writeln!(out, "  Periods/year  : {:>12}", n);
5817            let _ = writeln!(out, "  Future value  : {:>12.4}", fv);
5818            let _ = writeln!(out, "  Interest earned: {:>12.4}", fv - p);
5819            let _ = writeln!(out, "  Continuous FV : {:>12.4}", fv_cont);
5820            let eff_rate = (1.0 + r / n).powf(n) - 1.0;
5821            let _ = writeln!(out, "  Effective rate: {:>12.4}%", eff_rate * 100.0);
5822        }
5823        "bond" => {
5824            if tokens.len() < 6 {
5825                let _ = writeln!(
5826                    out,
5827                    "  Usage: bond FACE COUPON_PCT YIELD_PCT YEARS [PERIODS]"
5828                );
5829                let _ = writeln!(out, "{}", "=".repeat(w));
5830                return out;
5831            }
5832            let face: f64 = tokens[1].replace(',', "").parse().unwrap_or(1000.0);
5833            let coupon_rate: f64 = tokens[2]
5834                .trim_end_matches('%')
5835                .parse::<f64>()
5836                .unwrap_or(0.0)
5837                / 100.0;
5838            let yield_rate: f64 = tokens[3]
5839                .trim_end_matches('%')
5840                .parse::<f64>()
5841                .unwrap_or(0.0)
5842                / 100.0;
5843            let years: f64 = tokens[4].parse().unwrap_or(1.0);
5844            let m: f64 = tokens.get(5).and_then(|s| s.parse().ok()).unwrap_or(2.0); // semi-annual default
5845            let n = (years * m).round() as i32;
5846            let r = yield_rate / m;
5847            let c = face * coupon_rate / m;
5848            // Price = PV of coupons + PV of face
5849            let pv_coupons = if r.abs() < 1e-12 {
5850                c * n as f64
5851            } else {
5852                c * (1.0 - (1.0 + r).powi(-n)) / r
5853            };
5854            let pv_face = face / (1.0 + r).powi(n);
5855            let price = pv_coupons + pv_face;
5856            let duration_num: f64 = (1..=n)
5857                .map(|t| t as f64 / m * c / (1.0 + r).powi(t))
5858                .sum::<f64>()
5859                + years * pv_face;
5860            let duration = duration_num / price;
5861            let _ = writeln!(out, "  Bond Pricing");
5862            let _ = writeln!(out, "  Face value     : {:>12.2}", face);
5863            let _ = writeln!(
5864                out,
5865                "  Coupon rate    : {:>12.4}%  ({:.2} per period)",
5866                coupon_rate * 100.0,
5867                c
5868            );
5869            let _ = writeln!(out, "  Yield to mat.  : {:>12.4}%", yield_rate * 100.0);
5870            let _ = writeln!(out, "  Years to mat.  : {:>12}", years);
5871            let _ = writeln!(out, "  Periods/year   : {:>12}", m);
5872            let _ = writeln!(out, "  Total periods  : {:>12}", n);
5873            let _ = writeln!(out, "  Bond price     : {:>12.4}", price);
5874            let _ = writeln!(out, "  PV of coupons  : {:>12.4}", pv_coupons);
5875            let _ = writeln!(out, "  PV of face     : {:>12.4}", pv_face);
5876            let status = if price > face {
5877                "Premium"
5878            } else if price < face {
5879                "Discount"
5880            } else {
5881                "Par"
5882            };
5883            let _ = writeln!(
5884                out,
5885                "  Bond trades at : {} ({:.2}% of face)",
5886                status,
5887                price / face * 100.0
5888            );
5889            let _ = writeln!(out, "  Macaulay dur.  : {:>12.4} years", duration);
5890        }
5891        "bs" | "black-scholes" | "blackscholes" | "option" => {
5892            if tokens.len() < 6 {
5893                let _ = writeln!(
5894                    out,
5895                    "  Usage: bs SPOT STRIKE RATE_PCT SIGMA_PCT YEARS [call|put]"
5896                );
5897                let _ = writeln!(out, "{}", "=".repeat(w));
5898                return out;
5899            }
5900            let s: f64 = tokens[1].replace(',', "").parse().unwrap_or(0.0);
5901            let k: f64 = tokens[2].replace(',', "").parse().unwrap_or(0.0);
5902            let r: f64 = tokens[3]
5903                .trim_end_matches('%')
5904                .parse::<f64>()
5905                .unwrap_or(0.0)
5906                / 100.0;
5907            let sigma: f64 = tokens[4]
5908                .trim_end_matches('%')
5909                .parse::<f64>()
5910                .unwrap_or(0.0)
5911                / 100.0;
5912            let t: f64 = tokens[5].parse().unwrap_or(1.0);
5913            let opt_type = tokens.get(6).copied().unwrap_or("call");
5914            let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
5915            let d2 = d1 - sigma * t.sqrt();
5916            let nd1 = bs_ncdf(d1);
5917            let nd2 = bs_ncdf(d2);
5918            let (price, delta) = if opt_type.to_lowercase().starts_with('p') {
5919                let p = k * (-r * t).exp() * bs_ncdf(-d2) - s * bs_ncdf(-d1);
5920                (p, nd1 - 1.0)
5921            } else {
5922                let c = s * nd1 - k * (-r * t).exp() * nd2;
5923                (c, nd1)
5924            };
5925            let gamma = bs_npdf(d1) / (s * sigma * t.sqrt());
5926            let vega = s * bs_npdf(d1) * t.sqrt() / 100.0;
5927            let theta_call = (-s * bs_npdf(d1) * sigma / (2.0 * t.sqrt())
5928                - r * k * (-r * t).exp() * nd2)
5929                / 365.0;
5930            let _ = writeln!(out, "  Black-Scholes Option Pricing");
5931            let _ = writeln!(out, "  Spot price     : {:>12.4}", s);
5932            let _ = writeln!(out, "  Strike price   : {:>12.4}", k);
5933            let _ = writeln!(out, "  Risk-free rate : {:>12.4}%", r * 100.0);
5934            let _ = writeln!(out, "  Volatility (σ) : {:>12.4}%", sigma * 100.0);
5935            let _ = writeln!(out, "  Time (years)   : {:>12.4}", t);
5936            let _ = writeln!(out, "  Option type    : {:>12}", opt_type.to_uppercase());
5937            let _ = writeln!(out, "  ─────────────────────────────────────────────");
5938            let _ = writeln!(out, "  d1             : {:>12.6}", d1);
5939            let _ = writeln!(out, "  d2             : {:>12.6}", d2);
5940            let _ = writeln!(out, "  N(d1) / N(d2)  : {:>12.6} / {:>12.6}", nd1, nd2);
5941            let _ = writeln!(out, "  ─────────────────────────────────────────────");
5942            let _ = writeln!(out, "  Option price   : {:>12.6}", price);
5943            let _ = writeln!(out, "  Delta          : {:>12.6}", delta);
5944            let _ = writeln!(out, "  Gamma          : {:>12.6}", gamma);
5945            let _ = writeln!(out, "  Vega (per 1%σ) : {:>12.6}", vega);
5946            let _ = writeln!(out, "  Theta (per day): {:>12.6}", theta_call);
5947        }
5948        _ => {
5949            let _ = writeln!(out, "{}", finance_usage());
5950            let _ = writeln!(out, "{}", "=".repeat(w));
5951            return out;
5952        }
5953    }
5954
5955    let _ = writeln!(out, "{}", "=".repeat(w));
5956    out
5957}
5958
5959fn bs_ncdf(x: f64) -> f64 {
5960    // Abramowitz & Stegun approximation (max error 7.5e-8)
5961    if x < -8.0 {
5962        return 0.0;
5963    }
5964    if x > 8.0 {
5965        return 1.0;
5966    }
5967    if x >= 0.0 {
5968        0.5 * (1.0 + erf_approx(x / std::f64::consts::SQRT_2))
5969    } else {
5970        0.5 * (1.0 - erf_approx(-x / std::f64::consts::SQRT_2))
5971    }
5972}
5973
5974fn bs_npdf(x: f64) -> f64 {
5975    (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
5976}
5977
5978fn finance_usage() -> String {
5979    "Financial math:\n\
5980     hematite --finance 'npv 10% -1000 300 400 500 200'      NPV\n\
5981     hematite --finance 'irr -1000 300 400 500 200'           IRR\n\
5982     hematite --finance 'loan 200000 6.5% 30'                 30yr mortgage\n\
5983     hematite --finance 'compound 10000 7% 10 12'             compound interest\n\
5984     hematite --finance 'bond 1000 5% 4% 10 2'               bond pricing\n\
5985     hematite --finance 'bs 100 100 5% 20% 1 call'           Black-Scholes call\n\
5986     hematite --finance 'bs 100 105 5% 20% 0.5 put'          Black-Scholes put"
5987        .into()
5988}
5989
5990// ── Graph theory ──────────────────────────────────────────────────────────────
5991// Parses an edge list, then runs BFS/DFS/Dijkstra/components/topo-sort.
5992//
5993// Input format — one edge per line or semicolon-separated:
5994//   A B          (unweighted, undirected)
5995//   A B 5        (weighted)
5996//   A->B or A->B:5   (directed)
5997//   A-B or A-B:5     (undirected)
5998//
5999// Modes (first word of query before the edge list):
6000//   bfs FROM       breadth-first search from a node
6001//   dfs FROM       depth-first search from a node
6002//   shortest FROM TO   Dijkstra shortest path
6003//   components     connected components
6004//   topo           topological sort (directed)
6005//   info           degree table + basic stats (default)
6006
6007pub fn graph_theory(query: &str) -> String {
6008    let q = query.trim();
6009
6010    // Split mode/args from edge list
6011    // Edge list starts when a line/token contains a separator or is all non-alpha… heuristic:
6012    // Look for the first token containing '-', '>' or a digit after a space — that's the edge list.
6013    // But first try to strip a known mode keyword from the front.
6014
6015    let (mode, rest) = {
6016        let tokens: Vec<&str> = q.splitn(2, ['\n', ';']).collect();
6017        let first_line = tokens[0].trim();
6018        let _fl_lower = first_line.to_lowercase();
6019        // Check if the entire first line looks like a mode+args header (no edge separators)
6020        let looks_like_mode = !first_line.contains("->")
6021            && !first_line.contains(" - ")
6022            && first_line.split_whitespace().count() <= 3;
6023        if looks_like_mode {
6024            let words: Vec<&str> = first_line.splitn(2, char::is_whitespace).collect();
6025            let m = words[0].to_lowercase();
6026            let after_mode = words.get(1).copied().unwrap_or("").trim();
6027            let rest_str = if tokens.len() > 1 {
6028                format!("{}\n{}", after_mode, tokens[1])
6029            } else {
6030                after_mode.to_string()
6031            };
6032            match m.as_str() {
6033                "bfs" | "dfs" | "shortest" | "path" | "components" | "topo" | "topological"
6034                | "info" | "degree" => (m, rest_str),
6035                _ => {
6036                    // The first line might be part of an edge list; treat the whole thing as "info"
6037                    ("info".to_string(), q.to_string())
6038                }
6039            }
6040        } else {
6041            ("info".to_string(), q.to_string())
6042        }
6043    };
6044
6045    // Parse edge list
6046    // Edges separated by newline or semicolon
6047    let edge_strs: Vec<&str> = rest
6048        .split(['\n', ';'])
6049        .map(str::trim)
6050        .filter(|s| !s.is_empty())
6051        .collect();
6052
6053    let mut directed = false;
6054    let mut nodes: Vec<String> = Vec::new();
6055    let mut edges: Vec<(String, String, f64)> = Vec::new();
6056
6057    let node_id = |name: &str, nodes: &mut Vec<String>| -> usize {
6058        if let Some(p) = nodes.iter().position(|n| n == name) {
6059            p
6060        } else {
6061            nodes.push(name.to_string());
6062            nodes.len() - 1
6063        }
6064    };
6065
6066    for line in &edge_strs {
6067        let line = line.trim();
6068        if line.is_empty() {
6069            continue;
6070        }
6071        // Detect directed
6072        let (a, b, w, dir) = if let Some(pos) = line.find("->") {
6073            directed = true;
6074            let a = line[..pos].trim().trim_matches(':');
6075            let rest2 = line[pos + 2..].trim();
6076            let (b, w) = parse_node_weight(rest2);
6077            (a, b, w, true)
6078        } else if let Some(pos) = line.find(" - ").or_else(|| {
6079            // "A-B" but avoid matching negative numbers
6080            let parts: Vec<&str> = line.splitn(3, char::is_whitespace).collect();
6081            if parts.len() >= 2 {
6082                // space-separated "A B [w]"
6083                None
6084            } else {
6085                // Check for single hyphen between non-numeric tokens
6086                let hp = line.find('-');
6087                hp.filter(|&h| h > 0 && line[..h].trim().parse::<f64>().is_err())
6088            }
6089        }) {
6090            let sep_len = if line[pos..].starts_with(" - ") { 3 } else { 1 };
6091            let a = line[..pos].trim();
6092            let rest2 = line[pos + sep_len..].trim();
6093            let (b, w) = parse_node_weight(rest2);
6094            (a, b, w, false)
6095        } else {
6096            // Space-separated: "A B [w]"
6097            let parts: Vec<&str> = line.splitn(3, char::is_whitespace).collect();
6098            if parts.len() < 2 {
6099                node_id(line, &mut nodes);
6100                continue;
6101            }
6102            let a = parts[0].trim();
6103            let b_raw = parts[1].trim();
6104            // b_raw may be "NodeName:weight" or just "NodeName"; weight may be parts[2]
6105            let (b, w) = if let Some(cp) = b_raw.find(':') {
6106                let wt = b_raw[cp + 1..].parse::<f64>().unwrap_or(1.0);
6107                (&b_raw[..cp], wt)
6108            } else {
6109                let wt = parts
6110                    .get(2)
6111                    .and_then(|s| s.trim().parse::<f64>().ok())
6112                    .unwrap_or(1.0);
6113                (b_raw, wt)
6114            };
6115            (a, b, w, false)
6116        };
6117
6118        if a.is_empty() || b.is_empty() {
6119            continue;
6120        }
6121        let ai = node_id(a, &mut nodes);
6122        let bi = node_id(b, &mut nodes);
6123        edges.push((nodes[ai].clone(), nodes[bi].clone(), w));
6124        if !dir { /* undirected edge added both ways below */ }
6125    }
6126
6127    if nodes.is_empty() {
6128        return graph_usage();
6129    }
6130
6131    let n = nodes.len();
6132
6133    // Build adjacency list: adj[i] = Vec<(j, weight)>
6134    let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
6135    for (a_name, b_name, w) in &edges {
6136        let ai = nodes.iter().position(|x| x == a_name).unwrap();
6137        let bi = nodes.iter().position(|x| x == b_name).unwrap();
6138        adj[ai].push((bi, *w));
6139        if !directed {
6140            adj[bi].push((ai, *w));
6141        }
6142    }
6143
6144    let mut out = String::new();
6145    let w = 64usize;
6146    let _ = writeln!(out, "{}", "=".repeat(w));
6147    let _ = writeln!(
6148        out,
6149        "  Graph Analysis  |  {} nodes  |  {} edges  |  {}",
6150        n,
6151        edges.len(),
6152        if directed { "directed" } else { "undirected" }
6153    );
6154    let _ = writeln!(out, "{}", "=".repeat(w));
6155
6156    match mode.as_str() {
6157        "bfs" => {
6158            let start_name = rest.split_whitespace().next().unwrap_or(&nodes[0]);
6159            let start = nodes.iter().position(|x| x == start_name).unwrap_or(0);
6160            let order = bfs_order(&adj, start, n);
6161            let _ = writeln!(out, "  BFS from \"{}\":", nodes[start]);
6162            let _ = writeln!(
6163                out,
6164                "  Visit order: {}",
6165                order
6166                    .iter()
6167                    .map(|&i| nodes[i].as_str())
6168                    .collect::<Vec<_>>()
6169                    .join(" → ")
6170            );
6171        }
6172        "dfs" => {
6173            let start_name = rest.split_whitespace().next().unwrap_or(&nodes[0]);
6174            let start = nodes.iter().position(|x| x == start_name).unwrap_or(0);
6175            let order = dfs_order(&adj, start, n);
6176            let _ = writeln!(out, "  DFS from \"{}\":", nodes[start]);
6177            let _ = writeln!(
6178                out,
6179                "  Visit order: {}",
6180                order
6181                    .iter()
6182                    .map(|&i| nodes[i].as_str())
6183                    .collect::<Vec<_>>()
6184                    .join(" → ")
6185            );
6186        }
6187        "shortest" | "path" => {
6188            let parts: Vec<&str> = rest.split_whitespace().collect();
6189            let from_name = parts.first().copied().unwrap_or(&nodes[0]);
6190            let to_name = parts.get(1).copied().unwrap_or(&nodes[n - 1]);
6191            let from = nodes.iter().position(|x| x == from_name).unwrap_or(0);
6192            let to = nodes
6193                .iter()
6194                .position(|x| x == to_name)
6195                .unwrap_or(n.saturating_sub(1));
6196            match dijkstra(&adj, from, to, n) {
6197                Some((dist, path)) => {
6198                    let path_str = path
6199                        .iter()
6200                        .map(|&i| nodes[i].as_str())
6201                        .collect::<Vec<_>>()
6202                        .join(" → ");
6203                    let _ = writeln!(out, "  Shortest path: {} → {}", nodes[from], nodes[to]);
6204                    let _ = writeln!(out, "  Distance: {:.4}", dist);
6205                    let _ = writeln!(out, "  Path: {}", path_str);
6206                }
6207                None => {
6208                    let _ = writeln!(
6209                        out,
6210                        "  No path from \"{}\" to \"{}\"",
6211                        nodes[from], nodes[to]
6212                    );
6213                }
6214            }
6215            // Also show all-pairs distances from source
6216            let dists = dijkstra_all(&adj, from, n);
6217            let _ = writeln!(out, "\n  All distances from \"{}\":", nodes[from]);
6218            for (i, d) in dists.iter().enumerate() {
6219                if i == from {
6220                    continue;
6221                }
6222                if *d == f64::INFINITY {
6223                    let _ = writeln!(out, "    → {:<20}  unreachable", &nodes[i]);
6224                } else {
6225                    let _ = writeln!(out, "    → {:<20}  {:.4}", &nodes[i], d);
6226                }
6227            }
6228        }
6229        "components" => {
6230            let comps = connected_components(&adj, n, directed);
6231            let _ = writeln!(out, "  Connected components: {}", comps.len());
6232            for (ci, comp) in comps.iter().enumerate() {
6233                let names: Vec<&str> = comp.iter().map(|&i| nodes[i].as_str()).collect();
6234                let _ = writeln!(out, "  [{}] {}", ci + 1, names.join(", "));
6235            }
6236        }
6237        "topo" | "topological" => match topo_sort(&adj, n) {
6238            Ok(order) => {
6239                let _ = writeln!(out, "  Topological sort:");
6240                let _ = writeln!(
6241                    out,
6242                    "  {}",
6243                    order
6244                        .iter()
6245                        .map(|&i| nodes[i].as_str())
6246                        .collect::<Vec<_>>()
6247                        .join(" → ")
6248                );
6249            }
6250            Err(_) => {
6251                let _ = writeln!(out, "  Cycle detected — topological sort not possible.");
6252            }
6253        },
6254        "centrality" | "betweenness" => {
6255            // Brandes algorithm for betweenness centrality (unweighted BFS)
6256            let bc = betweenness_centrality(&adj, n);
6257            let mut ranked: Vec<(usize, f64)> = bc.iter().copied().enumerate().collect();
6258            ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
6259            let _ = writeln!(
6260                out,
6261                "  Betweenness Centrality (fraction of shortest paths through node):"
6262            );
6263            let _ = writeln!(out, "  {:<22}  {:>12}  bar", "Node", "Centrality");
6264            let _ = writeln!(out, "  {}", "-".repeat(w - 2));
6265            let max_bc = ranked.first().map(|x| x.1).unwrap_or(1.0).max(1e-9);
6266            for &(i, val) in &ranked {
6267                let bar_len = (val / max_bc * 30.0) as usize;
6268                let _ = writeln!(
6269                    out,
6270                    "  {:<22}  {:>12.6}  {}",
6271                    &nodes[i],
6272                    val,
6273                    "#".repeat(bar_len)
6274                );
6275            }
6276        }
6277        "pagerank" | "pr" => {
6278            let d: f64 = rest
6279                .split_whitespace()
6280                .next()
6281                .and_then(|s| s.parse().ok())
6282                .unwrap_or(0.85);
6283            let pr = pagerank(&adj, n, d, 100);
6284            let mut ranked: Vec<(usize, f64)> = pr.iter().copied().enumerate().collect();
6285            ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
6286            let _ = writeln!(out, "  PageRank  (damping={:.2}, 100 iterations):", d);
6287            let _ = writeln!(out, "  {:<22}  {:>10}  bar", "Node", "Score");
6288            let _ = writeln!(out, "  {}", "-".repeat(w - 2));
6289            let max_pr = ranked.first().map(|x| x.1).unwrap_or(1e-9).max(1e-9);
6290            for &(i, val) in &ranked {
6291                let bar_len = (val / max_pr * 30.0) as usize;
6292                let _ = writeln!(
6293                    out,
6294                    "  {:<22}  {:>10.6}  {}",
6295                    &nodes[i],
6296                    val,
6297                    "#".repeat(bar_len)
6298                );
6299            }
6300        }
6301        "clustering" | "cluster" => {
6302            let cc = clustering_coefficients(&adj, n, directed);
6303            let global_cc = if n > 0 {
6304                cc.iter().sum::<f64>() / n as f64
6305            } else {
6306                0.0
6307            };
6308            let mut ranked: Vec<(usize, f64)> = cc.iter().copied().enumerate().collect();
6309            ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
6310            let _ = writeln!(
6311                out,
6312                "  Clustering Coefficients  (global avg: {:.4}):",
6313                global_cc
6314            );
6315            let _ = writeln!(out, "  {:<22}  {:>12}  bar", "Node", "Coefficient");
6316            let _ = writeln!(out, "  {}", "-".repeat(w - 2));
6317            for &(i, val) in &ranked {
6318                let bar_len = (val * 30.0) as usize;
6319                let _ = writeln!(
6320                    out,
6321                    "  {:<22}  {:>12.6}  {}",
6322                    &nodes[i],
6323                    val,
6324                    "#".repeat(bar_len)
6325                );
6326            }
6327        }
6328        "diameter" | "stats" | "metrics" => {
6329            // All-pairs shortest paths via repeated Dijkstra
6330            let (diameter, avg_path, eccentricities) = graph_diameter(&adj, n);
6331            let _ = writeln!(out, "  Network Metrics:");
6332            if diameter == f64::INFINITY {
6333                let _ = writeln!(out, "  Diameter        : ∞ (disconnected graph)");
6334                let _ = writeln!(out, "  Avg path length : N/A");
6335            } else {
6336                let _ = writeln!(out, "  Diameter        : {:.4}", diameter);
6337                let _ = writeln!(out, "  Avg path length : {:.4}", avg_path);
6338            }
6339            // Density
6340            let max_edges = if directed {
6341                n * (n - 1)
6342            } else {
6343                n * (n - 1) / 2
6344            };
6345            let density = if max_edges > 0 {
6346                edges.len() as f64 / max_edges as f64
6347            } else {
6348                0.0
6349            };
6350            let _ = writeln!(
6351                out,
6352                "  Density         : {:.4}  ({} / {} possible edges)",
6353                density,
6354                edges.len(),
6355                max_edges
6356            );
6357            // Eccentricity table
6358            let _ = writeln!(
6359                out,
6360                "\n  Eccentricities (max distance from node to any other):"
6361            );
6362            let _ = writeln!(out, "  {:<22}  {:>12}", "Node", "Eccentricity");
6363            let _ = writeln!(out, "  {}", "-".repeat(36));
6364            let mut ecc_sorted: Vec<(usize, f64)> =
6365                eccentricities.into_iter().enumerate().collect();
6366            ecc_sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
6367            for (i, ecc) in &ecc_sorted {
6368                if *ecc == f64::INFINITY {
6369                    let _ = writeln!(out, "  {:<22}  {:>12}", &nodes[*i], "∞");
6370                } else {
6371                    let _ = writeln!(out, "  {:<22}  {:>12.4}", &nodes[*i], ecc);
6372                }
6373            }
6374            // Center nodes (minimum eccentricity)
6375            let min_ecc = ecc_sorted
6376                .iter()
6377                .map(|x| x.1)
6378                .filter(|x| x.is_finite())
6379                .fold(f64::INFINITY, f64::min);
6380            if min_ecc.is_finite() {
6381                let centers: Vec<&str> = ecc_sorted
6382                    .iter()
6383                    .filter(|&&(_, e)| (e - min_ecc).abs() < 1e-9)
6384                    .map(|&(i, _)| nodes[i].as_str())
6385                    .collect();
6386                let _ = writeln!(out, "\n  Center node(s): {}", centers.join(", "));
6387            }
6388        }
6389        _ => {
6390            // Default: degree table + basic stats
6391            let mut in_deg = vec![0usize; n];
6392            let mut out_deg = vec![0usize; n];
6393            for (ai, nbrs) in adj.iter().enumerate() {
6394                out_deg[ai] = nbrs.len();
6395                for &(bi, _) in nbrs {
6396                    in_deg[bi] += 1;
6397                }
6398            }
6399            let _ = writeln!(
6400                out,
6401                "  {:<20}  {:>8}  {:>8}",
6402                "Node",
6403                if directed { "Out-deg" } else { "Degree" },
6404                if directed { "In-deg" } else { "" }
6405            );
6406            let _ = writeln!(out, "  {}", "-".repeat(40));
6407            let mut sorted_nodes: Vec<usize> = (0..n).collect();
6408            sorted_nodes.sort_by(|&a, &b| out_deg[b].cmp(&out_deg[a]));
6409            for &i in &sorted_nodes {
6410                if directed {
6411                    let _ = writeln!(
6412                        out,
6413                        "  {:<20}  {:>8}  {:>8}",
6414                        &nodes[i], out_deg[i], in_deg[i]
6415                    );
6416                } else {
6417                    let _ = writeln!(out, "  {:<20}  {:>8}", &nodes[i], out_deg[i]);
6418                }
6419            }
6420            // Connectivity
6421            let comps = connected_components(&adj, n, directed);
6422            let _ = writeln!(
6423                out,
6424                "\n  Components: {}  |  {}",
6425                comps.len(),
6426                if comps.len() == 1 {
6427                    "connected".to_string()
6428                } else {
6429                    "disconnected".to_string()
6430                }
6431            );
6432            // Check for cycles via DFS
6433            let has_cycle = detect_cycle(&adj, n, directed);
6434            let _ = writeln!(
6435                out,
6436                "  Cycles: {}",
6437                if has_cycle { "yes" } else { "none detected" }
6438            );
6439            if directed {
6440                if let Ok(order) = topo_sort(&adj, n) {
6441                    let _ = writeln!(
6442                        out,
6443                        "  Topo order: {}",
6444                        order
6445                            .iter()
6446                            .map(|&i| nodes[i].as_str())
6447                            .collect::<Vec<_>>()
6448                            .join(" → ")
6449                    );
6450                }
6451            }
6452        }
6453    }
6454
6455    let _ = writeln!(out, "{}", "=".repeat(w));
6456    out
6457}
6458
6459fn parse_node_weight(s: &str) -> (&str, f64) {
6460    // "NodeName:weight" or "NodeName weight"
6461    if let Some(pos) = s.find(':') {
6462        let name = &s[..pos];
6463        let w = s[pos + 1..].trim().parse::<f64>().unwrap_or(1.0);
6464        (name.trim(), w)
6465    } else {
6466        let parts: Vec<&str> = s.splitn(2, char::is_whitespace).collect();
6467        let name = parts[0].trim();
6468        let w = parts
6469            .get(1)
6470            .and_then(|x| x.trim().parse::<f64>().ok())
6471            .unwrap_or(1.0);
6472        (name, w)
6473    }
6474}
6475
6476fn bfs_order(adj: &[Vec<(usize, f64)>], start: usize, n: usize) -> Vec<usize> {
6477    let mut visited = vec![false; n];
6478    let mut queue = std::collections::VecDeque::new();
6479    let mut order = Vec::new();
6480    visited[start] = true;
6481    queue.push_back(start);
6482    while let Some(u) = queue.pop_front() {
6483        order.push(u);
6484        let mut nbrs: Vec<usize> = adj[u].iter().map(|&(v, _)| v).collect();
6485        nbrs.sort();
6486        for v in nbrs {
6487            if !visited[v] {
6488                visited[v] = true;
6489                queue.push_back(v);
6490            }
6491        }
6492    }
6493    order
6494}
6495
6496fn dfs_order(adj: &[Vec<(usize, f64)>], start: usize, n: usize) -> Vec<usize> {
6497    let mut visited = vec![false; n];
6498    let mut stack = vec![start];
6499    let mut order = Vec::new();
6500    while let Some(u) = stack.pop() {
6501        if visited[u] {
6502            continue;
6503        }
6504        visited[u] = true;
6505        order.push(u);
6506        let mut nbrs: Vec<usize> = adj[u].iter().map(|&(v, _)| v).collect();
6507        nbrs.sort_by(|a, b| b.cmp(a));
6508        for v in nbrs {
6509            if !visited[v] {
6510                stack.push(v);
6511            }
6512        }
6513    }
6514    order
6515}
6516
6517fn dijkstra(
6518    adj: &[Vec<(usize, f64)>],
6519    from: usize,
6520    to: usize,
6521    n: usize,
6522) -> Option<(f64, Vec<usize>)> {
6523    use std::cmp::Ordering;
6524    use std::collections::BinaryHeap;
6525    #[derive(PartialEq)]
6526    struct State {
6527        cost: f64,
6528        node: usize,
6529    }
6530    impl Eq for State {}
6531    impl PartialOrd for State {
6532        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
6533            Some(self.cmp(other))
6534        }
6535    }
6536    impl Ord for State {
6537        fn cmp(&self, other: &Self) -> Ordering {
6538            other
6539                .cost
6540                .partial_cmp(&self.cost)
6541                .unwrap_or(Ordering::Equal)
6542        }
6543    }
6544
6545    let mut dist = vec![f64::INFINITY; n];
6546    let mut prev = vec![usize::MAX; n];
6547    dist[from] = 0.0;
6548    let mut heap = BinaryHeap::new();
6549    heap.push(State {
6550        cost: 0.0,
6551        node: from,
6552    });
6553
6554    while let Some(State { cost, node }) = heap.pop() {
6555        if node == to {
6556            break;
6557        }
6558        if cost > dist[node] {
6559            continue;
6560        }
6561        for &(v, w) in &adj[node] {
6562            let next_cost = dist[node] + w;
6563            if next_cost < dist[v] {
6564                dist[v] = next_cost;
6565                prev[v] = node;
6566                heap.push(State {
6567                    cost: next_cost,
6568                    node: v,
6569                });
6570            }
6571        }
6572    }
6573
6574    if dist[to] == f64::INFINITY {
6575        return None;
6576    }
6577    let mut path = Vec::new();
6578    let mut cur = to;
6579    while cur != usize::MAX {
6580        path.push(cur);
6581        cur = prev[cur];
6582    }
6583    path.reverse();
6584    Some((dist[to], path))
6585}
6586
6587fn dijkstra_all(adj: &[Vec<(usize, f64)>], from: usize, n: usize) -> Vec<f64> {
6588    use std::cmp::Ordering;
6589    use std::collections::BinaryHeap;
6590    #[derive(PartialEq)]
6591    struct State {
6592        cost: f64,
6593        node: usize,
6594    }
6595    impl Eq for State {}
6596    impl PartialOrd for State {
6597        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
6598            Some(self.cmp(other))
6599        }
6600    }
6601    impl Ord for State {
6602        fn cmp(&self, other: &Self) -> Ordering {
6603            other
6604                .cost
6605                .partial_cmp(&self.cost)
6606                .unwrap_or(Ordering::Equal)
6607        }
6608    }
6609    let mut dist = vec![f64::INFINITY; n];
6610    dist[from] = 0.0;
6611    let mut heap = BinaryHeap::new();
6612    heap.push(State {
6613        cost: 0.0,
6614        node: from,
6615    });
6616    while let Some(State { cost, node }) = heap.pop() {
6617        if cost > dist[node] {
6618            continue;
6619        }
6620        for &(v, w) in &adj[node] {
6621            let nc = dist[node] + w;
6622            if nc < dist[v] {
6623                dist[v] = nc;
6624                heap.push(State { cost: nc, node: v });
6625            }
6626        }
6627    }
6628    dist
6629}
6630
6631fn connected_components(adj: &[Vec<(usize, f64)>], n: usize, directed: bool) -> Vec<Vec<usize>> {
6632    // For directed graphs, use weakly connected components (ignore direction)
6633    let mut visited = vec![false; n];
6634    let mut comps = Vec::new();
6635    for start in 0..n {
6636        if visited[start] {
6637            continue;
6638        }
6639        let mut comp = Vec::new();
6640        let mut stack = vec![start];
6641        while let Some(u) = stack.pop() {
6642            if visited[u] {
6643                continue;
6644            }
6645            visited[u] = true;
6646            comp.push(u);
6647            for &(v, _) in &adj[u] {
6648                if !visited[v] {
6649                    stack.push(v);
6650                }
6651            }
6652            if directed {
6653                // Also traverse reverse edges for weak connectivity
6654                for other in 0..n {
6655                    if !visited[other] && adj[other].iter().any(|&(t, _)| t == u) {
6656                        stack.push(other);
6657                    }
6658                }
6659            }
6660        }
6661        comp.sort();
6662        comps.push(comp);
6663    }
6664    comps
6665}
6666
6667fn topo_sort(adj: &[Vec<(usize, f64)>], n: usize) -> Result<Vec<usize>, ()> {
6668    let mut in_deg = vec![0usize; n];
6669    for u in 0..n {
6670        for &(v, _) in &adj[u] {
6671            in_deg[v] += 1;
6672        }
6673    }
6674    let mut queue: std::collections::VecDeque<usize> = (0..n).filter(|&i| in_deg[i] == 0).collect();
6675    let mut order = Vec::new();
6676    while let Some(u) = queue.pop_front() {
6677        order.push(u);
6678        for &(v, _) in &adj[u] {
6679            in_deg[v] -= 1;
6680            if in_deg[v] == 0 {
6681                queue.push_back(v);
6682            }
6683        }
6684    }
6685    if order.len() == n {
6686        Ok(order)
6687    } else {
6688        Err(())
6689    }
6690}
6691
6692fn detect_cycle(adj: &[Vec<(usize, f64)>], n: usize, directed: bool) -> bool {
6693    // DFS-based cycle detection
6694    let mut color = vec![0u8; n]; // 0=white 1=gray 2=black
6695    fn dfs_cycle(
6696        u: usize,
6697        adj: &[Vec<(usize, f64)>],
6698        color: &mut Vec<u8>,
6699        directed: bool,
6700        parent: usize,
6701    ) -> bool {
6702        color[u] = 1;
6703        for &(v, _) in &adj[u] {
6704            if color[v] == 0 {
6705                if dfs_cycle(v, adj, color, directed, u) {
6706                    return true;
6707                }
6708            } else if (directed && color[v] == 1) || (!directed && v != parent) {
6709                return true;
6710            }
6711        }
6712        color[u] = 2;
6713        false
6714    }
6715    for start in 0..n {
6716        if color[start] == 0 && dfs_cycle(start, adj, &mut color, directed, usize::MAX) {
6717            return true;
6718        }
6719    }
6720    false
6721}
6722
6723// Brandes betweenness centrality (unweighted, normalized)
6724fn betweenness_centrality(adj: &[Vec<(usize, f64)>], n: usize) -> Vec<f64> {
6725    let mut bc = vec![0.0f64; n];
6726    for s in 0..n {
6727        let mut stack = Vec::new();
6728        let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
6729        let mut sigma = vec![0.0f64; n];
6730        sigma[s] = 1.0;
6731        let mut dist = vec![-1i64; n];
6732        dist[s] = 0;
6733        let mut queue = std::collections::VecDeque::new();
6734        queue.push_back(s);
6735        while let Some(v) = queue.pop_front() {
6736            stack.push(v);
6737            for &(w, _) in &adj[v] {
6738                if dist[w] < 0 {
6739                    queue.push_back(w);
6740                    dist[w] = dist[v] + 1;
6741                }
6742                if dist[w] == dist[v] + 1 {
6743                    sigma[w] += sigma[v];
6744                    pred[w].push(v);
6745                }
6746            }
6747        }
6748        let mut delta = vec![0.0f64; n];
6749        while let Some(w) = stack.pop() {
6750            for &v in &pred[w] {
6751                delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]);
6752            }
6753            if w != s {
6754                bc[w] += delta[w];
6755            }
6756        }
6757    }
6758    // Normalize
6759    let norm = if n > 2 {
6760        ((n - 1) * (n - 2)) as f64
6761    } else {
6762        1.0
6763    };
6764    bc.iter_mut().for_each(|x| *x /= norm);
6765    bc
6766}
6767
6768// PageRank (power iteration)
6769fn pagerank(adj: &[Vec<(usize, f64)>], n: usize, damping: f64, iters: usize) -> Vec<f64> {
6770    let mut pr = vec![1.0 / n as f64; n];
6771    let out_deg: Vec<usize> = adj.iter().map(|nbrs| nbrs.len()).collect();
6772    for _ in 0..iters {
6773        let mut new_pr = vec![(1.0 - damping) / n as f64; n];
6774        for v in 0..n {
6775            if out_deg[v] == 0 {
6776                // dangling node: distribute evenly
6777                let share = damping * pr[v] / n as f64;
6778                new_pr.iter_mut().for_each(|x| *x += share);
6779            } else {
6780                let share = damping * pr[v] / out_deg[v] as f64;
6781                for &(u, _) in &adj[v] {
6782                    new_pr[u] += share;
6783                }
6784            }
6785        }
6786        pr = new_pr;
6787    }
6788    pr
6789}
6790
6791// Local clustering coefficient
6792fn clustering_coefficients(adj: &[Vec<(usize, f64)>], n: usize, directed: bool) -> Vec<f64> {
6793    let mut cc = vec![0.0f64; n];
6794    for u in 0..n {
6795        let nbrs: Vec<usize> = adj[u].iter().map(|&(v, _)| v).collect();
6796        let k = nbrs.len();
6797        if k < 2 {
6798            continue;
6799        }
6800        let mut triangles = 0usize;
6801        for i in 0..k {
6802            for j in (i + 1)..k {
6803                let vi = nbrs[i];
6804                let vj = nbrs[j];
6805                if adj[vi].iter().any(|&(x, _)| x == vj) || adj[vj].iter().any(|&(x, _)| x == vi) {
6806                    triangles += 1;
6807                }
6808            }
6809        }
6810        let denom = if directed {
6811            k * (k - 1)
6812        } else {
6813            k * (k - 1) / 2
6814        };
6815        if denom > 0 {
6816            cc[u] = triangles as f64 / denom as f64;
6817        }
6818    }
6819    cc
6820}
6821
6822// Graph diameter, average path length, eccentricities via all-pairs Dijkstra
6823fn graph_diameter(adj: &[Vec<(usize, f64)>], n: usize) -> (f64, f64, Vec<f64>) {
6824    let mut diameter = 0.0f64;
6825    let mut path_sum = 0.0f64;
6826    let mut path_cnt = 0u64;
6827    let mut ecc = vec![0.0f64; n];
6828    for s in 0..n {
6829        let dists = dijkstra_all(adj, s, n);
6830        let finite: Vec<f64> = dists
6831            .iter()
6832            .copied()
6833            .filter(|d| d.is_finite() && *d > 0.0)
6834            .collect();
6835        let max_d = finite.iter().cloned().fold(0.0f64, f64::max);
6836        if finite.len() < n - 1 {
6837            ecc[s] = f64::INFINITY; // disconnected
6838        } else {
6839            ecc[s] = max_d;
6840        }
6841        if max_d.is_finite() && max_d > diameter {
6842            diameter = max_d;
6843        }
6844        for d in &finite {
6845            path_sum += d;
6846            path_cnt += 1;
6847        }
6848    }
6849    let avg = if path_cnt > 0 {
6850        path_sum / path_cnt as f64
6851    } else {
6852        f64::INFINITY
6853    };
6854    let diam = if ecc.iter().any(|x| x.is_infinite()) {
6855        f64::INFINITY
6856    } else {
6857        diameter
6858    };
6859    (diam, avg, ecc)
6860}
6861
6862fn graph_usage() -> String {
6863    "Graph theory — edge list input:\n\
6864     hematite --graph 'A B\\nB C\\nC D'                  info (degree table, components)\n\
6865     hematite --graph 'bfs A\\nA B\\nB C\\nA C'           BFS from node A\n\
6866     hematite --graph 'dfs A\\nA B\\nB C\\nA C'           DFS from node A\n\
6867     hematite --graph 'shortest A D\\nA B 2\\nB D 3\\nA D 10'  Dijkstra shortest path\n\
6868     hematite --graph 'components\\nA B\\nC D'            connected components\n\
6869     hematite --graph 'topo\\nA->B\\nA->C\\nB->D'          topological sort\n\
6870     hematite --graph 'centrality\\nA B\\nB C\\nA C'       betweenness centrality\n\
6871     hematite --graph 'pagerank\\nA->B\\nB->C\\nC->A'      PageRank scores\n\
6872     hematite --graph 'clustering\\nA B\\nB C\\nA C'       local clustering coefficients\n\
6873     hematite --graph 'diameter\\nA B 1\\nB C 2\\nA C 4'   diameter, avg path, eccentricity\n\
6874     \n\
6875     Edge formats: 'A B' 'A B 5' 'A->B' 'A->B:5' 'A-B:3'\n\
6876     Weighted edges: add weight as third token or after colon"
6877        .into()
6878}
6879
6880// ── Symbolic calculus ─────────────────────────────────────────────────────────
6881// Recursive-descent parser → AST → symbolic diff/integrate → pretty-printer.
6882// Supported: +  -  *  /  ^  unary-  sin  cos  tan  ln  log  exp  sqrt  abs
6883// Variable: default x, overridable with "wrt y" suffix.
6884//
6885// Modes:
6886//   diff EXPR [wrt VAR]      symbolic derivative
6887//   integrate EXPR [wrt VAR] symbolic integral (table lookup + linearity)
6888//   simplify EXPR            simplify/reduce
6889//   eval EXPR at VAR=VALUE   numeric evaluation
6890
6891#[derive(Clone, Debug)]
6892enum Expr {
6893    Num(f64),
6894    Var(String),
6895    Add(Box<Expr>, Box<Expr>),
6896    Sub(Box<Expr>, Box<Expr>),
6897    Mul(Box<Expr>, Box<Expr>),
6898    Div(Box<Expr>, Box<Expr>),
6899    Pow(Box<Expr>, Box<Expr>),
6900    Neg(Box<Expr>),
6901    Sin(Box<Expr>),
6902    Cos(Box<Expr>),
6903    Tan(Box<Expr>),
6904    Ln(Box<Expr>),
6905    Exp(Box<Expr>),
6906    Sqrt(Box<Expr>),
6907    Abs(Box<Expr>),
6908}
6909
6910// ── Parser ─────────────────────────────────────────────────────────────────
6911
6912struct Parser<'a> {
6913    chars: &'a [char],
6914    pos: usize,
6915}
6916
6917impl<'a> Parser<'a> {
6918    fn new(chars: &'a [char]) -> Self {
6919        Self { chars, pos: 0 }
6920    }
6921
6922    fn peek(&self) -> Option<char> {
6923        self.chars.get(self.pos).copied()
6924    }
6925
6926    fn consume(&mut self) -> Option<char> {
6927        let c = self.chars.get(self.pos).copied();
6928        self.pos += 1;
6929        c
6930    }
6931
6932    fn skip_ws(&mut self) {
6933        while matches!(self.peek(), Some(' ') | Some('\t')) {
6934            self.pos += 1;
6935        }
6936    }
6937
6938    fn parse_expr(&mut self) -> Result<Expr, String> {
6939        self.parse_add()
6940    }
6941
6942    fn parse_add(&mut self) -> Result<Expr, String> {
6943        let mut left = self.parse_mul()?;
6944        loop {
6945            self.skip_ws();
6946            match self.peek() {
6947                Some('+') => {
6948                    self.consume();
6949                    let r = self.parse_mul()?;
6950                    left = Expr::Add(Box::new(left), Box::new(r));
6951                }
6952                Some('-') => {
6953                    self.consume();
6954                    let r = self.parse_mul()?;
6955                    left = Expr::Sub(Box::new(left), Box::new(r));
6956                }
6957                _ => break,
6958            }
6959        }
6960        Ok(left)
6961    }
6962
6963    fn parse_mul(&mut self) -> Result<Expr, String> {
6964        let mut left = self.parse_pow()?;
6965        loop {
6966            self.skip_ws();
6967            match self.peek() {
6968                Some('*') => {
6969                    self.consume();
6970                    let r = self.parse_pow()?;
6971                    left = Expr::Mul(Box::new(left), Box::new(r));
6972                }
6973                Some('/') => {
6974                    self.consume();
6975                    let r = self.parse_pow()?;
6976                    left = Expr::Div(Box::new(left), Box::new(r));
6977                }
6978                // Implicit multiplication: if next token is a function or '(' or var
6979                Some(c) if c.is_alphabetic() || c == '(' => {
6980                    let r = self.parse_pow()?;
6981                    left = Expr::Mul(Box::new(left), Box::new(r));
6982                }
6983                _ => break,
6984            }
6985        }
6986        Ok(left)
6987    }
6988
6989    fn parse_pow(&mut self) -> Result<Expr, String> {
6990        let base = self.parse_unary()?;
6991        self.skip_ws();
6992        if self.peek() == Some('^') {
6993            self.consume();
6994            let exp = self.parse_unary()?;
6995            return Ok(Expr::Pow(Box::new(base), Box::new(exp)));
6996        }
6997        Ok(base)
6998    }
6999
7000    fn parse_unary(&mut self) -> Result<Expr, String> {
7001        self.skip_ws();
7002        if self.peek() == Some('-') {
7003            self.consume();
7004            let inner = self.parse_atom()?;
7005            return Ok(Expr::Neg(Box::new(inner)));
7006        }
7007        if self.peek() == Some('+') {
7008            self.consume();
7009        }
7010        self.parse_atom()
7011    }
7012
7013    fn parse_atom(&mut self) -> Result<Expr, String> {
7014        self.skip_ws();
7015        match self.peek() {
7016            Some('(') => {
7017                self.consume();
7018                let inner = self.parse_expr()?;
7019                self.skip_ws();
7020                if self.peek() == Some(')') {
7021                    self.consume();
7022                }
7023                Ok(inner)
7024            }
7025            Some(c) if c.is_ascii_digit() || c == '.' => self.parse_number(),
7026            Some(c) if c.is_alphabetic() || c == '_' => self.parse_name(),
7027            Some(c) => Err(format!("unexpected char '{}'", c)),
7028            None => Err("unexpected end".into()),
7029        }
7030    }
7031
7032    fn parse_number(&mut self) -> Result<Expr, String> {
7033        let start = self.pos;
7034        while matches!(self.peek(), Some(c) if c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E')
7035        {
7036            self.pos += 1;
7037            // handle e+/e-
7038            if matches!(self.chars.get(self.pos - 1), Some('e') | Some('E'))
7039                && matches!(self.peek(), Some('+') | Some('-'))
7040            {
7041                self.pos += 1;
7042            }
7043        }
7044        let s: String = self.chars[start..self.pos].iter().collect();
7045        s.parse::<f64>()
7046            .map(Expr::Num)
7047            .map_err(|_| format!("bad number: {}", s))
7048    }
7049
7050    fn parse_name(&mut self) -> Result<Expr, String> {
7051        let start = self.pos;
7052        while matches!(self.peek(), Some(c) if c.is_alphanumeric() || c == '_') {
7053            self.pos += 1;
7054        }
7055        let name: String = self.chars[start..self.pos].iter().collect();
7056        self.skip_ws();
7057        // Check if it's a function call
7058        if self.peek() == Some('(') {
7059            self.consume();
7060            let arg = self.parse_expr()?;
7061            self.skip_ws();
7062            if self.peek() == Some(')') {
7063                self.consume();
7064            }
7065            let e = Box::new(arg);
7066            return match name.to_lowercase().as_str() {
7067                "sin" => Ok(Expr::Sin(e)),
7068                "cos" => Ok(Expr::Cos(e)),
7069                "tan" => Ok(Expr::Tan(e)),
7070                "ln" => Ok(Expr::Ln(e)),
7071                "log" => Ok(Expr::Ln(e)), // treat log as natural log
7072                "exp" => Ok(Expr::Exp(e)),
7073                "sqrt" => Ok(Expr::Sqrt(e)),
7074                "abs" => Ok(Expr::Abs(e)),
7075                _ => Err(format!("unknown function: {}", name)),
7076            };
7077        }
7078        // Constants
7079        match name.as_str() {
7080            "pi" | "PI" => return Ok(Expr::Num(std::f64::consts::PI)),
7081            "e" | "E" => return Ok(Expr::Num(std::f64::consts::E)),
7082            _ => {}
7083        }
7084        Ok(Expr::Var(name))
7085    }
7086}
7087
7088fn parse_sym(s: &str) -> Result<Expr, String> {
7089    let chars: Vec<char> = s.chars().collect();
7090    let mut p = Parser::new(&chars);
7091    let e = p.parse_expr()?;
7092    p.skip_ws();
7093    if p.pos < p.chars.len() {
7094        // Tolerate trailing whitespace/comments
7095        let rest: String = p.chars[p.pos..].iter().collect();
7096        if !rest.trim().is_empty() {
7097            return Err(format!("unexpected trailing: '{}'", rest.trim()));
7098        }
7099    }
7100    Ok(e)
7101}
7102
7103// ── Pretty-printer ──────────────────────────────────────────────────────────
7104
7105fn fmt_expr(e: &Expr) -> String {
7106    match e {
7107        Expr::Num(n) => {
7108            if n.fract() == 0.0 && n.abs() < 1e12 {
7109                format!("{}", *n as i64)
7110            } else {
7111                format!("{}", n)
7112            }
7113        }
7114        Expr::Var(v) => v.clone(),
7115        Expr::Add(a, b) => format!("({} + {})", fmt_expr(a), fmt_expr(b)),
7116        Expr::Sub(a, b) => format!("({} - {})", fmt_expr(a), fmt_expr(b)),
7117        Expr::Mul(a, b) => format!("({} * {})", fmt_expr(a), fmt_expr(b)),
7118        Expr::Div(a, b) => format!("({} / {})", fmt_expr(a), fmt_expr(b)),
7119        Expr::Pow(a, b) => format!("({}^{})", fmt_expr(a), fmt_expr(b)),
7120        Expr::Neg(a) => format!("(-{})", fmt_expr(a)),
7121        Expr::Sin(a) => format!("sin({})", fmt_expr(a)),
7122        Expr::Cos(a) => format!("cos({})", fmt_expr(a)),
7123        Expr::Tan(a) => format!("tan({})", fmt_expr(a)),
7124        Expr::Ln(a) => format!("ln({})", fmt_expr(a)),
7125        Expr::Exp(a) => format!("exp({})", fmt_expr(a)),
7126        Expr::Sqrt(a) => format!("sqrt({})", fmt_expr(a)),
7127        Expr::Abs(a) => format!("abs({})", fmt_expr(a)),
7128    }
7129}
7130
7131// ── Simplification ─────────────────────────────────────────────────────────
7132// Single-pass algebraic simplification: fold constants, remove identity ops.
7133
7134fn simplify(e: Expr) -> Expr {
7135    match e {
7136        Expr::Add(a, b) => {
7137            let a = simplify(*a);
7138            let b = simplify(*b);
7139            match (&a, &b) {
7140                (Expr::Num(x), Expr::Num(y)) => Expr::Num(x + y),
7141                (Expr::Num(0.0), _) => b,
7142                (_, Expr::Num(0.0)) => a,
7143                _ => Expr::Add(Box::new(a), Box::new(b)),
7144            }
7145        }
7146        Expr::Sub(a, b) => {
7147            let a = simplify(*a);
7148            let b = simplify(*b);
7149            match (&a, &b) {
7150                (Expr::Num(x), Expr::Num(y)) => Expr::Num(x - y),
7151                (_, Expr::Num(0.0)) => a,
7152                _ if fmt_expr(&a) == fmt_expr(&b) => Expr::Num(0.0),
7153                _ => Expr::Sub(Box::new(a), Box::new(b)),
7154            }
7155        }
7156        Expr::Mul(a, b) => {
7157            let a = simplify(*a);
7158            let b = simplify(*b);
7159            match (&a, &b) {
7160                (Expr::Num(x), Expr::Num(y)) => Expr::Num(x * y),
7161                (Expr::Num(0.0), _) | (_, Expr::Num(0.0)) => Expr::Num(0.0),
7162                (Expr::Num(1.0), _) => b,
7163                (_, Expr::Num(1.0)) => a,
7164                (Expr::Num(-1.0), _) => Expr::Neg(Box::new(b)),
7165                _ => Expr::Mul(Box::new(a), Box::new(b)),
7166            }
7167        }
7168        Expr::Div(a, b) => {
7169            let a = simplify(*a);
7170            let b = simplify(*b);
7171            match (&a, &b) {
7172                (_, Expr::Num(1.0)) => a,
7173                (Expr::Num(x), Expr::Num(y)) if *y != 0.0 => Expr::Num(x / y),
7174                _ => Expr::Div(Box::new(a), Box::new(b)),
7175            }
7176        }
7177        Expr::Pow(a, b) => {
7178            let a = simplify(*a);
7179            let b = simplify(*b);
7180            match (&a, &b) {
7181                (_, Expr::Num(0.0)) => Expr::Num(1.0),
7182                (_, Expr::Num(1.0)) => a,
7183                (Expr::Num(1.0), _) => Expr::Num(1.0),
7184                (Expr::Num(x), Expr::Num(y)) => Expr::Num(x.powf(*y)),
7185                _ => Expr::Pow(Box::new(a), Box::new(b)),
7186            }
7187        }
7188        Expr::Neg(a) => {
7189            let a = simplify(*a);
7190            match a {
7191                Expr::Num(n) => Expr::Num(-n),
7192                Expr::Neg(inner) => *inner,
7193                _ => Expr::Neg(Box::new(a)),
7194            }
7195        }
7196        Expr::Sin(a) => {
7197            let a = simplify(*a);
7198            Expr::Sin(Box::new(a))
7199        }
7200        Expr::Cos(a) => {
7201            let a = simplify(*a);
7202            Expr::Cos(Box::new(a))
7203        }
7204        Expr::Tan(a) => {
7205            let a = simplify(*a);
7206            Expr::Tan(Box::new(a))
7207        }
7208        Expr::Ln(a) => {
7209            let a = simplify(*a);
7210            if let Expr::Num(n) = &a {
7211                if (*n - std::f64::consts::E).abs() < 1e-12 {
7212                    return Expr::Num(1.0);
7213                }
7214            }
7215            Expr::Ln(Box::new(a))
7216        }
7217        Expr::Exp(a) => {
7218            let a = simplify(*a);
7219            if let Expr::Num(n) = &a {
7220                return Expr::Num(n.exp());
7221            }
7222            if let Expr::Num(0.0) = &a {
7223                return Expr::Num(1.0);
7224            }
7225            Expr::Exp(Box::new(a))
7226        }
7227        Expr::Sqrt(a) => {
7228            let a = simplify(*a);
7229            if let Expr::Num(n) = &a {
7230                if *n >= 0.0 {
7231                    return Expr::Num(n.sqrt());
7232                }
7233            }
7234            Expr::Sqrt(Box::new(a))
7235        }
7236        other => other,
7237    }
7238}
7239
7240// ── Differentiation ────────────────────────────────────────────────────────
7241
7242fn diff(e: &Expr, var: &str) -> Expr {
7243    match e {
7244        Expr::Num(_) => Expr::Num(0.0),
7245        Expr::Var(v) => {
7246            if v == var {
7247                Expr::Num(1.0)
7248            } else {
7249                Expr::Num(0.0)
7250            }
7251        }
7252        Expr::Add(a, b) => simplify(Expr::Add(Box::new(diff(a, var)), Box::new(diff(b, var)))),
7253        Expr::Sub(a, b) => simplify(Expr::Sub(Box::new(diff(a, var)), Box::new(diff(b, var)))),
7254        Expr::Mul(a, b) => {
7255            // product rule: f'g + fg'
7256            let fp_g = Expr::Mul(Box::new(diff(a, var)), Box::new(*b.clone()));
7257            let f_gp = Expr::Mul(Box::new(*a.clone()), Box::new(diff(b, var)));
7258            simplify(Expr::Add(Box::new(fp_g), Box::new(f_gp)))
7259        }
7260        Expr::Div(a, b) => {
7261            // quotient rule: (f'g - fg') / g^2
7262            let fp_g = Expr::Mul(Box::new(diff(a, var)), Box::new(*b.clone()));
7263            let f_gp = Expr::Mul(Box::new(*a.clone()), Box::new(diff(b, var)));
7264            let num = Expr::Sub(Box::new(fp_g), Box::new(f_gp));
7265            let den = Expr::Pow(Box::new(*b.clone()), Box::new(Expr::Num(2.0)));
7266            simplify(Expr::Div(Box::new(num), Box::new(den)))
7267        }
7268        Expr::Pow(base, exp) => {
7269            // If exp is a constant: power rule n*x^(n-1) * base'
7270            if let Expr::Num(n) = exp.as_ref() {
7271                let new_exp = Expr::Num(n - 1.0);
7272                let power = Expr::Pow(Box::new(*base.clone()), Box::new(new_exp));
7273                let coeff = Expr::Mul(Box::new(Expr::Num(*n)), Box::new(power));
7274                let chain = diff(base, var);
7275                return simplify(Expr::Mul(Box::new(coeff), Box::new(chain)));
7276            }
7277            // General: e^(exp * ln(base)) rule: f^g * (g' ln f + g f'/f)
7278            let ln_base = Expr::Ln(Box::new(*base.clone()));
7279            let g_ln_f = Expr::Mul(Box::new(*exp.clone()), Box::new(ln_base));
7280            let g_ln_f_d = diff(&g_ln_f, var);
7281            let result = Expr::Mul(Box::new(e.clone()), Box::new(g_ln_f_d));
7282            simplify(result)
7283        }
7284        Expr::Neg(a) => simplify(Expr::Neg(Box::new(diff(a, var)))),
7285        Expr::Sin(a) => {
7286            let cos_a = Expr::Cos(Box::new(*a.clone()));
7287            simplify(Expr::Mul(Box::new(cos_a), Box::new(diff(a, var))))
7288        }
7289        Expr::Cos(a) => {
7290            let neg_sin = Expr::Neg(Box::new(Expr::Sin(Box::new(*a.clone()))));
7291            simplify(Expr::Mul(Box::new(neg_sin), Box::new(diff(a, var))))
7292        }
7293        Expr::Tan(a) => {
7294            // sec^2(a) * a' = 1/cos^2(a) * a'
7295            let cos_a = Expr::Cos(Box::new(*a.clone()));
7296            let cos2 = Expr::Pow(Box::new(cos_a), Box::new(Expr::Num(2.0)));
7297            let sec2 = Expr::Div(Box::new(Expr::Num(1.0)), Box::new(cos2));
7298            simplify(Expr::Mul(Box::new(sec2), Box::new(diff(a, var))))
7299        }
7300        Expr::Ln(a) => {
7301            // 1/a * a'
7302            let inv_a = Expr::Div(Box::new(Expr::Num(1.0)), Box::new(*a.clone()));
7303            simplify(Expr::Mul(Box::new(inv_a), Box::new(diff(a, var))))
7304        }
7305        Expr::Exp(a) => {
7306            // exp(a) * a'
7307            simplify(Expr::Mul(Box::new(e.clone()), Box::new(diff(a, var))))
7308        }
7309        Expr::Sqrt(a) => {
7310            // 1/(2*sqrt(a)) * a'
7311            let two_sqrt = Expr::Mul(
7312                Box::new(Expr::Num(2.0)),
7313                Box::new(Expr::Sqrt(Box::new(*a.clone()))),
7314            );
7315            let inv = Expr::Div(Box::new(Expr::Num(1.0)), Box::new(two_sqrt));
7316            simplify(Expr::Mul(Box::new(inv), Box::new(diff(a, var))))
7317        }
7318        Expr::Abs(a) => {
7319            // d/dx |a| = a/|a| * a'  (sign(a) * a')
7320            let sign = Expr::Div(
7321                Box::new(*a.clone()),
7322                Box::new(Expr::Abs(Box::new(*a.clone()))),
7323            );
7324            simplify(Expr::Mul(Box::new(sign), Box::new(diff(a, var))))
7325        }
7326    }
7327}
7328
7329// ── Integration (table lookup + linearity) ──────────────────────────────────
7330// Returns Some(integral) for forms in the table, None for unknowns.
7331// Adds no "+ C" — the caller adds it.
7332
7333fn integrate(e: &Expr, var: &str) -> Option<Expr> {
7334    match e {
7335        Expr::Num(n) => {
7336            // ∫ n dx = n*x
7337            Some(Expr::Mul(
7338                Box::new(Expr::Num(*n)),
7339                Box::new(Expr::Var(var.to_string())),
7340            ))
7341        }
7342        Expr::Var(v) => {
7343            if v == var {
7344                // ∫ x dx = x^2/2
7345                Some(Expr::Div(
7346                    Box::new(Expr::Pow(
7347                        Box::new(Expr::Var(v.clone())),
7348                        Box::new(Expr::Num(2.0)),
7349                    )),
7350                    Box::new(Expr::Num(2.0)),
7351                ))
7352            } else {
7353                // ∫ c dx where c is another variable treated as constant
7354                Some(Expr::Mul(
7355                    Box::new(Expr::Var(v.clone())),
7356                    Box::new(Expr::Var(var.to_string())),
7357                ))
7358            }
7359        }
7360        Expr::Add(a, b) => {
7361            let ia = integrate(a, var)?;
7362            let ib = integrate(b, var)?;
7363            Some(simplify(Expr::Add(Box::new(ia), Box::new(ib))))
7364        }
7365        Expr::Sub(a, b) => {
7366            let ia = integrate(a, var)?;
7367            let ib = integrate(b, var)?;
7368            Some(simplify(Expr::Sub(Box::new(ia), Box::new(ib))))
7369        }
7370        Expr::Neg(a) => {
7371            let ia = integrate(a, var)?;
7372            Some(simplify(Expr::Neg(Box::new(ia))))
7373        }
7374        Expr::Mul(a, b) => {
7375            // c * f(x) where c is constant
7376            if !contains_var(a, var) {
7377                let ib = integrate(b, var)?;
7378                return Some(simplify(Expr::Mul(Box::new(*a.clone()), Box::new(ib))));
7379            }
7380            if !contains_var(b, var) {
7381                let ia = integrate(a, var)?;
7382                return Some(simplify(Expr::Mul(Box::new(*b.clone()), Box::new(ia))));
7383            }
7384            None // general product — no IBP
7385        }
7386        Expr::Pow(base, exp) => {
7387            if let Expr::Var(v) = base.as_ref() {
7388                if v == var {
7389                    if let Expr::Num(n) = exp.as_ref() {
7390                        if (*n + 1.0).abs() < 1e-12 {
7391                            // ∫ x^-1 = ln|x|
7392                            return Some(Expr::Ln(Box::new(Expr::Abs(Box::new(Expr::Var(
7393                                v.clone(),
7394                            ))))));
7395                        }
7396                        // ∫ x^n = x^(n+1)/(n+1)
7397                        let new_exp = Expr::Num(n + 1.0);
7398                        let pow =
7399                            Expr::Pow(Box::new(Expr::Var(v.clone())), Box::new(new_exp.clone()));
7400                        return Some(simplify(Expr::Div(Box::new(pow), Box::new(new_exp))));
7401                    }
7402                }
7403            }
7404            None
7405        }
7406        Expr::Sin(a) => {
7407            if let Expr::Var(v) = a.as_ref() {
7408                if v == var {
7409                    return Some(Expr::Neg(Box::new(Expr::Cos(Box::new(*a.clone())))));
7410                }
7411            }
7412            // ∫ sin(n*x) = -cos(n*x)/n
7413            if let Some((coeff, _inner_var)) = linear_coeff(a, var) {
7414                let cos_part = Expr::Cos(Box::new(*a.clone()));
7415                let neg_cos = Expr::Neg(Box::new(cos_part));
7416                return Some(simplify(Expr::Div(
7417                    Box::new(neg_cos),
7418                    Box::new(Expr::Num(coeff)),
7419                )));
7420            }
7421            None
7422        }
7423        Expr::Cos(a) => {
7424            if let Expr::Var(v) = a.as_ref() {
7425                if v == var {
7426                    return Some(Expr::Sin(Box::new(*a.clone())));
7427                }
7428            }
7429            if let Some((coeff, _)) = linear_coeff(a, var) {
7430                let sin_part = Expr::Sin(Box::new(*a.clone()));
7431                return Some(simplify(Expr::Div(
7432                    Box::new(sin_part),
7433                    Box::new(Expr::Num(coeff)),
7434                )));
7435            }
7436            None
7437        }
7438        Expr::Exp(a) => {
7439            if let Expr::Var(v) = a.as_ref() {
7440                if v == var {
7441                    return Some(e.clone()); // ∫ e^x = e^x
7442                }
7443            }
7444            if let Some((coeff, _)) = linear_coeff(a, var) {
7445                return Some(simplify(Expr::Div(
7446                    Box::new(e.clone()),
7447                    Box::new(Expr::Num(coeff)),
7448                )));
7449            }
7450            None
7451        }
7452        Expr::Ln(a) => {
7453            if let Expr::Var(v) = a.as_ref() {
7454                if v == var {
7455                    // ∫ ln(x) = x*ln(x) - x
7456                    let x_ln_x = Expr::Mul(Box::new(Expr::Var(v.clone())), Box::new(e.clone()));
7457                    return Some(simplify(Expr::Sub(
7458                        Box::new(x_ln_x),
7459                        Box::new(Expr::Var(v.clone())),
7460                    )));
7461                }
7462            }
7463            None
7464        }
7465        Expr::Div(a, b) => {
7466            // ∫ 1/x = ln|x|
7467            if let (Expr::Num(1.0), Expr::Var(v)) = (a.as_ref(), b.as_ref()) {
7468                if v == var {
7469                    return Some(Expr::Ln(Box::new(Expr::Abs(Box::new(Expr::Var(
7470                        v.clone(),
7471                    ))))));
7472                }
7473            }
7474            // ∫ c/x = c*ln|x|
7475            if let Expr::Var(v) = b.as_ref() {
7476                if v == var && !contains_var(a, var) {
7477                    let ln_abs = Expr::Ln(Box::new(Expr::Abs(Box::new(Expr::Var(v.clone())))));
7478                    return Some(simplify(Expr::Mul(Box::new(*a.clone()), Box::new(ln_abs))));
7479                }
7480            }
7481            None
7482        }
7483        _ => None,
7484    }
7485}
7486
7487// Returns Some((coeff, var)) if expr = coeff * var + constant (linear in var)
7488fn linear_coeff<'a>(e: &'a Expr, var: &'a str) -> Option<(f64, &'a str)> {
7489    match e {
7490        Expr::Mul(a, b) => {
7491            if let (Expr::Num(c), Expr::Var(v)) = (a.as_ref(), b.as_ref()) {
7492                if v == var {
7493                    return Some((*c, var));
7494                }
7495            }
7496            if let (Expr::Var(v), Expr::Num(c)) = (a.as_ref(), b.as_ref()) {
7497                if v == var {
7498                    return Some((*c, var));
7499                }
7500            }
7501            None
7502        }
7503        _ => None,
7504    }
7505}
7506
7507fn contains_var(e: &Expr, var: &str) -> bool {
7508    match e {
7509        Expr::Var(v) => v == var,
7510        Expr::Num(_) => false,
7511        Expr::Add(a, b) | Expr::Sub(a, b) | Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Pow(a, b) => {
7512            contains_var(a, var) || contains_var(b, var)
7513        }
7514        Expr::Neg(a)
7515        | Expr::Sin(a)
7516        | Expr::Cos(a)
7517        | Expr::Tan(a)
7518        | Expr::Ln(a)
7519        | Expr::Exp(a)
7520        | Expr::Sqrt(a)
7521        | Expr::Abs(a) => contains_var(a, var),
7522    }
7523}
7524
7525// ── Numeric eval ──────────────────────────────────────────────────────────
7526
7527fn eval_expr(e: &Expr, var: &str, val: f64) -> Result<f64, String> {
7528    match e {
7529        Expr::Num(n) => Ok(*n),
7530        Expr::Var(v) => {
7531            if v == var {
7532                Ok(val)
7533            } else {
7534                Err(format!("unbound variable: {}", v))
7535            }
7536        }
7537        Expr::Add(a, b) => Ok(eval_expr(a, var, val)? + eval_expr(b, var, val)?),
7538        Expr::Sub(a, b) => Ok(eval_expr(a, var, val)? - eval_expr(b, var, val)?),
7539        Expr::Mul(a, b) => Ok(eval_expr(a, var, val)? * eval_expr(b, var, val)?),
7540        Expr::Div(a, b) => {
7541            let d = eval_expr(b, var, val)?;
7542            if d.abs() < 1e-300 {
7543                return Err("division by zero".into());
7544            }
7545            Ok(eval_expr(a, var, val)? / d)
7546        }
7547        Expr::Pow(a, b) => Ok(eval_expr(a, var, val)?.powf(eval_expr(b, var, val)?)),
7548        Expr::Neg(a) => Ok(-eval_expr(a, var, val)?),
7549        Expr::Sin(a) => Ok(eval_expr(a, var, val)?.sin()),
7550        Expr::Cos(a) => Ok(eval_expr(a, var, val)?.cos()),
7551        Expr::Tan(a) => Ok(eval_expr(a, var, val)?.tan()),
7552        Expr::Ln(a) => Ok(eval_expr(a, var, val)?.ln()),
7553        Expr::Exp(a) => Ok(eval_expr(a, var, val)?.exp()),
7554        Expr::Sqrt(a) => Ok(eval_expr(a, var, val)?.sqrt()),
7555        Expr::Abs(a) => Ok(eval_expr(a, var, val)?.abs()),
7556    }
7557}
7558
7559// ── Public entry point ────────────────────────────────────────────────────
7560
7561pub fn symbolic_calc(query: &str) -> String {
7562    let q = query.trim();
7563
7564    // Parse optional "wrt VAR" suffix
7565    let (q_body, var) = if let Some(pos) = q.to_lowercase().rfind(" wrt ") {
7566        let v = q[pos + 5..].trim().to_string();
7567        (q[..pos].trim(), v)
7568    } else {
7569        (q, "x".to_string())
7570    };
7571
7572    // Parse mode
7573    let (mode, expr_str) = {
7574        let low = q_body.to_lowercase();
7575        if low.starts_with("diff ")
7576            || low.starts_with("differentiate ")
7577            || low.starts_with("d/dx ")
7578            || low.starts_with("d/d")
7579        {
7580            // d/dy expr — extract var from d/dy if present
7581            let (m, rest, var_from_mode) = if low.starts_with("d/d") {
7582                let after = &q_body[3..];
7583                let sp = after.find(char::is_whitespace).unwrap_or(after.len());
7584                let v = after[..sp].to_string();
7585                let rest = after[sp..].trim();
7586                ("diff", rest, Some(v))
7587            } else {
7588                let rest = q_body
7589                    .split_once(char::is_whitespace)
7590                    .map(|x| x.1)
7591                    .unwrap_or("")
7592                    .trim();
7593                ("diff", rest, None)
7594            };
7595            let var2 = var_from_mode.unwrap_or_else(|| var.clone());
7596            (m, (rest.to_string(), var2))
7597        } else if low.starts_with("int ") || low.starts_with("integrate ") || low.starts_with("∫")
7598        {
7599            let rest = q_body
7600                .split_once(char::is_whitespace)
7601                .map(|x| x.1)
7602                .unwrap_or("")
7603                .trim();
7604            ("integrate", (rest.to_string(), var.clone()))
7605        } else if low.starts_with("simplify ") || low.starts_with("simplify") {
7606            let rest = q_body
7607                .split_once(char::is_whitespace)
7608                .map(|x| x.1)
7609                .unwrap_or("")
7610                .trim();
7611            ("simplify", (rest.to_string(), var.clone()))
7612        } else if low.contains(" at ") {
7613            ("eval", (q_body.to_string(), var.clone()))
7614        } else {
7615            // Default: try to detect if expr contains d/dx or integral sign
7616            ("diff", (q_body.to_string(), var.clone()))
7617        }
7618    };
7619
7620    let (expr_text, var_name) = expr_str;
7621    let var_name = var_name.trim().to_string();
7622    let var_name = if var_name.is_empty() {
7623        "x".to_string()
7624    } else {
7625        var_name
7626    };
7627
7628    let mut out = String::new();
7629    let w = 64usize;
7630    let _ = writeln!(out, "{}", "=".repeat(w));
7631    let _ = writeln!(out, "  Symbolic Calculus");
7632
7633    if mode == "eval" {
7634        // "expr at var=value"
7635        let parts: Vec<&str> = expr_text.splitn(2, " at ").collect();
7636        if parts.len() != 2 {
7637            let _ = writeln!(out, "  Error: use 'EXPR at VAR=VALUE'");
7638            return out;
7639        }
7640        let e_str = parts[0].trim();
7641        let at_str = parts[1].trim();
7642        let (av, val_str) = if let Some(eq) = at_str.find('=') {
7643            (&at_str[..eq], &at_str[eq + 1..])
7644        } else {
7645            (&var_name[..], at_str)
7646        };
7647        let val: f64 = match val_str.trim().parse() {
7648            Ok(v) => v,
7649            Err(_) => {
7650                let _ = writeln!(out, "  Error: bad value '{}'", val_str);
7651                return out;
7652            }
7653        };
7654        match parse_sym(e_str) {
7655            Ok(expr) => {
7656                let _ = writeln!(out, "  f({}) = {}", av, fmt_expr(&expr));
7657                match eval_expr(&expr, av.trim(), val) {
7658                    Ok(result) => {
7659                        let _ = writeln!(out, "  f({} = {}) = {}", av.trim(), val, result);
7660                    }
7661                    Err(e) => {
7662                        let _ = writeln!(out, "  Eval error: {}", e);
7663                    }
7664                }
7665            }
7666            Err(e) => {
7667                let _ = writeln!(out, "  Parse error: {}", e);
7668            }
7669        }
7670        let _ = writeln!(out, "{}", "=".repeat(w));
7671        return out;
7672    }
7673
7674    let expr_text = expr_text.trim();
7675    match parse_sym(expr_text) {
7676        Err(e) => {
7677            let _ = writeln!(out, "  Parse error: {}", e);
7678            let _ = writeln!(out, "  Input: {}", expr_text);
7679            let _ = writeln!(out, "{}", "=".repeat(w));
7680            return out;
7681        }
7682        Ok(expr) => {
7683            let simplified = simplify(expr.clone());
7684            let _ = writeln!(out, "  f({}) = {}", var_name, fmt_expr(&simplified));
7685            match mode {
7686                "diff" => {
7687                    let d = diff(&simplified, &var_name);
7688                    let d_simp = simplify(d);
7689                    let _ = writeln!(out, "  d/d{} = {}", var_name, fmt_expr(&d_simp));
7690                    // Spot-check with numeric diff at x=1.5
7691                    let h = 1e-6f64;
7692                    let x0 = 1.5f64;
7693                    if let (Ok(fp), Ok(fm)) = (
7694                        eval_expr(&simplified, &var_name, x0 + h),
7695                        eval_expr(&simplified, &var_name, x0 - h),
7696                    ) {
7697                        let numeric = (fp - fm) / (2.0 * h);
7698                        if let Ok(symbolic_val) = eval_expr(&d_simp, &var_name, x0) {
7699                            let err = (symbolic_val - numeric).abs();
7700                            if err < 1e-4 {
7701                                let _ = writeln!(out, "  ✓ Verified: numeric check at {}={} → diff={:.6}, numeric={:.6}", var_name, x0, symbolic_val, numeric);
7702                            } else {
7703                                let _ = writeln!(out, "  ⚠ Numeric check mismatch at {}={}: symbolic={:.6}, numeric={:.6}", var_name, x0, symbolic_val, numeric);
7704                            }
7705                        }
7706                    }
7707                }
7708                "integrate" => {
7709                    match integrate(&simplified, &var_name) {
7710                        Some(integral) => {
7711                            let i_simp = simplify(integral);
7712                            let _ = writeln!(out, "  ∫f d{} = {} + C", var_name, fmt_expr(&i_simp));
7713                            // Verify by differentiating the result
7714                            let check = simplify(diff(&i_simp, &var_name));
7715                            let orig = fmt_expr(&simplified);
7716                            let back = fmt_expr(&check);
7717                            if orig == back {
7718                                let _ =
7719                                    writeln!(out, "  ✓ Verified: d/d{} of integral = f", var_name);
7720                            } else {
7721                                // Numeric check at a sample point
7722                                let x0 = 1.5f64;
7723                                if let (Ok(v1), Ok(v2)) = (
7724                                    eval_expr(&simplified, &var_name, x0),
7725                                    eval_expr(&check, &var_name, x0),
7726                                ) {
7727                                    if (v1 - v2).abs() < 1e-6 {
7728                                        let _ = writeln!(out, "  ✓ Numerically verified: d/d{}(integral) = f at {}={}", var_name, var_name, x0);
7729                                    } else {
7730                                        let _ = writeln!(
7731                                            out,
7732                                            "  ⚠ Verification: d/d{}(integral) = {} (expected {})",
7733                                            var_name, back, orig
7734                                        );
7735                                    }
7736                                }
7737                            }
7738                        }
7739                        None => {
7740                            let _ = writeln!(
7741                                out,
7742                                "  ∫f d{} = (not in table — try --simulate or a CAS)",
7743                                var_name
7744                            );
7745                        }
7746                    }
7747                }
7748                "simplify" => {
7749                    let _ = writeln!(out, "  simplified: {}", fmt_expr(&simplified));
7750                }
7751                _ => {}
7752            }
7753        }
7754    }
7755
7756    let _ = writeln!(out, "{}", "=".repeat(w));
7757    out
7758}
7759
7760#[allow(dead_code)]
7761fn symbolic_usage() -> String {
7762    "Symbolic calculus:\n\
7763     hematite --symbolic 'diff x^3 + 2*x'          differentiate (wrt x)\n\
7764     hematite --symbolic 'diff sin(x)*cos(x)'        product rule\n\
7765     hematite --symbolic 'diff x^2 + y wrt y'        differentiate wrt y\n\
7766     hematite --symbolic 'integrate x^3'             antiderivative\n\
7767     hematite --symbolic 'integrate sin(x) + 3*x^2' linearity\n\
7768     hematite --symbolic 'simplify (x+1)*(x+1)'      simplify\n\
7769     hematite --symbolic 'x^2 + 2*x at x=3'          numeric eval\n\
7770     Supported: + - * / ^ sin cos tan ln exp sqrt abs"
7771        .into()
7772}
7773
7774// ── Signal processing (DSP) ───────────────────────────────────────────────────
7775// DFT/IDFT, convolution, cross-correlation, moving average,
7776// FIR window filter design, waveform generation, RMS/SNR stats.
7777// All pure-Rust — no Python subprocess, no external crates.
7778
7779use std::f64::consts::PI;
7780
7781fn dft(signal: &[f64]) -> Vec<(f64, f64)> {
7782    let n = signal.len();
7783    (0..n)
7784        .map(|k| {
7785            let (mut re, mut im) = (0.0_f64, 0.0_f64);
7786            for (t, &x) in signal.iter().enumerate() {
7787                let angle = -2.0 * PI * (k * t) as f64 / n as f64;
7788                re += x * angle.cos();
7789                im += x * angle.sin();
7790            }
7791            (re, im)
7792        })
7793        .collect()
7794}
7795
7796fn idft(spectrum: &[(f64, f64)]) -> Vec<f64> {
7797    let n = spectrum.len();
7798    (0..n)
7799        .map(|t| {
7800            let mut val = 0.0_f64;
7801            for (k, &(re, im)) in spectrum.iter().enumerate() {
7802                let angle = 2.0 * PI * (k * t) as f64 / n as f64;
7803                val += re * angle.cos() - im * angle.sin();
7804            }
7805            val / n as f64
7806        })
7807        .collect()
7808}
7809
7810fn convolve(x: &[f64], h: &[f64]) -> Vec<f64> {
7811    let n = x.len() + h.len() - 1;
7812    (0..n)
7813        .map(|i| {
7814            let mut s = 0.0_f64;
7815            for (j, &hv) in h.iter().enumerate() {
7816                if i >= j && i - j < x.len() {
7817                    s += x[i - j] * hv;
7818                }
7819            }
7820            s
7821        })
7822        .collect()
7823}
7824
7825fn xcorr(x: &[f64], y: &[f64]) -> Vec<f64> {
7826    let n = x.len();
7827    let m = y.len();
7828    let out_len = n + m - 1;
7829    (0..out_len)
7830        .map(|lag| {
7831            let lag_i = lag as isize - (m as isize - 1);
7832            let mut s = 0.0_f64;
7833            for (i, &xv) in x.iter().enumerate() {
7834                let j = i as isize - lag_i;
7835                if j >= 0 && j < m as isize {
7836                    s += xv * y[j as usize];
7837                }
7838            }
7839            s
7840        })
7841        .collect()
7842}
7843
7844fn moving_avg(signal: &[f64], window: usize) -> Vec<f64> {
7845    let w = window.max(1);
7846    signal
7847        .windows(w)
7848        .map(|s| s.iter().sum::<f64>() / w as f64)
7849        .collect()
7850}
7851
7852fn hann_window(n: usize) -> Vec<f64> {
7853    (0..n)
7854        .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f64 / (n - 1) as f64).cos()))
7855        .collect()
7856}
7857
7858fn hamming_window(n: usize) -> Vec<f64> {
7859    (0..n)
7860        .map(|i| 0.54 - 0.46 * (2.0 * PI * i as f64 / (n - 1) as f64).cos())
7861        .collect()
7862}
7863
7864fn blackman_window(n: usize) -> Vec<f64> {
7865    (0..n)
7866        .map(|i| {
7867            let a = 2.0 * PI * i as f64 / (n - 1) as f64;
7868            0.42 - 0.5 * a.cos() + 0.08 * (2.0 * a).cos()
7869        })
7870        .collect()
7871}
7872
7873fn sinc(x: f64) -> f64 {
7874    if x == 0.0 {
7875        1.0
7876    } else {
7877        (PI * x).sin() / (PI * x)
7878    }
7879}
7880
7881fn fir_lowpass(n_taps: usize, cutoff_norm: f64, window: &str) -> Vec<f64> {
7882    let m = n_taps - 1;
7883    let wins: Vec<f64> = match window {
7884        "hann" | "hanning" => hann_window(n_taps),
7885        "hamming" => hamming_window(n_taps),
7886        "blackman" => blackman_window(n_taps),
7887        _ => vec![1.0; n_taps],
7888    };
7889    let mut h: Vec<f64> = (0..n_taps)
7890        .map(|i| {
7891            let n = i as f64 - m as f64 / 2.0;
7892            2.0 * cutoff_norm * sinc(2.0 * cutoff_norm * n) * wins[i]
7893        })
7894        .collect();
7895    let sum: f64 = h.iter().sum();
7896    if sum.abs() > 1e-12 {
7897        for v in &mut h {
7898            *v /= sum;
7899        }
7900    }
7901    h
7902}
7903
7904fn fir_highpass(n_taps: usize, cutoff_norm: f64, window: &str) -> Vec<f64> {
7905    let mut h = fir_lowpass(n_taps, cutoff_norm, window);
7906    for (i, v) in h.iter_mut().enumerate() {
7907        *v = if i == n_taps / 2 { 1.0 - *v } else { -*v };
7908    }
7909    h
7910}
7911
7912fn parse_signal(s: &str) -> Option<Vec<f64>> {
7913    let v: Vec<f64> = s
7914        .split([',', ' ', '\t', ';'].as_ref())
7915        .filter_map(|t| t.trim().parse::<f64>().ok())
7916        .collect();
7917    if v.is_empty() {
7918        None
7919    } else {
7920        Some(v)
7921    }
7922}
7923
7924fn signal_stats(sig: &[f64]) -> (f64, f64, f64, f64) {
7925    let n = sig.len() as f64;
7926    let mean = sig.iter().sum::<f64>() / n;
7927    let rms = (sig.iter().map(|x| x * x).sum::<f64>() / n).sqrt();
7928    let min = sig.iter().cloned().fold(f64::INFINITY, f64::min);
7929    let max = sig.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
7930    (mean, rms, min, max)
7931}
7932
7933fn ascii_waveform(sig: &[f64], width: usize, height: usize) -> String {
7934    if sig.is_empty() {
7935        return String::new();
7936    }
7937    let mn = sig.iter().cloned().fold(f64::INFINITY, f64::min);
7938    let mx = sig.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
7939    let range = (mx - mn).max(1e-12);
7940    let step = sig.len().max(1) as f64 / width as f64;
7941    let samples: Vec<f64> = (0..width)
7942        .map(|col| {
7943            let idx = ((col as f64 * step) as usize).min(sig.len() - 1);
7944            sig[idx]
7945        })
7946        .collect();
7947    let mut rows = vec![vec![' '; width]; height];
7948    for (col, &val) in samples.iter().enumerate() {
7949        let row = height - 1 - ((val - mn) / range * (height - 1) as f64).round() as usize;
7950        let row = row.min(height - 1);
7951        rows[row][col] = '█';
7952    }
7953    rows.iter()
7954        .map(|r| r.iter().collect::<String>())
7955        .collect::<Vec<_>>()
7956        .join("\n")
7957}
7958
7959pub fn signal_calc(query: &str) -> String {
7960    let mut out = String::new();
7961    let w = 60;
7962    let sep = "═".repeat(w);
7963    let _ = writeln!(out, "{}", sep);
7964    let _ = writeln!(out, "  SIGNAL PROCESSING");
7965    let _ = writeln!(out, "{}", sep);
7966
7967    let q = query.trim();
7968    let lower = q.to_lowercase();
7969
7970    // --- DFT ----------------------------------------------------------------
7971    if lower.starts_with("dft ") || lower.starts_with("fft ") {
7972        let rest = q[4..].trim();
7973        match parse_signal(rest) {
7974            None => {
7975                let _ = writeln!(out, "  ERROR: no numeric values found.");
7976            }
7977            Some(sig) => {
7978                let n = sig.len();
7979                let spectrum = dft(&sig);
7980                let (mean, rms, mn, mx) = signal_stats(&sig);
7981                let _ = writeln!(out, "  DFT of {}-point signal", n);
7982                let _ = writeln!(
7983                    out,
7984                    "  mean={:.4}  RMS={:.4}  min={:.4}  max={:.4}",
7985                    mean, rms, mn, mx
7986                );
7987                let _ = writeln!(out);
7988                let _ = writeln!(
7989                    out,
7990                    "  {:>5}  {:>10}  {:>10}  {:>10}  {:>10}",
7991                    "Bin", "Re", "Im", "Magnitude", "Phase°"
7992                );
7993                let _ = writeln!(out, "  {}", "-".repeat(52));
7994                let show = (n / 2 + 1).min(20);
7995                for k in 0..show {
7996                    let (re, im) = spectrum[k];
7997                    let mag = (re * re + im * im).sqrt();
7998                    let phase = im.atan2(re).to_degrees();
7999                    let _ = writeln!(
8000                        out,
8001                        "  {:>5}  {:>10.4}  {:>10.4}  {:>10.4}  {:>10.2}",
8002                        k, re, im, mag, phase
8003                    );
8004                }
8005                if show < n / 2 + 1 {
8006                    let _ = writeln!(out, "  … ({} bins total)", n / 2 + 1);
8007                }
8008                let dc = spectrum[0].0 / n as f64;
8009                let _ = writeln!(out);
8010                let _ = writeln!(out, "  DC component: {:.6}", dc);
8011                let dominant = spectrum[1..n / 2 + 1]
8012                    .iter()
8013                    .enumerate()
8014                    .map(|(i, &(r, im))| (i + 1, (r * r + im * im).sqrt()))
8015                    .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
8016                if let Some((k, mag)) = dominant {
8017                    let _ = writeln!(out, "  Dominant frequency bin: {} (mag={:.4})", k, mag);
8018                }
8019            }
8020        }
8021    }
8022    // --- IDFT ---------------------------------------------------------------
8023    else if lower.starts_with("idft ") {
8024        let rest = q[5..].trim();
8025        match parse_signal(rest) {
8026            None => {
8027                let _ = writeln!(out, "  ERROR: no numeric values found.");
8028            }
8029            Some(vals) => {
8030                if vals.len() % 2 != 0 {
8031                    let _ = writeln!(
8032                        out,
8033                        "  ERROR: IDFT needs even number of values (re,im pairs)."
8034                    );
8035                } else {
8036                    let spectrum: Vec<(f64, f64)> = vals.chunks(2).map(|c| (c[0], c[1])).collect();
8037                    let sig = idft(&spectrum);
8038                    let _ = writeln!(out, "  IDFT result ({} samples):", sig.len());
8039                    let _ = writeln!(
8040                        out,
8041                        "  {:?}",
8042                        sig.iter()
8043                            .map(|v| format!("{:.6}", v))
8044                            .collect::<Vec<_>>()
8045                            .join(", ")
8046                    );
8047                }
8048            }
8049        }
8050    }
8051    // --- CONVOLVE -----------------------------------------------------------
8052    else if lower.starts_with("conv ") || lower.starts_with("convolve ") {
8053        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8054        if let Some(mid) = rest
8055            .find(" ; ")
8056            .or_else(|| rest.find(" with "))
8057            .or_else(|| rest.find(" | "))
8058        {
8059            let (a_str, b_str) = rest.split_at(mid);
8060            let b_str = b_str
8061                .trim_start_matches([' ', ';', '|'].as_ref())
8062                .trim_start_matches("with")
8063                .trim();
8064            match (parse_signal(a_str.trim()), parse_signal(b_str)) {
8065                (Some(x), Some(h)) => {
8066                    let y = convolve(&x, &h);
8067                    let _ = writeln!(
8068                        out,
8069                        "  Convolution  x[{}] * h[{}] = y[{}]",
8070                        x.len(),
8071                        h.len(),
8072                        y.len()
8073                    );
8074                    let _ = writeln!(
8075                        out,
8076                        "  x: {}",
8077                        x.iter()
8078                            .map(|v| format!("{:.4}", v))
8079                            .collect::<Vec<_>>()
8080                            .join(", ")
8081                    );
8082                    let _ = writeln!(
8083                        out,
8084                        "  h: {}",
8085                        h.iter()
8086                            .map(|v| format!("{:.4}", v))
8087                            .collect::<Vec<_>>()
8088                            .join(", ")
8089                    );
8090                    let _ = writeln!(
8091                        out,
8092                        "  y: {}",
8093                        y.iter()
8094                            .map(|v| format!("{:.4}", v))
8095                            .collect::<Vec<_>>()
8096                            .join(", ")
8097                    );
8098                    let (mean, rms, mn, mx) = signal_stats(&y);
8099                    let _ = writeln!(
8100                        out,
8101                        "  mean={:.4}  RMS={:.4}  min={:.4}  max={:.4}",
8102                        mean, rms, mn, mx
8103                    );
8104                }
8105                _ => {
8106                    let _ = writeln!(
8107                        out,
8108                        "  ERROR: use  conv  A,B,C ; D,E,F  (separate signals with ;)"
8109                    );
8110                }
8111            }
8112        } else {
8113            let _ = writeln!(
8114                out,
8115                "  ERROR: use  conv  A,B,C ; D,E,F  (separate signals with ;)"
8116            );
8117        }
8118    }
8119    // --- XCORR --------------------------------------------------------------
8120    else if lower.starts_with("xcorr ") || lower.starts_with("correlate ") {
8121        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8122        if let Some(mid) = rest.find(" ; ").or_else(|| rest.find(" | ")) {
8123            let (a_str, b_str) = rest.split_at(mid);
8124            let b_str = b_str.trim_start_matches([' ', ';', '|'].as_ref()).trim();
8125            match (parse_signal(a_str.trim()), parse_signal(b_str)) {
8126                (Some(x), Some(y)) => {
8127                    let r = xcorr(&x, &y);
8128                    let peak_lag = r
8129                        .iter()
8130                        .enumerate()
8131                        .max_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).unwrap())
8132                        .map(|(i, _)| i as isize - (y.len() as isize - 1));
8133                    let _ = writeln!(
8134                        out,
8135                        "  Cross-correlation  x[{}] ⋆ y[{}] = r[{}]",
8136                        x.len(),
8137                        y.len(),
8138                        r.len()
8139                    );
8140                    let _ = writeln!(
8141                        out,
8142                        "  r: {}",
8143                        r.iter()
8144                            .map(|v| format!("{:.4}", v))
8145                            .collect::<Vec<_>>()
8146                            .join(", ")
8147                    );
8148                    if let Some(lag) = peak_lag {
8149                        let _ = writeln!(out, "  Peak lag: {} samples", lag);
8150                    }
8151                }
8152                _ => {
8153                    let _ = writeln!(out, "  ERROR: use  xcorr  A,B,C ; D,E,F");
8154                }
8155            }
8156        } else {
8157            let _ = writeln!(out, "  ERROR: use  xcorr  A,B,C ; D,E,F");
8158        }
8159    }
8160    // --- MOVING AVERAGE -----------------------------------------------------
8161    else if lower.starts_with("movavg ")
8162        || lower.starts_with("moving-avg ")
8163        || lower.starts_with("sma ")
8164    {
8165        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8166        let parts: Vec<&str> = rest.splitn(2, ' ').collect();
8167        let window = parts[0].parse::<usize>().unwrap_or(3);
8168        let data_str = if parts.len() > 1 { parts[1] } else { "" };
8169        match parse_signal(data_str) {
8170            None => {
8171                let _ = writeln!(out, "  ERROR: use  movavg WINDOW v1,v2,...");
8172            }
8173            Some(sig) => {
8174                let smoothed = moving_avg(&sig, window);
8175                let _ = writeln!(out, "  Simple Moving Average  window={}", window);
8176                let _ = writeln!(
8177                    out,
8178                    "  Input ({} pts): {}",
8179                    sig.len(),
8180                    sig.iter()
8181                        .take(8)
8182                        .map(|v| format!("{:.3}", v))
8183                        .collect::<Vec<_>>()
8184                        .join(", ")
8185                );
8186                let _ = writeln!(
8187                    out,
8188                    "  Output ({} pts): {}",
8189                    smoothed.len(),
8190                    smoothed
8191                        .iter()
8192                        .take(8)
8193                        .map(|v| format!("{:.4}", v))
8194                        .collect::<Vec<_>>()
8195                        .join(", ")
8196                );
8197                if sig.len() > 8 {
8198                    let _ = writeln!(out, "  (showing first 8 of {} values)", sig.len());
8199                }
8200            }
8201        }
8202    }
8203    // --- FIR LOW-PASS FILTER ------------------------------------------------
8204    else if lower.starts_with("fir-lp ")
8205        || lower.starts_with("lowpass ")
8206        || lower.starts_with("lp ")
8207    {
8208        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8209        let parts: Vec<&str> = rest.splitn(3, ' ').collect();
8210        let cutoff = parts
8211            .first()
8212            .and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
8213            .unwrap_or(0.25)
8214            / if rest.contains('%') { 100.0 } else { 1.0 };
8215        let n_taps = parts
8216            .get(1)
8217            .and_then(|s| s.parse::<usize>().ok())
8218            .unwrap_or(21);
8219        let window = parts.get(2).map(|s| s.trim()).unwrap_or("hamming");
8220        let h = fir_lowpass(n_taps, cutoff.min(0.5), window);
8221        let _ = writeln!(out, "  FIR Low-Pass Filter");
8222        let _ = writeln!(
8223            out,
8224            "  Cutoff: {:.4} (normalized, 0.5 = Nyquist)  Taps: {}  Window: {}",
8225            cutoff, n_taps, window
8226        );
8227        let _ = writeln!(out, "  Coefficients:");
8228        for (i, c) in h.iter().enumerate() {
8229            let _ = write!(out, "  h[{:2}]={:>10.6}", i, c);
8230            if (i + 1) % 4 == 0 {
8231                let _ = writeln!(out);
8232            }
8233        }
8234        let _ = writeln!(out);
8235        let sum: f64 = h.iter().sum();
8236        let _ = writeln!(
8237            out,
8238            "  Sum of taps: {:.6}  (DC gain = {:.4} dB)",
8239            sum,
8240            20.0 * sum.abs().log10()
8241        );
8242    }
8243    // --- FIR HIGH-PASS FILTER -----------------------------------------------
8244    else if lower.starts_with("fir-hp ")
8245        || lower.starts_with("highpass ")
8246        || lower.starts_with("hp ")
8247    {
8248        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8249        let parts: Vec<&str> = rest.splitn(3, ' ').collect();
8250        let cutoff = parts
8251            .first()
8252            .and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
8253            .unwrap_or(0.25)
8254            / if rest.contains('%') { 100.0 } else { 1.0 };
8255        let n_taps = parts
8256            .get(1)
8257            .and_then(|s| s.parse::<usize>().ok())
8258            .unwrap_or(21);
8259        let window = parts.get(2).map(|s| s.trim()).unwrap_or("hamming");
8260        let h = fir_highpass(n_taps, cutoff.min(0.49), window);
8261        let _ = writeln!(out, "  FIR High-Pass Filter");
8262        let _ = writeln!(
8263            out,
8264            "  Cutoff: {:.4}  Taps: {}  Window: {}",
8265            cutoff, n_taps, window
8266        );
8267        let _ = writeln!(out, "  Coefficients:");
8268        for (i, c) in h.iter().enumerate() {
8269            let _ = write!(out, "  h[{:2}]={:>10.6}", i, c);
8270            if (i + 1) % 4 == 0 {
8271                let _ = writeln!(out);
8272            }
8273        }
8274        let _ = writeln!(out);
8275    }
8276    // --- APPLY FILTER -------------------------------------------------------
8277    else if lower.starts_with("filter ") {
8278        let rest = q[7..].trim();
8279        if let Some(mid) = rest.find(" ; ") {
8280            let (a_str, b_str) = rest.split_at(mid);
8281            let b_str = b_str[3..].trim();
8282            match (parse_signal(a_str.trim()), parse_signal(b_str)) {
8283                (Some(h), Some(x)) => {
8284                    let y = convolve(&x, &h);
8285                    let _ = writeln!(
8286                        out,
8287                        "  Filter applied  h[{}] * x[{}] = y[{}]",
8288                        h.len(),
8289                        x.len(),
8290                        y.len()
8291                    );
8292                    let (_, rms_x, _, _) = signal_stats(&x);
8293                    let (_, rms_y, _, _) = signal_stats(&y);
8294                    let _ = writeln!(out, "  Input  RMS: {:.4}", rms_x);
8295                    let _ = writeln!(out, "  Output RMS: {:.4}", rms_y);
8296                    let _ = writeln!(
8297                        out,
8298                        "  y: {}",
8299                        y.iter()
8300                            .map(|v| format!("{:.4}", v))
8301                            .collect::<Vec<_>>()
8302                            .join(", ")
8303                    );
8304                }
8305                _ => {
8306                    let _ = writeln!(out, "  ERROR: use  filter h1,h2,... ; x1,x2,...");
8307                }
8308            }
8309        } else {
8310            let _ = writeln!(out, "  ERROR: use  filter h1,h2,... ; x1,x2,...");
8311        }
8312    }
8313    // --- STATS / INFO -------------------------------------------------------
8314    else if lower.starts_with("stats ") || lower.starts_with("info ") || lower.starts_with("rms ")
8315    {
8316        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8317        match parse_signal(rest) {
8318            None => {
8319                let _ = writeln!(out, "  ERROR: no numeric values.");
8320            }
8321            Some(sig) => {
8322                let n = sig.len();
8323                let (mean, rms, mn, mx) = signal_stats(&sig);
8324                let variance = sig.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n as f64;
8325                let std_dev = variance.sqrt();
8326                let energy: f64 = sig.iter().map(|x| x * x).sum();
8327                let _ = writeln!(out, "  Signal Statistics  ({} samples)", n);
8328                let _ = writeln!(out, "  Mean:     {:>12.6}", mean);
8329                let _ = writeln!(out, "  RMS:      {:>12.6}", rms);
8330                let _ = writeln!(out, "  Std dev:  {:>12.6}", std_dev);
8331                let _ = writeln!(out, "  Min:      {:>12.6}", mn);
8332                let _ = writeln!(out, "  Max:      {:>12.6}", mx);
8333                let _ = writeln!(out, "  Range:    {:>12.6}", mx - mn);
8334                let _ = writeln!(out, "  Energy:   {:>12.6}", energy);
8335                let _ = writeln!(out, "  Power:    {:>12.6}", energy / n as f64);
8336                if rms > 1e-12 {
8337                    let crest = mx.abs().max(mn.abs()) / rms;
8338                    let _ = writeln!(
8339                        out,
8340                        "  Crest:    {:>12.6}  ({:.2} dB)",
8341                        crest,
8342                        20.0 * crest.log10()
8343                    );
8344                }
8345                let _ = writeln!(out);
8346                let _ = writeln!(out, "  Waveform ({}×8):", sig.len().min(60));
8347                let wave = ascii_waveform(&sig, sig.len().min(60), 8);
8348                for line in wave.lines() {
8349                    let _ = writeln!(out, "  |{}|", line);
8350                }
8351            }
8352        }
8353    }
8354    // --- WAVEFORM GENERATE --------------------------------------------------
8355    else if lower.starts_with("gen ")
8356        || lower.starts_with("wave ")
8357        || lower.starts_with("generate ")
8358    {
8359        let rest = q[q.find(' ').unwrap_or(0)..].trim();
8360        let parts: Vec<&str> = rest.splitn(4, ' ').collect();
8361        let shape = parts
8362            .first()
8363            .map(|s| s.to_lowercase())
8364            .unwrap_or_else(|| "sine".into());
8365        let freq: f64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(1.0);
8366        let n: usize = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(64);
8367        let amp: f64 = parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(1.0);
8368        let sig: Vec<f64> = (0..n)
8369            .map(|i| {
8370                let t = i as f64 / n as f64;
8371                let phase = 2.0 * PI * freq * t;
8372                match shape.as_str() {
8373                    "cos" | "cosine" => amp * phase.cos(),
8374                    "square" => amp * if phase.sin() >= 0.0 { 1.0 } else { -1.0 },
8375                    "sawtooth" | "saw" => amp * (2.0 * (freq * t - (freq * t + 0.5).floor())),
8376                    "triangle" | "tri" => {
8377                        amp * 2.0 * (2.0 * (freq * t - (freq * t + 0.5).floor())).abs() - 1.0
8378                    }
8379                    "noise" | "rand" => {
8380                        amp * (((i * 6364136223846793005 + 1442695040888963407) >> 33) as f64
8381                            / u32::MAX as f64
8382                            * 2.0
8383                            - 1.0)
8384                    }
8385                    _ => amp * phase.sin(),
8386                }
8387            })
8388            .collect();
8389        let (mean, rms, mn, mx) = signal_stats(&sig);
8390        let _ = writeln!(
8391            out,
8392            "  Waveform: {}  freq={} cycles  n={}  amp={}",
8393            shape, freq, n, amp
8394        );
8395        let _ = writeln!(
8396            out,
8397            "  mean={:.4}  RMS={:.4}  min={:.4}  max={:.4}",
8398            mean, rms, mn, mx
8399        );
8400        let _ = writeln!(out);
8401        let wave = ascii_waveform(&sig, sig.len().min(60), 8);
8402        for line in wave.lines() {
8403            let _ = writeln!(out, "  |{}|", line);
8404        }
8405        let _ = writeln!(out);
8406        let first = sig
8407            .iter()
8408            .take(16)
8409            .map(|v| format!("{:.4}", v))
8410            .collect::<Vec<_>>()
8411            .join(", ");
8412        let _ = writeln!(
8413            out,
8414            "  First 16 samples: {}{}",
8415            first,
8416            if n > 16 { " …" } else { "" }
8417        );
8418    }
8419    // --- WINDOW PREVIEW -----------------------------------------------------
8420    else if lower.starts_with("window ") {
8421        let parts: Vec<&str> = q.splitn(3, ' ').collect();
8422        let win_type = parts
8423            .get(1)
8424            .map(|s| s.to_lowercase())
8425            .unwrap_or_else(|| "hann".into());
8426        let n: usize = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(32);
8427        let w = match win_type.as_str() {
8428            "hamming" => hamming_window(n),
8429            "blackman" => blackman_window(n),
8430            _ => hann_window(n),
8431        };
8432        let (mean, rms, mn, mx) = signal_stats(&w);
8433        let _ = writeln!(out, "  {} window  n={}", win_type, n);
8434        let _ = writeln!(
8435            out,
8436            "  mean={:.4}  RMS={:.4}  min={:.4}  max={:.4}",
8437            mean, rms, mn, mx
8438        );
8439        let _ = writeln!(out);
8440        let wave = ascii_waveform(&w, n.min(60), 6);
8441        for line in wave.lines() {
8442            let _ = writeln!(out, "  |{}|", line);
8443        }
8444        let _ = writeln!(out);
8445        let first8 = w
8446            .iter()
8447            .take(8)
8448            .map(|v| format!("{:.4}", v))
8449            .collect::<Vec<_>>()
8450            .join(", ");
8451        let _ = writeln!(out, "  First 8 coeffs: {}", first8);
8452    }
8453    // --- HELP ---------------------------------------------------------------
8454    else {
8455        let _ = writeln!(out, "{}", signal_usage());
8456    }
8457
8458    let _ = writeln!(out, "{}", sep);
8459    out
8460}
8461
8462fn signal_usage() -> String {
8463    "Signal processing (DSP) — no model, no cloud:\n\
8464     \n\
8465     hematite --signal 'dft 1,0,-1,0,1,0,-1,0'         Discrete Fourier Transform\n\
8466     hematite --signal 'idft 4,0,0,0 ; 0,0,0,0'        Inverse DFT (re,im pairs)\n\
8467     hematite --signal 'conv 1,2,3 ; 1,-1'              Convolution (separate with ;)\n\
8468     hematite --signal 'xcorr 1,0,1 ; 0,1,0'            Cross-correlation\n\
8469     hematite --signal 'movavg 3 1,3,5,7,5,3,1'         3-point moving average\n\
8470     hematite --signal 'lowpass 0.1 31 hamming'         FIR low-pass (cutoff taps window)\n\
8471     hematite --signal 'highpass 0.3 21 hann'           FIR high-pass\n\
8472     hematite --signal 'filter 0.25,0.5,0.25 ; 1,2,3,4' Apply FIR filter to signal\n\
8473     hematite --signal 'stats 1,2,3,4,5'                RMS, energy, crest, waveform plot\n\
8474     hematite --signal 'gen sine 2 64'                  Generate 64-pt sine, 2 cycles\n\
8475     hematite --signal 'gen square 1 32 2.5'            Square wave, amp=2.5\n\
8476     hematite --signal 'gen sawtooth 3 128'             Sawtooth wave\n\
8477     hematite --signal 'window hann 64'                 Preview Hann window coefficients\n\
8478     \n\
8479     Window types: rectangular hann hamming blackman\n\
8480     Wave shapes:  sine cosine square sawtooth triangle noise"
8481        .into()
8482}
8483
8484// ── Interpolation & curve fitting ─────────────────────────────────────────────
8485// Linear, Lagrange polynomial, natural cubic spline, nearest-neighbor,
8486// and linear extrapolation — all pure-Rust, instant, no subprocess.
8487
8488fn interp_parse_points(s: &str) -> Option<Vec<(f64, f64)>> {
8489    // Accept:  "x1,y1 x2,y2 ..."  or  "x1,y1; x2,y2; ..."  or  "(x,y),(x,y)"
8490    let clean = s.replace(['(', ')'], "").replace(';', " ");
8491    let tokens: Vec<f64> = clean
8492        .split([',', ' ', '\t'].as_ref())
8493        .filter_map(|t| t.trim().parse::<f64>().ok())
8494        .collect();
8495    if tokens.len() < 4 || tokens.len() % 2 != 0 {
8496        return None;
8497    }
8498    Some(tokens.chunks(2).map(|c| (c[0], c[1])).collect())
8499}
8500
8501fn interp_linear(points: &[(f64, f64)], x: f64) -> f64 {
8502    let n = points.len();
8503    if n == 0 {
8504        return f64::NAN;
8505    }
8506    if n == 1 {
8507        return points[0].1;
8508    }
8509    // extrapolate with end segments
8510    if x <= points[0].0 {
8511        let (x0, y0) = points[0];
8512        let (x1, y1) = points[1];
8513        return y0 + (x - x0) * (y1 - y0) / (x1 - x0);
8514    }
8515    if x >= points[n - 1].0 {
8516        let (x0, y0) = points[n - 2];
8517        let (x1, y1) = points[n - 1];
8518        return y0 + (x - x0) * (y1 - y0) / (x1 - x0);
8519    }
8520    for i in 0..n - 1 {
8521        let (x0, y0) = points[i];
8522        let (x1, y1) = points[i + 1];
8523        if x >= x0 && x <= x1 {
8524            return y0 + (x - x0) * (y1 - y0) / (x1 - x0);
8525        }
8526    }
8527    f64::NAN
8528}
8529
8530fn interp_nearest(points: &[(f64, f64)], x: f64) -> f64 {
8531    points
8532        .iter()
8533        .min_by(|a, b| (a.0 - x).abs().partial_cmp(&(b.0 - x).abs()).unwrap())
8534        .map(|p| p.1)
8535        .unwrap_or(f64::NAN)
8536}
8537
8538fn interp_lagrange(points: &[(f64, f64)], x: f64) -> f64 {
8539    let n = points.len();
8540    (0..n)
8541        .map(|i| {
8542            let (xi, yi) = points[i];
8543            let li = (0..n).filter(|&j| j != i).fold(1.0_f64, |acc, j| {
8544                acc * (x - points[j].0) / (xi - points[j].0)
8545            });
8546            yi * li
8547        })
8548        .sum()
8549}
8550
8551// Natural cubic spline via tridiagonal solve
8552fn interp_spline_build(points: &[(f64, f64)]) -> Vec<(f64, f64, f64, f64)> {
8553    let n = points.len();
8554    if n < 2 {
8555        return vec![];
8556    }
8557    let h: Vec<f64> = (0..n - 1).map(|i| points[i + 1].0 - points[i].0).collect();
8558    let mut alpha = vec![0.0_f64; n];
8559    for i in 1..n - 1 {
8560        alpha[i] = (3.0 / h[i]) * (points[i + 1].1 - points[i].1)
8561            - (3.0 / h[i - 1]) * (points[i].1 - points[i - 1].1);
8562    }
8563    let mut l = vec![1.0_f64; n];
8564    let mut mu = vec![0.0_f64; n];
8565    let mut z = vec![0.0_f64; n];
8566    for i in 1..n - 1 {
8567        l[i] = 2.0 * (points[i + 1].0 - points[i - 1].0) - h[i - 1] * mu[i - 1];
8568        mu[i] = h[i] / l[i];
8569        z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
8570    }
8571    let mut c = vec![0.0_f64; n];
8572    let mut b = vec![0.0_f64; n];
8573    let mut d = vec![0.0_f64; n];
8574    for j in (0..n - 1).rev() {
8575        c[j] = z[j] - mu[j] * c[j + 1];
8576        b[j] = (points[j + 1].1 - points[j].1) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
8577        d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
8578    }
8579    (0..n - 1)
8580        .map(|i| (b[i], c[i], d[i], points[i].1))
8581        .collect()
8582}
8583
8584fn interp_spline_eval(points: &[(f64, f64)], coeffs: &[(f64, f64, f64, f64)], x: f64) -> f64 {
8585    let n = points.len();
8586    if n == 0 {
8587        return f64::NAN;
8588    }
8589    let i = if x <= points[0].0 {
8590        0
8591    } else if x >= points[n - 1].0 {
8592        n - 2
8593    } else {
8594        points[..n - 1]
8595            .iter()
8596            .enumerate()
8597            .find(|(i, p)| x >= p.0 && x <= points[i + 1].0)
8598            .map(|(i, _)| i)
8599            .unwrap_or(n - 2)
8600    };
8601    let i = i.min(coeffs.len().saturating_sub(1));
8602    let dx = x - points[i].0;
8603    let (b, c, d, a) = coeffs[i];
8604    a + b * dx + c * dx * dx + d * dx * dx * dx
8605}
8606
8607fn interp_ascii_curve(
8608    points: &[(f64, f64)],
8609    eval_fn: &dyn Fn(f64) -> f64,
8610    width: usize,
8611    height: usize,
8612) -> String {
8613    if points.is_empty() {
8614        return String::new();
8615    }
8616    let xs: Vec<f64> = points.iter().map(|p| p.0).collect();
8617    let ys: Vec<f64> = points.iter().map(|p| p.1).collect();
8618    let xmin = xs.iter().cloned().fold(f64::INFINITY, f64::min);
8619    let xmax = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
8620    let step = (xmax - xmin) / (width - 1) as f64;
8621    let curve_y: Vec<f64> = (0..width)
8622        .map(|i| eval_fn(xmin + i as f64 * step))
8623        .collect();
8624    let ymin = curve_y
8625        .iter()
8626        .cloned()
8627        .chain(ys.iter().cloned())
8628        .fold(f64::INFINITY, f64::min);
8629    let ymax = curve_y
8630        .iter()
8631        .cloned()
8632        .chain(ys.iter().cloned())
8633        .fold(f64::NEG_INFINITY, f64::max);
8634    let yrange = (ymax - ymin).max(1e-12);
8635    let mut grid = vec![vec![' '; width]; height];
8636    for (col, &y) in curve_y.iter().enumerate() {
8637        let row = height - 1 - ((y - ymin) / yrange * (height - 1) as f64).round() as usize;
8638        let row = row.min(height - 1);
8639        grid[row][col] = '·';
8640    }
8641    for &(xp, yp) in points {
8642        let col = ((xp - xmin) / (xmax - xmin) * (width - 1) as f64).round() as usize;
8643        let row = height - 1 - ((yp - ymin) / yrange * (height - 1) as f64).round() as usize;
8644        let col = col.min(width - 1);
8645        let row = row.min(height - 1);
8646        grid[row][col] = '●';
8647    }
8648    let result = grid
8649        .iter()
8650        .map(|r| r.iter().collect::<String>())
8651        .collect::<Vec<_>>()
8652        .join("\n");
8653    format!(
8654        "{}\n  y: [{:.4} .. {:.4}]\n  x: [{:.4} .. {:.4}]\n  ● = data point  · = curve",
8655        result, ymin, ymax, xmin, xmax
8656    )
8657}
8658
8659pub fn interpolate_calc(query: &str) -> String {
8660    let mut out = String::new();
8661    let sep = "═".repeat(60);
8662    let _ = writeln!(out, "{}", sep);
8663    let _ = writeln!(out, "  INTERPOLATION & CURVE FITTING");
8664    let _ = writeln!(out, "{}", sep);
8665
8666    let q = query.trim();
8667    let lower = q.to_lowercase();
8668
8669    // parse method prefix: "linear POINTS at X", "spline POINTS at X", etc.
8670    let (method, rest) = if lower.starts_with("linear ") {
8671        ("linear", &q[7..])
8672    } else if lower.starts_with("spline ") || lower.starts_with("cubic ") {
8673        ("spline", &q[7..])
8674    } else if lower.starts_with("lagrange ") || lower.starts_with("poly ") {
8675        ("lagrange", &q[lower.find(' ').unwrap_or(0) + 1..])
8676    } else if lower.starts_with("nearest ") {
8677        ("nearest", &q[8..])
8678    } else {
8679        ("linear", q)
8680    };
8681
8682    // split on "at" keyword for evaluation point(s)
8683    let (pts_str, query_str) = if let Some(pos) = rest.to_lowercase().rfind(" at ") {
8684        (&rest[..pos], rest[pos + 4..].trim())
8685    } else {
8686        (rest, "")
8687    };
8688
8689    let mut points = match interp_parse_points(pts_str.trim()) {
8690        Some(p) => p,
8691        None => {
8692            let _ = writeln!(out, "  ERROR: could not parse data points.");
8693            let _ = writeln!(out, "  Format: x1,y1 x2,y2 x3,y3 ...");
8694            let _ = writeln!(out, "{}", sep);
8695            return out;
8696        }
8697    };
8698    points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
8699
8700    let _ = writeln!(out, "  Method: {}  |  {} data points", method, points.len());
8701    let _ = writeln!(
8702        out,
8703        "  Points: {}",
8704        points
8705            .iter()
8706            .map(|(x, y)| format!("({:.4},{:.4})", x, y))
8707            .collect::<Vec<_>>()
8708            .join("  ")
8709    );
8710    let _ = writeln!(out);
8711
8712    // Build spline coefficients once if needed
8713    let spline_coeffs = if method == "spline" {
8714        interp_spline_build(&points)
8715    } else {
8716        vec![]
8717    };
8718
8719    let eval_fn: Box<dyn Fn(f64) -> f64> = match method {
8720        "spline" => {
8721            let pts = points.clone();
8722            let sc = spline_coeffs.clone();
8723            Box::new(move |x| interp_spline_eval(&pts, &sc, x))
8724        }
8725        "lagrange" => {
8726            let pts = points.clone();
8727            Box::new(move |x| interp_lagrange(&pts, x))
8728        }
8729        "nearest" => {
8730            let pts = points.clone();
8731            Box::new(move |x| interp_nearest(&pts, x))
8732        }
8733        _ => {
8734            let pts = points.clone();
8735            Box::new(move |x| interp_linear(&pts, x))
8736        }
8737    };
8738
8739    // Evaluate at requested x values
8740    if !query_str.is_empty() {
8741        let xs: Vec<f64> = query_str
8742            .split([',', ' '].as_ref())
8743            .filter_map(|s| s.trim().parse::<f64>().ok())
8744            .collect();
8745        if !xs.is_empty() {
8746            let _ = writeln!(out, "  {:>12}  {:>14}", "x", "y (interpolated)");
8747            let _ = writeln!(out, "  {}", "-".repeat(28));
8748            for &x in &xs {
8749                let y = eval_fn(x);
8750                let xmin = points[0].0;
8751                let xmax = points[points.len() - 1].0;
8752                let tag = if x < xmin || x > xmax {
8753                    "  [extrapolated]"
8754                } else {
8755                    ""
8756                };
8757                let _ = writeln!(out, "  {:>12.6}  {:>14.8}{}", x, y, tag);
8758            }
8759            let _ = writeln!(out);
8760        }
8761    }
8762
8763    // Always show a dense evaluation table and ASCII curve
8764    let xmin = points[0].0;
8765    let xmax = points[points.len() - 1].0;
8766    let steps = 9_usize;
8767    let _ = writeln!(out, "  Sampled curve ({} pts across range):", steps + 1);
8768    let _ = writeln!(out, "  {:>10}  {:>14}", "x", "y");
8769    let _ = writeln!(out, "  {}", "-".repeat(26));
8770    for i in 0..=steps {
8771        let x = xmin + i as f64 * (xmax - xmin) / steps as f64;
8772        let y = eval_fn(x);
8773        let _ = writeln!(out, "  {:>10.4}  {:>14.8}", x, y);
8774    }
8775    let _ = writeln!(out);
8776
8777    // ASCII curve
8778    let curve_str = interp_ascii_curve(&points, &eval_fn, 56, 10);
8779    for line in curve_str.lines() {
8780        let _ = writeln!(out, "  {}", line);
8781    }
8782
8783    let _ = writeln!(out, "{}", sep);
8784    out
8785}
8786
8787// ── Unit conversion ───────────────────────────────────────────────────────────
8788// Pure-Rust lookup tables. Handles VALUE FROM_UNIT to TO_UNIT, or just
8789// VALUE UNIT for a multi-target broadcast. Temperature uses offset math.
8790
8791struct UnitDef {
8792    names: &'static [&'static str],
8793    to_base: f64, // multiply by this to reach SI base; NaN for temperature
8794}
8795
8796struct UnitCat {
8797    name: &'static str,
8798    base: &'static str,
8799    units: &'static [UnitDef],
8800}
8801
8802static UNIT_TABLE: &[UnitCat] = &[
8803    UnitCat {
8804        name: "Length",
8805        base: "m",
8806        units: &[
8807            UnitDef {
8808                names: &["m", "meter", "metre", "meters", "metres"],
8809                to_base: 1.0,
8810            },
8811            UnitDef {
8812                names: &["km", "kilometer", "kilometre", "kilometers", "kilometres"],
8813                to_base: 1e3,
8814            },
8815            UnitDef {
8816                names: &[
8817                    "cm",
8818                    "centimeter",
8819                    "centimetre",
8820                    "centimeters",
8821                    "centimetres",
8822                ],
8823                to_base: 0.01,
8824            },
8825            UnitDef {
8826                names: &[
8827                    "mm",
8828                    "millimeter",
8829                    "millimetre",
8830                    "millimeters",
8831                    "millimetres",
8832                ],
8833                to_base: 0.001,
8834            },
8835            UnitDef {
8836                names: &["um", "micrometer", "micrometre", "micron"],
8837                to_base: 1e-6,
8838            },
8839            UnitDef {
8840                names: &["nm", "nanometer", "nanometre"],
8841                to_base: 1e-9,
8842            },
8843            UnitDef {
8844                names: &["mi", "mile", "miles"],
8845                to_base: 1609.344,
8846            },
8847            UnitDef {
8848                names: &["yd", "yard", "yards"],
8849                to_base: 0.9144,
8850            },
8851            UnitDef {
8852                names: &["ft", "foot", "feet"],
8853                to_base: 0.3048,
8854            },
8855            UnitDef {
8856                names: &["in", "inch", "inches"],
8857                to_base: 0.0254,
8858            },
8859            UnitDef {
8860                names: &["nmi", "nautical_mile", "nm_sea"],
8861                to_base: 1852.0,
8862            },
8863            UnitDef {
8864                names: &["ly", "lightyear", "light_year"],
8865                to_base: 9.461e15,
8866            },
8867            UnitDef {
8868                names: &["au", "astronomical_unit"],
8869                to_base: 1.496e11,
8870            },
8871            UnitDef {
8872                names: &["pc", "parsec"],
8873                to_base: 3.086e16,
8874            },
8875            UnitDef {
8876                names: &["angstrom", "ang"],
8877                to_base: 1e-10,
8878            },
8879        ],
8880    },
8881    UnitCat {
8882        name: "Area",
8883        base: "m2",
8884        units: &[
8885            UnitDef {
8886                names: &["m2", "sqm", "square_meter", "square_metre"],
8887                to_base: 1.0,
8888            },
8889            UnitDef {
8890                names: &["km2", "sqkm", "square_kilometer"],
8891                to_base: 1e6,
8892            },
8893            UnitDef {
8894                names: &["cm2", "sqcm", "square_centimeter"],
8895                to_base: 1e-4,
8896            },
8897            UnitDef {
8898                names: &["mm2", "sqmm", "square_millimeter"],
8899                to_base: 1e-6,
8900            },
8901            UnitDef {
8902                names: &["ha", "hectare", "hectares"],
8903                to_base: 1e4,
8904            },
8905            UnitDef {
8906                names: &["acre", "acres"],
8907                to_base: 4046.856,
8908            },
8909            UnitDef {
8910                names: &["ft2", "sqft", "square_foot", "square_feet"],
8911                to_base: 0.09290304,
8912            },
8913            UnitDef {
8914                names: &["in2", "sqin", "square_inch"],
8915                to_base: 6.4516e-4,
8916            },
8917            UnitDef {
8918                names: &["mi2", "sqmi", "square_mile"],
8919                to_base: 2.58999e6,
8920            },
8921            UnitDef {
8922                names: &["yd2", "sqyd", "square_yard"],
8923                to_base: 0.83612736,
8924            },
8925        ],
8926    },
8927    UnitCat {
8928        name: "Volume",
8929        base: "m3",
8930        units: &[
8931            UnitDef {
8932                names: &["m3", "cbm", "cubic_meter", "cubic_metre"],
8933                to_base: 1.0,
8934            },
8935            UnitDef {
8936                names: &["l", "liter", "litre", "liters", "litres"],
8937                to_base: 0.001,
8938            },
8939            UnitDef {
8940                names: &[
8941                    "ml",
8942                    "milliliter",
8943                    "millilitre",
8944                    "milliliters",
8945                    "millilitres",
8946                ],
8947                to_base: 1e-6,
8948            },
8949            UnitDef {
8950                names: &["cl", "centiliter", "centilitre"],
8951                to_base: 1e-5,
8952            },
8953            UnitDef {
8954                names: &["dl", "deciliter", "decilitre"],
8955                to_base: 1e-4,
8956            },
8957            UnitDef {
8958                names: &["gal", "gallon", "gallons"],
8959                to_base: 0.00378541,
8960            },
8961            UnitDef {
8962                names: &["qt", "quart", "quarts"],
8963                to_base: 9.46353e-4,
8964            },
8965            UnitDef {
8966                names: &["pt", "pint", "pints"],
8967                to_base: 4.73176e-4,
8968            },
8969            UnitDef {
8970                names: &["cup", "cups"],
8971                to_base: 2.36588e-4,
8972            },
8973            UnitDef {
8974                names: &["floz", "fl_oz", "fluid_ounce", "fluid_ounces"],
8975                to_base: 2.95735e-5,
8976            },
8977            UnitDef {
8978                names: &["tbsp", "tablespoon", "tablespoons"],
8979                to_base: 1.47868e-5,
8980            },
8981            UnitDef {
8982                names: &["tsp", "teaspoon", "teaspoons"],
8983                to_base: 4.92892e-6,
8984            },
8985            UnitDef {
8986                names: &["ft3", "cbft", "cubic_foot", "cubic_feet"],
8987                to_base: 0.0283168,
8988            },
8989            UnitDef {
8990                names: &["in3", "cbin", "cubic_inch"],
8991                to_base: 1.63871e-5,
8992            },
8993            UnitDef {
8994                names: &["bbl", "barrel", "barrels"],
8995                to_base: 0.158987,
8996            },
8997        ],
8998    },
8999    UnitCat {
9000        name: "Mass",
9001        base: "kg",
9002        units: &[
9003            UnitDef {
9004                names: &["kg", "kilogram", "kilograms", "kilo"],
9005                to_base: 1.0,
9006            },
9007            UnitDef {
9008                names: &["g", "gram", "grams"],
9009                to_base: 0.001,
9010            },
9011            UnitDef {
9012                names: &["mg", "milligram", "milligrams"],
9013                to_base: 1e-6,
9014            },
9015            UnitDef {
9016                names: &["ug", "microgram", "micrograms"],
9017                to_base: 1e-9,
9018            },
9019            UnitDef {
9020                names: &["t", "tonne", "metric_ton", "tonnes"],
9021                to_base: 1000.0,
9022            },
9023            UnitDef {
9024                names: &["lb", "pound", "pounds", "lbs"],
9025                to_base: 0.453592,
9026            },
9027            UnitDef {
9028                names: &["oz", "ounce", "ounces"],
9029                to_base: 0.0283495,
9030            },
9031            UnitDef {
9032                names: &["st", "stone", "stones"],
9033                to_base: 6.35029,
9034            },
9035            UnitDef {
9036                names: &["ton", "short_ton", "tons"],
9037                to_base: 907.185,
9038            },
9039            UnitDef {
9040                names: &["long_ton", "long_tons"],
9041                to_base: 1016.05,
9042            },
9043            UnitDef {
9044                names: &["ct", "carat", "carats"],
9045                to_base: 2e-4,
9046            },
9047            UnitDef {
9048                names: &["gr", "grain", "grains"],
9049                to_base: 6.47989e-5,
9050            },
9051            UnitDef {
9052                names: &["u", "amu", "dalton"],
9053                to_base: 1.66054e-27,
9054            },
9055        ],
9056    },
9057    UnitCat {
9058        name: "Time",
9059        base: "s",
9060        units: &[
9061            UnitDef {
9062                names: &["s", "sec", "second", "seconds"],
9063                to_base: 1.0,
9064            },
9065            UnitDef {
9066                names: &["ms", "millisecond", "milliseconds"],
9067                to_base: 0.001,
9068            },
9069            UnitDef {
9070                names: &["us", "microsecond", "microseconds"],
9071                to_base: 1e-6,
9072            },
9073            UnitDef {
9074                names: &["ns", "nanosecond", "nanoseconds"],
9075                to_base: 1e-9,
9076            },
9077            UnitDef {
9078                names: &["min", "minute", "minutes"],
9079                to_base: 60.0,
9080            },
9081            UnitDef {
9082                names: &["h", "hr", "hour", "hours"],
9083                to_base: 3600.0,
9084            },
9085            UnitDef {
9086                names: &["d", "day", "days"],
9087                to_base: 86400.0,
9088            },
9089            UnitDef {
9090                names: &["wk", "week", "weeks"],
9091                to_base: 604800.0,
9092            },
9093            UnitDef {
9094                names: &["mo", "month", "months"],
9095                to_base: 2.628e6,
9096            },
9097            UnitDef {
9098                names: &["yr", "year", "years"],
9099                to_base: 3.156e7,
9100            },
9101            UnitDef {
9102                names: &["decade", "decades"],
9103                to_base: 3.156e8,
9104            },
9105            UnitDef {
9106                names: &["century", "centuries"],
9107                to_base: 3.156e9,
9108            },
9109        ],
9110    },
9111    UnitCat {
9112        name: "Speed",
9113        base: "m/s",
9114        units: &[
9115            UnitDef {
9116                names: &["mps", "m/s", "meter_per_second"],
9117                to_base: 1.0,
9118            },
9119            UnitDef {
9120                names: &["kph", "km/h", "kmh", "kilometer_per_hour"],
9121                to_base: 1.0 / 3.6,
9122            },
9123            UnitDef {
9124                names: &["mph", "mi/h", "mile_per_hour"],
9125                to_base: 0.44704,
9126            },
9127            UnitDef {
9128                names: &["kn", "kt", "knot", "knots"],
9129                to_base: 0.514444,
9130            },
9131            UnitDef {
9132                names: &["fps", "ft/s", "foot_per_second"],
9133                to_base: 0.3048,
9134            },
9135            UnitDef {
9136                names: &["mach"],
9137                to_base: 343.0,
9138            },
9139            UnitDef {
9140                names: &["c", "speed_of_light"],
9141                to_base: 2.998e8,
9142            },
9143        ],
9144    },
9145    UnitCat {
9146        name: "Pressure",
9147        base: "Pa",
9148        units: &[
9149            UnitDef {
9150                names: &["pa", "pascal", "pascals"],
9151                to_base: 1.0,
9152            },
9153            UnitDef {
9154                names: &["kpa", "kilopascal", "kilopascals"],
9155                to_base: 1000.0,
9156            },
9157            UnitDef {
9158                names: &["mpa", "megapascal", "megapascals"],
9159                to_base: 1e6,
9160            },
9161            UnitDef {
9162                names: &["bar", "bars"],
9163                to_base: 1e5,
9164            },
9165            UnitDef {
9166                names: &["mbar", "millibar", "millibars"],
9167                to_base: 100.0,
9168            },
9169            UnitDef {
9170                names: &["atm", "atmosphere", "atmospheres"],
9171                to_base: 101325.0,
9172            },
9173            UnitDef {
9174                names: &["torr", "mmhg"],
9175                to_base: 133.322,
9176            },
9177            UnitDef {
9178                names: &["psi", "pound_per_square_inch"],
9179                to_base: 6894.76,
9180            },
9181            UnitDef {
9182                names: &["inhg", "in_hg", "inches_of_mercury"],
9183                to_base: 3386.39,
9184            },
9185        ],
9186    },
9187    UnitCat {
9188        name: "Energy",
9189        base: "J",
9190        units: &[
9191            UnitDef {
9192                names: &["j", "joule", "joules"],
9193                to_base: 1.0,
9194            },
9195            UnitDef {
9196                names: &["kj", "kilojoule", "kilojoules"],
9197                to_base: 1000.0,
9198            },
9199            UnitDef {
9200                names: &["mj", "megajoule", "megajoules"],
9201                to_base: 1e6,
9202            },
9203            UnitDef {
9204                names: &["gj", "gigajoule", "gigajoules"],
9205                to_base: 1e9,
9206            },
9207            UnitDef {
9208                names: &["cal", "calorie", "calories"],
9209                to_base: 4.184,
9210            },
9211            UnitDef {
9212                names: &["kcal", "kilocalorie", "kilocalories", "food_calorie"],
9213                to_base: 4184.0,
9214            },
9215            UnitDef {
9216                names: &["wh", "watt_hour", "watt_hours"],
9217                to_base: 3600.0,
9218            },
9219            UnitDef {
9220                names: &["kwh", "kilowatt_hour", "kilowatt_hours"],
9221                to_base: 3.6e6,
9222            },
9223            UnitDef {
9224                names: &["mwh", "megawatt_hour"],
9225                to_base: 3.6e9,
9226            },
9227            UnitDef {
9228                names: &["gwh", "gigawatt_hour"],
9229                to_base: 3.6e12,
9230            },
9231            UnitDef {
9232                names: &["btu", "british_thermal_unit"],
9233                to_base: 1055.06,
9234            },
9235            UnitDef {
9236                names: &["ev", "electronvolt", "electron_volt"],
9237                to_base: 1.602e-19,
9238            },
9239            UnitDef {
9240                names: &["ftlb", "ft_lb", "foot_pound"],
9241                to_base: 1.35582,
9242            },
9243            UnitDef {
9244                names: &["erg", "ergs"],
9245                to_base: 1e-7,
9246            },
9247        ],
9248    },
9249    UnitCat {
9250        name: "Power",
9251        base: "W",
9252        units: &[
9253            UnitDef {
9254                names: &["w", "watt", "watts"],
9255                to_base: 1.0,
9256            },
9257            UnitDef {
9258                names: &["kw", "kilowatt", "kilowatts"],
9259                to_base: 1000.0,
9260            },
9261            UnitDef {
9262                names: &["mw", "megawatt", "megawatts"],
9263                to_base: 1e6,
9264            },
9265            UnitDef {
9266                names: &["gw", "gigawatt", "gigawatts"],
9267                to_base: 1e9,
9268            },
9269            UnitDef {
9270                names: &["hp", "horsepower"],
9271                to_base: 745.7,
9272            },
9273            UnitDef {
9274                names: &["btu_h", "btu_per_hour"],
9275                to_base: 0.29307,
9276            },
9277            UnitDef {
9278                names: &["mw_th", "milliwatt"],
9279                to_base: 1e-3,
9280            },
9281        ],
9282    },
9283    UnitCat {
9284        name: "Digital Storage",
9285        base: "bytes",
9286        units: &[
9287            UnitDef {
9288                names: &["b", "bit", "bits"],
9289                to_base: 0.125,
9290            },
9291            UnitDef {
9292                names: &["byte", "bytes"],
9293                to_base: 1.0,
9294            },
9295            UnitDef {
9296                names: &["kb", "kilobyte", "kilobytes"],
9297                to_base: 1000.0,
9298            },
9299            UnitDef {
9300                names: &["mb", "megabyte", "megabytes"],
9301                to_base: 1e6,
9302            },
9303            UnitDef {
9304                names: &["gb", "gigabyte", "gigabytes"],
9305                to_base: 1e9,
9306            },
9307            UnitDef {
9308                names: &["tb", "terabyte", "terabytes"],
9309                to_base: 1e12,
9310            },
9311            UnitDef {
9312                names: &["pb", "petabyte", "petabytes"],
9313                to_base: 1e15,
9314            },
9315            UnitDef {
9316                names: &["kib", "kibibyte", "kibibytes"],
9317                to_base: 1024.0,
9318            },
9319            UnitDef {
9320                names: &["mib", "mebibyte", "mebibytes"],
9321                to_base: 1048576.0,
9322            },
9323            UnitDef {
9324                names: &["gib", "gibibyte", "gibibytes"],
9325                to_base: 1073741824.0,
9326            },
9327            UnitDef {
9328                names: &["tib", "tebibyte", "tebibytes"],
9329                to_base: 1.0995e12,
9330            },
9331            UnitDef {
9332                names: &["kbps", "kilobit_per_second"],
9333                to_base: 125.0,
9334            },
9335            UnitDef {
9336                names: &["mbps", "megabit_per_second"],
9337                to_base: 125000.0,
9338            },
9339            UnitDef {
9340                names: &["gbps", "gigabit_per_second"],
9341                to_base: 125000000.0,
9342            },
9343        ],
9344    },
9345    UnitCat {
9346        name: "Angle",
9347        base: "rad",
9348        units: &[
9349            UnitDef {
9350                names: &["rad", "radian", "radians"],
9351                to_base: 1.0,
9352            },
9353            UnitDef {
9354                names: &["deg", "degree", "degrees"],
9355                to_base: std::f64::consts::PI / 180.0,
9356            },
9357            UnitDef {
9358                names: &["grad", "gradian", "gradians", "gon"],
9359                to_base: std::f64::consts::PI / 200.0,
9360            },
9361            UnitDef {
9362                names: &["rev", "revolution", "turn", "turns"],
9363                to_base: 2.0 * std::f64::consts::PI,
9364            },
9365            UnitDef {
9366                names: &["arcmin", "arcminute", "arcminutes"],
9367                to_base: std::f64::consts::PI / 10800.0,
9368            },
9369            UnitDef {
9370                names: &["arcsec", "arcsecond", "arcseconds"],
9371                to_base: std::f64::consts::PI / 648000.0,
9372            },
9373        ],
9374    },
9375    UnitCat {
9376        name: "Force",
9377        base: "N",
9378        units: &[
9379            UnitDef {
9380                names: &["n", "newton", "newtons"],
9381                to_base: 1.0,
9382            },
9383            UnitDef {
9384                names: &["kn_f", "kilonewton", "kilonewtons"],
9385                to_base: 1000.0,
9386            },
9387            UnitDef {
9388                names: &["lbf", "pound_force", "pounds_force"],
9389                to_base: 4.44822,
9390            },
9391            UnitDef {
9392                names: &["kgf", "kilogram_force"],
9393                to_base: 9.80665,
9394            },
9395            UnitDef {
9396                names: &["dyn", "dyne", "dynes"],
9397                to_base: 1e-5,
9398            },
9399            UnitDef {
9400                names: &["gf", "gram_force"],
9401                to_base: 0.00980665,
9402            },
9403        ],
9404    },
9405    UnitCat {
9406        name: "Temperature",
9407        base: "K",
9408        units: &[
9409            UnitDef {
9410                names: &["k", "kelvin", "kelvins"],
9411                to_base: f64::NAN,
9412            },
9413            UnitDef {
9414                names: &["c", "celsius", "degc"],
9415                to_base: f64::NAN,
9416            },
9417            UnitDef {
9418                names: &["f", "fahrenheit", "degf"],
9419                to_base: f64::NAN,
9420            },
9421            UnitDef {
9422                names: &["r", "rankine", "degr"],
9423                to_base: f64::NAN,
9424            },
9425        ],
9426    },
9427    UnitCat {
9428        name: "Fuel Economy",
9429        base: "L/100km",
9430        units: &[
9431            UnitDef {
9432                names: &["l/100km", "lpkm", "liter_per_100km"],
9433                to_base: 1.0,
9434            },
9435            UnitDef {
9436                names: &["mpg", "mile_per_gallon", "miles_per_gallon"],
9437                to_base: f64::NAN,
9438            },
9439            UnitDef {
9440                names: &["km/l", "kpl", "kilometer_per_liter"],
9441                to_base: f64::NAN,
9442            },
9443        ],
9444    },
9445];
9446
9447fn units_to_base(val: f64, from_lower: &str) -> Option<(f64, usize)> {
9448    for (cat_idx, cat) in UNIT_TABLE.iter().enumerate() {
9449        for entry in cat.units {
9450            if entry.names.contains(&from_lower) {
9451                if cat.name == "Temperature" {
9452                    let k = temp_to_kelvin(val, from_lower)?;
9453                    return Some((k, cat_idx));
9454                }
9455                if cat.name == "Fuel Economy" {
9456                    let l100 = fuel_to_l100(val, from_lower)?;
9457                    return Some((l100, cat_idx));
9458                }
9459                return Some((val * entry.to_base, cat_idx));
9460            }
9461        }
9462    }
9463    None
9464}
9465
9466fn units_from_base(base_val: f64, to_lower: &str, cat_idx: usize) -> Option<f64> {
9467    let cat = &UNIT_TABLE[cat_idx];
9468    for entry in cat.units {
9469        if entry.names.contains(&to_lower) {
9470            if cat.name == "Temperature" {
9471                return kelvin_to_temp(base_val, to_lower);
9472            }
9473            if cat.name == "Fuel Economy" {
9474                return l100_to_fuel(base_val, to_lower);
9475            }
9476            return Some(base_val / entry.to_base);
9477        }
9478    }
9479    None
9480}
9481
9482fn temp_to_kelvin(val: f64, unit: &str) -> Option<f64> {
9483    match unit {
9484        "k" | "kelvin" | "kelvins" => Some(val),
9485        "c" | "celsius" | "degc" => Some(val + 273.15),
9486        "f" | "fahrenheit" | "degf" => Some((val + 459.67) * 5.0 / 9.0),
9487        "r" | "rankine" | "degr" => Some(val * 5.0 / 9.0),
9488        _ => None,
9489    }
9490}
9491
9492fn kelvin_to_temp(k: f64, unit: &str) -> Option<f64> {
9493    match unit {
9494        "k" | "kelvin" | "kelvins" => Some(k),
9495        "c" | "celsius" | "degc" => Some(k - 273.15),
9496        "f" | "fahrenheit" | "degf" => Some(k * 9.0 / 5.0 - 459.67),
9497        "r" | "rankine" | "degr" => Some(k * 9.0 / 5.0),
9498        _ => None,
9499    }
9500}
9501
9502fn fuel_to_l100(val: f64, unit: &str) -> Option<f64> {
9503    match unit {
9504        "l/100km" | "lpkm" | "liter_per_100km" => Some(val),
9505        "mpg" | "mile_per_gallon" | "miles_per_gallon" => Some(235.215 / val),
9506        "km/l" | "kpl" | "kilometer_per_liter" => Some(100.0 / val),
9507        _ => None,
9508    }
9509}
9510
9511fn l100_to_fuel(l100: f64, unit: &str) -> Option<f64> {
9512    match unit {
9513        "l/100km" | "lpkm" | "liter_per_100km" => Some(l100),
9514        "mpg" | "mile_per_gallon" | "miles_per_gallon" => Some(235.215 / l100),
9515        "km/l" | "kpl" | "kilometer_per_liter" => Some(100.0 / l100),
9516        _ => None,
9517    }
9518}
9519
9520fn find_unit_category(unit_lower: &str) -> Option<usize> {
9521    for (i, cat) in UNIT_TABLE.iter().enumerate() {
9522        for entry in cat.units {
9523            if entry.names.contains(&unit_lower) {
9524                return Some(i);
9525            }
9526        }
9527    }
9528    None
9529}
9530
9531fn fmt_unit_val(v: f64) -> String {
9532    if v == 0.0 {
9533        return "0".into();
9534    }
9535    let abs = v.abs();
9536    if !(1e-4..1e9).contains(&abs) {
9537        format!("{:.6e}", v)
9538    } else if abs >= 1000.0 {
9539        format!("{:.4}", v)
9540    } else {
9541        format!("{:.8}", v)
9542    }
9543}
9544
9545pub fn units_calc(query: &str) -> String {
9546    let mut out = String::new();
9547    let sep = "═".repeat(60);
9548    let _ = writeln!(out, "{}", sep);
9549    let _ = writeln!(out, "  UNIT CONVERSION");
9550    let _ = writeln!(out, "{}", sep);
9551
9552    // Parse: "VALUE FROM [to|in|->] TO"  or  "VALUE FROM"  (broadcast)
9553    let q = query.trim();
9554    let tokens: Vec<&str> = q.split_whitespace().collect();
9555
9556    if tokens.is_empty() {
9557        let _ = writeln!(out, "{}", units_usage());
9558        let _ = writeln!(out, "{}", sep);
9559        return out;
9560    }
9561
9562    // First token must be a number
9563    let val = match tokens[0].replace(',', "").parse::<f64>() {
9564        Ok(v) => v,
9565        Err(_) => {
9566            // maybe "list CATEGORY"
9567            if tokens[0].to_lowercase() == "list" {
9568                let cat_query = tokens[1..].join(" ").to_lowercase();
9569                for cat in UNIT_TABLE {
9570                    if cat_query.is_empty() || cat.name.to_lowercase().contains(&cat_query) {
9571                        let _ = writeln!(out, "  {}  (base: {})", cat.name, cat.base);
9572                        for entry in cat.units {
9573                            let _ = writeln!(out, "    {}", entry.names[0]);
9574                        }
9575                        let _ = writeln!(out);
9576                    }
9577                }
9578                let _ = writeln!(out, "{}", sep);
9579                return out;
9580            }
9581            let _ = writeln!(
9582                out,
9583                "  ERROR: first token must be a number. Got '{}'",
9584                tokens[0]
9585            );
9586            let _ = writeln!(out, "{}", units_usage());
9587            let _ = writeln!(out, "{}", sep);
9588            return out;
9589        }
9590    };
9591
9592    if tokens.len() < 2 {
9593        let _ = writeln!(out, "  ERROR: need VALUE UNIT  e.g.  5 km");
9594        let _ = writeln!(out, "{}", sep);
9595        return out;
9596    }
9597
9598    let from_raw = tokens[1].to_lowercase();
9599
9600    // Skip connector tokens: "to", "in", "->", "as"
9601    let connector_set = ["to", "in", "->", "as", "into"];
9602    let to_raw: Option<String> = tokens[2..]
9603        .iter()
9604        .find(|t| !connector_set.contains(&t.to_lowercase().as_str()))
9605        .map(|t| t.to_lowercase());
9606
9607    // Look up source unit
9608    let (base_val, cat_idx) = match units_to_base(val, &from_raw) {
9609        Some(v) => v,
9610        None => {
9611            let _ = writeln!(out, "  ERROR: unknown unit '{}'.", tokens[1]);
9612            let _ = writeln!(out, "  Tip: run  hematite --units list  to see all units.");
9613            let _ = writeln!(out, "{}", sep);
9614            return out;
9615        }
9616    };
9617
9618    let cat = &UNIT_TABLE[cat_idx];
9619
9620    if let Some(to_unit) = to_raw {
9621        // Single target conversion
9622        match units_from_base(base_val, &to_unit, cat_idx) {
9623            Some(result) => {
9624                let _ = writeln!(
9625                    out,
9626                    "  {} {} = {} {}",
9627                    val,
9628                    tokens[1],
9629                    fmt_unit_val(result),
9630                    &to_unit
9631                );
9632            }
9633            None => {
9634                // Check if to_unit exists in a different category
9635                if let Some(other_idx) = find_unit_category(&to_unit) {
9636                    let _ = writeln!(
9637                        out,
9638                        "  ERROR: '{}' is a {} unit, but '{}' is {}.",
9639                        &to_unit, UNIT_TABLE[other_idx].name, tokens[1], cat.name
9640                    );
9641                } else {
9642                    let _ = writeln!(out, "  ERROR: unknown target unit '{}'.", &to_unit);
9643                }
9644            }
9645        }
9646    } else {
9647        // Broadcast: convert to all units in the category
9648        let _ = writeln!(out, "  {} {}  =", val, tokens[1]);
9649        let _ = writeln!(out, "  {:>30}  unit", "value");
9650        let _ = writeln!(out, "  {}", "-".repeat(42));
9651        for entry in cat.units {
9652            let to_name = entry.names[0];
9653            if to_name == from_raw {
9654                continue;
9655            }
9656            if let Some(result) = units_from_base(base_val, to_name, cat_idx) {
9657                let abs = result.abs();
9658                // skip values that are so tiny or huge they're not useful
9659                if abs > 0.0 && !(1e-30..=1e30).contains(&abs) {
9660                    continue;
9661                }
9662                let _ = writeln!(out, "  {:>30}  {}", fmt_unit_val(result), to_name);
9663            }
9664        }
9665    }
9666
9667    let _ = writeln!(out, "{}", sep);
9668    out
9669}
9670
9671fn units_usage() -> &'static str {
9672    "Usage:\n\
9673     hematite --units '100 km to miles'       convert single value\n\
9674     hematite --units '32 f to c'             temperature: 32°F → °C\n\
9675     hematite --units '5 kg'                  broadcast: kg to all mass units\n\
9676     hematite --units '1 kwh to j'            energy: kWh → Joules\n\
9677     hematite --units '1 gbps to mbps'        digital storage speed\n\
9678     hematite --units '2.5 atm to psi'        pressure\n\
9679     hematite --units 'list length'           list all length units\n\
9680     hematite --units 'list'                  list all categories\n\
9681     \n\
9682     Categories: Length Area Volume Mass Time Speed Pressure Energy\n\
9683                 Power Digital-Storage Angle Force Temperature Fuel-Economy"
9684}
9685
9686// ── ODE solver ────────────────────────────────────────────────────────────────
9687// Solves first-order ODEs and systems using Euler, RK4, or adaptive RK45.
9688// Equations are parsed as simple expressions using the symbolic engine.
9689// Supports preset equations for well-known models.
9690
9691// Evaluate a simple 1-variable function f(t, y) from a string.
9692// Handles: constants, t, y, arithmetic +−*/^, sin/cos/exp/ln/sqrt.
9693fn ode_eval_expr(expr_str: &str, t: f64, y: f64) -> f64 {
9694    let substituted = expr_str
9695        .replace("exp(", "\x00EXP\x00(")
9696        .replace('y', &format!("({:.17e})", y))
9697        .replace('t', &format!("({:.17e})", t))
9698        .replace("\x00EXP\x00(", "exp(");
9699    let expr = match parse_sym(&substituted) {
9700        Ok(e) => e,
9701        Err(_) => return f64::NAN,
9702    };
9703    eval_expr(&expr, "x", 0.0).unwrap_or(f64::NAN)
9704}
9705
9706// RK4 step for scalar ODE dy/dt = f(t, y)
9707fn rk4_step(f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64, h: f64) -> f64 {
9708    let k1 = f(t, y);
9709    let k2 = f(t + h / 2.0, y + h * k1 / 2.0);
9710    let k3 = f(t + h / 2.0, y + h * k2 / 2.0);
9711    let k4 = f(t + h, y + h * k3);
9712    y + h * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0
9713}
9714
9715// Euler step
9716fn euler_step(f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64, h: f64) -> f64 {
9717    y + h * f(t, y)
9718}
9719
9720// Adaptive RK45 (Dormand-Prince) — returns (y_new, error_estimate, h_used)
9721fn rk45_step(f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64, h: f64) -> (f64, f64) {
9722    // Dormand-Prince coefficients
9723    let k1 = f(t, y);
9724    let k2 = f(t + h / 5.0, y + h * (k1 / 5.0));
9725    let k3 = f(
9726        t + 3.0 * h / 10.0,
9727        y + h * (3.0 * k1 / 40.0 + 9.0 * k2 / 40.0),
9728    );
9729    let k4 = f(
9730        t + 4.0 * h / 5.0,
9731        y + h * (44.0 * k1 / 45.0 - 56.0 * k2 / 15.0 + 32.0 * k3 / 9.0),
9732    );
9733    let k5 = f(
9734        t + 8.0 * h / 9.0,
9735        y + h
9736            * (19372.0 * k1 / 6561.0 - 25360.0 * k2 / 2187.0 + 64448.0 * k3 / 6561.0
9737                - 212.0 * k4 / 729.0),
9738    );
9739    let k6 = f(
9740        t + h,
9741        y + h
9742            * (9017.0 * k1 / 3168.0 - 355.0 * k2 / 33.0
9743                + 46732.0 * k3 / 5247.0
9744                + 49.0 * k4 / 176.0
9745                - 5103.0 * k5 / 18656.0),
9746    );
9747    // 5th order solution
9748    let y5 = y + h
9749        * (35.0 * k1 / 384.0 + 500.0 * k3 / 1113.0 + 125.0 * k4 / 192.0 - 2187.0 * k5 / 6784.0
9750            + 11.0 * k6 / 84.0);
9751    // 4th order for error estimate
9752    let y4 = y + h
9753        * (5179.0 * k1 / 57600.0 + 7571.0 * k3 / 16695.0 + 393.0 * k4 / 640.0
9754            - 92097.0 * k5 / 339200.0
9755            + 187.0 * k6 / 2100.0
9756            + k6 / 40.0);
9757    let err = (y5 - y4).abs();
9758    (y5, err)
9759}
9760
9761// 2D system RK4: dy1/dt = f1(t,y1,y2), dy2/dt = f2(t,y1,y2)
9762fn rk4_system_step(
9763    f1: &dyn Fn(f64, f64, f64) -> f64,
9764    f2: &dyn Fn(f64, f64, f64) -> f64,
9765    t: f64,
9766    y1: f64,
9767    y2: f64,
9768    h: f64,
9769) -> (f64, f64) {
9770    let k1a = f1(t, y1, y2);
9771    let k1b = f2(t, y1, y2);
9772    let k2a = f1(t + h / 2.0, y1 + h * k1a / 2.0, y2 + h * k1b / 2.0);
9773    let k2b = f2(t + h / 2.0, y1 + h * k1a / 2.0, y2 + h * k1b / 2.0);
9774    let k3a = f1(t + h / 2.0, y1 + h * k2a / 2.0, y2 + h * k2b / 2.0);
9775    let k3b = f2(t + h / 2.0, y1 + h * k2a / 2.0, y2 + h * k2b / 2.0);
9776    let k4a = f1(t + h, y1 + h * k3a, y2 + h * k3b);
9777    let k4b = f2(t + h, y1 + h * k3a, y2 + h * k3b);
9778    (
9779        y1 + h * (k1a + 2.0 * k2a + 2.0 * k3a + k4a) / 6.0,
9780        y2 + h * (k1b + 2.0 * k2b + 2.0 * k3b + k4b) / 6.0,
9781    )
9782}
9783
9784fn ode_ascii_plot(ts: &[f64], ys: &[f64], width: usize, height: usize) -> String {
9785    if ts.is_empty() || ys.is_empty() {
9786        return String::new();
9787    }
9788    let mn = ys.iter().cloned().fold(f64::INFINITY, f64::min);
9789    let mx = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
9790    let yrange = (mx - mn).max(1e-12);
9791    let tmin = ts[0];
9792    let tmax = ts[ts.len() - 1];
9793    let trange = (tmax - tmin).max(1e-12);
9794    let mut grid = vec![vec![' '; width]; height];
9795    for (i, (&t, &y)) in ts.iter().zip(ys.iter()).enumerate() {
9796        let _ = i;
9797        let col = ((t - tmin) / trange * (width - 1) as f64).round() as usize;
9798        let row = height - 1 - ((y - mn) / yrange * (height - 1) as f64).round() as usize;
9799        let col = col.min(width - 1);
9800        let row = row.min(height - 1);
9801        grid[row][col] = '·';
9802    }
9803    let result = grid
9804        .iter()
9805        .map(|r| r.iter().collect::<String>())
9806        .collect::<Vec<_>>()
9807        .join("\n");
9808    format!(
9809        "{}\n  y: [{:.4} .. {:.4}]\n  t: [{:.4} .. {:.4}]",
9810        result, mn, mx, tmin, tmax
9811    )
9812}
9813
9814pub fn ode_solve(query: &str) -> String {
9815    let mut out = String::new();
9816    let sep = "═".repeat(60);
9817    let _ = writeln!(out, "{}", sep);
9818    let _ = writeln!(out, "  ODE SOLVER");
9819    let _ = writeln!(out, "{}", sep);
9820
9821    let q = query.trim().to_lowercase();
9822
9823    // --- Preset models ---
9824    if q.starts_with("logistic") || q.contains("logistic growth") {
9825        // dy/dt = r*y*(1 - y/K)
9826        let tokens: Vec<&str> = query.split_whitespace().collect();
9827        let r: f64 = tokens
9828            .iter()
9829            .position(|&t| t.to_lowercase() == "r")
9830            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9831            .unwrap_or(1.0);
9832        let k: f64 = tokens
9833            .iter()
9834            .position(|&t| t.to_lowercase() == "k")
9835            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9836            .unwrap_or(100.0);
9837        let y0: f64 = tokens
9838            .iter()
9839            .position(|&t| t.to_lowercase() == "y0")
9840            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9841            .unwrap_or(10.0);
9842        let t_end: f64 = tokens
9843            .iter()
9844            .position(|&t| t.to_lowercase() == "t")
9845            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9846            .unwrap_or(10.0);
9847        let n = 200_usize;
9848        let h = t_end / n as f64;
9849        let _ = writeln!(out, "  Logistic Growth:  dy/dt = r·y·(1 - y/K)");
9850        let _ = writeln!(
9851            out,
9852            "  r={:.4}  K={:.4}  y0={:.4}  t=[0,{:.4}]",
9853            r, k, y0, t_end
9854        );
9855        let _ = writeln!(out);
9856        let f = |_t: f64, y: f64| r * y * (1.0 - y / k);
9857        let mut t = 0.0_f64;
9858        let mut y = y0;
9859        let mut ts = vec![t];
9860        let mut ys = vec![y];
9861        for _ in 0..n {
9862            y = rk4_step(&f, t, y, h);
9863            t += h;
9864            ts.push(t);
9865            ys.push(y);
9866        }
9867        let analytical_k = k / (1.0 + (k / y0 - 1.0) * (-r * t_end).exp());
9868        let _ = writeln!(
9869            out,
9870            "  y(t_end) = {:.6}  [analytical: {:.6}]",
9871            ys[ys.len() - 1],
9872            analytical_k
9873        );
9874        let plot = ode_ascii_plot(&ts, &ys, 56, 10);
9875        let _ = writeln!(out);
9876        for line in plot.lines() {
9877            let _ = writeln!(out, "  {}", line);
9878        }
9879        let _ = writeln!(out, "{}", sep);
9880        return out;
9881    }
9882
9883    if q.starts_with("exponential") || q.starts_with("decay") || q.starts_with("growth") {
9884        let tokens: Vec<&str> = query.split_whitespace().collect();
9885        let r: f64 = tokens
9886            .iter()
9887            .position(|&t| t.to_lowercase() == "r")
9888            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9889            .unwrap_or(1.0);
9890        let y0: f64 = tokens
9891            .iter()
9892            .position(|&t| t.to_lowercase() == "y0")
9893            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9894            .unwrap_or(1.0);
9895        let t_end: f64 = tokens
9896            .iter()
9897            .position(|&t| t.to_lowercase() == "t")
9898            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9899            .unwrap_or(5.0);
9900        let n = 200_usize;
9901        let h = t_end / n as f64;
9902        let _ = writeln!(out, "  Exponential:  dy/dt = r·y");
9903        let _ = writeln!(out, "  r={:.4}  y0={:.4}  t=[0,{:.4}]", r, y0, t_end);
9904        let f = |_t: f64, y: f64| r * y;
9905        let mut t = 0.0_f64;
9906        let mut y = y0;
9907        let mut ts = vec![t];
9908        let mut ys = vec![y];
9909        for _ in 0..n {
9910            y = rk4_step(&f, t, y, h);
9911            t += h;
9912            ts.push(t);
9913            ys.push(y);
9914        }
9915        let analytical = y0 * (r * t_end).exp();
9916        let _ = writeln!(
9917            out,
9918            "  y(t_end) = {:.6}  [analytical: {:.6}]",
9919            ys[ys.len() - 1],
9920            analytical
9921        );
9922        let plot = ode_ascii_plot(&ts, &ys, 56, 10);
9923        let _ = writeln!(out);
9924        for line in plot.lines() {
9925            let _ = writeln!(out, "  {}", line);
9926        }
9927        let _ = writeln!(out, "{}", sep);
9928        return out;
9929    }
9930
9931    if q.starts_with("lotka") || q.contains("predator") || q.contains("prey") {
9932        // Lotka-Volterra: dx/dt = α*x - β*x*y,  dy/dt = δ*x*y - γ*y
9933        let tokens: Vec<&str> = query.split_whitespace().collect();
9934        let alpha: f64 = tokens
9935            .iter()
9936            .position(|&t| t.to_lowercase() == "alpha")
9937            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9938            .unwrap_or(1.0);
9939        let beta: f64 = tokens
9940            .iter()
9941            .position(|&t| t.to_lowercase() == "beta")
9942            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9943            .unwrap_or(0.1);
9944        let delta: f64 = tokens
9945            .iter()
9946            .position(|&t| t.to_lowercase() == "delta")
9947            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9948            .unwrap_or(0.075);
9949        let gamma: f64 = tokens
9950            .iter()
9951            .position(|&t| t.to_lowercase() == "gamma")
9952            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9953            .unwrap_or(1.5);
9954        let x0: f64 = tokens
9955            .iter()
9956            .position(|&t| t.to_lowercase() == "x0")
9957            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9958            .unwrap_or(10.0);
9959        let y0: f64 = tokens
9960            .iter()
9961            .position(|&t| t.to_lowercase() == "y0")
9962            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9963            .unwrap_or(5.0);
9964        let t_end: f64 = tokens
9965            .iter()
9966            .position(|&t| t.to_lowercase() == "t")
9967            .and_then(|i| tokens.get(i + 1)?.parse().ok())
9968            .unwrap_or(30.0);
9969        let n = 2000_usize;
9970        let h = t_end / n as f64;
9971        let _ = writeln!(out, "  Lotka-Volterra (predator-prey)");
9972        let _ = writeln!(out, "  dx/dt = {:.3}x - {:.3}xy  (prey)", alpha, beta);
9973        let _ = writeln!(out, "  dy/dt = {:.3}xy - {:.3}y  (predator)", delta, gamma);
9974        let _ = writeln!(out, "  x0={:.2}  y0={:.2}  t=[0,{:.1}]", x0, y0, t_end);
9975        let f1 = |_t: f64, x: f64, y: f64| alpha * x - beta * x * y;
9976        let f2 = |_t: f64, x: f64, y: f64| delta * x * y - gamma * y;
9977        let mut t = 0.0_f64;
9978        let mut x = x0;
9979        let mut y = y0;
9980        let mut ts = vec![t];
9981        let mut xs = vec![x];
9982        let mut ys = vec![y];
9983        for _ in 0..n {
9984            let (nx, ny) = rk4_system_step(&f1, &f2, t, x, y, h);
9985            x = nx;
9986            y = ny;
9987            t += h;
9988            ts.push(t);
9989            xs.push(x);
9990            ys.push(y);
9991        }
9992        let _ = writeln!(
9993            out,
9994            "  x(t_end)={:.4}  y(t_end)={:.4}",
9995            xs[xs.len() - 1],
9996            ys[ys.len() - 1]
9997        );
9998        let _ = writeln!(out, "  Prey trajectory:");
9999        let plot = ode_ascii_plot(&ts, &xs, 56, 8);
10000        for line in plot.lines() {
10001            let _ = writeln!(out, "  {}", line);
10002        }
10003        let _ = writeln!(out, "  Predator trajectory:");
10004        let plot2 = ode_ascii_plot(&ts, &ys, 56, 8);
10005        for line in plot2.lines() {
10006            let _ = writeln!(out, "  {}", line);
10007        }
10008        let _ = writeln!(out, "{}", sep);
10009        return out;
10010    }
10011
10012    if q.starts_with("sir") || q.contains("susceptible") || q.contains("epidemic") {
10013        // SIR model: dS/dt=-β*S*I/N, dI/dt=β*S*I/N - γ*I, dR/dt=γ*I
10014        let tokens: Vec<&str> = query.split_whitespace().collect();
10015        let n_pop: f64 = tokens
10016            .iter()
10017            .position(|&t| t.to_lowercase() == "n")
10018            .and_then(|i| tokens.get(i + 1)?.parse().ok())
10019            .unwrap_or(1000.0);
10020        let beta: f64 = tokens
10021            .iter()
10022            .position(|&t| t.to_lowercase() == "beta")
10023            .and_then(|i| tokens.get(i + 1)?.parse().ok())
10024            .unwrap_or(0.3);
10025        let gamma: f64 = tokens
10026            .iter()
10027            .position(|&t| t.to_lowercase() == "gamma")
10028            .and_then(|i| tokens.get(i + 1)?.parse().ok())
10029            .unwrap_or(0.05);
10030        let i0: f64 = tokens
10031            .iter()
10032            .position(|&t| t.to_lowercase() == "i0")
10033            .and_then(|i| tokens.get(i + 1)?.parse().ok())
10034            .unwrap_or(1.0);
10035        let t_end: f64 = tokens
10036            .iter()
10037            .position(|&t| t.to_lowercase() == "t")
10038            .and_then(|i| tokens.get(i + 1)?.parse().ok())
10039            .unwrap_or(200.0);
10040        let n_steps = 2000_usize;
10041        let h = t_end / n_steps as f64;
10042        let r0 = beta / gamma;
10043        let _ = writeln!(
10044            out,
10045            "  SIR Epidemic Model  (N={}, β={:.4}, γ={:.4}, R₀={:.2})",
10046            n_pop, beta, gamma, r0
10047        );
10048        let _ = writeln!(
10049            out,
10050            "  dS/dt = -β·S·I/N   dI/dt = β·S·I/N - γ·I   dR/dt = γ·I"
10051        );
10052        let mut s = n_pop - i0;
10053        let mut inf = i0;
10054        let mut r = 0.0_f64;
10055        let mut t = 0.0_f64;
10056        let mut ts = vec![t];
10057        let mut is = vec![inf];
10058        let mut peak_i = i0;
10059        let mut peak_t = 0.0_f64;
10060        for _ in 0..n_steps {
10061            let ds = -beta * s * inf / n_pop;
10062            let di = beta * s * inf / n_pop - gamma * inf;
10063            let dr = gamma * inf;
10064            s += h * ds;
10065            inf += h * di;
10066            r += h * dr;
10067            t += h;
10068            if inf > peak_i {
10069                peak_i = inf;
10070                peak_t = t;
10071            }
10072            ts.push(t);
10073            is.push(inf);
10074        }
10075        let _ = writeln!(out, "  Peak infected: {:.1} at t={:.1}", peak_i, peak_t);
10076        let _ = writeln!(out, "  Final: S={:.1}  I={:.1}  R={:.1}", s, inf, r);
10077        let plot = ode_ascii_plot(&ts, &is, 56, 10);
10078        for line in plot.lines() {
10079            let _ = writeln!(out, "  {}", line);
10080        }
10081        let _ = writeln!(out, "{}", sep);
10082        return out;
10083    }
10084
10085    // --- Generic: dy/dt = EXPR  y0=VALUE  t=END  [n=STEPS] [method=euler|rk4|rk45] ---
10086    let tokens: Vec<&str> = query.split_whitespace().collect();
10087
10088    // Parse: "dy/dt = EXPR" form or "EXPR y0=V t=END"
10089    let eq_str: String = if let Some(eq_pos) = query.find('=') {
10090        // Could be "dy/dt = ..." — find the first '=' and take what's after
10091        // But also y0= or t= have = so find the equation part
10092        let before = &query[..eq_pos].trim().to_lowercase();
10093        if before.ends_with("dy/dt") || before.ends_with("f") || before.ends_with("dydt") {
10094            query[eq_pos + 1..]
10095                .split_whitespace()
10096                .take_while(|t| {
10097                    !t.to_lowercase().starts_with("y0=")
10098                        && !t.to_lowercase().starts_with("t=")
10099                        && !t.to_lowercase().starts_with("n=")
10100                        && !t.to_lowercase().starts_with("method=")
10101                })
10102                .collect::<Vec<_>>()
10103                .join("")
10104        } else {
10105            tokens[0].to_string()
10106        }
10107    } else {
10108        tokens[0].to_string()
10109    };
10110
10111    // Extract named params
10112    let get_param = |key: &str| -> Option<f64> {
10113        query
10114            .split_whitespace()
10115            .find(|t| t.to_lowercase().starts_with(&format!("{}=", key)))
10116            .and_then(|t| t.split_once('=')?.1.parse().ok())
10117    };
10118    let get_str_param = |key: &str| -> Option<String> {
10119        query
10120            .split_whitespace()
10121            .find(|t| t.to_lowercase().starts_with(&format!("{}=", key)))
10122            .and_then(|t| t.split_once('=').map(|x| x.1).map(|s| s.to_lowercase()))
10123    };
10124
10125    let y0 = get_param("y0").unwrap_or(1.0);
10126    let t_end = get_param("t")
10127        .or_else(|| get_param("tend"))
10128        .or_else(|| get_param("t_end"))
10129        .unwrap_or(10.0);
10130    let n = get_param("n")
10131        .map(|v| v as usize)
10132        .unwrap_or(100)
10133        .clamp(4, 10000);
10134    let method = get_str_param("method").unwrap_or_else(|| "rk4".into());
10135
10136    if eq_str.is_empty() || eq_str == "0" {
10137        let _ = writeln!(out, "{}", ode_usage());
10138        let _ = writeln!(out, "{}", sep);
10139        return out;
10140    }
10141
10142    let eq = eq_str.clone();
10143    let f = move |t: f64, y: f64| ode_eval_expr(&eq, t, y);
10144    let h = t_end / n as f64;
10145
10146    let _ = writeln!(
10147        out,
10148        "  dy/dt = {}   y0={}   t=[0,{}]   n={}   method={}",
10149        eq_str, y0, t_end, n, method
10150    );
10151    let _ = writeln!(out);
10152
10153    let (ts, ys): (Vec<f64>, Vec<f64>) = match method.as_str() {
10154        "euler" => {
10155            let mut t = 0.0_f64;
10156            let mut y = y0;
10157            let mut ts = vec![t];
10158            let mut ys = vec![y];
10159            for _ in 0..n {
10160                y = euler_step(&f, t, y, h);
10161                t += h;
10162                ts.push(t);
10163                ys.push(y);
10164            }
10165            (ts, ys)
10166        }
10167        "rk45" | "adaptive" => {
10168            let mut t = 0.0_f64;
10169            let mut y = y0;
10170            let mut ts = vec![t];
10171            let mut ys = vec![y];
10172            let mut h_cur = h;
10173            let tol = 1e-6_f64;
10174            let mut steps = 0_usize;
10175            while t < t_end && steps < 100_000 {
10176                let (y_new, err) = rk45_step(&f, t, y, h_cur);
10177                if err < tol || h_cur < 1e-10 {
10178                    t += h_cur;
10179                    y = y_new;
10180                    ts.push(t);
10181                    ys.push(y);
10182                    steps += 1;
10183                }
10184                let scale = if err > 0.0 {
10185                    0.9 * (tol / err).powf(0.2)
10186                } else {
10187                    2.0
10188                };
10189                h_cur = (h_cur * scale.clamp(0.1, 5.0)).min(t_end - t + 1e-15);
10190            }
10191            (ts, ys)
10192        }
10193        _ => {
10194            let mut t = 0.0_f64;
10195            let mut y = y0;
10196            let mut ts = vec![t];
10197            let mut ys = vec![y];
10198            for _ in 0..n {
10199                y = rk4_step(&f, t, y, h);
10200                t += h;
10201                ts.push(t);
10202                ys.push(y);
10203            }
10204            (ts, ys)
10205        }
10206    };
10207
10208    // Print table (max 20 rows)
10209    let step = (ts.len() / 20).max(1);
10210    let _ = writeln!(out, "  {:>10}  {:>16}", "t", "y");
10211    let _ = writeln!(out, "  {}", "-".repeat(28));
10212    for (i, (&t, &y)) in ts.iter().zip(ys.iter()).enumerate() {
10213        if i % step == 0 || i == ts.len() - 1 {
10214            let _ = writeln!(out, "  {:>10.6}  {:>16.10}", t, y);
10215        }
10216    }
10217    let _ = writeln!(out);
10218
10219    // Plot
10220    let plot = ode_ascii_plot(&ts, &ys, 56, 10);
10221    for line in plot.lines() {
10222        let _ = writeln!(out, "  {}", line);
10223    }
10224
10225    let _ = writeln!(out, "{}", sep);
10226    out
10227}
10228
10229fn ode_usage() -> &'static str {
10230    "ODE solver — no model, no cloud:\n\
10231     \n\
10232     Generic:\n\
10233     hematite --ode 'dy/dt = -y  y0=1  t=5'          exponential decay\n\
10234     hematite --ode 'dy/dt = t*y  y0=1  t=3  n=50'   custom ODE\n\
10235     hematite --ode 'dy/dt = sin(t)-y  y0=0  t=20 method=rk45'  adaptive\n\
10236     hematite --ode 'dy/dt = r*y  y0=0.5  t=4 method=euler'\n\
10237     \n\
10238     Preset models:\n\
10239     hematite --ode 'exponential r=0.5 y0=1 t=5'\n\
10240     hematite --ode 'logistic r=1 K=100 y0=5 t=10'\n\
10241     hematite --ode 'lotka alpha=1 beta=0.1 delta=0.075 gamma=1.5 x0=10 y0=5 t=30'\n\
10242     hematite --ode 'sir N=10000 beta=0.3 gamma=0.05 i0=1 t=200'\n\
10243     \n\
10244     Methods: euler  rk4 (default)  rk45 (adaptive)\n\
10245     Params:  y0=INITIAL  t=TEND  n=STEPS  method=NAME"
10246}
10247
10248// ── Numerical optimization ─────────────────────────────────────────────────────
10249// Golden-section search (1D), gradient descent (nD), Nelder-Mead simplex (2D).
10250// Evaluates arbitrary expressions via the symbolic engine.
10251
10252fn opt_eval(expr_str: &str, x: f64, y: f64) -> f64 {
10253    let s = expr_str
10254        .replace("exp(", "\x00E\x00(")
10255        .replace('y', &format!("({:.17e})", y))
10256        .replace('x', &format!("({:.17e})", x))
10257        .replace("\x00E\x00(", "exp(");
10258    match parse_sym(&s) {
10259        Ok(e) => eval_expr(&e, "z", 0.0).unwrap_or(f64::NAN),
10260        Err(_) => f64::NAN,
10261    }
10262}
10263
10264// Golden-section search: minimize f over [a, b]
10265fn golden_section(
10266    f: &dyn Fn(f64) -> f64,
10267    mut a: f64,
10268    mut b: f64,
10269    tol: f64,
10270    max_iter: usize,
10271) -> (f64, f64, usize) {
10272    let phi = (5.0_f64.sqrt() - 1.0) / 2.0;
10273    let mut c = b - phi * (b - a);
10274    let mut d = a + phi * (b - a);
10275    let mut fc = f(c);
10276    let mut fd = f(d);
10277    let mut iters = 0;
10278    while (b - a).abs() > tol && iters < max_iter {
10279        if fc < fd {
10280            b = d;
10281            d = c;
10282            fd = fc;
10283            c = b - phi * (b - a);
10284            fc = f(c);
10285        } else {
10286            a = c;
10287            c = d;
10288            fc = fd;
10289            d = a + phi * (b - a);
10290            fd = f(d);
10291        }
10292        iters += 1;
10293    }
10294    let x_min = (a + b) / 2.0;
10295    (x_min, f(x_min), iters)
10296}
10297
10298// Finite-difference gradient
10299fn grad(f: &dyn Fn(&[f64]) -> f64, x: &[f64], h: f64) -> Vec<f64> {
10300    x.iter()
10301        .enumerate()
10302        .map(|(i, _)| {
10303            let mut xp = x.to_vec();
10304            xp[i] += h;
10305            let mut xm = x.to_vec();
10306            xm[i] -= h;
10307            (f(&xp) - f(&xm)) / (2.0 * h)
10308        })
10309        .collect()
10310}
10311
10312// Gradient descent with backtracking line search
10313fn gradient_descent(
10314    f: &dyn Fn(&[f64]) -> f64,
10315    x0: &[f64],
10316    max_iter: usize,
10317    tol: f64,
10318) -> (Vec<f64>, f64, usize) {
10319    let mut x = x0.to_vec();
10320    let mut iters = 0;
10321    let mut alpha = 0.1_f64;
10322    for _ in 0..max_iter {
10323        let g = grad(f, &x, 1e-6);
10324        let gnorm = g.iter().map(|v| v * v).sum::<f64>().sqrt();
10325        if gnorm < tol {
10326            break;
10327        }
10328        // backtracking
10329        let fx = f(&x);
10330        let mut step = alpha;
10331        let mut x_new = x
10332            .iter()
10333            .zip(g.iter())
10334            .map(|(&xi, &gi)| xi - step * gi)
10335            .collect::<Vec<_>>();
10336        let mut tries = 0;
10337        while f(&x_new) > fx - 0.5 * step * gnorm * gnorm && tries < 30 {
10338            step *= 0.5;
10339            tries += 1;
10340            x_new = x
10341                .iter()
10342                .zip(g.iter())
10343                .map(|(&xi, &gi)| xi - step * gi)
10344                .collect();
10345        }
10346        alpha = step;
10347        x = x_new;
10348        iters += 1;
10349    }
10350    let fval = f(&x);
10351    (x, fval, iters)
10352}
10353
10354// Nelder-Mead simplex for 2D
10355fn nelder_mead(
10356    f: &dyn Fn(f64, f64) -> f64,
10357    x0: f64,
10358    y0: f64,
10359    max_iter: usize,
10360    tol: f64,
10361) -> (f64, f64, f64, usize) {
10362    let g = |p: &[f64; 2]| f(p[0], p[1]);
10363    let mut s = [[x0, y0], [x0 + 1.0, y0], [x0, y0 + 1.0]];
10364    let mut iters = 0;
10365    loop {
10366        // sort by function value
10367        let mut fs: [(f64, usize); 3] = [(g(&s[0]), 0), (g(&s[1]), 1), (g(&s[2]), 2)];
10368        fs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
10369        let (_, i_best) = fs[0];
10370        let (_, i_worst) = fs[2];
10371        let (_, i_2nd) = fs[1];
10372        // convergence
10373        let spread = (fs[2].0 - fs[0].0).abs();
10374        if spread < tol || iters >= max_iter {
10375            break;
10376        }
10377        // centroid of best two
10378        let cx = (s[i_best][0] + s[i_2nd][0]) / 2.0;
10379        let cy = (s[i_best][1] + s[i_2nd][1]) / 2.0;
10380        // reflect
10381        let rx = 2.0 * cx - s[i_worst][0];
10382        let ry = 2.0 * cy - s[i_worst][1];
10383        let fr = g(&[rx, ry]);
10384        if fr < fs[0].0 {
10385            // expand
10386            let ex = 3.0 * cx - 2.0 * s[i_worst][0];
10387            let ey = 3.0 * cy - 2.0 * s[i_worst][1];
10388            if g(&[ex, ey]) < fr {
10389                s[i_worst] = [ex, ey];
10390            } else {
10391                s[i_worst] = [rx, ry];
10392            }
10393        } else if fr < fs[1].0 {
10394            s[i_worst] = [rx, ry];
10395        } else {
10396            // contract
10397            let kx = 0.5 * (cx + s[i_worst][0]);
10398            let ky = 0.5 * (cy + s[i_worst][1]);
10399            if g(&[kx, ky]) < fs[2].0 {
10400                s[i_worst] = [kx, ky];
10401            } else {
10402                // shrink
10403                for j in 1..3 {
10404                    let idx = fs[j].1;
10405                    s[idx] = [
10406                        (s[i_best][0] + s[idx][0]) / 2.0,
10407                        (s[i_best][1] + s[idx][1]) / 2.0,
10408                    ];
10409                }
10410            }
10411        }
10412        iters += 1;
10413    }
10414    let mut fs: [(f64, usize); 3] = [(g(&s[0]), 0), (g(&s[1]), 1), (g(&s[2]), 2)];
10415    fs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
10416    let best = s[fs[0].1];
10417    (best[0], best[1], fs[0].0, iters)
10418}
10419
10420pub fn optimize_calc(query: &str) -> String {
10421    let mut out = String::new();
10422    let sep = "═".repeat(60);
10423    let _ = writeln!(out, "{}", sep);
10424    let _ = writeln!(out, "  NUMERICAL OPTIMIZATION");
10425    let _ = writeln!(out, "{}", sep);
10426
10427    let tokens: Vec<&str> = query.split_whitespace().collect();
10428    if tokens.is_empty() {
10429        let _ = writeln!(out, "{}", optimize_usage());
10430        let _ = writeln!(out, "{}", sep);
10431        return out;
10432    }
10433
10434    let get_param = |key: &str| -> Option<f64> {
10435        query
10436            .split_whitespace()
10437            .find(|t| t.to_lowercase().starts_with(&format!("{}=", key)))
10438            .and_then(|t| t.split_once('=')?.1.parse().ok())
10439    };
10440    let mode = tokens[0].to_lowercase();
10441
10442    match mode.as_str() {
10443        // --- 1D minimize/maximize ---
10444        "min" | "minimize" | "max" | "maximize" | "find-min" | "find-max" => {
10445            let maximize = mode == "max" || mode == "maximize" || mode == "find-max";
10446            let f_str = tokens.get(1).copied().unwrap_or("x^2");
10447            let a = get_param("a")
10448                .or_else(|| get_param("from"))
10449                .unwrap_or(-10.0);
10450            let b = get_param("b").or_else(|| get_param("to")).unwrap_or(10.0);
10451            let tol = get_param("tol").unwrap_or(1e-8);
10452            let max_iter = get_param("iter").map(|v| v as usize).unwrap_or(500);
10453
10454            let sign = if maximize { -1.0_f64 } else { 1.0_f64 };
10455            let f = |x: f64| sign * opt_eval(f_str, x, 0.0);
10456            let (x_opt, f_opt_signed, iters) = golden_section(&f, a, b, tol, max_iter);
10457            let f_opt = if maximize {
10458                -f_opt_signed
10459            } else {
10460                f_opt_signed
10461            };
10462
10463            let _ = writeln!(
10464                out,
10465                "  {} f(x) = {}   x ∈ [{}, {}]",
10466                if maximize { "Maximize" } else { "Minimize" },
10467                f_str,
10468                a,
10469                b
10470            );
10471            let _ = writeln!(out, "  Converged in {} iterations (tol={:.2e})", iters, tol);
10472            let _ = writeln!(out);
10473            let _ = writeln!(out, "  x* = {:.10}  f(x*) = {:.10}", x_opt, f_opt);
10474            let _ = writeln!(out);
10475
10476            // ASCII plot
10477            let n_plot = 56_usize;
10478            let ys: Vec<f64> = (0..n_plot)
10479                .map(|i| {
10480                    let x = a + i as f64 * (b - a) / (n_plot - 1) as f64;
10481                    opt_eval(f_str, x, 0.0)
10482                })
10483                .collect();
10484            let ymin = ys.iter().cloned().fold(f64::INFINITY, f64::min);
10485            let ymax = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
10486            let yrange = (ymax - ymin).max(1e-12);
10487            let h = 10_usize;
10488            let mut grid = vec![vec![' '; n_plot]; h];
10489            for (col, &y) in ys.iter().enumerate() {
10490                let row = h - 1 - ((y - ymin) / yrange * (h - 1) as f64).round() as usize;
10491                let row = row.min(h - 1);
10492                grid[row][col] = '·';
10493            }
10494            // mark x*
10495            let opt_col = ((x_opt - a) / (b - a) * (n_plot - 1) as f64).round() as usize;
10496            let opt_row = h - 1 - ((f_opt - ymin) / yrange * (h - 1) as f64).round() as usize;
10497            if opt_col < n_plot && opt_row < h {
10498                grid[opt_row][opt_col] = '★';
10499            }
10500            for row in &grid {
10501                let _ = writeln!(out, "  |{}|", row.iter().collect::<String>());
10502            }
10503            let _ = writeln!(
10504                out,
10505                "  f: [{:.4} .. {:.4}]   x: [{:.4} .. {:.4}]   ★=optimum",
10506                ymin, ymax, a, b
10507            );
10508        }
10509
10510        // --- 2D Nelder-Mead ---
10511        "min2" | "minimize2" | "max2" | "maximize2" | "simplex" => {
10512            let maximize = mode.starts_with("max");
10513            let f_str = tokens.get(1).copied().unwrap_or("x^2+y^2");
10514            let x0 = get_param("x0").unwrap_or(0.0);
10515            let y0 = get_param("y0").unwrap_or(0.0);
10516            let max_iter = get_param("iter").map(|v| v as usize).unwrap_or(1000);
10517            let tol = get_param("tol").unwrap_or(1e-8);
10518            let sign = if maximize { -1.0_f64 } else { 1.0_f64 };
10519            let f = |x: f64, y: f64| sign * opt_eval(f_str, x, y);
10520            let (xopt, yopt, fval_s, iters) = nelder_mead(&f, x0, y0, max_iter, tol);
10521            let fval = if maximize { -fval_s } else { fval_s };
10522            let _ = writeln!(
10523                out,
10524                "  {} f(x,y) = {}   start: ({}, {})",
10525                if maximize { "Maximize" } else { "Minimize" },
10526                f_str,
10527                x0,
10528                y0
10529            );
10530            let _ = writeln!(out, "  Nelder-Mead simplex  iterations={}", iters);
10531            let _ = writeln!(out);
10532            let _ = writeln!(out, "  x* = {:.10}", xopt);
10533            let _ = writeln!(out, "  y* = {:.10}", yopt);
10534            let _ = writeln!(out, "  f(x*,y*) = {:.10}", fval);
10535        }
10536
10537        // --- nD gradient descent ---
10538        "gradient" | "gd" | "grad-descent" => {
10539            let f_str = tokens.get(1).copied().unwrap_or("x^2");
10540            let x0_str = tokens.get(2).copied().unwrap_or("0");
10541            let x0: Vec<f64> = x0_str.split(',').filter_map(|s| s.parse().ok()).collect();
10542            let x0 = if x0.is_empty() { vec![0.0] } else { x0 };
10543            let max_iter = get_param("iter").map(|v| v as usize).unwrap_or(2000);
10544            let tol = get_param("tol").unwrap_or(1e-7);
10545            let f = |v: &[f64]| opt_eval(f_str, v[0], v.get(1).copied().unwrap_or(0.0));
10546            let (x_opt, f_opt, iters) = gradient_descent(&f, &x0, max_iter, tol);
10547            let _ = writeln!(out, "  Gradient Descent  f = {}   x0={:?}", f_str, x0);
10548            let _ = writeln!(out, "  Iterations: {}", iters);
10549            let _ = writeln!(out);
10550            let _ = writeln!(
10551                out,
10552                "  x* = {:?}",
10553                x_opt
10554                    .iter()
10555                    .map(|v| format!("{:.10}", v))
10556                    .collect::<Vec<_>>()
10557            );
10558            let _ = writeln!(out, "  f(x*) = {:.10}", f_opt);
10559        }
10560
10561        // --- Root finding (bisection) ---
10562        "root" | "find-root" | "zero" => {
10563            let f_str = tokens.get(1).copied().unwrap_or("x^2-2");
10564            let a = get_param("a")
10565                .or_else(|| get_param("from"))
10566                .unwrap_or(-10.0);
10567            let b = get_param("b").or_else(|| get_param("to")).unwrap_or(10.0);
10568            let tol = get_param("tol").unwrap_or(1e-10);
10569            let max_iter = get_param("iter").map(|v| v as usize).unwrap_or(200);
10570            let f = |x: f64| opt_eval(f_str, x, 0.0);
10571            let fa = f(a);
10572            let fb = f(b);
10573            if fa * fb > 0.0 {
10574                let _ = writeln!(out, "  Root finding: f(x) = {}   [{}, {}]", f_str, a, b);
10575                let _ = writeln!(
10576                    out,
10577                    "  ERROR: f(a) and f(b) must have opposite signs for bisection."
10578                );
10579                let _ = writeln!(out, "  f({}) = {:.6}  f({}) = {:.6}", a, fa, b, fb);
10580            } else {
10581                let mut lo = a;
10582                let mut hi = b;
10583                let mut iters = 0;
10584                while (hi - lo).abs() > tol && iters < max_iter {
10585                    let mid = (lo + hi) / 2.0;
10586                    if f(lo) * f(mid) <= 0.0 {
10587                        hi = mid;
10588                    } else {
10589                        lo = mid;
10590                    }
10591                    iters += 1;
10592                }
10593                let root = (lo + hi) / 2.0;
10594                let _ = writeln!(out, "  Root finding: f(x) = {}   [{}, {}]", f_str, a, b);
10595                let _ = writeln!(out, "  Bisection: {} iterations (tol={:.2e})", iters, tol);
10596                let _ = writeln!(out);
10597                let _ = writeln!(out, "  x* = {:.12}  f(x*) = {:.3e}", root, f(root));
10598            }
10599        }
10600
10601        _ => {
10602            let _ = writeln!(out, "{}", optimize_usage());
10603        }
10604    }
10605
10606    let _ = writeln!(out, "{}", sep);
10607    out
10608}
10609
10610fn optimize_usage() -> &'static str {
10611    "Numerical optimization — no model, no cloud:\n\
10612     \n\
10613     hematite --optimize 'min x^2-4*x+3 a=0 b=5'       minimize 1D\n\
10614     hematite --optimize 'max sin(x) a=0 b=6.28'        maximize 1D\n\
10615     hematite --optimize 'min2 x^2+y^2 x0=3 y0=2'       minimize 2D (Nelder-Mead)\n\
10616     hematite --optimize 'gradient x^4-4*x^2 0'         gradient descent\n\
10617     hematite --optimize 'root x^3-2 a=0 b=2'           find root (bisection)\n\
10618     hematite --optimize 'min (x-2)^2+(x-3)^2 a=-5 b=8' least-squares style\n\
10619     \n\
10620     Parameters:  a=  b=  range bounds  x0=  y0=  start point\n\
10621                  tol=  tolerance (default 1e-8)\n\
10622                  iter=  max iterations (default 500)\n\
10623     Expressions: x y t sin cos exp ln sqrt ^ + - * /"
10624}
10625
10626// ─── Bitwise calculator ────────────────────────────────────────────────────────
10627
10628pub fn bitwise_calc(query: &str) -> String {
10629    let mut out = String::new();
10630    let sep = "─".repeat(60);
10631    let _ = writeln!(out, "{}", sep);
10632    let _ = writeln!(out, "  BITWISE CALCULATOR");
10633    let _ = writeln!(out, "{}", sep);
10634
10635    let q = query.trim();
10636
10637    // Parse a token that might be decimal, 0x hex, 0b binary, 0o octal, or a single char
10638    let parse_val = |s: &str| -> Option<u64> {
10639        let s = s.trim();
10640        if s.is_empty() {
10641            return None;
10642        }
10643        if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
10644            return u64::from_str_radix(h, 16).ok();
10645        }
10646        if let Some(b) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) {
10647            return u64::from_str_radix(b, 2).ok();
10648        }
10649        if let Some(o) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) {
10650            return u64::from_str_radix(o, 8).ok();
10651        }
10652        if s.starts_with('\'') && s.ends_with('\'') && s.len() >= 3 {
10653            let inner = &s[1..s.len() - 1];
10654            let mut chars = inner.chars();
10655            if let Some(c) = chars.next() {
10656                if chars.next().is_none() {
10657                    return Some(c as u64);
10658                }
10659            }
10660        }
10661        s.parse::<u64>()
10662            .ok()
10663            .or_else(|| s.parse::<i64>().ok().map(|v| v as u64))
10664    };
10665
10666    let fmt_row = |out: &mut String, label: &str, v: u64| {
10667        let _ = writeln!(out, "  {:<18} {:>20}  (0x{:016X})", label, v as i64, v);
10668    };
10669
10670    let fmt_bin = |out: &mut String, label: &str, v: u64| {
10671        // Show as 4 groups of 16 bits
10672        let b = format!("{:064b}", v);
10673        let groups: Vec<&str> = [&b[0..16], &b[16..32], &b[32..48], &b[48..64]].to_vec();
10674        let _ = writeln!(
10675            out,
10676            "  {:<18} {}_{}_{}_{}",
10677            label, groups[0], groups[1], groups[2], groups[3]
10678        );
10679    };
10680
10681    // ─── ieee754: float bit pattern analysis ───
10682    if q.starts_with("ieee754 ") || q.starts_with("float ") {
10683        let raw = q.split_once(' ').map(|x| x.1).unwrap_or("").trim();
10684        let fval: f64 = match raw.parse::<f64>() {
10685            Ok(v) => v,
10686            Err(_) => {
10687                let _ = writeln!(out, "  Cannot parse float: {}", raw);
10688                return out;
10689            }
10690        };
10691        let bits = fval.to_bits();
10692        let sign = (bits >> 63) & 1;
10693        let exp_raw = ((bits >> 52) & 0x7FF) as i64;
10694        let mantissa = bits & 0x000F_FFFF_FFFF_FFFF;
10695        let exp_actual = exp_raw - 1023;
10696
10697        let _ = writeln!(out, "  Value:    {}", fval);
10698        let _ = writeln!(out, "  Bits:     0x{:016X}", bits);
10699        let b = format!("{:064b}", bits);
10700        let _ = writeln!(
10701            out,
10702            "  Binary:   {} {} {} {}",
10703            &b[0..1],
10704            &b[1..12],
10705            &b[12..32],
10706            &b[32..64]
10707        );
10708        let _ = writeln!(
10709            out,
10710            "  Sign:     {} ({})",
10711            sign,
10712            if sign == 0 { "positive" } else { "negative" }
10713        );
10714        let _ = writeln!(
10715            out,
10716            "  Exponent: {:011b} raw={} actual={}",
10717            exp_raw, exp_raw, exp_actual
10718        );
10719        let _ = writeln!(out, "  Mantissa: {:052b}", mantissa);
10720        let category = if exp_raw == 0x7FF {
10721            if mantissa == 0 {
10722                if sign == 0 {
10723                    "+Infinity"
10724                } else {
10725                    "-Infinity"
10726                }
10727            } else {
10728                "NaN"
10729            }
10730        } else if exp_raw == 0 {
10731            if mantissa == 0 {
10732                "Zero"
10733            } else {
10734                "Subnormal"
10735            }
10736        } else {
10737            "Normal"
10738        };
10739        let _ = writeln!(out, "  Category: {}", category);
10740        let _ = writeln!(out, "{}", sep);
10741        return out;
10742    }
10743
10744    // ─── single value inspection ───
10745    let words: Vec<&str> = q.split_whitespace().collect();
10746
10747    if words.is_empty() {
10748        let _ = writeln!(out, "{}", bitwise_usage());
10749        let _ = writeln!(out, "{}", sep);
10750        return out;
10751    }
10752
10753    // Two-operand ops: A op B
10754    if words.len() >= 3 {
10755        let a_str = words[0];
10756        let op = words[1];
10757        let b_str = words[2];
10758        let a = match parse_val(a_str) {
10759            Some(v) => v,
10760            None => {
10761                let _ = writeln!(out, "  Cannot parse operand: {}", a_str);
10762                return out;
10763            }
10764        };
10765        match op.to_lowercase().as_str() {
10766            "and" | "&" => {
10767                let result = a & parse_val(b_str).unwrap_or(0);
10768                let _ = writeln!(out, "  A (dec):  {}", a as i64);
10769                let _ = writeln!(out, "  B (dec):  {}", parse_val(b_str).unwrap_or(0) as i64);
10770                let b_val = parse_val(b_str).unwrap_or(0);
10771                fmt_bin(&mut out, "  A binary:", a);
10772                fmt_bin(&mut out, "  B binary:", b_val);
10773                fmt_bin(&mut out, "  AND:     ", result);
10774                fmt_row(&mut out, "  Result:", result);
10775                let _ = writeln!(out, "{}", sep);
10776                return out;
10777            }
10778            "or" | "|" => {
10779                let b_val = parse_val(b_str).unwrap_or(0);
10780                let result = a | b_val;
10781                fmt_bin(&mut out, "  A binary:", a);
10782                fmt_bin(&mut out, "  B binary:", b_val);
10783                fmt_bin(&mut out, "  OR:      ", result);
10784                fmt_row(&mut out, "  Result:", result);
10785                let _ = writeln!(out, "{}", sep);
10786                return out;
10787            }
10788            "xor" | "^" => {
10789                let b_val = parse_val(b_str).unwrap_or(0);
10790                let result = a ^ b_val;
10791                fmt_bin(&mut out, "  A binary:", a);
10792                fmt_bin(&mut out, "  B binary:", b_val);
10793                fmt_bin(&mut out, "  XOR:     ", result);
10794                fmt_row(&mut out, "  Result:", result);
10795                let _ = writeln!(out, "{}", sep);
10796                return out;
10797            }
10798            "shl" | "<<" | "lsh" => {
10799                let n = parse_val(b_str).unwrap_or(0) as u32;
10800                let result = a.checked_shl(n).unwrap_or(0);
10801                fmt_bin(&mut out, "  Before:  ", a);
10802                fmt_bin(&mut out, "  SHL by:", result);
10803                let _ = writeln!(out, "  Shift by: {}", n);
10804                fmt_row(&mut out, "  Result:", result);
10805                let _ = writeln!(out, "{}", sep);
10806                return out;
10807            }
10808            "shr" | ">>" | "rsh" => {
10809                let n = parse_val(b_str).unwrap_or(0) as u32;
10810                let result = a.checked_shr(n).unwrap_or(0);
10811                fmt_bin(&mut out, "  Before:  ", a);
10812                fmt_bin(&mut out, "  SHR by:", result);
10813                let _ = writeln!(out, "  Shift by: {}", n);
10814                fmt_row(&mut out, "  Result:", result);
10815                let _ = writeln!(out, "{}", sep);
10816                return out;
10817            }
10818            "rol" | "rotl" => {
10819                let n = (parse_val(b_str).unwrap_or(0) % 64) as u32;
10820                let result = a.rotate_left(n);
10821                fmt_bin(&mut out, "  Before:  ", a);
10822                fmt_bin(&mut out, "  ROL:     ", result);
10823                let _ = writeln!(out, "  Rotate by: {}", n);
10824                fmt_row(&mut out, "  Result:", result);
10825                let _ = writeln!(out, "{}", sep);
10826                return out;
10827            }
10828            "ror" | "rotr" => {
10829                let n = (parse_val(b_str).unwrap_or(0) % 64) as u32;
10830                let result = a.rotate_right(n);
10831                fmt_bin(&mut out, "  Before:  ", a);
10832                fmt_bin(&mut out, "  ROR:     ", result);
10833                let _ = writeln!(out, "  Rotate by: {}", n);
10834                fmt_row(&mut out, "  Result:", result);
10835                let _ = writeln!(out, "{}", sep);
10836                return out;
10837            }
10838            _ => {}
10839        }
10840    }
10841
10842    // Single operand or unknown → full inspection
10843    let val_str = if words.len() == 2 && words[0].to_lowercase() == "not" {
10844        words[1]
10845    } else {
10846        words[0]
10847    };
10848    let is_not = words.len() == 2 && words[0].to_lowercase() == "not";
10849
10850    let val = match parse_val(val_str) {
10851        Some(v) => v,
10852        None => {
10853            let _ = writeln!(out, "{}", bitwise_usage());
10854            let _ = writeln!(out, "{}", sep);
10855            return out;
10856        }
10857    };
10858    let display_val = if is_not { !val } else { val };
10859    let label = if is_not { "NOT result" } else { "Value" };
10860
10861    let popcount = display_val.count_ones();
10862    let parity = popcount % 2;
10863    let leading = display_val.leading_zeros();
10864    let trailing = display_val.trailing_zeros();
10865    let msb_pos = if display_val == 0 { 0u32 } else { 63 - leading };
10866
10867    let _ = writeln!(out, "  {} (input): {}", label, val as i64);
10868    if is_not {
10869        let _ = writeln!(out, "  NOT {:016X} = {:016X}", val, !val);
10870    }
10871    let _ = writeln!(out);
10872    let _ = writeln!(out, "  ─── Representations ───");
10873    let _ = writeln!(out, "  Decimal (signed):    {}", display_val as i64);
10874    let _ = writeln!(out, "  Decimal (unsigned):  {}", display_val);
10875    let _ = writeln!(out, "  Hexadecimal:         0x{:016X}", display_val);
10876    let _ = writeln!(out, "  Octal:               0o{:o}", display_val);
10877
10878    // Binary with spacing every 8 bits
10879    let b = format!("{:064b}", display_val);
10880    let _ = writeln!(out, "  Binary (64-bit):");
10881    let _ = writeln!(out, "    Bit 63-48:  {}_{}", &b[0..8], &b[8..16]);
10882    let _ = writeln!(out, "    Bit 47-32:  {}_{}", &b[16..24], &b[24..32]);
10883    let _ = writeln!(out, "    Bit 31-16:  {}_{}", &b[32..40], &b[40..48]);
10884    let _ = writeln!(out, "    Bit 15- 0:  {}_{}", &b[48..56], &b[56..64]);
10885
10886    // Two's complement (flip bits + 1)
10887    let twos = (!display_val).wrapping_add(1);
10888    let _ = writeln!(out);
10889    let _ = writeln!(out, "  ─── Bit Properties ───");
10890    let _ = writeln!(out, "  Popcount (set bits): {}", popcount);
10891    let _ = writeln!(
10892        out,
10893        "  Parity:              {} ({})",
10894        parity,
10895        if parity == 0 { "even" } else { "odd" }
10896    );
10897    let _ = writeln!(out, "  Leading zeros:       {}", leading);
10898    let _ = writeln!(out, "  Trailing zeros:      {}", trailing);
10899    let _ = writeln!(out, "  MSB position:        {}", msb_pos);
10900    let _ = writeln!(
10901        out,
10902        "  Two's complement:    {} (0x{:016X})",
10903        twos as i64, twos
10904    );
10905    let is_pow2 = display_val != 0 && (display_val & display_val.wrapping_sub(1)) == 0;
10906    let _ = writeln!(
10907        out,
10908        "  Power of 2:          {}",
10909        if is_pow2 { "yes" } else { "no" }
10910    );
10911
10912    // Low byte / word / dword breakdown
10913    let _ = writeln!(out);
10914    let _ = writeln!(out, "  ─── Byte Decomposition (little-endian) ───");
10915    for i in 0..8usize {
10916        let byte = ((display_val >> (i * 8)) & 0xFF) as u8;
10917        let _ = writeln!(
10918            out,
10919            "  Byte {}: 0x{:02X}  {:08b}  dec={}",
10920            i, byte, byte, byte
10921        );
10922    }
10923
10924    let _ = writeln!(out, "{}", sep);
10925    out
10926}
10927
10928fn bitwise_usage() -> &'static str {
10929    "Bitwise calculator — no model, no cloud:\n\
10930     \n\
10931     hematite --bitwise '255'                  inspect value (dec/hex/bin/octal/bytes)\n\
10932     hematite --bitwise '0xFF'                 hex input\n\
10933     hematite --bitwise '0b11001010'           binary input\n\
10934     hematite --bitwise 'NOT 0xFF'             bitwise NOT\n\
10935     hematite --bitwise '0xF0 AND 0x3C'        AND\n\
10936     hematite --bitwise '0xF0 OR 0x0F'         OR\n\
10937     hematite --bitwise '0xAB XOR 0xFF'        XOR\n\
10938     hematite --bitwise '1 SHL 7'              left shift\n\
10939     hematite --bitwise '256 SHR 4'            right shift\n\
10940     hematite --bitwise '0xDEAD ROL 4'         rotate left\n\
10941     hematite --bitwise '0xDEAD ROR 4'         rotate right\n\
10942     hematite --bitwise 'ieee754 3.14159'       IEEE 754 float breakdown\n\
10943     hematite --bitwise 'ieee754 NaN'           special float analysis\n\
10944     \n\
10945     Inputs: decimal, 0x hex, 0b binary, 0o octal, negative (-1)"
10946}
10947
10948// ─── Set theory calculator ─────────────────────────────────────────────────────
10949
10950pub fn set_calc(query: &str) -> String {
10951    let mut out = String::new();
10952    let sep = "─".repeat(60);
10953    let _ = writeln!(out, "{}", sep);
10954    let _ = writeln!(out, "  SET THEORY CALCULATOR");
10955    let _ = writeln!(out, "{}", sep);
10956
10957    let q = query.trim();
10958
10959    // Parse a set literal: {1,2,3} or [1,2,3] or bare comma list
10960    // Items are strings (so sets of anything work)
10961    let parse_set = |s: &str| -> Vec<String> {
10962        let s = s
10963            .trim()
10964            .trim_start_matches('{')
10965            .trim_end_matches('}')
10966            .trim_start_matches('[')
10967            .trim_end_matches(']');
10968        let mut items: Vec<String> = s
10969            .split(',')
10970            .map(|x| x.trim().to_string())
10971            .filter(|x| !x.is_empty())
10972            .collect();
10973        items.sort();
10974        items.dedup();
10975        items
10976    };
10977
10978    let fmt_set = |v: &[String]| -> String {
10979        if v.is_empty() {
10980            "{}".to_string()
10981        } else {
10982            format!("{{{}}}", v.join(", "))
10983        }
10984    };
10985
10986    let q_lower = q.to_lowercase();
10987
10988    // ─── power set ───
10989    if q_lower.starts_with("powerset ")
10990        || q_lower.starts_with("power_set ")
10991        || q_lower.starts_with("power set ")
10992    {
10993        let raw = q.split_once(' ').map(|x| x.1).unwrap_or("").trim();
10994        let set = parse_set(raw);
10995        if set.len() > 20 {
10996            let _ = writeln!(out, "  Set too large for power set (max 20 elements).");
10997            let _ = writeln!(out, "{}", sep);
10998            return out;
10999        }
11000        let n = set.len();
11001        let count = 1usize << n;
11002        let _ = writeln!(out, "  Set A = {}", fmt_set(&set));
11003        let _ = writeln!(out, "  |A| = {}   |P(A)| = {}", n, count);
11004        let _ = writeln!(out);
11005        let _ = writeln!(out, "  Power set P(A):");
11006        let mut subsets: Vec<Vec<String>> = Vec::with_capacity(count);
11007        for mask in 0..count {
11008            let subset: Vec<String> = (0..n)
11009                .filter(|&i| mask & (1 << i) != 0)
11010                .map(|i| set[i].clone())
11011                .collect();
11012            subsets.push(subset);
11013        }
11014        // Sort by cardinality then lexicographic
11015        subsets.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
11016        for (i, subset) in subsets.iter().enumerate() {
11017            let _ = writeln!(out, "  {:3}. {}", i + 1, fmt_set(subset));
11018        }
11019        let _ = writeln!(out, "{}", sep);
11020        return out;
11021    }
11022
11023    // ─── cartesian product ───
11024    if q_lower.starts_with("cartesian ")
11025        || q_lower.starts_with("product ")
11026        || q_lower.contains(" x ")
11027    {
11028        // Split on " x " or keyword
11029        let parts: Vec<&str> = if q_lower.contains(" x ") {
11030            q.splitn(3, " x ").collect()
11031        } else {
11032            let kw = if q_lower.starts_with("cartesian ") {
11033                "cartesian "
11034            } else {
11035                "product "
11036            };
11037            let rest = &q[kw.len()..];
11038            rest.splitn(2, " x ").collect()
11039        };
11040        if parts.len() < 2 {
11041            let _ = writeln!(out, "  Usage: cartesian {{1,2}} x {{a,b,c}}");
11042            let _ = writeln!(out, "{}", sep);
11043            return out;
11044        }
11045        let a = parse_set(parts[0]);
11046        let b = parse_set(parts[1]);
11047        let _ = writeln!(out, "  A = {}  (|A|={})", fmt_set(&a), a.len());
11048        let _ = writeln!(out, "  B = {}  (|B|={})", fmt_set(&b), b.len());
11049        let _ = writeln!(out, "  |A × B| = {}", a.len() * b.len());
11050        let _ = writeln!(out);
11051        let _ = writeln!(out, "  A × B:");
11052        for x in &a {
11053            for y in &b {
11054                let _ = writeln!(out, "    ({}, {})", x, y);
11055            }
11056        }
11057        let _ = writeln!(out, "{}", sep);
11058        return out;
11059    }
11060
11061    // ─── two-set operations — split on keyword ───
11062    // Supported: union, intersection, difference, symmetric_difference, subset, superset, disjoint
11063    let ops = [
11064        "symmetric_difference",
11065        "sym_diff",
11066        "symdiff",
11067        "difference",
11068        "intersection",
11069        "intersect",
11070        "union",
11071        "subset",
11072        "superset",
11073        "disjoint",
11074        "equal",
11075    ];
11076    let mut op_found: Option<(&str, Vec<String>, Vec<String>)> = None;
11077    for &op in &ops {
11078        let needle = format!(" {} ", op);
11079        if let Some(pos) = q_lower.find(needle.as_str()) {
11080            let a_raw = q[..pos].trim();
11081            let b_raw = q[pos + needle.len()..].trim();
11082            op_found = Some((op, parse_set(a_raw), parse_set(b_raw)));
11083            break;
11084        }
11085    }
11086
11087    if let Some((op, a, b)) = op_found {
11088        let _ = writeln!(out, "  A = {}  (|A|={})", fmt_set(&a), a.len());
11089        let _ = writeln!(out, "  B = {}  (|B|={})", fmt_set(&b), b.len());
11090        let _ = writeln!(out);
11091        match op {
11092            "union" => {
11093                let mut result = a.clone();
11094                for x in &b {
11095                    if !result.contains(x) {
11096                        result.push(x.clone());
11097                    }
11098                }
11099                result.sort();
11100                let _ = writeln!(out, "  A ∪ B = {}", fmt_set(&result));
11101                let _ = writeln!(out, "  |A ∪ B| = {}", result.len());
11102            }
11103            "intersection" | "intersect" => {
11104                let result: Vec<String> = a.iter().filter(|x| b.contains(x)).cloned().collect();
11105                let _ = writeln!(out, "  A ∩ B = {}", fmt_set(&result));
11106                let _ = writeln!(out, "  |A ∩ B| = {}", result.len());
11107            }
11108            "difference" => {
11109                let result: Vec<String> = a.iter().filter(|x| !b.contains(x)).cloned().collect();
11110                let _ = writeln!(out, "  A \\ B = {}", fmt_set(&result));
11111                let _ = writeln!(out, "  |A \\ B| = {}", result.len());
11112                let result2: Vec<String> = b.iter().filter(|x| !a.contains(x)).cloned().collect();
11113                let _ = writeln!(out, "  B \\ A = {}", fmt_set(&result2));
11114            }
11115            "symmetric_difference" | "sym_diff" | "symdiff" => {
11116                let in_a_not_b: Vec<String> =
11117                    a.iter().filter(|x| !b.contains(x)).cloned().collect();
11118                let in_b_not_a: Vec<String> =
11119                    b.iter().filter(|x| !a.contains(x)).cloned().collect();
11120                let mut result = in_a_not_b.clone();
11121                result.extend(in_b_not_a.clone());
11122                result.sort();
11123                let _ = writeln!(out, "  A Δ B = {}", fmt_set(&result));
11124                let _ = writeln!(out, "  |A Δ B| = {}", result.len());
11125                let _ = writeln!(out, "  (in A not B: {})", fmt_set(&in_a_not_b));
11126                let _ = writeln!(out, "  (in B not A: {})", fmt_set(&in_b_not_a));
11127            }
11128            "subset" => {
11129                let is_sub = a.iter().all(|x| b.contains(x));
11130                let is_proper = is_sub && a.len() < b.len();
11131                let _ = writeln!(out, "  A ⊆ B: {}", if is_sub { "YES" } else { "NO" });
11132                let _ = writeln!(
11133                    out,
11134                    "  A ⊂ B (proper): {}",
11135                    if is_proper { "YES" } else { "NO" }
11136                );
11137            }
11138            "superset" => {
11139                let is_super = b.iter().all(|x| a.contains(x));
11140                let is_proper = is_super && a.len() > b.len();
11141                let _ = writeln!(out, "  A ⊇ B: {}", if is_super { "YES" } else { "NO" });
11142                let _ = writeln!(
11143                    out,
11144                    "  A ⊃ B (proper): {}",
11145                    if is_proper { "YES" } else { "NO" }
11146                );
11147            }
11148            "disjoint" => {
11149                let is_disj = !a.iter().any(|x| b.contains(x));
11150                let _ = writeln!(
11151                    out,
11152                    "  A ∩ B = ∅ (disjoint): {}",
11153                    if is_disj { "YES" } else { "NO" }
11154                );
11155                if !is_disj {
11156                    let common: Vec<String> = a.iter().filter(|x| b.contains(x)).cloned().collect();
11157                    let _ = writeln!(out, "  Common elements: {}", fmt_set(&common));
11158                }
11159            }
11160            "equal" => {
11161                let equal = a.iter().all(|x| b.contains(x)) && b.iter().all(|x| a.contains(x));
11162                let _ = writeln!(out, "  A = B: {}", if equal { "YES" } else { "NO" });
11163            }
11164            _ => {}
11165        }
11166        let _ = writeln!(out, "{}", sep);
11167        return out;
11168    }
11169
11170    // ─── single set inspection ───
11171    let set = parse_set(q);
11172    if !set.is_empty() {
11173        let _ = writeln!(out, "  Set = {}", fmt_set(&set));
11174        let _ = writeln!(out, "  Cardinality: {}", set.len());
11175        let _ = writeln!(
11176            out,
11177            "  Min: {}   Max: {}",
11178            set.first().unwrap_or(&String::new()),
11179            set.last().unwrap_or(&String::new())
11180        );
11181        // numeric stats if all parse as f64
11182        let nums: Vec<f64> = set.iter().filter_map(|x| x.parse::<f64>().ok()).collect();
11183        if nums.len() == set.len() && !nums.is_empty() {
11184            let sum: f64 = nums.iter().sum();
11185            let mean = sum / nums.len() as f64;
11186            let _ = writeln!(out, "  Sum: {}   Mean: {:.4}", sum, mean);
11187        }
11188        let _ = writeln!(out, "{}", sep);
11189        return out;
11190    }
11191
11192    let _ = writeln!(out, "{}", set_usage());
11193    let _ = writeln!(out, "{}", sep);
11194    out
11195}
11196
11197fn set_usage() -> &'static str {
11198    "Set theory calculator — no model, no cloud:\n\
11199     \n\
11200     hematite --set '{1,2,3} union {3,4,5}'           A ∪ B\n\
11201     hematite --set '{1,2,3} intersection {2,3,4}'    A ∩ B\n\
11202     hematite --set '{1,2,3,4} difference {2,4}'      A \\ B\n\
11203     hematite --set '{1,2,3} sym_diff {2,3,4}'        A Δ B\n\
11204     hematite --set '{1,2} subset {1,2,3}'            subset check\n\
11205     hematite --set '{1,2,3} superset {1,2}'          superset check\n\
11206     hematite --set '{1,2,3} disjoint {4,5,6}'        disjoint check\n\
11207     hematite --set 'powerset {a,b,c}'                P(A) — all subsets\n\
11208     hematite --set 'cartesian {1,2} x {a,b}'         A × B\n\
11209     hematite --set '{5,3,1,4,2}'                     inspect a set\n\
11210     \n\
11211     Elements can be numbers or strings. Sets are auto-deduplicated and sorted."
11212}
11213
11214// ─── Classical cipher encoder / decoder ───────────────────────────────────────
11215
11216pub fn cipher_calc(query: &str) -> String {
11217    let mut out = String::new();
11218    let sep = "─".repeat(60);
11219    let _ = writeln!(out, "{}", sep);
11220    let _ = writeln!(out, "  CLASSICAL CIPHER");
11221    let _ = writeln!(out, "{}", sep);
11222
11223    let q = query.trim();
11224    let words: Vec<&str> = q.splitn(3, ' ').collect();
11225    if words.is_empty() {
11226        let _ = writeln!(out, "{}", cipher_usage());
11227        let _ = writeln!(out, "{}", sep);
11228        return out;
11229    }
11230
11231    let cipher_name = words[0].to_lowercase();
11232
11233    // ─── ROT13 ───
11234    if cipher_name == "rot13" {
11235        let text = words[1..].join(" ");
11236        let result: String = text
11237            .chars()
11238            .map(|c| {
11239                if c.is_ascii_alphabetic() {
11240                    let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11241                    (((c as u8 - base + 13) % 26) + base) as char
11242                } else {
11243                    c
11244                }
11245            })
11246            .collect();
11247        let _ = writeln!(out, "  Cipher:  ROT13 (symmetric)");
11248        let _ = writeln!(out, "  Input:   {}", text);
11249        let _ = writeln!(out, "  Output:  {}", result);
11250        let _ = writeln!(out, "{}", sep);
11251        return out;
11252    }
11253
11254    // ─── Atbash ───
11255    if cipher_name == "atbash" {
11256        let text = words[1..].join(" ");
11257        let result: String = text
11258            .chars()
11259            .map(|c| {
11260                if c.is_ascii_alphabetic() {
11261                    let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11262                    (base + 25 - (c as u8 - base)) as char
11263                } else {
11264                    c
11265                }
11266            })
11267            .collect();
11268        let _ = writeln!(out, "  Cipher:  Atbash (symmetric)");
11269        let _ = writeln!(out, "  Input:   {}", text);
11270        let _ = writeln!(out, "  Output:  {}", result);
11271        let _ = writeln!(out, "{}", sep);
11272        return out;
11273    }
11274
11275    // ─── Caesar / ROT-N ───
11276    if cipher_name == "caesar" || cipher_name == "rot" {
11277        if words.len() < 3 {
11278            let _ = writeln!(out, "  Usage: caesar <shift> <text>   or   rot <n> <text>");
11279            let _ = writeln!(out, "{}", sep);
11280            return out;
11281        }
11282        let shift: i32 = words[1].parse().unwrap_or(13);
11283        let text = words[2];
11284        let shift_norm = ((shift % 26) + 26) as u8 % 26;
11285        let result: String = text
11286            .chars()
11287            .map(|c| {
11288                if c.is_ascii_alphabetic() {
11289                    let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11290                    ((c as u8 - base + shift_norm) % 26 + base) as char
11291                } else {
11292                    c
11293                }
11294            })
11295            .collect();
11296        let decode: String = text
11297            .chars()
11298            .map(|c| {
11299                if c.is_ascii_alphabetic() {
11300                    let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11301                    let s = (26 - shift_norm) % 26;
11302                    ((c as u8 - base + s) % 26 + base) as char
11303                } else {
11304                    c
11305                }
11306            })
11307            .collect();
11308        let _ = writeln!(out, "  Cipher:  Caesar / ROT-{}", shift);
11309        let _ = writeln!(out, "  Input:   {}", text);
11310        let _ = writeln!(out, "  Encoded: {}", result);
11311        let _ = writeln!(out, "  Decoded: {}", decode);
11312        // Show all 25 rotations in compact form
11313        let _ = writeln!(out);
11314        let _ = writeln!(out, "  ─── Brute-force all rotations ───");
11315        for n in 0u8..26 {
11316            let r: String = text
11317                .chars()
11318                .map(|c| {
11319                    if c.is_ascii_alphabetic() {
11320                        let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11321                        ((c as u8 - base + n) % 26 + base) as char
11322                    } else {
11323                        c
11324                    }
11325                })
11326                .collect();
11327            let _ = writeln!(out, "  ROT-{:2}:  {}", n, r);
11328        }
11329        let _ = writeln!(out, "{}", sep);
11330        return out;
11331    }
11332
11333    // ─── Vigenère encode/decode ───
11334    if cipher_name == "vigenere" || cipher_name == "vigenère" {
11335        // vigenere encode <key> <text>  OR  vigenere decode <key> <text>
11336        let parts: Vec<&str> = q.splitn(4, ' ').collect();
11337        if parts.len() < 4 {
11338            let _ = writeln!(out, "  Usage: vigenere encode <key> <text>");
11339            let _ = writeln!(out, "         vigenere decode <key> <text>");
11340            let _ = writeln!(out, "{}", sep);
11341            return out;
11342        }
11343        let mode = parts[1].to_lowercase();
11344        let key = parts[2];
11345        let text = parts[3];
11346        let decode = mode == "decode" || mode == "dec" || mode == "d";
11347
11348        let key_clean: Vec<u8> = key
11349            .chars()
11350            .filter(|c| c.is_ascii_alphabetic())
11351            .map(|c| c.to_ascii_uppercase() as u8 - b'A')
11352            .collect();
11353        if key_clean.is_empty() {
11354            let _ = writeln!(out, "  Key must contain at least one letter.");
11355            let _ = writeln!(out, "{}", sep);
11356            return out;
11357        }
11358        let mut ki = 0usize;
11359        let result: String = text
11360            .chars()
11361            .map(|c| {
11362                if c.is_ascii_alphabetic() {
11363                    let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
11364                    let shift = key_clean[ki % key_clean.len()];
11365                    ki += 1;
11366                    if decode {
11367                        ((c as u8 - base + 26 - shift) % 26 + base) as char
11368                    } else {
11369                        ((c as u8 - base + shift) % 26 + base) as char
11370                    }
11371                } else {
11372                    c
11373                }
11374            })
11375            .collect();
11376        let _ = writeln!(out, "  Cipher:  Vigenère");
11377        let _ = writeln!(
11378            out,
11379            "  Mode:    {}",
11380            if decode { "decode" } else { "encode" }
11381        );
11382        let _ = writeln!(out, "  Key:     {}", key);
11383        let _ = writeln!(out, "  Input:   {}", text);
11384        let _ = writeln!(out, "  Output:  {}", result);
11385        let _ = writeln!(out, "{}", sep);
11386        return out;
11387    }
11388
11389    // ─── Rail Fence encode/decode ───
11390    if cipher_name == "railfence" || cipher_name == "rail_fence" || cipher_name == "rail" {
11391        let parts: Vec<&str> = q.splitn(4, ' ').collect();
11392        if parts.len() < 4 {
11393            let _ = writeln!(out, "  Usage: railfence encode <rails> <text>");
11394            let _ = writeln!(out, "         railfence decode <rails> <text>");
11395            let _ = writeln!(out, "{}", sep);
11396            return out;
11397        }
11398        let mode = parts[1].to_lowercase();
11399        let rails: usize = parts[2].parse().unwrap_or(2).max(2);
11400        let text = parts[3];
11401        let decode = mode == "decode" || mode == "dec" || mode == "d";
11402
11403        let chars: Vec<char> = text.chars().collect();
11404        let n = chars.len();
11405
11406        if decode {
11407            // Reconstruct rail assignment pattern, fill, then read
11408            let mut pattern = vec![0usize; n];
11409            let mut rail = 0i32;
11410            let mut dir = 1i32;
11411            for i in 0..n {
11412                pattern[i] = rail as usize;
11413                if rail == 0 {
11414                    dir = 1;
11415                } else if rail == (rails as i32 - 1) {
11416                    dir = -1;
11417                }
11418                rail += dir;
11419            }
11420            let mut rail_len = vec![0usize; rails];
11421            for &r in &pattern {
11422                rail_len[r] += 1;
11423            }
11424            let mut rail_start = vec![0usize; rails];
11425            for r in 1..rails {
11426                rail_start[r] = rail_start[r - 1] + rail_len[r - 1];
11427            }
11428            let char_arr: Vec<char> = chars.to_vec();
11429            let mut result = vec![' '; n];
11430            let mut rail_idx: Vec<usize> = rail_start.clone();
11431            for i in 0..n {
11432                result[i] = char_arr[rail_idx[pattern[i]]];
11433                rail_idx[pattern[i]] += 1;
11434            }
11435            let decoded: String = result.into_iter().collect();
11436            let _ = writeln!(out, "  Cipher:  Rail Fence");
11437            let _ = writeln!(out, "  Rails:   {}", rails);
11438            let _ = writeln!(out, "  Input:   {}", text);
11439            let _ = writeln!(out, "  Decoded: {}", decoded);
11440        } else {
11441            let mut fence: Vec<Vec<char>> = vec![Vec::new(); rails];
11442            let mut rail = 0i32;
11443            let mut dir = 1i32;
11444            for &ch in &chars {
11445                fence[rail as usize].push(ch);
11446                if rail == 0 {
11447                    dir = 1;
11448                } else if rail == (rails as i32 - 1) {
11449                    dir = -1;
11450                }
11451                rail += dir;
11452            }
11453            let encoded: String = fence.iter().flat_map(|r| r.iter()).collect();
11454            let _ = writeln!(out, "  Cipher:  Rail Fence");
11455            let _ = writeln!(out, "  Rails:   {}", rails);
11456            let _ = writeln!(out, "  Input:   {}", text);
11457            let _ = writeln!(out, "  Encoded: {}", encoded);
11458            let _ = writeln!(out);
11459            let _ = writeln!(out, "  ─── Rail diagram ───");
11460            for (i, rail_row) in fence.iter().enumerate() {
11461                let s: String = rail_row.iter().collect();
11462                let _ = writeln!(out, "  Rail {}: {}", i, s);
11463            }
11464        }
11465        let _ = writeln!(out, "{}", sep);
11466        return out;
11467    }
11468
11469    // ─── Columnar transposition ───
11470    if cipher_name == "columnar" || cipher_name == "transposition" {
11471        let parts: Vec<&str> = q.splitn(4, ' ').collect();
11472        if parts.len() < 4 {
11473            let _ = writeln!(out, "  Usage: columnar encode <key> <text>");
11474            let _ = writeln!(out, "         columnar decode <key> <text>");
11475            let _ = writeln!(out, "{}", sep);
11476            return out;
11477        }
11478        let mode = parts[1].to_lowercase();
11479        let key = parts[2];
11480        let text = parts[3];
11481        let decode = mode == "decode" || mode == "dec" || mode == "d";
11482
11483        let num_cols = key.len();
11484        let mut key_chars: Vec<(usize, char)> = key.chars().enumerate().collect();
11485        key_chars.sort_by_key(|&(_, c)| c);
11486        let col_order: Vec<usize> = key_chars.iter().map(|&(i, _)| i).collect();
11487
11488        let pad: usize = if text.len() % num_cols == 0 {
11489            0
11490        } else {
11491            num_cols - text.len() % num_cols
11492        };
11493        let padded: Vec<char> = text.chars().chain(std::iter::repeat_n('_', pad)).collect();
11494        let num_rows = padded.len() / num_cols;
11495
11496        if decode {
11497            // Reconstruct by filling columns in sorted key order
11498            let col_lens: Vec<usize> = (0..num_cols).map(|_| num_rows).collect();
11499            let mut cols: Vec<Vec<char>> = vec![Vec::new(); num_cols];
11500            let mut idx = 0;
11501            for &ci in &col_order {
11502                for _ in 0..col_lens[ci] {
11503                    if idx < text.len() {
11504                        cols[ci].push(text.chars().nth(idx).unwrap_or('_'));
11505                        idx += 1;
11506                    }
11507                }
11508            }
11509            let mut decoded = String::new();
11510            for r in 0..num_rows {
11511                for c in 0..num_cols {
11512                    if r < cols[c].len() {
11513                        decoded.push(cols[c][r]);
11514                    }
11515                }
11516            }
11517            let decoded = decoded.trim_end_matches('_');
11518            let _ = writeln!(out, "  Cipher:  Columnar Transposition");
11519            let _ = writeln!(out, "  Key:     {}", key);
11520            let _ = writeln!(out, "  Input:   {}", text);
11521            let _ = writeln!(out, "  Decoded: {}", decoded);
11522        } else {
11523            // Read row by row, output column by column in sorted key order
11524            let grid: Vec<Vec<char>> = padded.chunks(num_cols).map(|r| r.to_vec()).collect();
11525            let mut encoded = String::new();
11526            for &ci in &col_order {
11527                for row in &grid {
11528                    encoded.push(row[ci]);
11529                }
11530            }
11531            let _ = writeln!(out, "  Cipher:  Columnar Transposition");
11532            let _ = writeln!(out, "  Key:     {} (sorted order: {:?})", key, col_order);
11533            let _ = writeln!(out, "  Input:   {}", text);
11534            let _ = writeln!(out, "  Encoded: {}", encoded);
11535            let _ = writeln!(out);
11536            let _ = writeln!(out, "  ─── Grid ───");
11537            // Print key header
11538            let _ = writeln!(
11539                out,
11540                "  Key:  {}",
11541                key.chars().map(|c| format!("{} ", c)).collect::<String>()
11542            );
11543            for row in &grid {
11544                let row_str: String = row.iter().map(|c| format!("{} ", c)).collect();
11545                let _ = writeln!(out, "        {}", row_str);
11546            }
11547        }
11548        let _ = writeln!(out, "{}", sep);
11549        return out;
11550    }
11551
11552    // ─── Morse code ───
11553    if cipher_name == "morse" {
11554        let parts: Vec<&str> = q.splitn(3, ' ').collect();
11555        if parts.len() < 3 {
11556            let _ = writeln!(out, "  Usage: morse encode <text>");
11557            let _ = writeln!(out, "         morse decode <morse>");
11558            let _ = writeln!(out, "{}", sep);
11559            return out;
11560        }
11561        let mode = parts[1].to_lowercase();
11562        let text = parts[2];
11563        let decode = mode == "decode" || mode == "dec" || mode == "d";
11564
11565        let morse_table: &[(&str, &str)] = &[
11566            ("A", ".-"),
11567            ("B", "-..."),
11568            ("C", "-.-."),
11569            ("D", "-.."),
11570            ("E", "."),
11571            ("F", "..-."),
11572            ("G", "--."),
11573            ("H", "...."),
11574            ("I", ".."),
11575            ("J", ".---"),
11576            ("K", "-.-"),
11577            ("L", ".-.."),
11578            ("M", "--"),
11579            ("N", "-."),
11580            ("O", "---"),
11581            ("P", ".--."),
11582            ("Q", "--.-"),
11583            ("R", ".-."),
11584            ("S", "..."),
11585            ("T", "-"),
11586            ("U", "..-"),
11587            ("V", "...-"),
11588            ("W", ".--"),
11589            ("X", "-..-"),
11590            ("Y", "-.--"),
11591            ("Z", "--.."),
11592            ("0", "-----"),
11593            ("1", ".----"),
11594            ("2", "..---"),
11595            ("3", "...--"),
11596            ("4", "....-"),
11597            ("5", "....."),
11598            ("6", "-...."),
11599            ("7", "--..."),
11600            ("8", "---.."),
11601            ("9", "----."),
11602            (",", "--..--"),
11603            (".", ".-.-.-"),
11604            ("?", "..--.."),
11605            ("!", "-.-.--"),
11606            ("/", "-..-."),
11607            ("@", ".--.-."),
11608            ("&", ".-..."),
11609        ];
11610
11611        if decode {
11612            let code_to_char: std::collections::HashMap<&str, &str> =
11613                morse_table.iter().map(|&(c, m)| (m, c)).collect();
11614            let decoded: String = text
11615                .split("   ") // triple space = word boundary
11616                .map(|word| {
11617                    word.split(' ')
11618                        .map(|sym| code_to_char.get(sym).copied().unwrap_or("?"))
11619                        .collect::<String>()
11620                })
11621                .collect::<Vec<_>>()
11622                .join(" ");
11623            let _ = writeln!(out, "  Cipher:  Morse Code");
11624            let _ = writeln!(out, "  Input:   {}", text);
11625            let _ = writeln!(out, "  Decoded: {}", decoded);
11626        } else {
11627            let char_to_morse: std::collections::HashMap<&str, &str> =
11628                morse_table.iter().map(|&(c, m)| (c, m)).collect();
11629            let encoded: String = text
11630                .to_uppercase()
11631                .chars()
11632                .map(|c| {
11633                    if c == ' ' {
11634                        "   ".to_string()
11635                    } else {
11636                        let s = c.to_string();
11637                        char_to_morse
11638                            .get(s.as_str())
11639                            .copied()
11640                            .unwrap_or("?")
11641                            .to_string()
11642                            + " "
11643                    }
11644                })
11645                .collect();
11646            let _ = writeln!(out, "  Cipher:  Morse Code");
11647            let _ = writeln!(out, "  Input:   {}", text);
11648            let _ = writeln!(out, "  Encoded: {}", encoded.trim());
11649        }
11650        let _ = writeln!(out, "{}", sep);
11651        return out;
11652    }
11653
11654    // ─── Unknown ───
11655    let _ = writeln!(out, "{}", cipher_usage());
11656    let _ = writeln!(out, "{}", sep);
11657    out
11658}
11659
11660fn cipher_usage() -> &'static str {
11661    "Classical cipher encoder/decoder — no model, no cloud:\n\
11662     \n\
11663     hematite --cipher 'rot13 Hello World'               ROT13 (symmetric)\n\
11664     hematite --cipher 'atbash Hello World'              Atbash (symmetric)\n\
11665     hematite --cipher 'caesar 13 Hello World'           Caesar shift (with brute-force)\n\
11666     hematite --cipher 'vigenere encode KEY plaintext'   Vigenère encode\n\
11667     hematite --cipher 'vigenere decode KEY ciphertext'  Vigenère decode\n\
11668     hematite --cipher 'railfence encode 3 WEAREDISCOVERED'  Rail Fence encode\n\
11669     hematite --cipher 'railfence decode 3 WECRLTEERDSOEEVEAAID'  Rail Fence decode\n\
11670     hematite --cipher 'columnar encode ZEBRA plaintext'  Columnar transposition encode\n\
11671     hematite --cipher 'morse encode Hello World'        Text to Morse\n\
11672     hematite --cipher 'morse decode .... . .-.. .-.. ---'  Morse to text\n\
11673     \n\
11674     Ciphers: rot13, atbash, caesar, vigenere, railfence, columnar, morse"
11675}
11676
11677// ─── Validation toolkit (Luhn, ISBN, EAN, IBAN, UUID) ────────────────────────
11678
11679pub fn validate_calc(input: &str) -> String {
11680    let mut out = String::new();
11681    let sep = "─".repeat(60);
11682    let _ = writeln!(out, "{}", sep);
11683    let _ = writeln!(out, "  VALIDATION TOOLKIT");
11684    let _ = writeln!(out, "{}", sep);
11685
11686    let q = input.trim();
11687    let _ = writeln!(out, "  Input: \"{}\"", q);
11688    let _ = writeln!(out);
11689
11690    // Strip spaces/dashes for most checks
11691    let clean: String = q
11692        .chars()
11693        .filter(|c| !c.is_whitespace() && *c != '-' && *c != ' ')
11694        .collect();
11695
11696    // ─── Luhn algorithm (credit card) ───
11697    let luhn_result = {
11698        let digits: Vec<u32> = clean.chars().filter_map(|c| c.to_digit(10)).collect();
11699        if digits.len() >= 2 {
11700            let sum: u32 = digits
11701                .iter()
11702                .rev()
11703                .enumerate()
11704                .map(|(i, &d)| {
11705                    if i % 2 == 1 {
11706                        let doubled = d * 2;
11707                        if doubled > 9 {
11708                            doubled - 9
11709                        } else {
11710                            doubled
11711                        }
11712                    } else {
11713                        d
11714                    }
11715                })
11716                .sum();
11717            Some(sum % 10 == 0)
11718        } else {
11719            None
11720        }
11721    };
11722
11723    // ─── Detect card network from prefix ───
11724    let card_network = {
11725        let digits_only: String = clean.chars().filter(|c| c.is_ascii_digit()).collect();
11726        let n = digits_only.len();
11727        if n >= 1 {
11728            let d1: u32 = digits_only
11729                .chars()
11730                .next()
11731                .unwrap()
11732                .to_digit(10)
11733                .unwrap_or(0);
11734            let d2: u32 = if n >= 2 {
11735                digits_only[..2].parse().unwrap_or(0)
11736            } else {
11737                0
11738            };
11739            let d4: u32 = if n >= 4 {
11740                digits_only[..4].parse().unwrap_or(0)
11741            } else {
11742                0
11743            };
11744            let d6: u32 = if n >= 6 {
11745                digits_only[..6].parse().unwrap_or(0)
11746            } else {
11747                0
11748            };
11749            if d1 == 4 {
11750                Some("Visa (starts with 4)")
11751            } else if d2 == 51 || d2 == 52 || d2 == 53 || d2 == 54 || d2 == 55 {
11752                Some("Mastercard (51-55)")
11753            } else if (622126..=622925).contains(&d6) || d4 == 6011 || d2 == 65 {
11754                Some("Discover")
11755            } else if d4 == 3782 || d4 == 3714 || d4 == 3787 || d4 == 3728 || d2 == 34 || d2 == 37 {
11756                Some("American Express")
11757            } else if d4 == 3528 || d4 == 3589 {
11758                Some("JCB")
11759            } else {
11760                Some("Unknown network")
11761            }
11762        } else {
11763            None
11764        }
11765    };
11766
11767    if let Some(valid) = luhn_result {
11768        let _ = writeln!(out, "  ─── Luhn (Credit Card) ───");
11769        let _ = writeln!(
11770            out,
11771            "  Valid: {}  {}",
11772            if valid { "YES ✓" } else { "NO ✗" },
11773            if valid {
11774                "passes Luhn check"
11775            } else {
11776                "fails Luhn check"
11777            }
11778        );
11779        if let Some(network) = card_network {
11780            let digits_only: String = clean.chars().filter(|c| c.is_ascii_digit()).collect();
11781            if digits_only.len() >= 12 {
11782                let _ = writeln!(out, "  Network: {}", network);
11783                let _ = writeln!(out, "  Length:  {} digits", digits_only.len());
11784            }
11785        }
11786        // Compute check digit needed to make it valid
11787        if !valid {
11788            let digits: Vec<u32> = clean.chars().filter_map(|c| c.to_digit(10)).collect();
11789            if digits.len() >= 2 {
11790                let without_last: Vec<u32> = digits[..digits.len() - 1].to_vec();
11791                for check in 0u32..10 {
11792                    let mut test = without_last.clone();
11793                    test.push(check);
11794                    let sum: u32 = test
11795                        .iter()
11796                        .rev()
11797                        .enumerate()
11798                        .map(|(i, &d)| {
11799                            if i % 2 == 1 {
11800                                let x = d * 2;
11801                                if x > 9 {
11802                                    x - 9
11803                                } else {
11804                                    x
11805                                }
11806                            } else {
11807                                d
11808                            }
11809                        })
11810                        .sum();
11811                    if sum % 10 == 0 {
11812                        let _ =
11813                            writeln!(out, "  Correct check digit: {} (replace last digit)", check);
11814                        break;
11815                    }
11816                }
11817            }
11818        }
11819        let _ = writeln!(out);
11820    }
11821
11822    // ─── ISBN-10 ───
11823    let isbn10_digits: Vec<u32> = clean
11824        .chars()
11825        .filter_map(|c| {
11826            if c == 'X' || c == 'x' {
11827                Some(10)
11828            } else {
11829                c.to_digit(10)
11830            }
11831        })
11832        .collect();
11833    if isbn10_digits.len() == 10 {
11834        let sum: u32 = isbn10_digits
11835            .iter()
11836            .enumerate()
11837            .map(|(i, &d)| (10 - i as u32) * d)
11838            .sum();
11839        let valid = sum % 11 == 0;
11840        let _ = writeln!(out, "  ─── ISBN-10 ───");
11841        let _ = writeln!(
11842            out,
11843            "  Valid: {}  (sum mod 11 = {})",
11844            if valid { "YES ✓" } else { "NO ✗" },
11845            sum % 11
11846        );
11847        if !valid {
11848            // Compute correct check digit
11849            let sum9: u32 = isbn10_digits[..9]
11850                .iter()
11851                .enumerate()
11852                .map(|(i, &d)| (10 - i as u32) * d)
11853                .sum();
11854            let check = (11 - (sum9 % 11)) % 11;
11855            let check_str = if check == 10 {
11856                "X".to_string()
11857            } else {
11858                check.to_string()
11859            };
11860            let _ = writeln!(out, "  Correct check digit: {}", check_str);
11861        }
11862        let _ = writeln!(out);
11863    }
11864
11865    // ─── ISBN-13 / EAN-13 ───
11866    let isbn13_digits: Vec<u32> = clean.chars().filter_map(|c| c.to_digit(10)).collect();
11867    if isbn13_digits.len() == 13 {
11868        let sum: u32 = isbn13_digits
11869            .iter()
11870            .enumerate()
11871            .map(|(i, &d)| if i % 2 == 0 { d } else { d * 3 })
11872            .sum();
11873        let valid = sum % 10 == 0;
11874        let prefix = &clean[..3];
11875        let kind = if prefix == "978" || prefix == "979" {
11876            "ISBN-13"
11877        } else {
11878            "EAN-13"
11879        };
11880        let _ = writeln!(out, "  ─── {} ───", kind);
11881        let _ = writeln!(
11882            out,
11883            "  Valid: {}  (weighted sum mod 10 = {})",
11884            if valid { "YES ✓" } else { "NO ✗" },
11885            sum % 10
11886        );
11887        if kind == "ISBN-13" {
11888            let _ = writeln!(out, "  Prefix: {} (Bookland)", prefix);
11889        }
11890        if !valid {
11891            let sum12: u32 = isbn13_digits[..12]
11892                .iter()
11893                .enumerate()
11894                .map(|(i, &d)| if i % 2 == 0 { d } else { d * 3 })
11895                .sum();
11896            let check = (10 - (sum12 % 10)) % 10;
11897            let _ = writeln!(out, "  Correct check digit: {}", check);
11898        }
11899        let _ = writeln!(out);
11900    }
11901
11902    // ─── IBAN ───
11903    // IBAN: move first 4 chars to end, replace letters A=10..Z=35, check mod 97 == 1
11904    let iban_upper = clean.to_uppercase();
11905    if iban_upper.len() >= 15
11906        && iban_upper.len() <= 34
11907        && iban_upper.chars().take(2).all(|c| c.is_ascii_uppercase())
11908        && iban_upper
11909            .chars()
11910            .skip(2)
11911            .take(2)
11912            .all(|c| c.is_ascii_digit())
11913    {
11914        let rearranged = format!("{}{}", &iban_upper[4..], &iban_upper[..4]);
11915        let numeric: String = rearranged
11916            .chars()
11917            .map(|c| {
11918                if c.is_ascii_uppercase() {
11919                    format!("{}", c as u32 - 'A' as u32 + 10)
11920                } else {
11921                    c.to_string()
11922                }
11923            })
11924            .collect();
11925        // Modular arithmetic on long number string
11926        let remainder = numeric.chars().fold(0u64, |acc, c| {
11927            let d = c.to_digit(10).unwrap_or(0) as u64;
11928            (acc * 10 + d) % 97
11929        });
11930        let valid = remainder == 1;
11931        let country = &iban_upper[..2];
11932        let _ = writeln!(out, "  ─── IBAN ───");
11933        let _ = writeln!(out, "  Country: {}", country);
11934        let _ = writeln!(out, "  Length:  {} characters", iban_upper.len());
11935        let _ = writeln!(
11936            out,
11937            "  Valid:   {}  (mod 97 = {})",
11938            if valid { "YES ✓" } else { "NO ✗" },
11939            remainder
11940        );
11941        let _ = writeln!(out);
11942    }
11943
11944    // ─── UUID ───
11945    let uuid_clean: String = clean.to_lowercase();
11946    let uuid_nodash: String = uuid_clean.chars().filter(|c| *c != '-').collect();
11947    if uuid_nodash.len() == 32 && uuid_nodash.chars().all(|c| c.is_ascii_hexdigit()) {
11948        let formatted = format!(
11949            "{}-{}-{}-{}-{}",
11950            &uuid_nodash[0..8],
11951            &uuid_nodash[8..12],
11952            &uuid_nodash[12..16],
11953            &uuid_nodash[16..20],
11954            &uuid_nodash[20..32]
11955        );
11956        let version = u8::from_str_radix(&uuid_nodash[12..13], 16).unwrap_or(0);
11957        let variant_bits = u8::from_str_radix(&uuid_nodash[16..17], 16).unwrap_or(0);
11958        let variant = if variant_bits & 0xC == 0xC {
11959            "Microsoft (variant 11)"
11960        } else if variant_bits & 0x8 != 0 {
11961            "RFC 4122 (variant 10)"
11962        } else {
11963            "NCS/backward compat (variant 0)"
11964        };
11965        let _ = writeln!(out, "  ─── UUID ───");
11966        let _ = writeln!(out, "  Format:  {}", formatted);
11967        let _ = writeln!(out, "  Version: {}", version);
11968        let _ = writeln!(out, "  Variant: {}", variant);
11969        let _ = writeln!(out);
11970    }
11971
11972    // ─── If nothing matched, show usage ───
11973    let nothing = luhn_result.is_none()
11974        && isbn10_digits.len() != 10
11975        && isbn13_digits.len() != 13
11976        && !clean
11977            .to_uppercase()
11978            .starts_with(|c: char| c.is_ascii_uppercase());
11979    if nothing {
11980        let _ = writeln!(out, "  No recognizable format detected. Examples:");
11981        let _ = writeln!(
11982            out,
11983            "  hematite --validate '4532015112830366'    credit card (Luhn)"
11984        );
11985        let _ = writeln!(out, "  hematite --validate '0-306-40615-2'       ISBN-10");
11986        let _ = writeln!(
11987            out,
11988            "  hematite --validate '978-0-306-40615-7'   ISBN-13 / EAN-13"
11989        );
11990        let _ = writeln!(out, "  hematite --validate 'GB82WEST12345698765432'  IBAN");
11991        let _ = writeln!(
11992            out,
11993            "  hematite --validate '550e8400-e29b-41d4-a716-446655440000'  UUID"
11994        );
11995    }
11996
11997    let _ = writeln!(out, "{}", sep);
11998    out
11999}
12000
12001// ─── Checksum calculator ──────────────────────────────────────────────────────
12002
12003pub fn checksum_calc(input: &str) -> String {
12004    let mut out = String::new();
12005    let sep = "─".repeat(60);
12006    let _ = writeln!(out, "{}", sep);
12007    let _ = writeln!(out, "  CHECKSUM CALCULATOR");
12008    let _ = writeln!(out, "{}", sep);
12009
12010    let bytes: Vec<u8> = input.as_bytes().to_vec();
12011    let len = bytes.len();
12012
12013    let _ = writeln!(out, "  Input:   \"{}\"", input);
12014    let _ = writeln!(out, "  Length:  {} bytes", len);
12015    let _ = writeln!(out);
12016
12017    // ─── CRC-32 (IEEE 802.3 polynomial 0xEDB88320) ───
12018    let crc32 = {
12019        let mut table = [0u32; 256];
12020        for i in 0u32..256 {
12021            let mut c = i;
12022            for _ in 0..8 {
12023                if c & 1 != 0 {
12024                    c = 0xEDB8_8320 ^ (c >> 1);
12025                } else {
12026                    c >>= 1;
12027                }
12028            }
12029            table[i as usize] = c;
12030        }
12031        let mut crc: u32 = 0xFFFF_FFFF;
12032        for &b in &bytes {
12033            crc = (crc >> 8) ^ table[((crc ^ b as u32) & 0xFF) as usize];
12034        }
12035        crc ^ 0xFFFF_FFFF
12036    };
12037
12038    // ─── CRC-16 (CCITT / X.25) ───
12039    let crc16 = {
12040        let mut crc: u16 = 0xFFFF;
12041        for &b in &bytes {
12042            crc ^= (b as u16) << 8;
12043            for _ in 0..8 {
12044                if crc & 0x8000 != 0 {
12045                    crc = (crc << 1) ^ 0x1021;
12046                } else {
12047                    crc <<= 1;
12048                }
12049            }
12050        }
12051        crc
12052    };
12053
12054    // ─── Adler-32 ───
12055    let adler32 = {
12056        let (mut a, mut b) = (1u32, 0u32);
12057        for &byte in &bytes {
12058            a = (a + byte as u32) % 65521;
12059            b = (b + a) % 65521;
12060        }
12061        (b << 16) | a
12062    };
12063
12064    // ─── FNV-1a 32-bit ───
12065    let fnv1a_32 = {
12066        let mut h: u32 = 2166136261;
12067        for &b in &bytes {
12068            h ^= b as u32;
12069            h = h.wrapping_mul(16777619);
12070        }
12071        h
12072    };
12073
12074    // ─── FNV-1a 64-bit ───
12075    let fnv1a_64 = {
12076        let mut h: u64 = 14695981039346656037;
12077        for &b in &bytes {
12078            h ^= b as u64;
12079            h = h.wrapping_mul(1099511628211);
12080        }
12081        h
12082    };
12083
12084    // ─── DJB2 ───
12085    let djb2 = {
12086        let mut h: u64 = 5381;
12087        for &b in &bytes {
12088            h = h.wrapping_mul(33).wrapping_add(b as u64);
12089        }
12090        h
12091    };
12092
12093    // ─── SDBM ───
12094    let sdbm = {
12095        let mut h: u64 = 0;
12096        for &b in &bytes {
12097            h = (b as u64)
12098                .wrapping_add(h.wrapping_shl(6))
12099                .wrapping_add(h.wrapping_shl(16))
12100                .wrapping_sub(h);
12101        }
12102        h
12103    };
12104
12105    // ─── XOR checksum ───
12106    let xor8: u8 = bytes.iter().fold(0u8, |acc, &b| acc ^ b);
12107    let xor16: u16 = bytes.chunks(2).fold(0u16, |acc, chunk| {
12108        let w = if chunk.len() == 2 {
12109            (chunk[0] as u16) << 8 | chunk[1] as u16
12110        } else {
12111            (chunk[0] as u16) << 8
12112        };
12113        acc ^ w
12114    });
12115
12116    // ─── Sum checksums ───
12117    let sum8: u8 = bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
12118    let sum16: u16 = bytes
12119        .iter()
12120        .fold(0u16, |acc, &b| acc.wrapping_add(b as u16));
12121    let sum32: u32 = bytes
12122        .iter()
12123        .fold(0u32, |acc, &b| acc.wrapping_add(b as u32));
12124
12125    let _ = writeln!(out, "  ─── Cyclic Redundancy Checks ───");
12126    let _ = writeln!(out, "  CRC-32 (IEEE):     0x{:08X}  ({:10})", crc32, crc32);
12127    let _ = writeln!(
12128        out,
12129        "  CRC-16 (CCITT):    0x{:04X}      ({:6})",
12130        crc16, crc16
12131    );
12132    let _ = writeln!(out);
12133    let _ = writeln!(out, "  ─── Hash Functions ───");
12134    let _ = writeln!(
12135        out,
12136        "  Adler-32:          0x{:08X}  ({:10})",
12137        adler32, adler32
12138    );
12139    let _ = writeln!(
12140        out,
12141        "  FNV-1a 32:         0x{:08X}  ({:10})",
12142        fnv1a_32, fnv1a_32
12143    );
12144    let _ = writeln!(out, "  FNV-1a 64:         0x{:016X}", fnv1a_64);
12145    let _ = writeln!(out, "  DJB2:              0x{:016X}", djb2);
12146    let _ = writeln!(out, "  SDBM:              0x{:016X}", sdbm);
12147    let _ = writeln!(out);
12148    let _ = writeln!(out, "  ─── Simple Checksums ───");
12149    let _ = writeln!(out, "  XOR-8:             0x{:02X}  ({})", xor8, xor8);
12150    let _ = writeln!(out, "  XOR-16:            0x{:04X}  ({})", xor16, xor16);
12151    let _ = writeln!(out, "  Sum-8 (mod 256):   0x{:02X}  ({})", sum8, sum8);
12152    let _ = writeln!(out, "  Sum-16:            0x{:04X}  ({})", sum16, sum16);
12153    let _ = writeln!(out, "  Sum-32:            0x{:08X}  ({})", sum32, sum32);
12154    let _ = writeln!(out);
12155    let _ = writeln!(out, "  ─── Byte Analysis ───");
12156    let _ = writeln!(
12157        out,
12158        "  Min byte:          0x{:02X}  ({})",
12159        bytes.iter().min().copied().unwrap_or(0),
12160        bytes.iter().min().copied().unwrap_or(0)
12161    );
12162    let _ = writeln!(
12163        out,
12164        "  Max byte:          0x{:02X}  ({})",
12165        bytes.iter().max().copied().unwrap_or(0),
12166        bytes.iter().max().copied().unwrap_or(0)
12167    );
12168    let avg_byte = bytes.iter().map(|&b| b as f64).sum::<f64>() / len.max(1) as f64;
12169    let _ = writeln!(out, "  Avg byte value:    {:.2}", avg_byte);
12170    // Hex dump (first 32 bytes)
12171    if len <= 32 {
12172        let _ = writeln!(
12173            out,
12174            "  Hex: {}",
12175            bytes
12176                .iter()
12177                .map(|b| format!("{:02X}", b))
12178                .collect::<Vec<_>>()
12179                .join(" ")
12180        );
12181    } else {
12182        let preview: String = bytes[..32]
12183            .iter()
12184            .map(|b| format!("{:02X}", b))
12185            .collect::<Vec<_>>()
12186            .join(" ");
12187        let _ = writeln!(out, "  Hex (first 32): {} ...", preview);
12188    }
12189
12190    let _ = writeln!(out, "{}", sep);
12191    out
12192}
12193
12194// ─── Sorting algorithm visualizer ─────────────────────────────────────────────
12195
12196pub fn sort_viz(query: &str) -> String {
12197    let mut out = String::new();
12198    let sep = "─".repeat(60);
12199    let _ = writeln!(out, "{}", sep);
12200    let _ = writeln!(out, "  SORTING ALGORITHM VISUALIZER");
12201    let _ = writeln!(out, "{}", sep);
12202
12203    let q = query.trim();
12204    // Format: "<algorithm> <n1,n2,n3,...>"  or just "<n1,n2,...>" for all algorithms
12205    let words: Vec<&str> = q.splitn(2, ' ').collect();
12206    let algos_all = ["bubble", "insertion", "selection", "merge", "quick", "heap"];
12207
12208    let (algos, data_str) = if words.len() == 2 {
12209        let first_lower = words[0].to_lowercase();
12210        if algos_all.contains(&first_lower.as_str()) {
12211            (vec![first_lower], words[1])
12212        } else {
12213            (algos_all.iter().map(|s| s.to_string()).collect(), q)
12214        }
12215    } else {
12216        (algos_all.iter().map(|s| s.to_string()).collect(), q)
12217    };
12218
12219    // Parse data
12220    let data: Vec<i64> = data_str
12221        .split(|c: char| !c.is_numeric() && c != '-')
12222        .filter_map(|s| s.parse().ok())
12223        .collect();
12224
12225    if data.is_empty() || data.len() < 2 {
12226        let _ = writeln!(
12227            out,
12228            "  Provide at least 2 numbers: hematite --sort-viz '5,3,8,1,9,2'"
12229        );
12230        let _ = writeln!(
12231            out,
12232            "  Or specify an algorithm:    hematite --sort-viz 'bubble 5,3,8,1'"
12233        );
12234        let _ = writeln!(out, "{}", sep);
12235        return out;
12236    }
12237    if data.len() > 20 {
12238        let _ = writeln!(
12239            out,
12240            "  Max 20 elements for visualization (got {})",
12241            data.len()
12242        );
12243        let _ = writeln!(out, "{}", sep);
12244        return out;
12245    }
12246
12247    let _ = writeln!(out, "  Input: {:?}", data);
12248    let _ = writeln!(out);
12249
12250    let max_val = *data.iter().max().unwrap_or(&1);
12251    let min_val = *data.iter().min().unwrap_or(&0);
12252    let range = (max_val - min_val).max(1);
12253
12254    // Bar chart renderer
12255    let bar_height = 8usize;
12256    let bar_render = |arr: &[i64]| -> String {
12257        let mut lines = vec![String::new(); bar_height + 1];
12258        for &v in arr {
12259            let h = ((v - min_val) as f64 / range as f64 * bar_height as f64).round() as usize;
12260            let h = h.min(bar_height);
12261            for row in 0..bar_height {
12262                let bar_row = bar_height - 1 - row;
12263                if bar_row < h {
12264                    lines[row].push_str("██ ");
12265                } else {
12266                    lines[row].push_str("   ");
12267                }
12268            }
12269            lines[bar_height].push_str(&format!("{:<3}", v));
12270        }
12271        lines
12272            .iter()
12273            .map(|l| format!("  {}", l))
12274            .collect::<Vec<_>>()
12275            .join("\n")
12276    };
12277
12278    for algo in &algos {
12279        let _ = writeln!(out, "  ═══ {} Sort ═══", algo.to_uppercase());
12280        let _ = writeln!(out);
12281
12282        let mut arr = data.clone();
12283        let mut steps: Vec<(Vec<i64>, String)> = Vec::new();
12284        let mut comparisons = 0usize;
12285        let mut swaps = 0usize;
12286
12287        match algo.as_str() {
12288            "bubble" => {
12289                let n = arr.len();
12290                for i in 0..n {
12291                    let mut swapped = false;
12292                    for j in 0..n - 1 - i {
12293                        comparisons += 1;
12294                        if arr[j] > arr[j + 1] {
12295                            arr.swap(j, j + 1);
12296                            swaps += 1;
12297                            swapped = true;
12298                        }
12299                    }
12300                    steps.push((
12301                        arr.clone(),
12302                        format!("Pass {} (sorted tail: {})", i + 1, i + 1),
12303                    ));
12304                    if !swapped {
12305                        break;
12306                    }
12307                }
12308            }
12309            "insertion" => {
12310                for i in 1..arr.len() {
12311                    let key = arr[i];
12312                    let mut j = i as i64 - 1;
12313                    while j >= 0 {
12314                        comparisons += 1;
12315                        if arr[j as usize] > key {
12316                            arr[(j + 1) as usize] = arr[j as usize];
12317                            swaps += 1;
12318                            j -= 1;
12319                        } else {
12320                            break;
12321                        }
12322                    }
12323                    arr[(j + 1) as usize] = key;
12324                    steps.push((arr.clone(), format!("Insert {} at position {}", key, j + 1)));
12325                }
12326            }
12327            "selection" => {
12328                let n = arr.len();
12329                for i in 0..n - 1 {
12330                    let mut min_idx = i;
12331                    for j in i + 1..n {
12332                        comparisons += 1;
12333                        if arr[j] < arr[min_idx] {
12334                            min_idx = j;
12335                        }
12336                    }
12337                    if min_idx != i {
12338                        arr.swap(i, min_idx);
12339                        swaps += 1;
12340                    }
12341                    steps.push((
12342                        arr.clone(),
12343                        format!("Select min={} → position {}", arr[i], i),
12344                    ));
12345                }
12346            }
12347            "merge" => {
12348                fn merge_sort_steps(
12349                    arr: &mut Vec<i64>,
12350                    steps: &mut Vec<(Vec<i64>, String)>,
12351                    comparisons: &mut usize,
12352                    swaps: &mut usize,
12353                    lo: usize,
12354                    hi: usize,
12355                ) {
12356                    if hi - lo <= 1 {
12357                        return;
12358                    }
12359                    let mid = (lo + hi) / 2;
12360                    merge_sort_steps(arr, steps, comparisons, swaps, lo, mid);
12361                    merge_sort_steps(arr, steps, comparisons, swaps, mid, hi);
12362                    let left: Vec<i64> = arr[lo..mid].to_vec();
12363                    let right: Vec<i64> = arr[mid..hi].to_vec();
12364                    let (mut li, mut ri) = (0, 0);
12365                    let mut idx = lo;
12366                    while li < left.len() && ri < right.len() {
12367                        *comparisons += 1;
12368                        if left[li] <= right[ri] {
12369                            arr[idx] = left[li];
12370                            li += 1;
12371                        } else {
12372                            arr[idx] = right[ri];
12373                            ri += 1;
12374                            *swaps += 1;
12375                        }
12376                        idx += 1;
12377                    }
12378                    while li < left.len() {
12379                        arr[idx] = left[li];
12380                        li += 1;
12381                        idx += 1;
12382                    }
12383                    while ri < right.len() {
12384                        arr[idx] = right[ri];
12385                        ri += 1;
12386                        idx += 1;
12387                    }
12388                    steps.push((arr.clone(), format!("Merge [{}, {})", lo, hi)));
12389                }
12390                let n = arr.len();
12391                merge_sort_steps(&mut arr, &mut steps, &mut comparisons, &mut swaps, 0, n);
12392            }
12393            "quick" => {
12394                fn quick_sort_steps(
12395                    arr: &mut Vec<i64>,
12396                    steps: &mut Vec<(Vec<i64>, String)>,
12397                    comparisons: &mut usize,
12398                    swaps: &mut usize,
12399                    lo: usize,
12400                    hi: usize,
12401                ) {
12402                    if lo + 1 >= hi {
12403                        return;
12404                    }
12405                    let pivot = arr[hi - 1];
12406                    let mut i = lo;
12407                    for j in lo..hi - 1 {
12408                        *comparisons += 1;
12409                        if arr[j] <= pivot {
12410                            arr.swap(i, j);
12411                            i += 1;
12412                            *swaps += 1;
12413                        }
12414                    }
12415                    arr.swap(i, hi - 1);
12416                    *swaps += 1;
12417                    steps.push((arr.clone(), format!("Pivot={} partitioned at {}", pivot, i)));
12418                    if i > lo {
12419                        quick_sort_steps(arr, steps, comparisons, swaps, lo, i);
12420                    }
12421                    if i + 1 < hi {
12422                        quick_sort_steps(arr, steps, comparisons, swaps, i + 1, hi);
12423                    }
12424                }
12425                let n = arr.len();
12426                quick_sort_steps(&mut arr, &mut steps, &mut comparisons, &mut swaps, 0, n);
12427            }
12428            "heap" => {
12429                let n = arr.len();
12430                // Build max-heap
12431                for i in (0..n / 2).rev() {
12432                    let mut root = i;
12433                    loop {
12434                        let left = 2 * root + 1;
12435                        let right = 2 * root + 2;
12436                        let mut largest = root;
12437                        if left < n {
12438                            comparisons += 1;
12439                            if arr[left] > arr[largest] {
12440                                largest = left;
12441                            }
12442                        }
12443                        if right < n {
12444                            comparisons += 1;
12445                            if arr[right] > arr[largest] {
12446                                largest = right;
12447                            }
12448                        }
12449                        if largest != root {
12450                            arr.swap(root, largest);
12451                            swaps += 1;
12452                            root = largest;
12453                        } else {
12454                            break;
12455                        }
12456                    }
12457                }
12458                steps.push((arr.clone(), "Heap built".to_string()));
12459                for end in (1..n).rev() {
12460                    arr.swap(0, end);
12461                    swaps += 1;
12462                    // Sift down
12463                    let mut root = 0;
12464                    loop {
12465                        let left = 2 * root + 1;
12466                        let right = 2 * root + 2;
12467                        let mut largest = root;
12468                        if left < end {
12469                            comparisons += 1;
12470                            if arr[left] > arr[largest] {
12471                                largest = left;
12472                            }
12473                        }
12474                        if right < end {
12475                            comparisons += 1;
12476                            if arr[right] > arr[largest] {
12477                                largest = right;
12478                            }
12479                        }
12480                        if largest != root {
12481                            arr.swap(root, largest);
12482                            swaps += 1;
12483                            root = largest;
12484                        } else {
12485                            break;
12486                        }
12487                    }
12488                    steps.push((
12489                        arr.clone(),
12490                        format!("Extracted max, sorted: {} elements", n - end),
12491                    ));
12492                }
12493            }
12494            _ => {}
12495        }
12496
12497        // Show steps (cap at 12 to avoid walls of text)
12498        let max_steps = 12usize;
12499        let step_count = steps.len();
12500        let show_steps = if step_count > max_steps {
12501            // Show first 4, last 4, mark ellipsis
12502            let mut s = steps[..4].to_vec();
12503            s.push((vec![], format!("... {} more steps ...", step_count - 8)));
12504            s.extend_from_slice(&steps[step_count - 4..]);
12505            s
12506        } else {
12507            steps.clone()
12508        };
12509
12510        for (step_arr, label) in &show_steps {
12511            if step_arr.is_empty() {
12512                let _ = writeln!(out, "  {}", label);
12513                continue;
12514            }
12515            let _ = writeln!(out, "  → {}", label);
12516            let _ = write!(out, "{}", bar_render(step_arr));
12517            let _ = writeln!(out);
12518        }
12519
12520        let _ = writeln!(out);
12521        let _ = writeln!(out, "  Final: {:?}", arr);
12522        let _ = writeln!(
12523            out,
12524            "  Comparisons: {}  |  Swaps/moves: {}  |  Steps: {}",
12525            comparisons, swaps, step_count
12526        );
12527        let _ = writeln!(out);
12528    }
12529
12530    let _ = writeln!(out, "{}", sep);
12531    out
12532}
12533
12534// ─── Number formatter ─────────────────────────────────────────────────────────
12535
12536pub fn number_format(query: &str) -> String {
12537    let mut out = String::new();
12538    let sep = "─".repeat(60);
12539    let _ = writeln!(out, "{}", sep);
12540    let _ = writeln!(out, "  NUMBER FORMAT CONVERTER");
12541    let _ = writeln!(out, "{}", sep);
12542
12543    let q = query.trim();
12544
12545    // Try parsing as integer first, then float
12546    let val: f64 = match q.replace(['_', ','], "").parse::<f64>() {
12547        Ok(v) => v,
12548        Err(_) => {
12549            let _ = writeln!(out, "  Cannot parse: {}", q);
12550            let _ = writeln!(out, "  Usage: hematite --number-format '1234567890'");
12551            let _ = writeln!(out, "         hematite --number-format '6.022e23'");
12552            let _ = writeln!(out, "{}", sep);
12553            return out;
12554        }
12555    };
12556
12557    let _ = writeln!(out, "  Input: {}", q);
12558    let _ = writeln!(out);
12559
12560    // ─── Thousands-separated form ───
12561    let fmt_thousands = |v: f64| -> String {
12562        if v == v.floor() && v.abs() < 1e15 {
12563            let i = v as i64;
12564            let s = format!("{}", i.abs());
12565            let with_sep: String = s
12566                .chars()
12567                .rev()
12568                .enumerate()
12569                .flat_map(|(i, c)| {
12570                    if i > 0 && i % 3 == 0 {
12571                        vec![',', c]
12572                    } else {
12573                        vec![c]
12574                    }
12575                })
12576                .collect::<String>()
12577                .chars()
12578                .rev()
12579                .collect();
12580            if i < 0 {
12581                format!("-{}", with_sep)
12582            } else {
12583                with_sep
12584            }
12585        } else {
12586            // Show decimal with commas on integer part
12587            let int_part = v.abs().floor() as i64;
12588            let frac = (v.abs() - int_part as f64).to_string();
12589            let frac_str = if frac.len() > 2 { &frac[1..] } else { "" };
12590            let s = format!("{}", int_part);
12591            let with_sep: String = s
12592                .chars()
12593                .rev()
12594                .enumerate()
12595                .flat_map(|(i, c)| {
12596                    if i > 0 && i % 3 == 0 {
12597                        vec![',', c]
12598                    } else {
12599                        vec![c]
12600                    }
12601                })
12602                .collect::<String>()
12603                .chars()
12604                .rev()
12605                .collect();
12606            let signed = if v < 0.0 {
12607                format!("-{}{}", with_sep, frac_str)
12608            } else {
12609                format!("{}{}", with_sep, frac_str)
12610            };
12611            signed
12612        }
12613    };
12614
12615    // ─── Scientific notation ───
12616    let sci = if val == 0.0 {
12617        "0.000000e0".to_string()
12618    } else {
12619        let exp = val.abs().log10().floor() as i32;
12620        let mantissa = val / 10f64.powi(exp);
12621        format!("{:.6}e{}", mantissa, exp)
12622    };
12623
12624    // ─── Engineering notation (exponent multiple of 3) ───
12625    let eng = if val == 0.0 {
12626        "0.000e0".to_string()
12627    } else {
12628        let exp = val.abs().log10().floor() as i32;
12629        let eng_exp = (exp as f64 / 3.0).floor() as i32 * 3;
12630        let mantissa = val / 10f64.powi(eng_exp);
12631        format!("{:.3}e{}", mantissa, eng_exp)
12632    };
12633
12634    // ─── SI prefix ───
12635    let si = {
12636        let si_prefixes: &[(f64, &str, &str)] = &[
12637            (1e24, "Y", "yotta"),
12638            (1e21, "Z", "zetta"),
12639            (1e18, "E", "exa"),
12640            (1e15, "P", "peta"),
12641            (1e12, "T", "tera"),
12642            (1e9, "G", "giga"),
12643            (1e6, "M", "mega"),
12644            (1e3, "k", "kilo"),
12645            (1e0, "", ""),
12646            (1e-3, "m", "milli"),
12647            (1e-6, "μ", "micro"),
12648            (1e-9, "n", "nano"),
12649            (1e-12, "p", "pico"),
12650            (1e-15, "f", "femto"),
12651        ];
12652        let abs_val = val.abs();
12653        let mut result = format!("{:.6}", val);
12654        for &(scale, sym, name) in si_prefixes {
12655            if abs_val >= scale * 0.999 || scale <= 1e-12 {
12656                let scaled = val / scale;
12657                if sym.is_empty() {
12658                    result = format!("{:.4}", scaled);
12659                } else {
12660                    result = format!("{:.4} {} ({})", scaled, sym, name);
12661                }
12662                break;
12663            }
12664        }
12665        result
12666    };
12667
12668    // ─── Binary / Hex (integers only) ───
12669    let int_forms = if val == val.floor() && val.abs() < 2e63 {
12670        let i = val as i64;
12671        let u = i as u64;
12672        Some((
12673            format!("0x{:X}", u),
12674            format!("0b{:b}", u),
12675            format!("0o{:o}", u),
12676        ))
12677    } else {
12678        None
12679    };
12680
12681    // ─── Word form (English) ───
12682    let word_form = number_to_words(val);
12683
12684    let _ = writeln!(out, "  ─── Number Representations ───");
12685    let _ = writeln!(out, "  Decimal (formatted):  {}", fmt_thousands(val));
12686    let _ = writeln!(out, "  Scientific notation:  {}", sci);
12687    let _ = writeln!(out, "  Engineering notation: {}", eng);
12688    let _ = writeln!(out, "  SI prefix:            {}", si);
12689    if let Some((hex, bin, oct)) = int_forms {
12690        let _ = writeln!(out, "  Hexadecimal:          {}", hex);
12691        let _ = writeln!(out, "  Binary:               {}", bin);
12692        let _ = writeln!(out, "  Octal:                {}", oct);
12693    }
12694    let _ = writeln!(out, "  Word form:            {}", word_form);
12695    if val != 0.0 {
12696        let _ = writeln!(out, "  Reciprocal:           {:.6} (1/{})", 1.0 / val, q);
12697        let _ = writeln!(out, "  Percentage:           {:.4}%", val * 100.0);
12698        let log10 = val.abs().log10();
12699        let ln_v = val.abs().ln();
12700        let _ = writeln!(out, "  log₁₀:               {:.6}", log10);
12701        let _ = writeln!(out, "  ln:                   {:.6}", ln_v);
12702        let _ = writeln!(out, "  √:                    {:.6}", val.abs().sqrt());
12703        if val == val.floor() && val.abs() < 1e15 {
12704            let _ = writeln!(out, "  ²:                    {}", fmt_thousands(val * val));
12705        }
12706    }
12707    let _ = writeln!(out, "{}", sep);
12708    out
12709}
12710
12711fn number_to_words(v: f64) -> String {
12712    if v == 0.0 {
12713        return "zero".to_string();
12714    }
12715    let is_neg = v < 0.0;
12716    let abs_v = v.abs();
12717
12718    // Only handle integers up to ~999 trillion
12719    if abs_v != abs_v.floor() || abs_v >= 1e15 {
12720        return "(too large or fractional for word form)".to_string();
12721    }
12722    let n = abs_v as u64;
12723
12724    let ones = [
12725        "",
12726        "one",
12727        "two",
12728        "three",
12729        "four",
12730        "five",
12731        "six",
12732        "seven",
12733        "eight",
12734        "nine",
12735        "ten",
12736        "eleven",
12737        "twelve",
12738        "thirteen",
12739        "fourteen",
12740        "fifteen",
12741        "sixteen",
12742        "seventeen",
12743        "eighteen",
12744        "nineteen",
12745    ];
12746    let tens = [
12747        "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
12748    ];
12749
12750    let under100 = |n: u64| -> String {
12751        if n < 20 {
12752            ones[n as usize].to_string()
12753        } else {
12754            let t = tens[(n / 10) as usize];
12755            let o = ones[(n % 10) as usize];
12756            if o.is_empty() {
12757                t.to_string()
12758            } else {
12759                format!("{}-{}", t, o)
12760            }
12761        }
12762    };
12763
12764    let under1000 = |n: u64| -> String {
12765        if n < 100 {
12766            under100(n)
12767        } else {
12768            let h = ones[(n / 100) as usize];
12769            let rem = n % 100;
12770            if rem == 0 {
12771                format!("{} hundred", h)
12772            } else {
12773                format!("{} hundred {}", h, under100(rem))
12774            }
12775        }
12776    };
12777
12778    let scales: &[(u64, &str)] = &[
12779        (1_000_000_000_000, "trillion"),
12780        (1_000_000_000, "billion"),
12781        (1_000_000, "million"),
12782        (1_000, "thousand"),
12783    ];
12784
12785    let mut parts: Vec<String> = Vec::new();
12786    let mut remaining = n;
12787    for &(scale, name) in scales {
12788        if remaining >= scale {
12789            let chunk = remaining / scale;
12790            remaining %= scale;
12791            parts.push(format!("{} {}", under1000(chunk), name));
12792        }
12793    }
12794    if remaining > 0 {
12795        parts.push(under1000(remaining));
12796    }
12797
12798    let result = parts.join(" ");
12799    if is_neg {
12800        format!("negative {}", result)
12801    } else {
12802        result
12803    }
12804}
12805
12806// ─── String distance metrics ──────────────────────────────────────────────────
12807
12808pub fn string_dist(query: &str) -> String {
12809    let mut out = String::new();
12810    let sep = "─".repeat(60);
12811    let _ = writeln!(out, "{}", sep);
12812    let _ = writeln!(out, "  STRING DISTANCE METRICS");
12813    let _ = writeln!(out, "{}", sep);
12814
12815    // Split on " vs " or " | " to get two strings
12816    let (a, b) = if let Some(pos) = query.find(" vs ") {
12817        (query[..pos].trim(), query[pos + 4..].trim())
12818    } else if let Some(pos) = query.find(" | ") {
12819        (query[..pos].trim(), query[pos + 3..].trim())
12820    } else {
12821        // Try splitting on comma if no quoted form
12822        let parts: Vec<&str> = query.splitn(2, ',').collect();
12823        if parts.len() == 2 {
12824            (parts[0].trim(), parts[1].trim())
12825        } else {
12826            let _ = writeln!(
12827                out,
12828                "  Usage: hematite --levenshtein '<string1> vs <string2>'"
12829            );
12830            let _ = writeln!(out, "         hematite --levenshtein 'kitten vs sitting'");
12831            let _ = writeln!(out, "         hematite --levenshtein 'hello, helo'");
12832            let _ = writeln!(out, "{}", sep);
12833            return out;
12834        }
12835    };
12836
12837    let _ = writeln!(out, "  String A: \"{}\"  (len={})", a, a.chars().count());
12838    let _ = writeln!(out, "  String B: \"{}\"  (len={})", b, b.chars().count());
12839    let _ = writeln!(out);
12840
12841    let ac: Vec<char> = a.chars().collect();
12842    let bc: Vec<char> = b.chars().collect();
12843    let m = ac.len();
12844    let n = bc.len();
12845
12846    // ─── Levenshtein edit distance ───
12847    let lev_dist = {
12848        let mut dp = vec![vec![0usize; n + 1]; m + 1];
12849        for i in 0..=m {
12850            dp[i][0] = i;
12851        }
12852        for j in 0..=n {
12853            dp[0][j] = j;
12854        }
12855        for i in 1..=m {
12856            for j in 1..=n {
12857                if ac[i - 1] == bc[j - 1] {
12858                    dp[i][j] = dp[i - 1][j - 1];
12859                } else {
12860                    dp[i][j] = 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1]);
12861                }
12862            }
12863        }
12864        dp[m][n]
12865    };
12866    let max_len = m.max(n).max(1);
12867    let lev_sim = 1.0 - lev_dist as f64 / max_len as f64;
12868
12869    // ─── Damerau-Levenshtein (transpositions allowed) ───
12870    let dl_dist = {
12871        let mut dp = vec![vec![0usize; n + 1]; m + 1];
12872        for i in 0..=m {
12873            dp[i][0] = i;
12874        }
12875        for j in 0..=n {
12876            dp[0][j] = j;
12877        }
12878        for i in 1..=m {
12879            for j in 1..=n {
12880                let cost = if ac[i - 1] == bc[j - 1] { 0 } else { 1 };
12881                dp[i][j] = (dp[i - 1][j] + 1)
12882                    .min(dp[i][j - 1] + 1)
12883                    .min(dp[i - 1][j - 1] + cost);
12884                if i > 1 && j > 1 && ac[i - 1] == bc[j - 2] && ac[i - 2] == bc[j - 1] {
12885                    dp[i][j] = dp[i][j].min(dp[i - 2][j - 2] + cost);
12886                }
12887            }
12888        }
12889        dp[m][n]
12890    };
12891
12892    // ─── Hamming distance (same-length strings only) ───
12893    let hamming = if m == n {
12894        let h = ac.iter().zip(bc.iter()).filter(|(a, b)| a != b).count();
12895        Some(h)
12896    } else {
12897        None
12898    };
12899
12900    // ─── Jaro similarity ───
12901    let jaro = {
12902        if m == 0 && n == 0 {
12903            1.0f64
12904        } else if m == 0 || n == 0 {
12905            0.0f64
12906        } else {
12907            let match_dist = (m.max(n) / 2).saturating_sub(1);
12908            let mut s1_matches = vec![false; m];
12909            let mut s2_matches = vec![false; n];
12910            let mut matches = 0usize;
12911            for i in 0..m {
12912                let start = i.saturating_sub(match_dist);
12913                let end = (i + match_dist + 1).min(n);
12914                for j in start..end {
12915                    if !s2_matches[j] && ac[i] == bc[j] {
12916                        s1_matches[i] = true;
12917                        s2_matches[j] = true;
12918                        matches += 1;
12919                        break;
12920                    }
12921                }
12922            }
12923            if matches == 0 {
12924                0.0
12925            } else {
12926                let s1m: Vec<char> = ac
12927                    .iter()
12928                    .enumerate()
12929                    .filter(|(i, _)| s1_matches[*i])
12930                    .map(|(_, c)| *c)
12931                    .collect();
12932                let s2m: Vec<char> = bc
12933                    .iter()
12934                    .enumerate()
12935                    .filter(|(i, _)| s2_matches[*i])
12936                    .map(|(_, c)| *c)
12937                    .collect();
12938                let transpositions = s1m.iter().zip(s2m.iter()).filter(|(a, b)| a != b).count() / 2;
12939                let mf = matches as f64;
12940                (mf / m as f64 + mf / n as f64 + (mf - transpositions as f64) / mf) / 3.0
12941            }
12942        }
12943    };
12944
12945    // ─── Jaro-Winkler ───
12946    let jaro_winkler = {
12947        let prefix_len = ac
12948            .iter()
12949            .zip(bc.iter())
12950            .take(4)
12951            .take_while(|(a, b)| a == b)
12952            .count();
12953        let p = 0.1f64;
12954        jaro + prefix_len as f64 * p * (1.0 - jaro)
12955    };
12956    let jaro_winkler = jaro_winkler.min(1.0);
12957
12958    // ─── LCS length ───
12959    let lcs_len = {
12960        let mut dp = vec![vec![0usize; n + 1]; m + 1];
12961        for i in 1..=m {
12962            for j in 1..=n {
12963                if ac[i - 1] == bc[j - 1] {
12964                    dp[i][j] = dp[i - 1][j - 1] + 1;
12965                } else {
12966                    dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
12967                }
12968            }
12969        }
12970        dp[m][n]
12971    };
12972    let lcs_sim = if max_len > 0 {
12973        lcs_len as f64 / max_len as f64
12974    } else {
12975        0.0
12976    };
12977
12978    // ─── Longest common substring ───
12979    let (lcsub_len, lcsub) = {
12980        let mut best_len = 0usize;
12981        let mut best_end = 0usize;
12982        let mut dp = vec![vec![0usize; n + 1]; m + 1];
12983        for i in 1..=m {
12984            for j in 1..=n {
12985                if ac[i - 1] == bc[j - 1] {
12986                    dp[i][j] = dp[i - 1][j - 1] + 1;
12987                    if dp[i][j] > best_len {
12988                        best_len = dp[i][j];
12989                        best_end = i;
12990                    }
12991                }
12992            }
12993        }
12994        let substr: String = ac[best_end.saturating_sub(best_len)..best_end]
12995            .iter()
12996            .collect();
12997        (best_len, substr)
12998    };
12999
13000    // ─── Output ───
13001    let _ = writeln!(out, "  ─── Edit Distance ───");
13002    let _ = writeln!(
13003        out,
13004        "  Levenshtein:            {:>6}  (similarity: {:.1}%)",
13005        lev_dist,
13006        lev_sim * 100.0
13007    );
13008    let _ = writeln!(
13009        out,
13010        "  Damerau-Levenshtein:    {:>6}  (allows transpositions)",
13011        dl_dist
13012    );
13013    if let Some(h) = hamming {
13014        let _ = writeln!(
13015            out,
13016            "  Hamming:                {:>6}  (substitutions only)",
13017            h
13018        );
13019    } else {
13020        let _ = writeln!(out, "  Hamming:          n/a (strings must be same length)");
13021    }
13022    let _ = writeln!(out);
13023    let _ = writeln!(out, "  ─── Similarity Scores (0=no match, 1=identical) ───");
13024    let _ = writeln!(
13025        out,
13026        "  Jaro:                 {:.6}  ({:.1}%)",
13027        jaro,
13028        jaro * 100.0
13029    );
13030    let _ = writeln!(
13031        out,
13032        "  Jaro-Winkler:         {:.6}  ({:.1}%)",
13033        jaro_winkler,
13034        jaro_winkler * 100.0
13035    );
13036    let _ = writeln!(
13037        out,
13038        "  LCS similarity:       {:.6}  ({:.1}%)",
13039        lcs_sim,
13040        lcs_sim * 100.0
13041    );
13042    let _ = writeln!(out);
13043    let _ = writeln!(out, "  ─── Common Subsequence / Substring ───");
13044    let _ = writeln!(
13045        out,
13046        "  LCS length:           {}  ({:.1}% of max length)",
13047        lcs_len,
13048        lcs_sim * 100.0
13049    );
13050    let _ = writeln!(
13051        out,
13052        "  Longest common sub:   \"{}\"  (length {})",
13053        lcsub, lcsub_len
13054    );
13055
13056    let _ = writeln!(out, "{}", sep);
13057    out
13058}
13059
13060// ─── Text statistics and readability analyzer ─────────────────────────────────
13061
13062pub fn text_stats(input: &str) -> String {
13063    let mut out = String::new();
13064    let sep = "─".repeat(60);
13065    let _ = writeln!(out, "{}", sep);
13066    let _ = writeln!(out, "  TEXT STATISTICS & READABILITY");
13067    let _ = writeln!(out, "{}", sep);
13068
13069    let text = input.trim();
13070    if text.is_empty() {
13071        let _ = writeln!(out, "  No text provided. Pass text as the argument.");
13072        let _ = writeln!(out, "{}", sep);
13073        return out;
13074    }
13075
13076    // ─── Basic counts ───
13077    let char_count = text.chars().count();
13078    let char_no_space = text.chars().filter(|c| !c.is_whitespace()).count();
13079    let byte_count = text.len();
13080
13081    // Words: split on whitespace
13082    let words: Vec<&str> = text.split_whitespace().collect();
13083    let word_count = words.len();
13084
13085    // Sentences: end with . ! ? (crude but accurate enough for readability)
13086    let sentence_count = text
13087        .chars()
13088        .filter(|&c| c == '.' || c == '!' || c == '?')
13089        .count()
13090        .max(1);
13091
13092    // Paragraphs: blank lines
13093    let paragraph_count = text
13094        .split("\n\n")
13095        .filter(|p| !p.trim().is_empty())
13096        .count()
13097        .max(1);
13098
13099    // ─── Syllable counting (heuristic: vowel groups) ───
13100    let count_syllables = |word: &str| -> usize {
13101        let w = word.to_lowercase();
13102        let w: String = w.chars().filter(|c| c.is_alphabetic()).collect();
13103        if w.is_empty() {
13104            return 0;
13105        }
13106        let vowels = "aeiouy";
13107        let mut count = 0usize;
13108        let mut prev_vowel = false;
13109        let chars: Vec<char> = w.chars().collect();
13110        for &ch in &chars {
13111            let is_v = vowels.contains(ch);
13112            if is_v && !prev_vowel {
13113                count += 1;
13114            }
13115            prev_vowel = is_v;
13116        }
13117        // Silent trailing 'e'
13118        if w.ends_with('e') && count > 1 {
13119            count = count.saturating_sub(1);
13120        }
13121        count.max(1)
13122    };
13123
13124    let total_syllables: usize = words.iter().map(|w| count_syllables(w)).sum();
13125    let avg_syllables = if word_count > 0 {
13126        total_syllables as f64 / word_count as f64
13127    } else {
13128        0.0
13129    };
13130
13131    // ─── Readability scores ───
13132    let words_per_sentence = if sentence_count > 0 {
13133        word_count as f64 / sentence_count as f64
13134    } else {
13135        0.0
13136    };
13137    let syllables_per_word = avg_syllables;
13138
13139    // Flesch Reading Ease: 206.835 - 1.015*(words/sentences) - 84.6*(syllables/word)
13140    let flesch_ease = 206.835 - 1.015 * words_per_sentence - 84.6 * syllables_per_word;
13141    let flesch_ease = flesch_ease.clamp(0.0, 100.0);
13142
13143    // Flesch-Kincaid Grade Level: 0.39*(words/sentences) + 11.8*(syllables/word) - 15.59
13144    let fk_grade = 0.39 * words_per_sentence + 11.8 * syllables_per_word - 15.59;
13145    let fk_grade = fk_grade.max(0.0);
13146
13147    // Gunning Fog Index: 0.4 * ((words/sentences) + 100*(complex_words/words))
13148    // Complex = 3+ syllables, not proper nouns (crude: just count syllables >= 3)
13149    let complex_words = words.iter().filter(|w| count_syllables(w) >= 3).count();
13150    let fog_index =
13151        0.4 * (words_per_sentence + 100.0 * complex_words as f64 / word_count.max(1) as f64);
13152
13153    // SMOG: sqrt(complex * (30 / sentences)) + 3
13154    let smog = (complex_words as f64 * 30.0 / sentence_count as f64).sqrt() + 3.0;
13155
13156    // Coleman-Liau: 0.0588*L - 0.296*S - 15.8  (L=avg letters/100 words, S=avg sentences/100 words)
13157    let letters_per_100: f64 = char_no_space as f64 / word_count.max(1) as f64 * 100.0;
13158    let sent_per_100: f64 = sentence_count as f64 / word_count.max(1) as f64 * 100.0;
13159    let coleman_liau = 0.0588 * letters_per_100 - 0.296 * sent_per_100 - 15.8;
13160
13161    // Flesch ease → grade label
13162    let ease_label = if flesch_ease >= 90.0 {
13163        "Very Easy (5th grade)"
13164    } else if flesch_ease >= 80.0 {
13165        "Easy (6th grade)"
13166    } else if flesch_ease >= 70.0 {
13167        "Fairly Easy (7th grade)"
13168    } else if flesch_ease >= 60.0 {
13169        "Standard (8th–9th grade)"
13170    } else if flesch_ease >= 50.0 {
13171        "Fairly Difficult (10th–12th grade)"
13172    } else if flesch_ease >= 30.0 {
13173        "Difficult (College)"
13174    } else {
13175        "Very Confusing (Professional)"
13176    };
13177
13178    // ─── Word frequency (top 20 excluding common stop words) ───
13179    let stop_words: &[&str] = &[
13180        "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is",
13181        "it", "its", "was", "are", "were", "be", "been", "being", "have", "has", "had", "do",
13182        "does", "did", "will", "would", "could", "should", "may", "might", "shall", "that", "this",
13183        "these", "those", "i", "you", "he", "she", "we", "they", "them", "his", "her", "our",
13184        "their", "my", "your", "as", "by", "from", "up", "about", "into", "through", "than",
13185        "more", "also", "if", "not", "so", "all", "can",
13186    ];
13187    let mut freq: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
13188    for word in &words {
13189        let clean: String = word
13190            .chars()
13191            .filter(|c| c.is_alphanumeric())
13192            .collect::<String>()
13193            .to_lowercase();
13194        if !clean.is_empty() && clean.len() > 1 && !stop_words.contains(&clean.as_str()) {
13195            *freq.entry(clean).or_insert(0) += 1;
13196        }
13197    }
13198    let mut freq_sorted: Vec<(String, usize)> = freq.into_iter().collect();
13199    freq_sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
13200
13201    // ─── Character frequency ───
13202    let mut char_freq: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
13203    for ch in text.chars() {
13204        if ch.is_alphabetic() {
13205            *char_freq.entry(ch.to_ascii_lowercase()).or_insert(0) += 1;
13206        }
13207    }
13208    let mut char_sorted: Vec<(char, usize)> = char_freq.into_iter().collect();
13209    char_sorted.sort_by(|a, b| b.1.cmp(&a.1));
13210
13211    // ─── Average word length ───
13212    let total_word_chars: usize = words
13213        .iter()
13214        .map(|w| w.chars().filter(|c| c.is_alphabetic()).count())
13215        .sum();
13216    let avg_word_len = if word_count > 0 {
13217        total_word_chars as f64 / word_count as f64
13218    } else {
13219        0.0
13220    };
13221
13222    // ─── Longest words ───
13223    let mut word_lengths: Vec<(&&str, usize)> = words
13224        .iter()
13225        .map(|w| (w, w.chars().filter(|c| c.is_alphabetic()).count()))
13226        .collect();
13227    word_lengths.sort_by(|a, b| b.1.cmp(&a.1));
13228    word_lengths.dedup_by_key(|w| w.1);
13229
13230    // ─── Output ───
13231    let _ = writeln!(out, "  ─── Basic Counts ───");
13232    let _ = writeln!(out, "  Characters (total):     {:>8}", char_count);
13233    let _ = writeln!(out, "  Characters (no spaces): {:>8}", char_no_space);
13234    let _ = writeln!(out, "  Bytes:                  {:>8}", byte_count);
13235    let _ = writeln!(out, "  Words:                  {:>8}", word_count);
13236    let _ = writeln!(out, "  Sentences:              {:>8}", sentence_count);
13237    let _ = writeln!(out, "  Paragraphs:             {:>8}", paragraph_count);
13238    let _ = writeln!(out, "  Syllables (est.):       {:>8}", total_syllables);
13239    let _ = writeln!(out, "  Complex words (3+ syl): {:>8}", complex_words);
13240    let _ = writeln!(out);
13241    let _ = writeln!(out, "  ─── Averages ───");
13242    let _ = writeln!(out, "  Avg word length:        {:>8.2} chars", avg_word_len);
13243    let _ = writeln!(out, "  Avg syllables/word:     {:>8.2}", avg_syllables);
13244    let _ = writeln!(out, "  Avg words/sentence:     {:>8.2}", words_per_sentence);
13245    let _ = writeln!(out);
13246    let _ = writeln!(out, "  ─── Readability Scores ───");
13247    let _ = writeln!(
13248        out,
13249        "  Flesch Reading Ease:    {:>8.1}  ({})",
13250        flesch_ease, ease_label
13251    );
13252    let _ = writeln!(
13253        out,
13254        "  Flesch-Kincaid Grade:   {:>8.1}  (Grade {:.0})",
13255        fk_grade, fk_grade
13256    );
13257    let _ = writeln!(
13258        out,
13259        "  Gunning Fog Index:      {:>8.1}  (Grade {:.0})",
13260        fog_index, fog_index
13261    );
13262    let _ = writeln!(
13263        out,
13264        "  SMOG Index:             {:>8.1}  (Grade {:.0})",
13265        smog, smog
13266    );
13267    let _ = writeln!(
13268        out,
13269        "  Coleman-Liau Index:     {:>8.1}  (Grade {:.0})",
13270        coleman_liau, coleman_liau
13271    );
13272    let _ = writeln!(out);
13273    let _ = writeln!(out, "  ─── Top Words (excluding stop words) ───");
13274    for (word, count) in freq_sorted.iter().take(20) {
13275        let bar: String = "█".repeat((count * 30 / freq_sorted[0].1.max(1)).min(30));
13276        let _ = writeln!(out, "  {:>4}x  {:<20}  {}", count, word, bar);
13277    }
13278    let _ = writeln!(out);
13279    let _ = writeln!(out, "  ─── Letter Frequency ───");
13280    let total_letters: usize = char_sorted.iter().map(|&(_, c)| c).sum();
13281    for (ch, count) in char_sorted.iter().take(10) {
13282        let pct = *count as f64 / total_letters.max(1) as f64 * 100.0;
13283        let bar: String = "█".repeat((pct as usize * 2).min(40));
13284        let _ = writeln!(out, "  '{}': {:>5} ({:5.2}%)  {}", ch, count, pct, bar);
13285    }
13286    if !word_lengths.is_empty() {
13287        let _ = writeln!(out);
13288        let _ = writeln!(out, "  ─── Longest Words ───");
13289        for (w, len) in word_lengths.iter().take(5) {
13290            let clean: String = w.chars().filter(|c| c.is_alphanumeric()).collect();
13291            let _ = writeln!(out, "  {} ({} chars)", clean, len);
13292        }
13293    }
13294
13295    let _ = writeln!(out, "{}", sep);
13296    out
13297}