Skip to main content

bicmath_core/
number.rs

1//! Numeric representations and the promotion/conversion contract.
2//!
3//! Four scalar representations are supported:
4//!
5//! | Kind | Rust type | Exactness |
6//! |------|-----------|-----------|
7//! | Integer | `num_bigint::BigInt` | exact |
8//! | Rational | `num_rational::BigRational` (normalized, positive denominator) | exact |
9//! | Decimal | [`Decimal`] (BigInt mantissa + decimal scale) | exact until a rounding context is applied |
10//! | Float64 | [`Float64`] (finite IEEE-754 binary64) | approximate |
11//!
12//! Promotion rules (also documented in `docs/numerics.md`):
13//!
14//! 1. Float64 is never mixed into exact arithmetic silently. Any operation that
15//!    involves a Float64 requires [`NumericMode::Scientific`]; otherwise the
16//!    engine returns [`ErrorCode::UnsupportedNumericMode`].
17//! 2. Integer op Integer is Integer for `+ - *` and integer powers; division is
18//!    Integer when exact and Rational otherwise.
19//! 3. Rational op Rational is Rational.
20//! 4. Decimal op Decimal/Integer is Decimal and exact for `+ - *`.
21//! 5. Decimal division (and mixed Rational/Decimal operations that cannot be
22//!    represented exactly as decimals) rounds under the declared context and
23//!    reports inexactness. In [`NumericMode::Exact`] an inexact operation is an
24//!    error.
25//! 6. Comparisons are exact across Integer/Rational/Decimal/Float64: every
26//!    value is compared as a real number, with Float64 decomposed to its exact
27//!    binary rational value.
28
29use std::cmp::Ordering;
30use std::fmt;
31use std::str::FromStr;
32
33use num_bigint::{BigInt, Sign};
34use num_integer::Integer;
35use num_rational::BigRational;
36use num_traits::{One, Signed, ToPrimitive, Zero};
37use serde::{Deserialize, Serialize};
38
39use crate::error::{EngineError, ErrorCode};
40use crate::limits::Limits;
41
42/// Declared numeric mode for a calculation.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum NumericMode {
46    /// Only exact operations are permitted. Inexact division, irrational roots,
47    /// and any Float64 involvement are rejected.
48    Exact,
49    /// Exact where mathematically possible; decimal division rounds under the
50    /// declared context and reports inexactness. Float64 is rejected.
51    Auto,
52    /// Float64 arithmetic is permitted and results are classified approximate.
53    Scientific,
54}
55
56impl NumericMode {
57    pub fn as_str(self) -> &'static str {
58        match self {
59            NumericMode::Exact => "exact",
60            NumericMode::Auto => "auto",
61            NumericMode::Scientific => "scientific",
62        }
63    }
64}
65
66impl fmt::Display for NumericMode {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(self.as_str())
69    }
70}
71
72/// Rounding modes for explicit quantization and context rounding.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum RoundingMode {
76    /// Round to nearest; ties to even last digit.
77    HalfEven,
78    /// Round to nearest; ties away from zero.
79    HalfAwayFromZero,
80    /// Truncate toward zero.
81    TowardZero,
82    /// Round toward negative infinity.
83    Floor,
84    /// Round toward positive infinity.
85    Ceiling,
86}
87
88impl RoundingMode {
89    pub fn as_str(self) -> &'static str {
90        match self {
91            RoundingMode::HalfEven => "half_even",
92            RoundingMode::HalfAwayFromZero => "half_away_from_zero",
93            RoundingMode::TowardZero => "toward_zero",
94            RoundingMode::Floor => "floor",
95            RoundingMode::Ceiling => "ceiling",
96        }
97    }
98}
99
100impl fmt::Display for RoundingMode {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(self.as_str())
103    }
104}
105
106/// Round `numerator / denominator` to an integer with the given mode.
107///
108/// `denominator` must be non-zero; the sign of the quotient is derived from both
109/// operands, and rounding is defined on the real value of the fraction.
110pub fn div_round(numerator: &BigInt, denominator: &BigInt, mode: RoundingMode) -> BigInt {
111    debug_assert!(!denominator.is_zero());
112    let (q, r) = numerator.div_rem(denominator);
113    if r.is_zero() {
114        return q;
115    }
116    let negative = (numerator.sign() == Sign::Minus) ^ (denominator.sign() == Sign::Minus);
117    match mode {
118        RoundingMode::TowardZero => q,
119        RoundingMode::Floor => {
120            if negative {
121                q - 1
122            } else {
123                q
124            }
125        }
126        RoundingMode::Ceiling => {
127            if negative {
128                q
129            } else {
130                q + 1
131            }
132        }
133        RoundingMode::HalfAwayFromZero | RoundingMode::HalfEven => {
134            let twice_remainder = r.abs() * 2u32;
135            let divisor = denominator.abs();
136            match twice_remainder.cmp(&divisor) {
137                Ordering::Less => q,
138                Ordering::Greater => {
139                    if negative {
140                        q - 1
141                    } else {
142                        q + 1
143                    }
144                }
145                Ordering::Equal => match mode {
146                    RoundingMode::HalfAwayFromZero => {
147                        if negative {
148                            q - 1
149                        } else {
150                            q + 1
151                        }
152                    }
153                    RoundingMode::HalfEven => {
154                        if (&q % 2u32).is_zero() {
155                            q
156                        } else if negative {
157                            q - 1
158                        } else {
159                            q + 1
160                        }
161                    }
162                    _ => unreachable!("outer match restricts mode"),
163                },
164            }
165        }
166    }
167}
168
169/// 10^n as a `BigInt`.
170fn pow10(n: u64) -> BigInt {
171    debug_assert!(n <= 10_000_000, "pow10 exponent out of supported range");
172    BigInt::from(10u32).pow(n as u32)
173}
174
175/// An exact decimal: `mantissa * 10^-scale`.
176///
177/// The scale is preserved as given so that `0.10` remains distinguishable from
178/// `0.1` for presentation and fingerprinting, while comparisons are numeric.
179#[derive(Clone, Debug)]
180pub struct Decimal {
181    mantissa: BigInt,
182    scale: u32,
183}
184
185impl Decimal {
186    pub fn from_parts(mantissa: BigInt, scale: u32) -> Decimal {
187        Decimal { mantissa, scale }
188    }
189
190    pub fn zero() -> Decimal {
191        Decimal {
192            mantissa: BigInt::zero(),
193            scale: 0,
194        }
195    }
196
197    pub fn from_bigint(value: BigInt) -> Decimal {
198        Decimal {
199            mantissa: value,
200            scale: 0,
201        }
202    }
203
204    pub fn mantissa(&self) -> &BigInt {
205        &self.mantissa
206    }
207
208    pub fn scale(&self) -> u32 {
209        self.scale
210    }
211
212    /// Parse a locale-independent decimal literal.
213    ///
214    /// Grammar: `[+-]? ( digits ('.' digits?)? | '.' digits ) ([eE] [+-]? digits)?`
215    /// No whitespace, no thousands separators, no locale decimal comma.
216    pub fn parse(source: &str, limits: &Limits) -> Result<Decimal, EngineError> {
217        let (mantissa, scale) = parse_decimal_parts(source, limits)?;
218        Ok(Decimal { mantissa, scale })
219    }
220
221    /// Parse using conservative default limits. Intended for tests and
222    /// constants; public entry points must pass explicit limits.
223    pub fn parse_default(source: &str) -> Result<Decimal, EngineError> {
224        Self::parse(source, &Limits::conservative())
225    }
226
227    pub fn is_zero(&self) -> bool {
228        self.mantissa.is_zero()
229    }
230
231    pub fn is_negative(&self) -> bool {
232        self.mantissa.sign() == Sign::Minus
233    }
234
235    pub fn is_positive(&self) -> bool {
236        self.mantissa.sign() == Sign::Plus
237    }
238
239    pub fn sign(&self) -> Sign {
240        self.mantissa.sign()
241    }
242
243    pub fn neg(&self) -> Decimal {
244        Decimal {
245            mantissa: -&self.mantissa,
246            scale: self.scale,
247        }
248    }
249
250    pub fn abs(&self) -> Decimal {
251        Decimal {
252            mantissa: self.mantissa.abs(),
253            scale: self.scale,
254        }
255    }
256
257    /// Strip trailing fractional zeros. `0.10` becomes `0.1`; `100` stays `100`.
258    pub fn normalized(&self) -> Decimal {
259        if self.mantissa.is_zero() {
260            return Decimal {
261                mantissa: BigInt::zero(),
262                scale: 0,
263            };
264        }
265        let mut mantissa = self.mantissa.clone();
266        let mut scale = self.scale;
267        while scale > 0 {
268            let (q, r) = mantissa.div_rem(&pow10(1));
269            if r.is_zero() {
270                mantissa = q;
271                scale -= 1;
272            } else {
273                break;
274            }
275        }
276        Decimal { mantissa, scale }
277    }
278
279    /// Canonical string used for hashing and cross-representation equality:
280    /// normalized, plain decimal notation, never exponent notation.
281    pub fn canonical_string(&self) -> String {
282        self.normalized().to_plain_string()
283    }
284
285    /// Display string that preserves the declared scale (`0.10` prints as `0.10`).
286    pub fn to_plain_string(&self) -> String {
287        let negative = self.mantissa.sign() == Sign::Minus;
288        let digits = self.mantissa.abs().to_string();
289        let scale = self.scale as usize;
290        let mut out = String::new();
291        if negative {
292            out.push('-');
293        }
294        if scale == 0 {
295            out.push_str(&digits);
296        } else if digits.len() > scale {
297            let split = digits.len() - scale;
298            out.push_str(&digits[..split]);
299            out.push('.');
300            out.push_str(&digits[split..]);
301        } else {
302            out.push_str("0.");
303            for _ in 0..(scale - digits.len()) {
304                out.push('0');
305            }
306            out.push_str(&digits);
307        }
308        out
309    }
310
311    pub fn to_bigint_if_integral(&self) -> Option<BigInt> {
312        let divisor = pow10(self.scale as u64);
313        let (q, r) = self.mantissa.div_rem(&divisor);
314        if r.is_zero() { Some(q) } else { None }
315    }
316
317    pub fn to_rational(&self) -> BigRational {
318        BigRational::new(self.mantissa.clone(), pow10(self.scale as u64))
319    }
320
321    /// Approximate binary64 conversion. Returns `None` if the value overflows.
322    pub fn to_f64(&self) -> Option<f64> {
323        let v: f64 = self.to_plain_string().parse().ok()?;
324        if v.is_finite() { Some(v) } else { None }
325    }
326
327    /// Decimal representation of the shortest round-tripping string of `value`.
328    /// This is a *display* conversion, not an exact binary-to-decimal conversion.
329    pub fn from_f64_display(value: f64) -> Option<Decimal> {
330        if !value.is_finite() {
331            return None;
332        }
333        let text = format!("{value}");
334        Decimal::parse(&text, &Limits::conservative()).ok()
335    }
336
337    /// Exact conversion of a finite f64 to a rational (used for comparisons).
338    pub fn from_f64_exact(value: f64) -> Option<BigRational> {
339        float_to_rational(value)
340    }
341
342    /// Compare two decimals numerically, ignoring scale differences.
343    pub fn numeric_cmp(&self, other: &Decimal) -> Ordering {
344        let scale = self.scale.max(other.scale);
345        let lhs = &self.mantissa * pow10((scale - self.scale) as u64);
346        let rhs = &other.mantissa * pow10((scale - other.scale) as u64);
347        lhs.cmp(&rhs)
348    }
349
350    fn check_bits(&self, limits: &Limits, ctx: &NumericContext) -> Result<(), EngineError> {
351        let bits = self.mantissa.bits();
352        let cap = limits.max_integer_bits.min(ctx.max_integer_bits) as u64;
353        if bits > cap {
354            return Err(EngineError::new(
355                ErrorCode::ResourceLimit,
356                format!(
357                    "decimal result needs {bits} bits of mantissa, exceeding the limit of {cap}"
358                ),
359            ));
360        }
361        if self.scale > limits.max_decimal_scale.min(ctx.max_decimal_scale) {
362            return Err(EngineError::new(
363                ErrorCode::PrecisionLimit,
364                format!(
365                    "decimal scale {} exceeds the limit of {}",
366                    self.scale,
367                    limits.max_decimal_scale.min(ctx.max_decimal_scale)
368                ),
369            ));
370        }
371        Ok(())
372    }
373
374    pub fn add(
375        &self,
376        other: &Decimal,
377        ctx: &NumericContext,
378        limits: &Limits,
379    ) -> Result<Decimal, EngineError> {
380        let scale = self.scale.max(other.scale);
381        let lhs = &self.mantissa * pow10((scale - self.scale) as u64);
382        let rhs = &other.mantissa * pow10((scale - other.scale) as u64);
383        let out = Decimal {
384            mantissa: lhs + rhs,
385            scale,
386        };
387        out.check_bits(limits, ctx)?;
388        Ok(out)
389    }
390
391    pub fn sub(
392        &self,
393        other: &Decimal,
394        ctx: &NumericContext,
395        limits: &Limits,
396    ) -> Result<Decimal, EngineError> {
397        self.add(&other.neg(), ctx, limits)
398    }
399
400    pub fn mul(
401        &self,
402        other: &Decimal,
403        ctx: &NumericContext,
404        limits: &Limits,
405    ) -> Result<Decimal, EngineError> {
406        let out = Decimal {
407            mantissa: &self.mantissa * &other.mantissa,
408            scale: self.scale.saturating_add(other.scale),
409        };
410        out.check_bits(limits, ctx)?;
411        Ok(out)
412    }
413
414    /// Divide under the declared context. Returns `(value, inexact)`.
415    ///
416    /// When the quotient terminates it is returned exactly with trailing zeros
417    /// stripped. Otherwise the quotient is rounded to `ctx.precision`
418    /// significant digits and `inexact` is `true`.
419    pub fn div(
420        &self,
421        other: &Decimal,
422        ctx: &NumericContext,
423        limits: &Limits,
424    ) -> Result<(Decimal, bool), EngineError> {
425        if other.is_zero() {
426            return Err(EngineError::division_by_zero("decimal division by zero"));
427        }
428        let precision = ctx.precision.max(1) as u64;
429        let q_scale = precision + other.scale.saturating_sub(self.scale) as u64;
430        let shift = q_scale + other.scale as u64 - self.scale as u64;
431        let numerator = &self.mantissa * pow10(shift);
432        let (q, r) = numerator.div_rem(&other.mantissa);
433        let raw = Decimal {
434            mantissa: q,
435            scale: q_scale as u32,
436        };
437        if r.is_zero() {
438            let value = raw.normalized();
439            value.check_bits(limits, ctx)?;
440            return Ok((value, false));
441        }
442        if ctx.mode == NumericMode::Exact {
443            return Err(EngineError::new(
444                ErrorCode::UnsupportedNumericMode,
445                "division does not terminate in the selected representation; \
446                 use auto or scientific mode, or an explicit rounding context",
447            ));
448        }
449        let rounded = raw.round_significant(ctx.precision.max(1), ctx.rounding);
450        rounded.check_bits(limits, ctx)?;
451        Ok((rounded, true))
452    }
453
454    /// Round to a fixed number of fractional digits.
455    pub fn round_to(&self, scale: u32, mode: RoundingMode) -> Decimal {
456        self.round_to_scale(scale as i64, mode)
457    }
458
459    /// Round to a fixed number of significant digits.
460    pub fn round_significant(&self, digits: u32, mode: RoundingMode) -> Decimal {
461        if self.mantissa.is_zero() {
462            return self.clone();
463        }
464        let nd = self.mantissa.abs().to_string().len() as i64;
465        let target = self.scale as i64 - (nd - digits as i64);
466        self.round_to_scale(target, mode)
467    }
468
469    /// Round to a (possibly negative) decimal scale.
470    pub fn round_to_scale(&self, target_scale: i64, mode: RoundingMode) -> Decimal {
471        if target_scale >= self.scale as i64 {
472            let shift = target_scale as u64 - self.scale as u64;
473            return Decimal {
474                mantissa: &self.mantissa * pow10(shift),
475                scale: target_scale as u32,
476            };
477        }
478        let k = self.scale as u64 - target_scale as u64;
479        let divisor = pow10(k);
480        let q = div_round(&self.mantissa, &divisor, mode);
481        if target_scale >= 0 {
482            Decimal {
483                mantissa: q,
484                scale: target_scale as u32,
485            }
486        } else {
487            Decimal {
488                mantissa: q * pow10((-target_scale) as u64),
489                scale: 0,
490            }
491        }
492    }
493
494    pub fn floor(&self) -> Decimal {
495        self.round_to_scale(0, RoundingMode::Floor)
496    }
497
498    pub fn ceil(&self) -> Decimal {
499        self.round_to_scale(0, RoundingMode::Ceiling)
500    }
501
502    pub fn trunc(&self) -> Decimal {
503        self.round_to_scale(0, RoundingMode::TowardZero)
504    }
505
506    /// Exact square root when the decimal has an exact decimal square root.
507    pub fn sqrt_exact(&self) -> Option<Decimal> {
508        if self.mantissa.sign() == Sign::Minus {
509            return None;
510        }
511        if self.mantissa.is_zero() {
512            return Some(Decimal::zero());
513        }
514        let (mantissa, scale) = if self.scale.is_multiple_of(2) {
515            (self.mantissa.clone(), self.scale)
516        } else {
517            (&self.mantissa * 10u32, self.scale + 1)
518        };
519        let root = mantissa.sqrt();
520        if &root * &root == mantissa {
521            Some(Decimal {
522                mantissa: root,
523                scale: scale / 2,
524            })
525        } else {
526            None
527        }
528    }
529
530    /// Integer power. Negative exponents are handled by the caller (division).
531    pub fn pow_u32(
532        &self,
533        exponent: u32,
534        ctx: &NumericContext,
535        limits: &Limits,
536    ) -> Result<Decimal, EngineError> {
537        let predicted = self.mantissa.bits().saturating_mul(exponent as u64);
538        let cap = limits.max_integer_bits.min(ctx.max_integer_bits) as u64;
539        if predicted > cap {
540            return Err(EngineError::new(
541                ErrorCode::ResourceLimit,
542                format!("power would need about {predicted} bits, exceeding the limit of {cap}"),
543            ));
544        }
545        let out = Decimal {
546            mantissa: self.mantissa.pow(exponent),
547            scale: self.scale.saturating_mul(exponent),
548        };
549        out.check_bits(limits, ctx)?;
550        Ok(out)
551    }
552
553    /// Exact conversion from a rational, or a rounded conversion when allowed.
554    pub fn from_rational(
555        value: &BigRational,
556        ctx: &NumericContext,
557        limits: &Limits,
558    ) -> Result<(Decimal, bool), EngineError> {
559        let numer = value.numer();
560        let denom = value.denom();
561        // Determine whether the rational terminates in base 10.
562        let mut d = denom.clone();
563        let two = BigInt::from(2u32);
564        let five = BigInt::from(5u32);
565        let mut twos = 0u32;
566        let mut fives = 0u32;
567        while d.is_even() {
568            d /= &two;
569            twos += 1;
570        }
571        while (&d % &five).is_zero() {
572            d /= &five;
573            fives += 1;
574        }
575        if d.is_one() {
576            let scale = twos.max(fives);
577            let mantissa = numer * pow10(scale as u64) / denom;
578            let out = Decimal { mantissa, scale };
579            out.check_bits(limits, ctx)?;
580            return Ok((out, false));
581        }
582        if ctx.mode == NumericMode::Exact {
583            return Err(EngineError::new(
584                ErrorCode::UnsupportedNumericMode,
585                "rational value has a non-terminating decimal expansion; \
586                 use auto or scientific mode, or request a rational result",
587            ));
588        }
589        let precision = ctx.precision.max(1) as u64;
590        // Compute enough digits, then round to significant precision.
591        let shift = precision + 4;
592        let scaled = numer * pow10(shift);
593        let (q, _r) = scaled.div_rem(denom);
594        let raw = Decimal {
595            mantissa: q,
596            scale: shift as u32,
597        };
598        let rounded = raw.round_significant(ctx.precision.max(1), ctx.rounding);
599        rounded.check_bits(limits, ctx)?;
600        Ok((rounded, true))
601    }
602}
603
604impl PartialEq for Decimal {
605    fn eq(&self, other: &Self) -> bool {
606        self.numeric_cmp(other) == Ordering::Equal
607    }
608}
609
610impl Eq for Decimal {}
611
612impl PartialOrd for Decimal {
613    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
614        Some(self.cmp(other))
615    }
616}
617
618impl Ord for Decimal {
619    fn cmp(&self, other: &Self) -> Ordering {
620        self.numeric_cmp(other)
621    }
622}
623
624impl fmt::Display for Decimal {
625    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626        f.write_str(&self.to_plain_string())
627    }
628}
629
630/// Exact decomposition of a finite f64 into a rational number.
631fn float_to_rational(value: f64) -> Option<BigRational> {
632    if !value.is_finite() {
633        return None;
634    }
635    if value == 0.0 {
636        return Some(BigRational::zero());
637    }
638    let bits = value.to_bits();
639    let negative = (bits >> 63) & 1 == 1;
640    let exponent_bits = ((bits >> 52) & 0x7ff) as i32;
641    let fraction = bits & 0x000f_ffff_ffff_ffff;
642    let (mantissa, exponent) = if exponent_bits == 0 {
643        (fraction, -1074i32)
644    } else {
645        (fraction | (1u64 << 52), exponent_bits - 1075)
646    };
647    let mut numerator = BigInt::from(mantissa);
648    if negative {
649        numerator = -numerator;
650    }
651    let (numer, denom) = if exponent >= 0 {
652        (numerator << exponent as u32, BigInt::one())
653    } else {
654        (numerator, BigInt::one() << (-exponent) as u32)
655    };
656    Some(BigRational::new(numer, denom))
657}
658
659/// Finite IEEE-754 binary64 wrapper. Non-finite values cannot be constructed
660/// through the public API.
661#[derive(Clone, Copy, Debug)]
662pub struct Float64(f64);
663
664impl Float64 {
665    pub fn new(value: f64) -> Result<Float64, EngineError> {
666        if value.is_finite() {
667            Ok(Float64(value))
668        } else {
669            Err(EngineError::new(
670                ErrorCode::DomainViolation,
671                format!("non-finite float64 value {value} is not a valid public result"),
672            ))
673        }
674    }
675
676    pub fn get(self) -> f64 {
677        self.0
678    }
679
680    /// Canonicalize negative zero to positive zero for hashing/equality.
681    pub fn canonical(self) -> Float64 {
682        if self.0 == 0.0 { Float64(0.0) } else { self }
683    }
684
685    pub fn to_rational_exact(self) -> Option<BigRational> {
686        float_to_rational(self.0)
687    }
688
689    pub fn parse(source: &str) -> Result<Float64, EngineError> {
690        let trimmed = source.trim();
691        if trimmed.is_empty() {
692            return Err(EngineError::malformed("empty float64 literal"));
693        }
694        let value: f64 = trimmed
695            .parse()
696            .map_err(|_| EngineError::malformed(format!("invalid float64 literal {source:?}")))?;
697        Float64::new(value)
698    }
699}
700
701impl PartialEq for Float64 {
702    fn eq(&self, other: &Self) -> bool {
703        self.0 == other.0
704    }
705}
706
707impl Eq for Float64 {}
708
709impl Ord for Float64 {
710    fn cmp(&self, other: &Self) -> Ordering {
711        self.0.total_cmp(&other.0)
712    }
713}
714
715impl PartialOrd for Float64 {
716    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
717        Some(self.cmp(other))
718    }
719}
720
721impl fmt::Display for Float64 {
722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723        // Rust's default f64 formatting is shortest-round-trip.
724        write!(f, "{}", self.0)
725    }
726}
727
728/// The recursive scalar number type.
729#[derive(Clone, Debug, PartialEq, Eq)]
730pub enum Number {
731    Integer(BigInt),
732    Rational(BigRational),
733    Decimal(Decimal),
734    Float64(Float64),
735}
736
737/// Result of an arithmetic operation plus an explicit rounding flag.
738#[derive(Clone, Debug)]
739pub struct NumberResult {
740    pub value: Number,
741    pub rounded: bool,
742}
743
744impl NumberResult {
745    pub fn exact(value: Number) -> NumberResult {
746        NumberResult {
747            value,
748            rounded: false,
749        }
750    }
751
752    pub fn rounded(value: Number) -> NumberResult {
753        NumberResult {
754            value,
755            rounded: true,
756        }
757    }
758}
759
760impl Number {
761    pub fn integer<T: Into<BigInt>>(value: T) -> Number {
762        Number::Integer(value.into())
763    }
764
765    pub fn decimal(value: Decimal) -> Number {
766        Number::Decimal(value)
767    }
768
769    pub fn rational(numer: BigInt, denom: BigInt) -> Result<Number, EngineError> {
770        if denom.is_zero() {
771            return Err(EngineError::division_by_zero(
772                "rational with zero denominator",
773            ));
774        }
775        Ok(Number::Rational(BigRational::new(numer, denom)))
776    }
777
778    pub fn float(value: f64) -> Result<Number, EngineError> {
779        Ok(Number::Float64(Float64::new(value)?))
780    }
781
782    pub fn is_zero(&self) -> bool {
783        match self {
784            Number::Integer(v) => v.is_zero(),
785            Number::Rational(v) => v.is_zero(),
786            Number::Decimal(v) => v.is_zero(),
787            Number::Float64(v) => v.0 == 0.0,
788        }
789    }
790
791    pub fn is_negative(&self) -> bool {
792        match self {
793            Number::Integer(v) => v.sign() == Sign::Minus,
794            Number::Rational(v) => v.is_negative(),
795            Number::Decimal(v) => v.is_negative(),
796            Number::Float64(v) => v.0 < 0.0,
797        }
798    }
799
800    pub fn is_float(&self) -> bool {
801        matches!(self, Number::Float64(_))
802    }
803
804    pub fn kind_name(&self) -> &'static str {
805        match self {
806            Number::Integer(_) => "integer",
807            Number::Rational(_) => "rational",
808            Number::Decimal(_) => "decimal",
809            Number::Float64(_) => "float64",
810        }
811    }
812
813    /// Convert to an exact rational, failing for Float64 (use
814    /// [`Number::to_exact_rational`] to include floats via their exact bits).
815    pub fn as_exact_rational(&self) -> Option<BigRational> {
816        match self {
817            Number::Integer(v) => Some(BigRational::from_integer(v.clone())),
818            Number::Rational(v) => Some(v.clone()),
819            Number::Decimal(v) => Some(v.to_rational()),
820            Number::Float64(_) => None,
821        }
822    }
823
824    /// Convert to an exact rational, including Float64 via bit decomposition.
825    pub fn to_exact_rational(&self) -> Option<BigRational> {
826        match self {
827            Number::Float64(v) => v.to_rational_exact(),
828            other => other.as_exact_rational(),
829        }
830    }
831
832    pub fn to_f64(&self) -> Option<f64> {
833        match self {
834            Number::Integer(v) => v.to_f64(),
835            Number::Rational(v) => v.to_f64(),
836            Number::Decimal(v) => v.to_f64(),
837            Number::Float64(v) => Some(v.0),
838        }
839    }
840
841    pub fn neg(&self) -> Number {
842        match self {
843            Number::Integer(v) => Number::Integer(-v),
844            Number::Rational(v) => Number::Rational(-v),
845            Number::Decimal(v) => Number::Decimal(v.neg()),
846            Number::Float64(v) => Number::Float64(Float64(-v.0)),
847        }
848    }
849
850    pub fn abs(&self) -> Number {
851        match self {
852            Number::Integer(v) => Number::Integer(v.abs()),
853            Number::Rational(v) => Number::Rational(v.abs()),
854            Number::Decimal(v) => Number::Decimal(v.abs()),
855            Number::Float64(v) => Number::Float64(Float64(v.0.abs())),
856        }
857    }
858
859    /// Exact comparison across all representations.
860    pub fn compare(&self, other: &Number) -> Result<Ordering, EngineError> {
861        if let (Number::Float64(a), Number::Float64(b)) = (self, other) {
862            return Ok(a.get().total_cmp(&b.get()));
863        }
864        let lhs = self
865            .to_exact_rational()
866            .ok_or_else(|| EngineError::internal("unrepresentable comparison operand"))?;
867        let rhs = other
868            .to_exact_rational()
869            .ok_or_else(|| EngineError::internal("unrepresentable comparison operand"))?;
870        Ok(lhs.cmp(&rhs))
871    }
872
873    /// Numeric equality across representations (exact semantics).
874    pub fn numeric_eq(&self, other: &Number) -> Result<bool, EngineError> {
875        Ok(self.compare(other)? == Ordering::Equal)
876    }
877
878    fn require_float_allowed(
879        &self,
880        other: &Number,
881        ctx: &NumericContext,
882    ) -> Result<(), EngineError> {
883        if (self.is_float() || other.is_float()) && ctx.mode != NumericMode::Scientific {
884            return Err(EngineError::new(
885                ErrorCode::UnsupportedNumericMode,
886                format!(
887                    "float64 arithmetic requires scientific mode; current mode is {}",
888                    ctx.mode
889                ),
890            ));
891        }
892        Ok(())
893    }
894
895    pub fn add(
896        &self,
897        other: &Number,
898        ctx: &NumericContext,
899        limits: &Limits,
900    ) -> Result<NumberResult, EngineError> {
901        self.require_float_allowed(other, ctx)?;
902        if self.is_float() || other.is_float() {
903            let a = self.to_f64().unwrap_or(f64::NAN);
904            let b = other.to_f64().unwrap_or(f64::NAN);
905            return Ok(NumberResult::rounded(Number::float(a + b)?));
906        }
907        match (self, other) {
908            (Number::Integer(a), Number::Integer(b)) => {
909                let out = a + b;
910                check_bigint(&out, ctx, limits)?;
911                Ok(NumberResult::exact(Number::Integer(out)))
912            }
913            (Number::Rational(a), Number::Rational(b)) => {
914                let out = a + b;
915                check_rational(&out, ctx, limits)?;
916                Ok(NumberResult::exact(Number::Rational(out)))
917            }
918            (Number::Rational(a), Number::Integer(b)) => {
919                let out = a + BigRational::from_integer(b.clone());
920                check_rational(&out, ctx, limits)?;
921                Ok(NumberResult::exact(Number::Rational(out)))
922            }
923            (Number::Integer(a), Number::Rational(b)) => {
924                let out = BigRational::from_integer(a.clone()) + b;
925                check_rational(&out, ctx, limits)?;
926                Ok(NumberResult::exact(Number::Rational(out)))
927            }
928            (Number::Decimal(a), Number::Decimal(b)) => {
929                Ok(NumberResult::exact(Number::Decimal(a.add(b, ctx, limits)?)))
930            }
931            (Number::Decimal(a), Number::Integer(b)) => {
932                let b = Decimal::from_bigint(b.clone());
933                Ok(NumberResult::exact(Number::Decimal(
934                    a.add(&b, ctx, limits)?,
935                )))
936            }
937            (Number::Integer(a), Number::Decimal(b)) => {
938                let a = Decimal::from_bigint(a.clone());
939                Ok(NumberResult::exact(Number::Decimal(a.add(b, ctx, limits)?)))
940            }
941            (Number::Decimal(a), Number::Rational(b)) => {
942                let (b, inexact) = Decimal::from_rational(b, ctx, limits)?;
943                Ok(NumberResult {
944                    value: Number::Decimal(a.add(&b, ctx, limits)?),
945                    rounded: inexact,
946                })
947            }
948            (Number::Rational(a), Number::Decimal(b)) => {
949                let (a, inexact) = Decimal::from_rational(a, ctx, limits)?;
950                Ok(NumberResult {
951                    value: Number::Decimal(a.add(b, ctx, limits)?),
952                    rounded: inexact,
953                })
954            }
955            (Number::Float64(_), _) | (_, Number::Float64(_)) => unreachable!("handled above"),
956        }
957    }
958
959    pub fn sub(
960        &self,
961        other: &Number,
962        ctx: &NumericContext,
963        limits: &Limits,
964    ) -> Result<NumberResult, EngineError> {
965        self.add(&other.neg(), ctx, limits)
966    }
967
968    pub fn mul(
969        &self,
970        other: &Number,
971        ctx: &NumericContext,
972        limits: &Limits,
973    ) -> Result<NumberResult, EngineError> {
974        self.require_float_allowed(other, ctx)?;
975        if self.is_float() || other.is_float() {
976            let a = self.to_f64().unwrap_or(f64::NAN);
977            let b = other.to_f64().unwrap_or(f64::NAN);
978            return Ok(NumberResult::rounded(Number::float(a * b)?));
979        }
980        match (self, other) {
981            (Number::Integer(a), Number::Integer(b)) => {
982                let out = a * b;
983                check_bigint(&out, ctx, limits)?;
984                Ok(NumberResult::exact(Number::Integer(out)))
985            }
986            (Number::Rational(a), Number::Rational(b)) => {
987                let out = a * b;
988                check_rational(&out, ctx, limits)?;
989                Ok(NumberResult::exact(Number::Rational(out)))
990            }
991            (Number::Rational(a), Number::Integer(b)) => {
992                let out = a * BigRational::from_integer(b.clone());
993                check_rational(&out, ctx, limits)?;
994                Ok(NumberResult::exact(Number::Rational(out)))
995            }
996            (Number::Integer(a), Number::Rational(b)) => {
997                let out = BigRational::from_integer(a.clone()) * b;
998                check_rational(&out, ctx, limits)?;
999                Ok(NumberResult::exact(Number::Rational(out)))
1000            }
1001            (Number::Decimal(a), Number::Decimal(b)) => {
1002                Ok(NumberResult::exact(Number::Decimal(a.mul(b, ctx, limits)?)))
1003            }
1004            (Number::Decimal(a), Number::Integer(b)) => {
1005                let b = Decimal::from_bigint(b.clone());
1006                Ok(NumberResult::exact(Number::Decimal(
1007                    a.mul(&b, ctx, limits)?,
1008                )))
1009            }
1010            (Number::Integer(a), Number::Decimal(b)) => {
1011                let a = Decimal::from_bigint(a.clone());
1012                Ok(NumberResult::exact(Number::Decimal(a.mul(b, ctx, limits)?)))
1013            }
1014            (Number::Decimal(a), Number::Rational(b)) => {
1015                let (b, inexact) = Decimal::from_rational(b, ctx, limits)?;
1016                Ok(NumberResult {
1017                    value: Number::Decimal(a.mul(&b, ctx, limits)?),
1018                    rounded: inexact,
1019                })
1020            }
1021            (Number::Rational(a), Number::Decimal(b)) => {
1022                let (a, inexact) = Decimal::from_rational(a, ctx, limits)?;
1023                Ok(NumberResult {
1024                    value: Number::Decimal(a.mul(b, ctx, limits)?),
1025                    rounded: inexact,
1026                })
1027            }
1028            (Number::Float64(_), _) | (_, Number::Float64(_)) => unreachable!("handled above"),
1029        }
1030    }
1031
1032    pub fn div(
1033        &self,
1034        other: &Number,
1035        ctx: &NumericContext,
1036        limits: &Limits,
1037    ) -> Result<NumberResult, EngineError> {
1038        self.require_float_allowed(other, ctx)?;
1039        if other.is_zero() {
1040            return Err(EngineError::division_by_zero(format!(
1041                "division by zero ({} / {})",
1042                self.kind_name(),
1043                other.kind_name()
1044            )));
1045        }
1046        if self.is_float() || other.is_float() {
1047            let a = self.to_f64().unwrap_or(f64::NAN);
1048            let b = other.to_f64().unwrap_or(f64::NAN);
1049            return Ok(NumberResult::rounded(Number::float(a / b)?));
1050        }
1051        match (self, other) {
1052            (Number::Integer(a), Number::Integer(b)) => {
1053                let (q, r) = a.div_rem(b);
1054                if r.is_zero() {
1055                    check_bigint(&q, ctx, limits)?;
1056                    Ok(NumberResult::exact(Number::Integer(q)))
1057                } else {
1058                    let out = BigRational::new(a.clone(), b.clone());
1059                    Ok(NumberResult::exact(Number::Rational(out)))
1060                }
1061            }
1062            (Number::Rational(a), Number::Rational(b)) => {
1063                let out = a / b;
1064                check_rational(&out, ctx, limits)?;
1065                Ok(NumberResult::exact(Number::Rational(out)))
1066            }
1067            (Number::Rational(a), Number::Integer(b)) => {
1068                let out = a / BigRational::from_integer(b.clone());
1069                check_rational(&out, ctx, limits)?;
1070                Ok(NumberResult::exact(Number::Rational(out)))
1071            }
1072            (Number::Integer(a), Number::Rational(b)) => {
1073                let out = BigRational::from_integer(a.clone()) / b;
1074                check_rational(&out, ctx, limits)?;
1075                Ok(NumberResult::exact(Number::Rational(out)))
1076            }
1077            (Number::Decimal(a), Number::Decimal(b)) => {
1078                let (value, inexact) = a.div(b, ctx, limits)?;
1079                Ok(NumberResult {
1080                    value: Number::Decimal(value),
1081                    rounded: inexact,
1082                })
1083            }
1084            (Number::Decimal(a), Number::Integer(b)) => {
1085                let b = Decimal::from_bigint(b.clone());
1086                let (value, inexact) = a.div(&b, ctx, limits)?;
1087                Ok(NumberResult {
1088                    value: Number::Decimal(value),
1089                    rounded: inexact,
1090                })
1091            }
1092            (Number::Integer(a), Number::Decimal(b)) => {
1093                let a = Decimal::from_bigint(a.clone());
1094                let (value, inexact) = a.div(b, ctx, limits)?;
1095                Ok(NumberResult {
1096                    value: Number::Decimal(value),
1097                    rounded: inexact,
1098                })
1099            }
1100            (Number::Decimal(a), Number::Rational(b)) => {
1101                let (b, b_inexact) = Decimal::from_rational(b, ctx, limits)?;
1102                let (value, d_inexact) = a.div(&b, ctx, limits)?;
1103                Ok(NumberResult {
1104                    value: Number::Decimal(value),
1105                    rounded: b_inexact || d_inexact,
1106                })
1107            }
1108            (Number::Rational(a), Number::Decimal(b)) => {
1109                let (a, a_inexact) = Decimal::from_rational(a, ctx, limits)?;
1110                let (value, d_inexact) = a.div(b, ctx, limits)?;
1111                Ok(NumberResult {
1112                    value: Number::Decimal(value),
1113                    rounded: a_inexact || d_inexact,
1114                })
1115            }
1116            (Number::Float64(_), _) | (_, Number::Float64(_)) => unreachable!("handled above"),
1117        }
1118    }
1119
1120    /// Integer power. A non-integer exponent requires scientific mode.
1121    pub fn pow(
1122        &self,
1123        exponent: &Number,
1124        ctx: &NumericContext,
1125        limits: &Limits,
1126    ) -> Result<NumberResult, EngineError> {
1127        if self.is_float() || exponent.is_float() {
1128            if ctx.mode != NumericMode::Scientific {
1129                return Err(EngineError::new(
1130                    ErrorCode::UnsupportedNumericMode,
1131                    "exponentiation involving float64 requires scientific mode",
1132                ));
1133            }
1134            let a = self.to_f64().unwrap_or(f64::NAN);
1135            let b = exponent.to_f64().unwrap_or(f64::NAN);
1136            return Ok(NumberResult::rounded(Number::float(a.powf(b))?));
1137        }
1138        let exp_int = match exponent {
1139            Number::Integer(v) => v.clone(),
1140            Number::Rational(v) if v.is_integer() => v.to_integer(),
1141            Number::Decimal(v) => match v.to_bigint_if_integral() {
1142                Some(i) => i,
1143                None => {
1144                    if ctx.mode != NumericMode::Scientific {
1145                        return Err(EngineError::new(
1146                            ErrorCode::UnsupportedNumericMode,
1147                            "non-integer exponent requires scientific mode",
1148                        ));
1149                    }
1150                    let a = self.to_f64().unwrap_or(f64::NAN);
1151                    let b = exponent.to_f64().unwrap_or(f64::NAN);
1152                    return Ok(NumberResult::rounded(Number::float(a.powf(b))?));
1153                }
1154            },
1155            Number::Rational(_) => {
1156                if ctx.mode != NumericMode::Scientific {
1157                    return Err(EngineError::new(
1158                        ErrorCode::UnsupportedNumericMode,
1159                        "non-integer exponent requires scientific mode",
1160                    ));
1161                }
1162                let a = self.to_f64().unwrap_or(f64::NAN);
1163                let b = exponent.to_f64().unwrap_or(f64::NAN);
1164                return Ok(NumberResult::rounded(Number::float(a.powf(b))?));
1165            }
1166            Number::Float64(_) => unreachable!("handled above"),
1167        };
1168        let exp_u32 = match exp_int.to_u32() {
1169            Some(v) if v <= limits.max_exponent => v,
1170            _ => {
1171                return Err(EngineError::new(
1172                    ErrorCode::ResourceLimit,
1173                    format!(
1174                        "exponent {} exceeds the supported range 0..={}",
1175                        exp_int, limits.max_exponent
1176                    ),
1177                ));
1178            }
1179        };
1180        let negative_exponent = exp_int.sign() == Sign::Minus;
1181        match self {
1182            Number::Integer(base) => {
1183                if negative_exponent {
1184                    let power = base.pow(exp_u32);
1185                    if power.is_zero() {
1186                        return Err(EngineError::division_by_zero("zero to a negative power"));
1187                    }
1188                    let out = BigRational::new(BigInt::one(), power);
1189                    check_rational(&out, ctx, limits)?;
1190                    Ok(NumberResult::exact(Number::Rational(out)))
1191                } else {
1192                    let predicted = base.bits().saturating_mul(exp_u32 as u64);
1193                    let cap = limits.max_integer_bits.min(ctx.max_integer_bits) as u64;
1194                    if predicted > cap {
1195                        return Err(EngineError::new(
1196                            ErrorCode::ResourceLimit,
1197                            format!(
1198                                "integer power would need about {predicted} bits, exceeding the limit of {cap}"
1199                            ),
1200                        ));
1201                    }
1202                    let out = base.pow(exp_u32);
1203                    check_bigint(&out, ctx, limits)?;
1204                    Ok(NumberResult::exact(Number::Integer(out)))
1205                }
1206            }
1207            Number::Rational(base) => {
1208                if negative_exponent {
1209                    let flipped = BigRational::new(base.denom().clone(), base.numer().clone());
1210                    let out = flipped.pow(exp_u32 as i32);
1211                    check_rational(&out, ctx, limits)?;
1212                    Ok(NumberResult::exact(Number::Rational(out)))
1213                } else {
1214                    let out = base.pow(exp_u32 as i32);
1215                    check_rational(&out, ctx, limits)?;
1216                    Ok(NumberResult::exact(Number::Rational(out)))
1217                }
1218            }
1219            Number::Decimal(base) => {
1220                if negative_exponent {
1221                    let positive = base.pow_u32(exp_u32, ctx, limits)?;
1222                    let one = Decimal::from_bigint(BigInt::one());
1223                    let (value, inexact) = one.div(&positive, ctx, limits)?;
1224                    Ok(NumberResult {
1225                        value: Number::Decimal(value),
1226                        rounded: inexact,
1227                    })
1228                } else {
1229                    Ok(NumberResult::exact(Number::Decimal(
1230                        base.pow_u32(exp_u32, ctx, limits)?,
1231                    )))
1232                }
1233            }
1234            Number::Float64(_) => unreachable!("handled above"),
1235        }
1236    }
1237
1238    /// Truncated remainder: sign follows the dividend (like Rust's `%`).
1239    pub fn rem(
1240        &self,
1241        other: &Number,
1242        ctx: &NumericContext,
1243        limits: &Limits,
1244    ) -> Result<NumberResult, EngineError> {
1245        if other.is_zero() {
1246            return Err(EngineError::division_by_zero("remainder by zero"));
1247        }
1248        if self.is_float() || other.is_float() {
1249            if ctx.mode != NumericMode::Scientific {
1250                return Err(EngineError::new(
1251                    ErrorCode::UnsupportedNumericMode,
1252                    "float64 remainder requires scientific mode",
1253                ));
1254            }
1255            let a = self.to_f64().unwrap_or(f64::NAN);
1256            let b = other.to_f64().unwrap_or(f64::NAN);
1257            return Ok(NumberResult::rounded(Number::float(a % b)?));
1258        }
1259        let a = self
1260            .to_exact_rational()
1261            .ok_or_else(|| EngineError::internal("remainder operand not representable"))?;
1262        let b = other
1263            .to_exact_rational()
1264            .ok_or_else(|| EngineError::internal("remainder operand not representable"))?;
1265        let q = &a / &b;
1266        let truncated = q.trunc();
1267        let r = a - truncated * b;
1268        rational_result(r, self, other, ctx, limits)
1269    }
1270
1271    /// Euclidean modulo: result is non-negative for a positive modulus.
1272    pub fn modulo(
1273        &self,
1274        other: &Number,
1275        ctx: &NumericContext,
1276        limits: &Limits,
1277    ) -> Result<NumberResult, EngineError> {
1278        if other.is_zero() {
1279            return Err(EngineError::division_by_zero("modulo by zero"));
1280        }
1281        if self.is_float() || other.is_float() {
1282            if ctx.mode != NumericMode::Scientific {
1283                return Err(EngineError::new(
1284                    ErrorCode::UnsupportedNumericMode,
1285                    "float64 modulo requires scientific mode",
1286                ));
1287            }
1288            let a = self.to_f64().unwrap_or(f64::NAN);
1289            let b = other.to_f64().unwrap_or(f64::NAN);
1290            let mut r = a % b;
1291            if r != 0.0 && (r < 0.0) != (b < 0.0) {
1292                r += b;
1293            }
1294            return Ok(NumberResult::rounded(Number::float(r)?));
1295        }
1296        let a = self
1297            .to_exact_rational()
1298            .ok_or_else(|| EngineError::internal("modulo operand not representable"))?;
1299        let b = other
1300            .to_exact_rational()
1301            .ok_or_else(|| EngineError::internal("modulo operand not representable"))?;
1302        let q = &a / &b;
1303        let floored = q.floor();
1304        let r = a - floored * b;
1305        rational_result(r, self, other, ctx, limits)
1306    }
1307
1308    pub fn round_to_scale(&self, scale: i64, mode: RoundingMode) -> Result<Number, EngineError> {
1309        match self {
1310            Number::Integer(v) => {
1311                if scale >= 0 {
1312                    Ok(Number::Decimal(
1313                        Decimal::from_bigint(v.clone()).round_to(scale as u32, mode),
1314                    ))
1315                } else {
1316                    let k = pow10((-scale) as u64);
1317                    Ok(Number::Integer(div_round(v, &k, mode) * k))
1318                }
1319            }
1320            Number::Decimal(v) => Ok(Number::Decimal(v.round_to_scale(scale, mode))),
1321            Number::Rational(v) => {
1322                let scale = scale.clamp(-1_000_000, 1_000_000);
1323                if scale >= 0 {
1324                    let factor = pow10(scale as u64);
1325                    let scaled = v * BigRational::from_integer(factor);
1326                    let rounded = div_round(scaled.numer(), scaled.denom(), mode);
1327                    Ok(Number::Decimal(Decimal::from_parts(rounded, scale as u32)))
1328                } else {
1329                    let factor = pow10((-scale) as u64);
1330                    let scaled = v / BigRational::from_integer(factor.clone());
1331                    let rounded = div_round(scaled.numer(), scaled.denom(), mode);
1332                    Ok(Number::Integer(rounded * factor))
1333                }
1334            }
1335            Number::Float64(v) => {
1336                let factor = 10f64.powi(scale.clamp(-308, 308) as i32);
1337                let rounded = match mode {
1338                    RoundingMode::HalfEven => (v.0 * factor).round_ties_even() / factor,
1339                    RoundingMode::HalfAwayFromZero => (v.0 * factor).round() / factor,
1340                    RoundingMode::TowardZero => (v.0 * factor).trunc() / factor,
1341                    RoundingMode::Floor => (v.0 * factor).floor() / factor,
1342                    RoundingMode::Ceiling => (v.0 * factor).ceil() / factor,
1343                };
1344                Ok(Number::Float64(Float64::new(rounded)?))
1345            }
1346        }
1347    }
1348}
1349
1350fn rational_result(
1351    value: BigRational,
1352    lhs: &Number,
1353    rhs: &Number,
1354    ctx: &NumericContext,
1355    limits: &Limits,
1356) -> Result<NumberResult, EngineError> {
1357    match (lhs, rhs) {
1358        (Number::Integer(_), Number::Integer(_)) => {
1359            if value.is_integer() {
1360                Ok(NumberResult::exact(Number::Integer(value.to_integer())))
1361            } else {
1362                check_rational(&value, ctx, limits)?;
1363                Ok(NumberResult::exact(Number::Rational(value)))
1364            }
1365        }
1366        (Number::Decimal(_), _) | (_, Number::Decimal(_)) => {
1367            let (decimal, inexact) = Decimal::from_rational(&value, ctx, limits)?;
1368            Ok(NumberResult {
1369                value: Number::Decimal(decimal),
1370                rounded: inexact,
1371            })
1372        }
1373        _ => {
1374            check_rational(&value, ctx, limits)?;
1375            Ok(NumberResult::exact(Number::Rational(value)))
1376        }
1377    }
1378}
1379
1380fn check_bigint(value: &BigInt, ctx: &NumericContext, limits: &Limits) -> Result<(), EngineError> {
1381    let cap = limits.max_integer_bits.min(ctx.max_integer_bits) as u64;
1382    if value.bits() > cap {
1383        return Err(EngineError::new(
1384            ErrorCode::ResourceLimit,
1385            format!(
1386                "integer result needs {} bits, exceeding the limit of {cap}",
1387                value.bits()
1388            ),
1389        ));
1390    }
1391    Ok(())
1392}
1393
1394fn check_rational(
1395    value: &BigRational,
1396    ctx: &NumericContext,
1397    limits: &Limits,
1398) -> Result<(), EngineError> {
1399    let cap = limits.max_integer_bits.min(ctx.max_integer_bits) as u64;
1400    if value.numer().bits() > cap || value.denom().bits() > cap {
1401        return Err(EngineError::new(
1402            ErrorCode::ResourceLimit,
1403            format!(
1404                "rational result needs {}/{} bits, exceeding the limit of {cap}",
1405                value.numer().bits(),
1406                value.denom().bits()
1407            ),
1408        ));
1409    }
1410    Ok(())
1411}
1412
1413/// Parse a decimal literal into `(mantissa, scale)`.
1414fn parse_decimal_parts(source: &str, limits: &Limits) -> Result<(BigInt, u32), EngineError> {
1415    let bytes = source.as_bytes();
1416    if bytes.is_empty() {
1417        return Err(EngineError::malformed("empty numeric literal"));
1418    }
1419    let mut i = 0usize;
1420    let mut negative = false;
1421    match bytes[i] {
1422        b'+' => i += 1,
1423        b'-' => {
1424            negative = true;
1425            i += 1;
1426        }
1427        _ => {}
1428    }
1429    let mut digits = String::new();
1430    let mut frac_digits: i64 = 0;
1431    let mut seen_digit = false;
1432    let mut seen_point = false;
1433    while i < bytes.len() {
1434        match bytes[i] {
1435            b'0'..=b'9' => {
1436                digits.push(bytes[i] as char);
1437                if seen_point {
1438                    frac_digits += 1;
1439                }
1440                seen_digit = true;
1441                i += 1;
1442            }
1443            b'.' => {
1444                if seen_point {
1445                    return Err(EngineError::malformed(format!(
1446                        "invalid numeric literal {source:?}: multiple decimal points"
1447                    )));
1448                }
1449                seen_point = true;
1450                i += 1;
1451            }
1452            b'e' | b'E' => break,
1453            _ => {
1454                return Err(EngineError::malformed(format!(
1455                    "invalid numeric literal {source:?}: unexpected character {:?}",
1456                    bytes[i] as char
1457                )));
1458            }
1459        }
1460    }
1461    if !seen_digit {
1462        return Err(EngineError::malformed(format!(
1463            "invalid numeric literal {source:?}: no digits"
1464        )));
1465    }
1466    let mut exponent: i64 = 0;
1467    if i < bytes.len() {
1468        // Consume exponent.
1469        i += 1;
1470        let mut exp_negative = false;
1471        if i < bytes.len() {
1472            match bytes[i] {
1473                b'+' => i += 1,
1474                b'-' => {
1475                    exp_negative = true;
1476                    i += 1;
1477                }
1478                _ => {}
1479            }
1480        }
1481        let start = i;
1482        while i < bytes.len() && bytes[i].is_ascii_digit() {
1483            i += 1;
1484        }
1485        if start == i {
1486            return Err(EngineError::malformed(format!(
1487                "invalid numeric literal {source:?}: missing exponent digits"
1488            )));
1489        }
1490        if i != bytes.len() {
1491            return Err(EngineError::malformed(format!(
1492                "invalid numeric literal {source:?}: trailing characters"
1493            )));
1494        }
1495        let exp_text = &source[start..i];
1496        if exp_text.len() > 7 {
1497            return Err(EngineError::new(
1498                ErrorCode::PrecisionLimit,
1499                format!("exponent {exp_text} exceeds the supported range"),
1500            ));
1501        }
1502        exponent = exp_text.parse::<i64>().map_err(|_| {
1503            EngineError::malformed(format!("invalid numeric literal {source:?}: bad exponent"))
1504        })?;
1505        if exp_negative {
1506            exponent = -exponent;
1507        }
1508    }
1509    if digits.len() > limits.max_digits {
1510        return Err(EngineError::new(
1511            ErrorCode::ResourceLimit,
1512            format!(
1513                "numeric literal has {} digits, exceeding the limit of {}",
1514                digits.len(),
1515                limits.max_digits
1516            ),
1517        ));
1518    }
1519    let mut mantissa = BigInt::from_str(&digits)
1520        .map_err(|_| EngineError::malformed(format!("invalid numeric literal {source:?}")))?;
1521    if negative {
1522        mantissa = -mantissa;
1523    }
1524    let mut scale = frac_digits - exponent;
1525    if scale < 0 {
1526        mantissa *= pow10((-scale) as u64);
1527        scale = 0;
1528    }
1529    if scale > limits.max_decimal_scale as i64 {
1530        return Err(EngineError::new(
1531            ErrorCode::PrecisionLimit,
1532            format!(
1533                "numeric literal scale {scale} exceeds the limit of {}",
1534                limits.max_decimal_scale
1535            ),
1536        ));
1537    }
1538    if mantissa.bits() > limits.max_integer_bits as u64 {
1539        return Err(EngineError::new(
1540            ErrorCode::ResourceLimit,
1541            format!(
1542                "numeric literal needs {} bits, exceeding the limit of {}",
1543                mantissa.bits(),
1544                limits.max_integer_bits
1545            ),
1546        ));
1547    }
1548    Ok((mantissa, scale as u32))
1549}
1550
1551impl Number {
1552    /// Parse a literal from expression text or a numeric shorthand string.
1553    ///
1554    /// A literal containing `.` or an exponent becomes a [`Decimal`]; otherwise
1555    /// an [`Integer`](Number::Integer). Rationals and Float64 are only produced
1556    /// by explicit typed construction, never by guessing at text.
1557    pub fn parse_literal(source: &str, limits: &Limits) -> Result<Number, EngineError> {
1558        let trimmed = source.trim();
1559        if trimmed.is_empty() {
1560            return Err(EngineError::malformed("empty numeric literal"));
1561        }
1562        if trimmed.contains('.') || trimmed.contains('e') || trimmed.contains('E') {
1563            Ok(Number::Decimal(Decimal::parse(trimmed, limits)?))
1564        } else {
1565            let digits = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
1566            if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
1567                return Err(EngineError::malformed(format!(
1568                    "invalid numeric literal {source:?}"
1569                )));
1570            }
1571            if digits.len() > limits.max_digits {
1572                return Err(EngineError::new(
1573                    ErrorCode::ResourceLimit,
1574                    format!(
1575                        "numeric literal has {} digits, exceeding the limit of {}",
1576                        digits.len(),
1577                        limits.max_digits
1578                    ),
1579                ));
1580            }
1581            let value = BigInt::from_str(trimmed).map_err(|_| {
1582                EngineError::malformed(format!("invalid integer literal {source:?}"))
1583            })?;
1584            if value.bits() > limits.max_integer_bits as u64 {
1585                return Err(EngineError::new(
1586                    ErrorCode::ResourceLimit,
1587                    format!(
1588                        "integer literal needs {} bits, exceeding the limit of {}",
1589                        value.bits(),
1590                        limits.max_integer_bits
1591                    ),
1592                ));
1593            }
1594            Ok(Number::Integer(value))
1595        }
1596    }
1597}
1598
1599impl fmt::Display for Number {
1600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1601        match self {
1602            Number::Integer(v) => write!(f, "{v}"),
1603            Number::Rational(v) => {
1604                if v.is_integer() {
1605                    write!(f, "{}", v.to_integer())
1606                } else {
1607                    write!(f, "{}/{}", v.numer(), v.denom())
1608                }
1609            }
1610            Number::Decimal(v) => write!(f, "{v}"),
1611            Number::Float64(v) => write!(f, "{v}"),
1612        }
1613    }
1614}
1615
1616/// The numeric context shared by all operations.
1617#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1618pub struct NumericContext {
1619    pub mode: NumericMode,
1620    /// Significant digits used when a context rounding is required.
1621    pub precision: u32,
1622    /// Maximum bits for exact integer/rational results.
1623    pub max_integer_bits: u32,
1624    /// Maximum decimal scale for exact decimal results.
1625    pub max_decimal_scale: u32,
1626    /// Rounding mode used when the context must round.
1627    pub rounding: RoundingMode,
1628}
1629
1630impl Default for NumericContext {
1631    fn default() -> Self {
1632        NumericContext {
1633            mode: NumericMode::Auto,
1634            precision: 34,
1635            max_integer_bits: 8192,
1636            max_decimal_scale: 4096,
1637            rounding: RoundingMode::HalfEven,
1638        }
1639    }
1640}
1641
1642impl NumericContext {
1643    pub fn exact() -> Self {
1644        NumericContext {
1645            mode: NumericMode::Exact,
1646            ..Default::default()
1647        }
1648    }
1649
1650    pub fn scientific() -> Self {
1651        NumericContext {
1652            mode: NumericMode::Scientific,
1653            ..Default::default()
1654        }
1655    }
1656}
1657
1658#[cfg(test)]
1659mod tests {
1660    use super::*;
1661
1662    fn ctx() -> NumericContext {
1663        NumericContext::default()
1664    }
1665
1666    fn limits() -> Limits {
1667        Limits::conservative()
1668    }
1669
1670    #[test]
1671    fn decimal_parse_and_display_preserves_scale() {
1672        let d = Decimal::parse_default("0.10").unwrap();
1673        assert_eq!(d.mantissa(), &BigInt::from(10));
1674        assert_eq!(d.scale(), 2);
1675        assert_eq!(d.to_plain_string(), "0.10");
1676        assert_eq!(d.canonical_string(), "0.1");
1677    }
1678
1679    #[test]
1680    fn decimal_add_is_exact() {
1681        let a = Decimal::parse_default("0.1").unwrap();
1682        let b = Decimal::parse_default("0.2").unwrap();
1683        let sum = a.add(&b, &ctx(), &limits()).unwrap();
1684        assert_eq!(sum.to_plain_string(), "0.3");
1685        assert!(sum.to_plain_string() != "0.30000000000000004");
1686    }
1687
1688    #[test]
1689    fn rounding_modes_positive_and_negative_ties() {
1690        let cases = [
1691            ("1.005", RoundingMode::HalfEven, "1.00"),
1692            ("1.015", RoundingMode::HalfEven, "1.02"),
1693            ("1.005", RoundingMode::HalfAwayFromZero, "1.01"),
1694            ("-1.005", RoundingMode::HalfEven, "-1.00"),
1695            ("-1.015", RoundingMode::HalfEven, "-1.02"),
1696            ("-1.005", RoundingMode::HalfAwayFromZero, "-1.01"),
1697            ("1.009", RoundingMode::TowardZero, "1.00"),
1698            ("-1.009", RoundingMode::TowardZero, "-1.00"),
1699            ("-1.001", RoundingMode::Floor, "-1.01"),
1700            ("1.001", RoundingMode::Ceiling, "1.01"),
1701        ];
1702        for (input, mode, expected) in cases {
1703            let d = Decimal::parse_default(input).unwrap();
1704            let rounded = d.round_to(2, mode);
1705            assert_eq!(
1706                rounded.to_plain_string(),
1707                expected,
1708                "round({input}, {mode:?})"
1709            );
1710        }
1711    }
1712
1713    #[test]
1714    fn exact_division_terminates() {
1715        let a = Decimal::parse_default("1").unwrap();
1716        let b = Decimal::parse_default("8").unwrap();
1717        let (q, inexact) = a.div(&b, &ctx(), &limits()).unwrap();
1718        assert!(!inexact);
1719        assert_eq!(q.to_plain_string(), "0.125");
1720    }
1721
1722    #[test]
1723    fn inexact_division_reports_flag() {
1724        let a = Decimal::parse_default("1").unwrap();
1725        let b = Decimal::parse_default("3").unwrap();
1726        let (q, inexact) = a.div(&b, &ctx(), &limits()).unwrap();
1727        assert!(inexact);
1728        assert!(q.to_plain_string().starts_with("0.3333"));
1729    }
1730
1731    #[test]
1732    fn exact_mode_rejects_inexact_division() {
1733        let a = Decimal::parse_default("1").unwrap();
1734        let b = Decimal::parse_default("3").unwrap();
1735        let err = a.div(&b, &NumericContext::exact(), &limits()).unwrap_err();
1736        assert_eq!(err.code, ErrorCode::UnsupportedNumericMode);
1737    }
1738
1739    #[test]
1740    fn exact_sqrt_detection() {
1741        assert_eq!(
1742            Decimal::parse_default("0.25")
1743                .unwrap()
1744                .sqrt_exact()
1745                .unwrap()
1746                .to_plain_string(),
1747            "0.5"
1748        );
1749        assert!(Decimal::parse_default("2").unwrap().sqrt_exact().is_none());
1750    }
1751
1752    #[test]
1753    fn number_promotion_int_div() {
1754        let one = Number::integer(1);
1755        let three = Number::integer(3);
1756        let result = one.div(&three, &ctx(), &limits()).unwrap();
1757        assert!(!result.rounded);
1758        assert_eq!(result.value.to_string(), "1/3");
1759    }
1760
1761    #[test]
1762    fn float_arithmetic_requires_scientific_mode() {
1763        let a = Number::float(0.1).unwrap();
1764        let b = Number::integer(1);
1765        let err = a.add(&b, &ctx(), &limits()).unwrap_err();
1766        assert_eq!(err.code, ErrorCode::UnsupportedNumericMode);
1767        let ok = a.add(&b, &NumericContext::scientific(), &limits()).unwrap();
1768        assert!(ok.rounded);
1769    }
1770
1771    #[test]
1772    fn comparisons_are_exact_across_representations() {
1773        let dec = Number::Decimal(Decimal::parse_default("0.5").unwrap());
1774        let float = Number::float(0.5).unwrap();
1775        assert!(dec.numeric_eq(&float).unwrap());
1776        let dec_tenth = Number::Decimal(Decimal::parse_default("0.1").unwrap());
1777        let float_tenth = Number::float(0.1).unwrap();
1778        assert!(!dec_tenth.numeric_eq(&float_tenth).unwrap());
1779    }
1780
1781    #[test]
1782    fn pow_zero_zero_is_one() {
1783        let zero = Number::integer(0);
1784        let result = zero.pow(&zero, &ctx(), &limits()).unwrap();
1785        assert_eq!(result.value.to_string(), "1");
1786    }
1787
1788    #[test]
1789    fn division_by_zero_is_structured() {
1790        let one = Number::integer(1);
1791        let zero = Number::integer(0);
1792        let err = one.div(&zero, &ctx(), &limits()).unwrap_err();
1793        assert_eq!(err.code, ErrorCode::DivisionByZero);
1794    }
1795
1796    #[test]
1797    fn remainder_vs_modulo_negative_operands() {
1798        let a = Number::integer(-7);
1799        let b = Number::integer(3);
1800        let rem = a.rem(&b, &ctx(), &limits()).unwrap();
1801        let modulo = a.modulo(&b, &ctx(), &limits()).unwrap();
1802        assert_eq!(rem.value.to_string(), "-1");
1803        assert_eq!(modulo.value.to_string(), "2");
1804    }
1805}