Skip to main content

spg_storage/
bignum.rs

1//! v7.38 (read01, T3) — arbitrary-precision decimal for NUMERIC values that
2//! overflow `i128` (PG's NUMERIC is unbounded; SPG's `i128` fast path tops out
3//! near 38 digits). Clean-room schoolbook arithmetic on base-10^9 limbs (each
4//! limb holds 9 decimal digits, little-endian), a sign, and a decimal `scale`.
5//! This is phase C1: representation + add / sub / mul / cmp + the `i128` bridge
6//! + decimal-string conversion. Division (Knuth D) and sqrt (Newton) follow in
7//! later phases; this module is not yet wired into the engine.
8//!
9//! Learned from PG's NUMERIC design (base-10000 `NBASE` digits, the same
10//! schoolbook shape) but re-implemented over SPG's own `u32` base-10^9 limbs.
11
12use alloc::string::{String, ToString};
13use alloc::vec::Vec;
14
15/// Base of a limb: 10^9, so a limb is 9 decimal digits and fits in `u32`
16/// (10^9 < 2^32). A product of two limbs (< 10^18) fits in `u64`.
17const BASE: u64 = 1_000_000_000;
18const BASE_DIGITS: usize = 9;
19
20/// An arbitrary-precision decimal: `(-1)^neg · (Σ limbs[i]·BASE^i) · 10^-scale`.
21/// `limbs` is little-endian with no trailing (most-significant) zero limbs; the
22/// value zero is the empty limb vector with `neg == false`.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct BigNumeric {
25    neg: bool,
26    limbs: Vec<u32>,
27    scale: u16,
28}
29
30impl BigNumeric {
31    /// True when the magnitude is zero (regardless of sign / scale).
32    #[must_use]
33    pub fn is_zero(&self) -> bool {
34        self.limbs.is_empty()
35    }
36
37    #[must_use]
38    pub fn scale(&self) -> u16 {
39        self.scale
40    }
41
42    /// v7.38 (read01, T3.C3) — expose the on-disk parts (sign, base-10^9 limbs
43    /// little-endian, scale) for the codec.
44    #[must_use]
45    pub fn parts(&self) -> (bool, &[u32], u16) {
46        (self.neg, &self.limbs, self.scale)
47    }
48
49    /// Rebuild from codec parts. Normalizes (a canonical big value never has a
50    /// mantissa that fits `i128` — the caller collapses those to `Numeric`).
51    #[must_use]
52    pub fn from_parts(neg: bool, limbs: Vec<u32>, scale: u16) -> Self {
53        let mut out = BigNumeric { neg, limbs, scale };
54        out.normalize();
55        out
56    }
57
58    /// Drop most-significant zero limbs and canonicalize a zero to `+0`.
59    fn normalize(&mut self) {
60        while self.limbs.last() == Some(&0) {
61            self.limbs.pop();
62        }
63        if self.limbs.is_empty() {
64            self.neg = false;
65        }
66    }
67
68    /// Build from an `i128` mantissa at a given scale.
69    #[must_use]
70    pub fn from_i128(mut v: i128, scale: u16) -> Self {
71        let neg = v < 0;
72        let mut limbs = Vec::new();
73        // Use the unsigned magnitude; i128::MIN's magnitude still fits in u128.
74        let mut mag = v.unsigned_abs();
75        let _ = &mut v;
76        while mag != 0 {
77            limbs.push((mag % u128::from(BASE)) as u32);
78            mag /= u128::from(BASE);
79        }
80        let mut out = BigNumeric { neg, limbs, scale };
81        out.normalize();
82        out
83    }
84
85    /// Convert back to `(i128 mantissa, scale)` when the mantissa fits; `None`
86    /// on overflow (the caller keeps the big form). Scale is preserved.
87    #[must_use]
88    pub fn to_i128(&self) -> Option<i128> {
89        let mut mag: u128 = 0;
90        for &limb in self.limbs.iter().rev() {
91            mag = mag
92                .checked_mul(u128::from(BASE))?
93                .checked_add(u128::from(limb))?;
94        }
95        if self.neg {
96            // magnitude up to i128::MIN's magnitude (2^127) is representable.
97            if mag <= (i128::MAX as u128) + 1 {
98                Some((mag as i128).wrapping_neg())
99            } else {
100                None
101            }
102        } else {
103            i128::try_from(mag).ok()
104        }
105    }
106
107    /// Compare magnitudes only (ignores sign + scale alignment).
108    fn cmp_mag(a: &[u32], b: &[u32]) -> core::cmp::Ordering {
109        use core::cmp::Ordering;
110        match a.len().cmp(&b.len()) {
111            Ordering::Equal => {
112                for i in (0..a.len()).rev() {
113                    match a[i].cmp(&b[i]) {
114                        Ordering::Equal => {}
115                        other => return other,
116                    }
117                }
118                Ordering::Equal
119            }
120            other => other,
121        }
122    }
123
124    /// `a + b` on magnitudes (limb vectors), little-endian.
125    fn add_mag(a: &[u32], b: &[u32]) -> Vec<u32> {
126        let mut out = Vec::with_capacity(a.len().max(b.len()) + 1);
127        let mut carry: u64 = 0;
128        for i in 0..a.len().max(b.len()) {
129            let av = u64::from(a.get(i).copied().unwrap_or(0));
130            let bv = u64::from(b.get(i).copied().unwrap_or(0));
131            let s = av + bv + carry;
132            out.push((s % BASE) as u32);
133            carry = s / BASE;
134        }
135        if carry != 0 {
136            out.push(carry as u32);
137        }
138        out
139    }
140
141    /// `a - b` on magnitudes, requires `a >= b`; little-endian.
142    fn sub_mag(a: &[u32], b: &[u32]) -> Vec<u32> {
143        let mut out = Vec::with_capacity(a.len());
144        let mut borrow: i64 = 0;
145        for i in 0..a.len() {
146            let av = i64::from(a[i]);
147            let bv = i64::from(b.get(i).copied().unwrap_or(0));
148            let mut d = av - bv - borrow;
149            if d < 0 {
150                d += BASE as i64;
151                borrow = 1;
152            } else {
153                borrow = 0;
154            }
155            out.push(d as u32);
156        }
157        while out.last() == Some(&0) {
158            out.pop();
159        }
160        out
161    }
162
163    /// Multiply the magnitude by 10^k (used to align scales), little-endian.
164    fn mul_pow10(limbs: &[u32], k: u32) -> Vec<u32> {
165        if limbs.is_empty() {
166            return Vec::new();
167        }
168        let whole = (k as usize) / BASE_DIGITS;
169        let rem = (k as usize) % BASE_DIGITS;
170        // shift by `rem` decimal digits = multiply by 10^rem within the base
171        let mut cur: Vec<u32> = if rem == 0 {
172            limbs.to_vec()
173        } else {
174            let factor = 10u64.pow(rem as u32);
175            let mut out = Vec::with_capacity(limbs.len() + 1);
176            let mut carry: u64 = 0;
177            for &l in limbs {
178                let v = u64::from(l) * factor + carry;
179                out.push((v % BASE) as u32);
180                carry = v / BASE;
181            }
182            if carry != 0 {
183                out.push(carry as u32);
184            }
185            out
186        };
187        // then shift by `whole` full limbs
188        if whole > 0 {
189            let mut shifted = Vec::with_capacity(cur.len() + whole);
190            shifted.resize(whole, 0);
191            shifted.append(&mut cur);
192            cur = shifted;
193        }
194        cur
195    }
196
197    /// Align two values to a common scale (the larger of the two), returning the
198    /// scaled magnitude limb vectors and the shared scale.
199    fn align(&self, other: &Self) -> (Vec<u32>, Vec<u32>, u16) {
200        use core::cmp::Ordering;
201        match self.scale.cmp(&other.scale) {
202            Ordering::Equal => (self.limbs.clone(), other.limbs.clone(), self.scale),
203            Ordering::Less => {
204                let k = u32::from(other.scale - self.scale);
205                (
206                    Self::mul_pow10(&self.limbs, k),
207                    other.limbs.clone(),
208                    other.scale,
209                )
210            }
211            Ordering::Greater => {
212                let k = u32::from(self.scale - other.scale);
213                (
214                    self.limbs.clone(),
215                    Self::mul_pow10(&other.limbs, k),
216                    self.scale,
217                )
218            }
219        }
220    }
221
222    /// Signed comparison honoring sign + scale.
223    ///
224    /// Deliberately NOT `Ord`: this compares numeric VALUE (it aligns the two
225    /// scales first, so `1.5` and `1.50` are `Equal`), while the derived
226    /// `PartialEq` / `Eq` compare the representation field-wise and call those
227    /// two unequal. `Ord` requires `a == b` iff `a.cmp(b) == Equal`, so an
228    /// `Ord` impl here would be unsound-by-contract. Keep the inherent method.
229    #[allow(clippy::should_implement_trait)]
230    #[must_use]
231    pub fn cmp(&self, other: &Self) -> core::cmp::Ordering {
232        use core::cmp::Ordering;
233        if let (true, true) = (self.is_zero(), other.is_zero()) {
234            return Ordering::Equal;
235        }
236        match (self.neg, other.neg) {
237            (false, true) => return Ordering::Greater,
238            (true, false) => return Ordering::Less,
239            _ => {}
240        }
241        let (a, b, _) = self.align(other);
242        let mag = Self::cmp_mag(&a, &b);
243        if self.neg { mag.reverse() } else { mag }
244    }
245
246    #[must_use]
247    pub fn add(&self, other: &Self) -> Self {
248        let (a, b, scale) = self.align(other);
249        let out = if self.neg == other.neg {
250            BigNumeric {
251                neg: self.neg,
252                limbs: Self::add_mag(&a, &b),
253                scale,
254            }
255        } else {
256            // opposite signs → subtract the smaller magnitude from the larger.
257            match Self::cmp_mag(&a, &b) {
258                core::cmp::Ordering::Less => BigNumeric {
259                    neg: other.neg,
260                    limbs: Self::sub_mag(&b, &a),
261                    scale,
262                },
263                _ => BigNumeric {
264                    neg: self.neg,
265                    limbs: Self::sub_mag(&a, &b),
266                    scale,
267                },
268            }
269        };
270        let mut out = out;
271        out.normalize();
272        out
273    }
274
275    #[must_use]
276    pub fn neg(&self) -> Self {
277        let mut out = self.clone();
278        if !out.is_zero() {
279            out.neg = !out.neg;
280        }
281        out
282    }
283
284    #[must_use]
285    pub fn sub(&self, other: &Self) -> Self {
286        self.add(&other.neg())
287    }
288
289    #[must_use]
290    pub fn mul(&self, other: &Self) -> Self {
291        if self.is_zero() || other.is_zero() {
292            return BigNumeric {
293                neg: false,
294                limbs: Vec::new(),
295                scale: self.scale + other.scale,
296            };
297        }
298        let mut acc = alloc::vec![0u64; self.limbs.len() + other.limbs.len()];
299        for (i, &a) in self.limbs.iter().enumerate() {
300            let mut carry: u64 = 0;
301            for (j, &b) in other.limbs.iter().enumerate() {
302                let cur = acc[i + j] + u64::from(a) * u64::from(b) + carry;
303                acc[i + j] = cur % BASE;
304                carry = cur / BASE;
305            }
306            acc[i + other.limbs.len()] += carry;
307        }
308        // propagate any residual carries and narrow to u32 limbs
309        let mut limbs = Vec::with_capacity(acc.len());
310        let mut carry: u64 = 0;
311        for v in acc {
312            let cur = v + carry;
313            limbs.push((cur % BASE) as u32);
314            carry = cur / BASE;
315        }
316        while carry != 0 {
317            limbs.push((carry % BASE) as u32);
318            carry /= BASE;
319        }
320        let mut out = BigNumeric {
321            neg: self.neg != other.neg,
322            limbs,
323            scale: self.scale + other.scale,
324        };
325        out.normalize();
326        out
327    }
328
329    /// Multiply a magnitude by a small scalar `< BASE`, little-endian.
330    fn mul_scalar(limbs: &[u32], factor: u64) -> Vec<u32> {
331        if factor == 0 || limbs.is_empty() {
332            return Vec::new();
333        }
334        let mut out = Vec::with_capacity(limbs.len() + 1);
335        let mut carry: u64 = 0;
336        for &l in limbs {
337            let v = u64::from(l) * factor + carry;
338            out.push((v % BASE) as u32);
339            carry = v / BASE;
340        }
341        while carry != 0 {
342            out.push((carry % BASE) as u32);
343            carry /= BASE;
344        }
345        out
346    }
347
348    /// Integer magnitude division: `u / v` → `(quotient, remainder)`, both
349    /// little-endian, via Knuth's Algorithm D (TAOCP 4.3.1) over base-10^9 limbs.
350    /// `v` must be non-zero and normalized. Learned from the classic algorithm;
351    /// re-implemented on `u32` limbs with `u64`/`i64` intermediates.
352    fn div_rem_mag(u: &[u32], v: &[u32]) -> (Vec<u32>, Vec<u32>) {
353        use core::cmp::Ordering;
354        // u < v → quotient 0, remainder u.
355        if Self::cmp_mag(u, v) == Ordering::Less {
356            let mut r = u.to_vec();
357            while r.last() == Some(&0) {
358                r.pop();
359            }
360            return (Vec::new(), r);
361        }
362        let n = v.len();
363        // Short division by a single limb.
364        if n == 1 {
365            let d = u64::from(v[0]);
366            let mut rem: u64 = 0;
367            let mut q = alloc::vec![0u32; u.len()];
368            for i in (0..u.len()).rev() {
369                let cur = rem * BASE + u64::from(u[i]);
370                q[i] = (cur / d) as u32;
371                rem = cur % d;
372            }
373            while q.last() == Some(&0) {
374                q.pop();
375            }
376            let r = if rem == 0 {
377                Vec::new()
378            } else {
379                alloc::vec![rem as u32]
380            };
381            return (q, r);
382        }
383        // D1. Normalize so the divisor's top limb is >= BASE/2.
384        let d = BASE / (u64::from(v[n - 1]) + 1);
385        let vn = Self::mul_scalar(v, d);
386        let vn = {
387            let mut vn = vn;
388            vn.resize(n, 0); // exactly n limbs (d keeps v the same length)
389            vn
390        };
391        let mut un = Self::mul_scalar(u, d);
392        let m = u.len() - n; // quotient has m+1 limbs
393        un.resize(u.len() + 1, 0); // room for a leading limb
394        let mut q = alloc::vec![0u32; m + 1];
395        // D2..D7. Loop over quotient limbs from most significant.
396        for j in (0..=m).rev() {
397            // D3. Estimate qhat.
398            let num = u128::from(un[j + n]) * u128::from(BASE) + u128::from(un[j + n - 1]);
399            let mut qhat = num / u128::from(vn[n - 1]);
400            let mut rhat = num % u128::from(vn[n - 1]);
401            while qhat >= u128::from(BASE)
402                || qhat * u128::from(vn[n - 2])
403                    > rhat * u128::from(BASE) + u128::from(un[j + n - 2])
404            {
405                qhat -= 1;
406                rhat += u128::from(vn[n - 1]);
407                if rhat >= u128::from(BASE) {
408                    break;
409                }
410            }
411            // D4. Multiply and subtract qhat*vn from un[j..j+n+1].
412            let mut borrow: i64 = 0;
413            let mut carry: u64 = 0;
414            for i in 0..n {
415                let p = qhat * u128::from(vn[i]) + u128::from(carry);
416                carry = (p / u128::from(BASE)) as u64;
417                let sub = (p % u128::from(BASE)) as i64;
418                let mut t = i64::from(un[j + i]) - sub - borrow;
419                if t < 0 {
420                    t += BASE as i64;
421                    borrow = 1;
422                } else {
423                    borrow = 0;
424                }
425                un[j + i] = t as u32;
426            }
427            let mut t = i64::from(un[j + n]) - carry as i64 - borrow;
428            // D5/D6. If we subtracted too much, add back one multiple of vn.
429            if t < 0 {
430                qhat -= 1;
431                let mut c: u64 = 0;
432                for i in 0..n {
433                    let s = u64::from(un[j + i]) + u64::from(vn[i]) + c;
434                    un[j + i] = (s % BASE) as u32;
435                    c = s / BASE;
436                }
437                t += (BASE as i64) + c as i64;
438            }
439            un[j + n] = t as u32;
440            q[j] = qhat as u32;
441        }
442        // D8. Unnormalize the remainder: un[0..n] / d.
443        let mut rem = un[..n].to_vec();
444        while rem.last() == Some(&0) {
445            rem.pop();
446        }
447        let (rem, _) = Self::div_rem_mag(&rem, &[d as u32]);
448        while q.last() == Some(&0) {
449            q.pop();
450        }
451        (q, rem)
452    }
453
454    /// Signed integer division truncating toward zero (like `i128 / i128`),
455    /// ignoring scale. Returns `(quotient, remainder)`.
456    #[must_use]
457    pub fn div_rem_int(&self, other: &Self) -> (Self, Self) {
458        let (q, r) = Self::div_rem_mag(&self.limbs, &other.limbs);
459        let mut quo = BigNumeric {
460            neg: self.neg != other.neg,
461            limbs: q,
462            scale: 0,
463        };
464        let mut rem = BigNumeric {
465            neg: self.neg,
466            limbs: r,
467            scale: 0,
468        };
469        quo.normalize();
470        rem.normalize();
471        (quo, rem)
472    }
473
474    /// Fixed-point division to a target `result_scale`, rounded half-away-from-
475    /// zero — the shape PG's `numeric / numeric` uses. Errors are the caller's:
476    /// dividing by zero returns `None`.
477    #[must_use]
478    pub fn div(&self, other: &Self, result_scale: u16) -> Option<Self> {
479        if other.is_zero() {
480            return None;
481        }
482        // Scale the dividend so the integer quotient carries result_scale + 1
483        // guard digit, relative to the operands' own scales.
484        let want = i32::from(result_scale) + 1 + i32::from(other.scale) - i32::from(self.scale);
485        let num = if want > 0 {
486            Self::mul_pow10(&self.limbs, want as u32)
487        } else {
488            self.limbs.clone()
489        };
490        let den = if want < 0 {
491            Self::mul_pow10(&other.limbs, (-want) as u32)
492        } else {
493            other.limbs.clone()
494        };
495        let (mut q, _rem) = Self::div_rem_mag(&num, &den);
496        // The quotient carries one guard digit past result_scale. Round
497        // half-away-from-zero (PG numeric): guard >= 5 bumps, then drop it.
498        let guard = if q.is_empty() { 0 } else { q[0] % 10 };
499        q = Self::div_rem_mag(&q, &[10]).0; // drop the guard digit
500        if guard >= 5 {
501            q = Self::add_mag(&q, &[1]);
502        }
503        let mut out = BigNumeric {
504            neg: self.neg != other.neg,
505            limbs: q,
506            scale: result_scale,
507        };
508        out.normalize();
509        Some(out)
510    }
511
512    /// v7.38 (read01, C4) — floor of the integer square root of this value's
513    /// magnitude taken as an integer (scale ignored). Newton's method on the
514    /// base-10^9 limbs, starting from a decimal-digit overestimate and
515    /// descending to the floor. Zero → zero.
516    fn isqrt_mag(&self) -> Self {
517        use core::cmp::Ordering;
518        let n = BigNumeric {
519            neg: false,
520            limbs: self.limbs.clone(),
521            scale: 0,
522        };
523        if n.is_zero() {
524            return BigNumeric {
525                neg: false,
526                limbs: Vec::new(),
527                scale: 0,
528            };
529        }
530        // Decimal digit count of the magnitude.
531        let top = *n.limbs.last().unwrap();
532        let ndigits = (n.limbs.len() - 1) * BASE_DIGITS + top.to_string().len();
533        // Overestimate x0 = 10^ceil(ndigits/2) >= sqrt(n).
534        let half = ndigits.div_ceil(2);
535        let one = BigNumeric::from_i128(1, 0);
536        let two = BigNumeric::from_i128(2, 0);
537        let mut x = BigNumeric {
538            neg: false,
539            limbs: Self::mul_pow10(&one.limbs, half as u32),
540            scale: 0,
541        };
542        // Newton: x_{k+1} = (x + n/x) / 2, monotonically descending to the floor.
543        loop {
544            let (div, _) = n.div_rem_int(&x);
545            let sum = x.add(&div);
546            let (next, _) = sum.div_rem_int(&two);
547            if next.cmp(&x) != Ordering::Less {
548                break;
549            }
550            x = next;
551        }
552        // Descend any residual overshoot so x*x <= n exactly.
553        while x.mul(&x).cmp(&n) == Ordering::Greater {
554            x = x.sub(&one);
555        }
556        x
557    }
558
559    /// v7.38 (read01, C4) — square root at a target display scale, rounded
560    /// half-away-from-zero, the shape PG's numeric `sqrt` uses. `None` for a
561    /// negative value (the caller raises the domain error). The caller picks
562    /// `result_scale` (PG's ~16-significant-digit rule) and guarantees it is at
563    /// least the argument's own scale.
564    #[must_use]
565    pub fn sqrt(&self, result_scale: u16) -> Option<Self> {
566        use core::cmp::Ordering;
567        if self.neg && !self.is_zero() {
568            return None;
569        }
570        if self.is_zero() {
571            return Some(BigNumeric {
572                neg: false,
573                limbs: Vec::new(),
574                scale: result_scale,
575            });
576        }
577        // Compute one guard digit past result_scale, then round it off.
578        // radicand = mantissa * 10^(2*(result_scale+1) - scale); isqrt of it is
579        // floor(sqrt(value) * 10^(result_scale+1)).
580        let shift = 2 * (i32::from(result_scale) + 1) - i32::from(self.scale);
581        let mant = BigNumeric {
582            neg: false,
583            limbs: self.limbs.clone(),
584            scale: 0,
585        };
586        let radicand = if shift >= 0 {
587            BigNumeric {
588                neg: false,
589                limbs: Self::mul_pow10(&mant.limbs, shift as u32),
590                scale: 0,
591            }
592        } else {
593            let (q, _) = Self::div_rem_mag(&mant.limbs, &Self::mul_pow10(&[1], (-shift) as u32));
594            BigNumeric {
595                neg: false,
596                limbs: q,
597                scale: 0,
598            }
599        };
600        let root = radicand.isqrt_mag();
601        // Round the guard digit half-away-from-zero, drop it.
602        let ten = BigNumeric::from_i128(10, 0);
603        let (q, r) = root.div_rem_int(&ten);
604        let five = BigNumeric::from_i128(5, 0);
605        let mut rounded = if r.cmp(&five) != Ordering::Less {
606            q.add(&BigNumeric::from_i128(1, 0))
607        } else {
608            q
609        };
610        rounded.scale = result_scale;
611        rounded.normalize();
612        Some(rounded)
613    }
614
615    /// v7.38 (S1.1b) — round to `target_scale`, half-away-from-zero (PG numeric
616    /// `round_var`). Widening pads with zeros; narrowing drops digits with
617    /// rounding.
618    #[must_use]
619    pub fn round_to(&self, target_scale: u16) -> Self {
620        use core::cmp::Ordering;
621        match self.scale.cmp(&target_scale) {
622            Ordering::Equal => self.clone(),
623            Ordering::Less => {
624                let k = u32::from(target_scale - self.scale);
625                let mut out = BigNumeric {
626                    neg: self.neg,
627                    limbs: Self::mul_pow10(&self.limbs, k),
628                    scale: target_scale,
629                };
630                out.normalize();
631                out
632            }
633            Ordering::Greater => {
634                let k = u32::from(self.scale - target_scale);
635                let divisor = Self::mul_pow10(&[1], k);
636                let (mut q, r) = Self::div_rem_mag(&self.limbs, &divisor);
637                // half-away: bump when 2*rem >= divisor.
638                let two_r = Self::mul_scalar(&r, 2);
639                if Self::cmp_mag(&two_r, &divisor) != Ordering::Less {
640                    q = Self::add_mag(&q, &[1]);
641                }
642                let mut out = BigNumeric {
643                    neg: self.neg,
644                    limbs: q,
645                    scale: target_scale,
646                };
647                out.normalize();
648                out
649            }
650        }
651    }
652
653    /// v7.38 (S1.1b) — the display scale PG gives a numeric transcendental
654    /// result (`exp` / `ln` / fractional `^`): ~16 significant digits, i.e.
655    /// `17 - int_digits` fractional digits (16 for `|v| < 10`), floored at 0.
656    fn transcendental_scale(&self) -> u16 {
657        let (_, digits, scale) = self.parts();
658        // Decimal digit count of the mantissa.
659        let ndigits = if digits.is_empty() {
660            1
661        } else {
662            let top = *digits.last().unwrap();
663            (digits.len() - 1) * BASE_DIGITS + top.to_string().len()
664        };
665        let int_digits = ndigits as i64 - i64::from(scale);
666        if int_digits <= 1 {
667            16
668        } else {
669            (17 - int_digits).max(0) as u16
670        }
671    }
672
673    /// v7.38 (S1.1b) — e^self at PG's numeric display scale (~16 significant
674    /// digits), rounded half-away. Range-reduces by halving until the operand is
675    /// small (fast Taylor convergence), sums the series with guard digits, then
676    /// squares back. The final scale keys off the RESULT magnitude, so it is
677    /// computed at high precision first, then rounded. Matches PG18.4 to the
678    /// last digit across the differential set.
679    #[must_use]
680    pub fn exp(&self) -> Self {
681        const WS: u16 = 28;
682        let raw = if self.neg && !self.is_zero() {
683            // e^-x = 1 / e^x.
684            let pos = self.neg().exp_core(WS);
685            BigNumeric::from_i128(1, 0).div(&pos, WS).unwrap()
686        } else {
687            self.exp_core(WS)
688        };
689        raw.round_to(raw.transcendental_scale())
690    }
691
692    /// exp on a non-negative value computed at `ws` (working scale, includes the
693    /// guard digits); the caller rounds down to the display scale. Integer
694    /// constants are built at scale 0 (`from_i128(k, 0)` is the value `k`, not
695    /// `k * 10^-scale`) and gain scale through the arithmetic.
696    fn exp_core(&self, ws: u16) -> Self {
697        let one = BigNumeric::from_i128(1, 0);
698        if self.is_zero() {
699            return one.round_to(ws);
700        }
701        // Range reduction: halve until t <= 1/16 so the series converges in a
702        // handful of terms. `halvings` squarings undo it afterwards.
703        let sixteenth = BigNumeric::from_decimal_str("0.0625").unwrap();
704        let two = BigNumeric::from_i128(2, 0);
705        let mut halvings = 0u32;
706        let mut t = self.clone();
707        while t.cmp(&sixteenth) == core::cmp::Ordering::Greater {
708            t = t.div(&two, ws).unwrap();
709            halvings += 1;
710        }
711        // Taylor: 1 + t + t^2/2! + t^3/3! + …  term_k = term_{k-1} * t / k.
712        let mut sum = one.round_to(ws);
713        let mut term = one.round_to(ws);
714        let mut k: i128 = 1;
715        loop {
716            term = term.mul(&t).round_to(ws);
717            term = term.div(&BigNumeric::from_i128(k, 0), ws).unwrap();
718            if term.is_zero() {
719                break;
720            }
721            sum = sum.add(&term);
722            k += 1;
723        }
724        // Undo the halvings: square once per halving.
725        for _ in 0..halvings {
726            sum = sum.mul(&sum).round_to(ws);
727        }
728        sum
729    }
730
731    /// v7.38 (S1.1b) — `self^exp` for a positive base and any exponent, exact
732    /// to PG's numeric display scale, via `exp(exp · ln(self))`. `ln` is taken
733    /// at high internal precision (not its display scale) so the composition
734    /// keeps ~16 correct significant digits. `None` for a non-positive base.
735    #[must_use]
736    pub fn pow_numeric(&self, exp: &Self) -> Option<Self> {
737        if self.neg || self.is_zero() {
738            return None;
739        }
740        const WS: u16 = 30;
741        let ln_hi = self.ln_at(WS)?;
742        let prod = exp.mul(&ln_hi).round_to(WS);
743        Some(prod.exp())
744    }
745
746    /// natural log computed to a fixed working scale `ws` (no display-scale
747    /// rounding); shared by `ln` (rounds to display scale) and `pow_numeric`
748    /// (needs full precision for the exponent multiply). `None` for `self <= 0`.
749    fn ln_at(&self, ws: u16) -> Option<Self> {
750        if self.neg || self.is_zero() {
751            return None;
752        }
753        let two = BigNumeric::from_i128(2, 0);
754        let ln2 =
755            BigNumeric::from_decimal_str("0.6931471805599453094172321214581765680755").unwrap();
756        let four_thirds = BigNumeric::from_decimal_str("1.3333333333333333").unwrap();
757        let two_thirds = BigNumeric::from_decimal_str("0.6666666666666667").unwrap();
758        let mut m = self.round_to(ws);
759        let mut e: i128 = 0;
760        while m.cmp(&four_thirds) != core::cmp::Ordering::Less {
761            m = m.div(&two, ws).unwrap();
762            e += 1;
763        }
764        while m.cmp(&two_thirds) == core::cmp::Ordering::Less {
765            m = m.mul(&two).round_to(ws);
766            e -= 1;
767        }
768        let one = BigNumeric::from_i128(1, 0);
769        let t = m.sub(&one).div(&m.add(&one), ws).unwrap();
770        let t2 = t.mul(&t).round_to(ws);
771        let mut sum = t.clone();
772        let mut power = t.clone();
773        let mut k: i128 = 3;
774        loop {
775            power = power.mul(&t2).round_to(ws);
776            let term = power.div(&BigNumeric::from_i128(k, 0), ws).unwrap();
777            if term.is_zero() {
778                break;
779            }
780            sum = sum.add(&term);
781            k += 2;
782        }
783        let ln_m = sum.mul(&two).round_to(ws);
784        let e_ln2 = ln2.mul(&BigNumeric::from_i128(e, 0)).round_to(ws);
785        Some(ln_m.add(&e_ln2))
786    }
787
788    /// v7.38 (S1.1b) — natural log at a target display scale, rounded half-away.
789    /// `None` for a non-positive value (the caller raises the domain error).
790    /// Range-reduces `x = m · 2^e` to `m ∈ [2/3, 4/3)` where the atanh series
791    /// converges fast, then adds `e · ln2`. Matches PG18.4 to the last digit.
792    #[must_use]
793    pub fn ln(&self) -> Option<Self> {
794        let raw = self.ln_at(30)?;
795        Some(raw.round_to(raw.transcendental_scale()))
796    }
797
798    /// v7.38 (read01) — PG numeric `log(base, x)` = ln(x) / ln(base), computed
799    /// at a wide working scale and then rounded to PG's display scale. `None`
800    /// when either operand is non-positive (`ln_at` rejects it) or when
801    /// `ln(base)` is zero (base = 1) — the caller raises PG's specific
802    /// "logarithm of zero / of a negative number" / "division by zero" error.
803    #[must_use]
804    pub fn log_base(&self, base: &Self) -> Option<Self> {
805        const WS: u16 = 32;
806        let ln_x = self.ln_at(WS)?;
807        let ln_b = base.ln_at(WS)?;
808        if ln_b.is_zero() {
809            return None;
810        }
811        let raw = ln_x.div(&ln_b, WS)?;
812        Some(raw.round_to(raw.transcendental_scale()))
813    }
814
815    /// PG numeric `log(x)` / `log10(x)` — the base-10 logarithm.
816    #[must_use]
817    pub fn log10(&self) -> Option<Self> {
818        self.log_base(&Self::from_i128(10, 0))
819    }
820
821    /// Render as a decimal string (`-123.4500` style), inserting the scale point.
822    #[must_use]
823    pub fn to_decimal_str(&self) -> String {
824        if self.is_zero() {
825            if self.scale == 0 {
826                return String::from("0");
827            }
828            return alloc::format!("0.{}", "0".repeat(self.scale as usize));
829        }
830        // most-significant limb without leading zeros, the rest zero-padded to 9.
831        let mut digits = String::new();
832        for (idx, &limb) in self.limbs.iter().rev().enumerate() {
833            if idx == 0 {
834                digits.push_str(&limb.to_string());
835            } else {
836                digits.push_str(&alloc::format!("{limb:0width$}", width = BASE_DIGITS));
837            }
838        }
839        let scale = self.scale as usize;
840        let body = if scale == 0 {
841            digits
842        } else {
843            // ensure at least scale+1 digits so the point has an integer side.
844            if digits.len() <= scale {
845                let pad = scale + 1 - digits.len();
846                let padded = alloc::format!("{}{}", "0".repeat(pad), digits);
847                let point = padded.len() - scale;
848                alloc::format!("{}.{}", &padded[..point], &padded[point..])
849            } else {
850                let point = digits.len() - scale;
851                alloc::format!("{}.{}", &digits[..point], &digits[point..])
852            }
853        };
854        if self.neg {
855            alloc::format!("-{body}")
856        } else {
857            body
858        }
859    }
860
861    /// Parse a plain decimal string (`[-]digits[.digits]`, no exponent) into a
862    /// `BigNumeric`. Returns `None` on malformed input.
863    #[must_use]
864    pub fn from_decimal_str(s: &str) -> Option<Self> {
865        let s = s.trim();
866        let (neg, rest) = match s.strip_prefix('-') {
867            Some(r) => (true, r),
868            None => (false, s.strip_prefix('+').unwrap_or(s)),
869        };
870        let (int_part, frac_part) = match rest.split_once('.') {
871            Some((i, f)) => (i, f),
872            None => (rest, ""),
873        };
874        if int_part.is_empty() && frac_part.is_empty() {
875            return None;
876        }
877        if !int_part.bytes().all(|b| b.is_ascii_digit())
878            || !frac_part.bytes().all(|b| b.is_ascii_digit())
879        {
880            return None;
881        }
882        let scale = u16::try_from(frac_part.len()).ok()?;
883        let mut all: String = String::with_capacity(int_part.len() + frac_part.len());
884        all.push_str(int_part);
885        all.push_str(frac_part);
886        // strip leading zeros of the combined digit string (keep at least one).
887        let trimmed = all.trim_start_matches('0');
888        let digits = if trimmed.is_empty() { "0" } else { trimmed };
889        // group into base-10^9 limbs from the least-significant end.
890        let bytes = digits.as_bytes();
891        let mut limbs = Vec::new();
892        let mut i = bytes.len();
893        while i > 0 {
894            let start = i.saturating_sub(BASE_DIGITS);
895            let chunk = core::str::from_utf8(&bytes[start..i]).ok()?;
896            limbs.push(chunk.parse::<u32>().ok()?);
897            i = start;
898        }
899        let mut out = BigNumeric { neg, limbs, scale };
900        out.normalize();
901        Some(out)
902    }
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    // A small deterministic LCG so the fuzz is reproducible without std rand.
910    struct Lcg(u64);
911    impl Lcg {
912        fn next(&mut self) -> u64 {
913            self.0 = self
914                .0
915                .wrapping_mul(6364136223846793005)
916                .wrapping_add(1442695040888963407);
917            self.0
918        }
919        fn i128_small(&mut self) -> i128 {
920            // values with magnitude up to ~1e18 so products/sums stay in i128.
921            let m = (self.next() % 2_000_000_000_000_000_000) as i128;
922            if self.next() & 1 == 0 { m } else { -m }
923        }
924    }
925
926    #[test]
927    fn i128_bridge_round_trips() {
928        for v in [
929            0i128,
930            1,
931            -1,
932            123,
933            -456,
934            i64::MAX as i128,
935            i128::MAX,
936            i128::MIN,
937            10i128.pow(30),
938        ] {
939            assert_eq!(BigNumeric::from_i128(v, 0).to_i128(), Some(v), "v={v}");
940        }
941    }
942
943    #[test]
944    fn decimal_str_round_trips() {
945        for s in [
946            "0",
947            "123",
948            "-123",
949            "1.50",
950            "-0.001",
951            "1000000000",
952            "999999999999999999999999999999",
953        ] {
954            let b = BigNumeric::from_decimal_str(s).unwrap();
955            assert_eq!(b.to_decimal_str(), s, "s={s}");
956        }
957    }
958
959    #[test]
960    fn fuzz_vs_i128() {
961        let mut rng = Lcg(0x1234_5678_9abc_def0);
962        for _ in 0..20_000 {
963            let a = rng.i128_small();
964            let b = rng.i128_small();
965            let ba = BigNumeric::from_i128(a, 0);
966            let bb = BigNumeric::from_i128(b, 0);
967            assert_eq!(ba.add(&bb).to_i128(), Some(a + b), "add {a}+{b}");
968            assert_eq!(ba.sub(&bb).to_i128(), Some(a - b), "sub {a}-{b}");
969            assert_eq!(ba.mul(&bb).to_i128(), Some(a * b), "mul {a}*{b}");
970            assert_eq!(ba.cmp(&bb), a.cmp(&b), "cmp {a} vs {b}");
971        }
972    }
973
974    #[test]
975    fn overflow_stays_big() {
976        // 10^30 * 10^30 = 10^60 overflows i128 → to_i128 None, but decimal exact.
977        let a = BigNumeric::from_i128(10i128.pow(30), 0);
978        let p = a.mul(&a);
979        assert_eq!(p.to_i128(), None);
980        let mut expect = String::from("1");
981        expect.push_str(&"0".repeat(60));
982        assert_eq!(p.to_decimal_str(), expect);
983    }
984
985    #[test]
986    fn scale_align_add() {
987        // 1.5 + 0.25 = 1.75
988        let a = BigNumeric::from_decimal_str("1.5").unwrap();
989        let b = BigNumeric::from_decimal_str("0.25").unwrap();
990        assert_eq!(a.add(&b).to_decimal_str(), "1.75");
991    }
992
993    #[test]
994    fn fuzz_div_int_vs_i128() {
995        let mut rng = Lcg(0xdead_beef_cafe_babe);
996        for _ in 0..20_000 {
997            let a = rng.i128_small();
998            let mut b = rng.i128_small();
999            if b == 0 {
1000                b = 1;
1001            }
1002            let (q, r) = BigNumeric::from_i128(a, 0).div_rem_int(&BigNumeric::from_i128(b, 0));
1003            assert_eq!(q.to_i128(), Some(a / b), "quot {a}/{b}");
1004            assert_eq!(r.to_i128(), Some(a % b), "rem {a}%{b}");
1005        }
1006    }
1007
1008    #[test]
1009    fn div_multi_limb() {
1010        // A quotient that exercises the full Knuth D loop (multi-limb divisor).
1011        let a = BigNumeric::from_decimal_str("123456789012345678901234567890").unwrap();
1012        let b = BigNumeric::from_decimal_str("987654321987654321").unwrap();
1013        let (q, r) = a.div_rem_int(&b);
1014        // reconstruct: q*b + r == a
1015        let recon = q.mul(&b).add(&r);
1016        assert_eq!(recon.to_decimal_str(), a.to_decimal_str());
1017        assert_eq!(b.cmp(&r), core::cmp::Ordering::Greater); // r < b
1018    }
1019
1020    #[test]
1021    fn div_fixed_point() {
1022        let ten = BigNumeric::from_decimal_str("10").unwrap();
1023        let three = BigNumeric::from_decimal_str("3").unwrap();
1024        assert_eq!(ten.div(&three, 4).unwrap().to_decimal_str(), "3.3333");
1025        let one = BigNumeric::from_decimal_str("1").unwrap();
1026        let seven = BigNumeric::from_decimal_str("7").unwrap();
1027        assert_eq!(one.div(&seven, 6).unwrap().to_decimal_str(), "0.142857");
1028        // half-away rounding: 1/8 = 0.125 → scale 2 rounds to 0.13.
1029        let eight = BigNumeric::from_decimal_str("8").unwrap();
1030        assert_eq!(one.div(&eight, 2).unwrap().to_decimal_str(), "0.13");
1031        // division by zero → None.
1032        assert!(
1033            one.div(&BigNumeric::from_decimal_str("0").unwrap(), 4)
1034                .is_none()
1035        );
1036    }
1037
1038    #[test]
1039    fn isqrt_exact_and_floor() {
1040        // Perfect square far beyond i128: (12345678901234567890)^2.
1041        let n = BigNumeric::from_decimal_str("152415787532388367501905199875019052100").unwrap();
1042        assert_eq!(n.isqrt_mag().to_decimal_str(), "12345678901234567890");
1043        // Floor for a non-square: isqrt(10) = 3, isqrt(15) = 3, isqrt(16) = 4.
1044        for (v, want) in [
1045            ("0", "0"),
1046            ("1", "1"),
1047            ("2", "1"),
1048            ("10", "3"),
1049            ("15", "3"),
1050            ("16", "4"),
1051            ("99", "9"),
1052            ("100", "10"),
1053        ] {
1054            let b = BigNumeric::from_decimal_str(v).unwrap();
1055            assert_eq!(b.isqrt_mag().to_decimal_str(), want, "isqrt({v})");
1056        }
1057    }
1058
1059    #[test]
1060    fn sqrt_scale_and_rounding() {
1061        // sqrt(2) at scale 15 rounds to PG's value.
1062        let two = BigNumeric::from_decimal_str("2").unwrap();
1063        assert_eq!(two.sqrt(15).unwrap().to_decimal_str(), "1.414213562373095");
1064        // sqrt(10) rounds down (16th digit 3).
1065        let ten = BigNumeric::from_decimal_str("10").unwrap();
1066        assert_eq!(ten.sqrt(15).unwrap().to_decimal_str(), "3.162277660168379");
1067        // Perfect squares are exact at any scale.
1068        let nine = BigNumeric::from_decimal_str("9").unwrap();
1069        assert_eq!(nine.sqrt(15).unwrap().to_decimal_str(), "3.000000000000000");
1070        // A big perfect square, scale 0.
1071        let big = BigNumeric::from_decimal_str("152415787532388367501905199875019052100").unwrap();
1072        assert_eq!(
1073            big.sqrt(0).unwrap().to_decimal_str(),
1074            "12345678901234567890"
1075        );
1076        // Negative → None (caller raises the domain error); zero is fine.
1077        assert!(
1078            BigNumeric::from_decimal_str("-4")
1079                .unwrap()
1080                .sqrt(2)
1081                .is_none()
1082        );
1083        assert_eq!(
1084            BigNumeric::from_decimal_str("0")
1085                .unwrap()
1086                .sqrt(3)
1087                .unwrap()
1088                .to_decimal_str(),
1089            "0.000"
1090        );
1091    }
1092
1093    #[test]
1094    fn fuzz_isqrt_vs_i128() {
1095        // Deterministic LCG: isqrt of fit-i128 values matches the property
1096        // x^2 <= n < (x+1)^2.
1097        let mut state: u64 = 0x1234_5678_9abc_def0;
1098        let mut next = || {
1099            state = state
1100                .wrapping_mul(6364136223846793005)
1101                .wrapping_add(1442695040888963407);
1102            state
1103        };
1104        for _ in 0..20_000 {
1105            let n = u128::from(next()) | (u128::from(next()) << 64);
1106            let n = n % (1u128 << 100); // keep products in range
1107            let b = BigNumeric::from_i128(n as i128, 0);
1108            let root = b.isqrt_mag();
1109            let rl = root.to_i128().unwrap() as u128;
1110            assert!(rl * rl <= n, "root^2 > n for n={n}");
1111            assert!((rl + 1) * (rl + 1) > n, "(root+1)^2 <= n for n={n}");
1112        }
1113    }
1114}