Skip to main content

jsonschema_value/ext/
numeric.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    clippy::cast_precision_loss,
6    clippy::float_cmp,
7    clippy::must_use_candidate
8)]
9
10use fraction::{BigFraction, One, Zero};
11use serde_json::Number;
12#[cfg(feature = "arbitrary-precision")]
13use std::cmp::Ordering;
14
15macro_rules! define_num_cmp {
16    ($($trait_fn:ident => $fn_name:ident, $op:tt, $infinity_positive:literal, $ord_pat:pat),* $(,)?) => {
17        $(
18            pub fn $fn_name<T>(value: &Number, limit: T) -> bool
19            where
20                T: Copy + num_traits::ToPrimitive,
21                u64: num_cmp::NumCmp<T>,
22                i64: num_cmp::NumCmp<T>,
23                f64: num_cmp::NumCmp<T>,
24            {
25                if let Some(v) = value.as_u64() {
26                    num_cmp::NumCmp::$trait_fn(v, limit)
27                } else if let Some(v) = value.as_i64() {
28                    num_cmp::NumCmp::$trait_fn(v, limit)
29                } else if let Some(v) = value.as_f64() {
30                    // Integers outside u64/i64 lose precision in `as_f64` and can round exactly
31                    // onto the limit (e.g. -9223372036854775809 -> -2^63); compare them exactly.
32                    #[cfg(feature = "arbitrary-precision")]
33                    if v <= i64::MIN as f64 || v >= u64::MAX as f64 {
34                        if let Some(big_value) = bignum::try_parse_bigint(value) {
35                            if let Some(ordering) = bignum::compare_bigint_to_limit(&big_value, limit) {
36                                return matches!(ordering, $ord_pat);
37                            }
38                        }
39                    }
40                    num_cmp::NumCmp::$trait_fn(v, limit)
41                } else {
42                    #[cfg(feature = "arbitrary-precision")]
43                    {
44                        if let Some(big_value) = bignum::try_parse_bigfraction(value) {
45                            if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
46                                let limit_frac = BigFraction::from(limit_f64);
47                                return big_value $op limit_frac;
48                            }
49                        }
50                        // Treat unparsable numbers as infinity based on sign
51                        let is_negative = value.as_str().starts_with('-');
52                        if $infinity_positive {
53                            !is_negative
54                        } else {
55                            is_negative
56                        }
57                    }
58                    #[cfg(not(feature = "arbitrary-precision"))]
59                    {
60                        unreachable!("Always Some without `arbitrary-precision`")
61                    }
62                }
63            }
64        )*
65    };
66}
67
68define_num_cmp!(
69    num_ge => ge, >=, true, Ordering::Greater | Ordering::Equal,   // +infinity passes >=, >
70    num_le => le, <=, false, Ordering::Less | Ordering::Equal,  // -infinity passes <=, <
71    num_gt => gt, >, true, Ordering::Greater,
72    num_lt => lt, <, false, Ordering::Less,
73);
74
75#[cfg(feature = "macros")]
76pub fn eq<T>(value: &Number, limit: T) -> bool
77where
78    T: Copy + num_traits::ToPrimitive,
79    u64: num_cmp::NumCmp<T>,
80    i64: num_cmp::NumCmp<T>,
81    f64: num_cmp::NumCmp<T>,
82{
83    if let Some(v) = value.as_u64() {
84        num_cmp::NumCmp::num_eq(v, limit)
85    } else if let Some(v) = value.as_i64() {
86        num_cmp::NumCmp::num_eq(v, limit)
87    } else if let Some(v) = value.as_f64() {
88        num_cmp::NumCmp::num_eq(v, limit)
89    } else {
90        #[cfg(feature = "arbitrary-precision")]
91        {
92            if let Some(big_value) = bignum::try_parse_bigfraction(value) {
93                if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
94                    return big_value == BigFraction::from(limit_f64);
95                }
96            }
97            false
98        }
99        #[cfg(not(feature = "arbitrary-precision"))]
100        {
101            unreachable!("Always Some without `arbitrary-precision`")
102        }
103    }
104}
105
106pub fn is_multiple_of_float(value: &Number, multiple: f64) -> bool {
107    if let Some(value_f64) = value.as_f64() {
108        // Zero is a multiple of any non-zero number
109        // This check must come first to avoid division-related edge cases
110        if value_f64.is_zero() {
111            return true;
112        }
113        if value_f64.abs() < multiple {
114            return false;
115        }
116        // From the JSON Schema spec
117        //
118        // > A numeric instance is valid only if division by this keyword's value results in an integer.
119        //
120        // For fractions, integers have denominator equal to one.
121        //
122        // Ref: https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1
123        (BigFraction::from(value_f64) / BigFraction::from(multiple))
124            .denom()
125            .is_none_or(One::is_one)
126    } else {
127        // This branch is only possible for large floats in scientific notation, we don't really
128        // support it
129        false
130    }
131}
132
133/// The maximum integer that can be exactly represented in f64.
134/// Beyond this value, f64 loses precision and arithmetic operations become unreliable.
135const MAX_SAFE_INTEGER: u64 = 1u64 << 53;
136
137pub fn is_multiple_of_integer(value: &Number, multiple: f64) -> bool {
138    // Integer instances use integer modulo directly: it is exact and avoids the slower float
139    // `fract()` + `%`. The divisor guard keeps it exact - divisors above 2^53 may already have
140    // lost precision when converted to f64 during schema compilation, and `multiple > 0.0` avoids
141    // a divide-by-zero panic on the integer modulo. Non-integer or huge instances fall through to
142    // the f64 path below.
143    let divisor_ok =
144        multiple > 0.0 && multiple <= MAX_SAFE_INTEGER as f64 && multiple.fract() == 0.0;
145    if divisor_ok {
146        if let Some(v) = value.as_u64() {
147            return (v % (multiple as u64)) == 0;
148        }
149        if let Some(v) = value.as_i64() {
150            return (v % (multiple as i64)) == 0;
151        }
152    }
153
154    if let Some(value_f64) = value.as_f64() {
155        // As the divisor has its fractional part as zero, then any value with a non-zero
156        // fractional part can't be a multiple of this divisor, therefore it is short-circuited
157        value_f64.fract() == 0. && (value_f64 % multiple) == 0.
158    } else {
159        // Number doesn't fit in f64 - must be huge with arbitrary_precision
160        #[cfg(feature = "arbitrary-precision")]
161        {
162            // Try parsing as BigInt first for large integers
163            if let Some(big_value) = bignum::try_parse_bigint(value) {
164                use num_bigint::BigInt;
165                // Convert the multiple to BigInt.
166                // Note: For large divisors beyond i64/u64 range, the schema compilation
167                // should have created a MultipleOfBigIntValidator instead, which stores
168                // the divisor as BigInt directly. This path handles the case where the
169                // instance is huge but the divisor fits in f64.
170                // Since we know multiple is an integer (checked before calling this function),
171                // we can safely convert via i64 for divisors in the i64 range.
172                // For divisors beyond i64 but representable in f64, precision may be lost,
173                // but that's inherent to f64 representation.
174                let multiple_int = BigInt::from(multiple as i64);
175                return bignum::is_multiple_of_bigint(&big_value, &multiple_int);
176            }
177            // Not an integer - can't be a multiple of an integer divisor
178            false
179        }
180        #[cfg(not(feature = "arbitrary-precision"))]
181        {
182            unreachable!("Always Some without `arbitrary-precision`")
183        }
184    }
185}
186
187#[cfg(feature = "arbitrary-precision")]
188pub mod bignum {
189    use fraction::BigFraction;
190    use num_bigint::BigInt;
191    use num_traits::{ToPrimitive, Zero};
192    use serde_json::Number;
193    use std::str::FromStr;
194
195    /// Guardrail for how many decimal shifts we are willing to perform when normalizing
196    /// a JSON number written in scientific notation.
197    ///
198    /// Schema authors (and instances) are untrusted input: a literal like `"1e1000000000"`
199    /// would otherwise force us to append billions of zeros just to materialize the number,
200    /// opening the door to denial-of-service attacks. Limiting the exponent adjustment to
201    /// one million digits keeps conversions deterministic while still covering realistic
202    /// use-cases (`10^1_000_000` is already astronomically large for JSON Schema).
203    const MAX_EXPONENT_ADJUSTMENT: u32 = 1_000_000;
204
205    #[derive(Debug, Clone)]
206    struct DecimalComponents {
207        negative: bool,
208        digits: String,
209        fraction_digits: usize,
210        exponent: i64,
211    }
212
213    impl DecimalComponents {
214        fn parse(num_str: &str) -> Option<Self> {
215            let bytes = num_str.as_bytes();
216            if bytes.is_empty() {
217                return None;
218            }
219
220            let mut idx = 0;
221            let negative = if bytes[idx] == b'-' {
222                idx += 1;
223                true
224            } else {
225                false
226            };
227
228            if idx >= bytes.len() {
229                return None;
230            }
231
232            let mut digits = String::with_capacity(bytes.len());
233            let int_start = idx;
234            while idx < bytes.len() && bytes[idx].is_ascii_digit() {
235                idx += 1;
236            }
237            if int_start == idx {
238                return None;
239            }
240            digits.push_str(&num_str[int_start..idx]);
241
242            let mut fraction_digits = 0usize;
243            if idx < bytes.len() && bytes[idx] == b'.' {
244                idx += 1;
245                let frac_start = idx;
246                while idx < bytes.len() && bytes[idx].is_ascii_digit() {
247                    idx += 1;
248                }
249                if frac_start == idx {
250                    return None;
251                }
252                digits.push_str(&num_str[frac_start..idx]);
253                fraction_digits = idx - frac_start;
254            }
255
256            let mut exponent: i64 = 0;
257            if idx < bytes.len() && (bytes[idx] == b'e' || bytes[idx] == b'E') {
258                idx += 1;
259                if idx >= bytes.len() {
260                    return None;
261                }
262                let mut exp_sign: i64 = 1;
263                if bytes[idx] == b'+' {
264                    idx += 1;
265                } else if bytes[idx] == b'-' {
266                    exp_sign = -1;
267                    idx += 1;
268                }
269                let exp_start = idx;
270                while idx < bytes.len() && bytes[idx].is_ascii_digit() {
271                    idx += 1;
272                }
273                if exp_start == idx {
274                    return None;
275                }
276                let exp_value = num_str[exp_start..idx].parse::<i64>().ok()?;
277                exponent = exp_value.checked_mul(exp_sign)?;
278            }
279
280            if idx != bytes.len() {
281                return None;
282            }
283
284            Some(Self {
285                negative,
286                digits,
287                fraction_digits,
288                exponent,
289            })
290        }
291
292        #[inline]
293        fn decimal_shift(&self) -> i64 {
294            self.exponent - self.fraction_digits as i64
295        }
296    }
297
298    fn digits_are_zero(s: &str) -> bool {
299        s.bytes().all(|b| b == b'0')
300    }
301
302    fn trailing_zero_count(s: &str) -> usize {
303        s.as_bytes()
304            .iter()
305            .rev()
306            .take_while(|b| **b == b'0')
307            .count()
308    }
309
310    fn append_zeros(target: &mut String, count: usize) -> Option<()> {
311        let new_len = target.len().checked_add(count)?;
312        target.reserve(count);
313        target.extend(std::iter::repeat_n('0', count));
314        debug_assert_eq!(target.len(), new_len);
315        Some(())
316    }
317
318    fn pow10_bigint(exp: usize) -> Option<BigInt> {
319        if exp == 0 {
320            return Some(BigInt::from(1));
321        }
322        let exp_u32 = u32::try_from(exp).ok()?;
323        Some(BigInt::from(10).pow(exp_u32))
324    }
325
326    fn shift_exceeds_limit(shift: i64) -> bool {
327        if shift <= 0 {
328            return false;
329        }
330        shift as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT)
331    }
332
333    fn exponent_reduction_exceeds_limit(exponent: i64) -> bool {
334        if exponent >= 0 {
335            return false;
336        }
337        match exponent.checked_abs() {
338            Some(abs) => abs as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT),
339            None => true,
340        }
341    }
342
343    /// Try to parse a Number as `BigInt` if it's outside i64 range or for compile-time
344    /// schema values that need exact representation
345    pub fn try_parse_bigint(num: &Number) -> Option<BigInt> {
346        use super::MAX_SAFE_INTEGER;
347
348        let num_str = num.as_str();
349
350        // Parse as BigInt if it's beyond 2^53 (where f64 loses precision).
351        // Values beyond 2^53 need BigInt for accurate arithmetic even if they fit in i64/u64.
352        // Note: If as_i64() fails but as_u64() succeeds, the value is in [2^63, 2^64-1],
353        // which is always > 2^53, so no additional check needed for u64.
354        if let Some(v) = num.as_i64() {
355            if v.unsigned_abs() <= MAX_SAFE_INTEGER {
356                return None;
357            }
358        }
359
360        let has_fraction_or_exponent = num_str.bytes().any(|b| b == b'.' || b == b'e' || b == b'E');
361        if !has_fraction_or_exponent {
362            return BigInt::from_str(num_str).ok();
363        }
364
365        let mut components = DecimalComponents::parse(num_str)?;
366        let mut shift = components.decimal_shift();
367
368        if shift < 0 {
369            let needed = (-shift) as usize;
370            if digits_are_zero(&components.digits) {
371                components.digits.clear();
372                components.digits.push('0');
373                shift = 0;
374            } else {
375                if exponent_reduction_exceeds_limit(components.exponent) {
376                    return None;
377                }
378                let zeros = trailing_zero_count(&components.digits);
379                if zeros < needed {
380                    return None;
381                }
382                let new_len = components.digits.len() - needed;
383                components.digits.truncate(new_len);
384                shift = 0;
385            }
386        }
387
388        if shift > 0 {
389            if shift_exceeds_limit(shift) {
390                return None;
391            }
392            append_zeros(&mut components.digits, shift as usize)?;
393        }
394
395        let digits_trimmed = components.digits.trim_start_matches('0');
396        let digits_ref = if digits_trimmed.is_empty() {
397            "0"
398        } else {
399            digits_trimmed
400        };
401        let mut value = BigInt::from_str(digits_ref).ok()?;
402        if components.negative && !value.is_zero() {
403            value = -value;
404        }
405        Some(value)
406    }
407
408    /// Try to parse a Number as `BigFraction` for arbitrary precision decimal support
409    ///
410    /// Returns Some for numbers requiring exact decimal precision:
411    /// - Decimals with a decimal point (e.g., `0.1`, `123.456`)
412    /// - Scientific notation decimals that can't be represented exactly as f64
413    ///
414    /// Returns None for:
415    /// - Integers that fit in i64 (handled by standard numeric path)
416    /// - Large integers including u64 beyond `i64::MAX` (handled by `try_parse_bigint`)
417    pub fn try_parse_bigfraction(num: &Number) -> Option<BigFraction> {
418        // Skip integers that fit in i64 - they don't need BigFraction
419        if num.as_i64().is_some() {
420            return None;
421        }
422
423        let num_str = num.as_str();
424
425        // Check for decimal point and exponent in a single pass
426        let mut has_decimal_point = false;
427        let mut has_exponent = false;
428        for b in num_str.bytes() {
429            if b == b'.' {
430                has_decimal_point = true;
431            } else if b == b'e' || b == b'E' {
432                has_exponent = true;
433                break;
434            }
435        }
436
437        if !has_decimal_point && !has_exponent {
438            return None;
439        }
440
441        if !has_exponent {
442            return BigFraction::from_str(num_str).ok();
443        }
444
445        let components = DecimalComponents::parse(num_str)?;
446        let shift = components.decimal_shift();
447
448        // A number with exponent that still resolves to an integer is handled by BigInt.
449        if shift >= 0 {
450            return None;
451        }
452
453        if exponent_reduction_exceeds_limit(components.exponent) {
454            return None;
455        }
456
457        let denom_power = (-shift) as usize;
458        let denominator = pow10_bigint(denom_power)?;
459        let mut numerator = BigInt::from_str(&components.digits).ok()?;
460        if components.negative && !numerator.is_zero() {
461            numerator = -numerator;
462        }
463        Some(BigFraction::from(numerator) / BigFraction::from(denominator))
464    }
465
466    /// Exact ordering of a big-integer instance against a numeric limit.
467    ///
468    /// Integer-representable limits compare via `BigInt`; infinite limits (schema numbers
469    /// beyond the exponent cap) order every finite instance. `None` means the limit has no
470    /// exact integer form and the caller should fall back to `f64` comparison.
471    pub(crate) fn compare_bigint_to_limit<T>(big: &BigInt, limit: T) -> Option<std::cmp::Ordering>
472    where
473        T: Copy + ToPrimitive,
474    {
475        use std::cmp::Ordering;
476
477        let limit_f64 = limit.to_f64()?;
478        if limit_f64.fract() == 0.0 {
479            // `to_i64`/`to_u64` are exact for u64/i64 limits and for integer-valued f64 limits.
480            if let Some(limit_int) = limit.to_i64() {
481                return Some(big.cmp(&BigInt::from(limit_int)));
482            }
483            if let Some(limit_int) = limit.to_u64() {
484                return Some(big.cmp(&BigInt::from(limit_int)));
485            }
486        }
487        if limit_f64 == f64::INFINITY {
488            return Some(Ordering::Less);
489        }
490        if limit_f64 == f64::NEG_INFINITY {
491            return Some(Ordering::Greater);
492        }
493        None
494    }
495
496    macro_rules! define_bigint_cmp {
497        ($($fn_name:ident, $prim_type:ty, $to_prim:ident, $op:tt, $overflow_sign:expr);* $(;)?) => {
498            $(
499                pub fn $fn_name(bigint: &BigInt, value: $prim_type) -> bool {
500                    if let Some(converted) = bigint.$to_prim() {
501                        converted $op value
502                    } else {
503                        bigint.sign() == $overflow_sign
504                    }
505                }
506            )*
507        };
508    }
509
510    define_bigint_cmp!(
511        bigint_ge_u64, u64, to_u64, >=, num_bigint::Sign::Plus;
512        bigint_le_u64, u64, to_u64, <=, num_bigint::Sign::Minus;
513        bigint_gt_u64, u64, to_u64, >, num_bigint::Sign::Plus;
514        bigint_lt_u64, u64, to_u64, <, num_bigint::Sign::Minus;
515        bigint_ge_i64, i64, to_i64, >=, num_bigint::Sign::Plus;
516        bigint_le_i64, i64, to_i64, <=, num_bigint::Sign::Minus;
517        bigint_gt_i64, i64, to_i64, >, num_bigint::Sign::Plus;
518        bigint_lt_i64, i64, to_i64, <, num_bigint::Sign::Minus;
519        bigint_ge_f64, f64, to_f64, >=, num_bigint::Sign::Plus;
520        bigint_le_f64, f64, to_f64, <=, num_bigint::Sign::Minus;
521        bigint_gt_f64, f64, to_f64, >, num_bigint::Sign::Plus;
522        bigint_lt_f64, f64, to_f64, <, num_bigint::Sign::Minus;
523    );
524
525    // Generate reverse comparison functions (primitive op BigType -> BigType op primitive)
526    macro_rules! define_reverse_cmp {
527        ($($rev_ge:ident, $rev_le:ident, $rev_gt:ident, $rev_lt:ident, $prim_type:ty, $big_type:ty, $fwd_ge:ident, $fwd_le:ident, $fwd_gt:ident, $fwd_lt:ident);* $(;)?) => {
528            $(
529                pub fn $rev_ge(value: $prim_type, big: &$big_type) -> bool {
530                    $fwd_le(big, value)
531                }
532
533                pub fn $rev_le(value: $prim_type, big: &$big_type) -> bool {
534                    $fwd_ge(big, value)
535                }
536
537                pub fn $rev_gt(value: $prim_type, big: &$big_type) -> bool {
538                    $fwd_lt(big, value)
539                }
540
541                pub fn $rev_lt(value: $prim_type, big: &$big_type) -> bool {
542                    $fwd_gt(big, value)
543                }
544            )*
545        };
546    }
547
548    define_reverse_cmp!(
549        u64_ge_bigint, u64_le_bigint, u64_gt_bigint, u64_lt_bigint, u64, BigInt, bigint_ge_u64, bigint_le_u64, bigint_gt_u64, bigint_lt_u64;
550        i64_ge_bigint, i64_le_bigint, i64_gt_bigint, i64_lt_bigint, i64, BigInt, bigint_ge_i64, bigint_le_i64, bigint_gt_i64, bigint_lt_i64;
551        f64_ge_bigint, f64_le_bigint, f64_gt_bigint, f64_lt_bigint, f64, BigInt, bigint_ge_f64, bigint_le_f64, bigint_gt_f64, bigint_lt_f64;
552    );
553
554    /// Check if a Number (as `BigInt`) is a multiple of another `BigInt`
555    pub fn is_multiple_of_bigint(value: &BigInt, multiple: &BigInt) -> bool {
556        // Zero is a multiple of any non-zero number
557        // Mathematically: 0 = k * multiple for k = 0
558        if value.is_zero() {
559            return true;
560        }
561
562        // Note: multiple.is_zero() case is not handled here because JSON Schema
563        // validation rejects schemas with "multipleOf: 0" during compilation
564        // (exclusiveMinimum constraint requires multipleOf > 0).
565        // The modulo operation below would panic if multiple is zero, but this
566        // is prevented by schema validation.
567
568        (value % multiple).is_zero()
569    }
570
571    // BigFraction comparison functions
572    macro_rules! define_bigfraction_cmp {
573        ($($fn_name:ident, $prim_type:ty, $op:tt);* $(;)?) => {
574            $(
575                pub fn $fn_name(bigfrac: &BigFraction, value: $prim_type) -> bool {
576                    let value_frac = BigFraction::from(value);
577                    *bigfrac $op value_frac
578                }
579            )*
580        };
581    }
582
583    define_bigfraction_cmp!(
584        bigfrac_ge_u64, u64, >=;
585        bigfrac_le_u64, u64, <=;
586        bigfrac_gt_u64, u64, >;
587        bigfrac_lt_u64, u64, <;
588        bigfrac_ge_i64, i64, >=;
589        bigfrac_le_i64, i64, <=;
590        bigfrac_gt_i64, i64, >;
591        bigfrac_lt_i64, i64, <;
592        bigfrac_ge_f64, f64, >=;
593        bigfrac_le_f64, f64, <=;
594        bigfrac_gt_f64, f64, >;
595        bigfrac_lt_f64, f64, <;
596    );
597
598    define_reverse_cmp!(
599        u64_ge_bigfrac, u64_le_bigfrac, u64_gt_bigfrac, u64_lt_bigfrac, u64, BigFraction, bigfrac_ge_u64, bigfrac_le_u64, bigfrac_gt_u64, bigfrac_lt_u64;
600        i64_ge_bigfrac, i64_le_bigfrac, i64_gt_bigfrac, i64_lt_bigfrac, i64, BigFraction, bigfrac_ge_i64, bigfrac_le_i64, bigfrac_gt_i64, bigfrac_lt_i64;
601        f64_ge_bigfrac, f64_le_bigfrac, f64_gt_bigfrac, f64_lt_bigfrac, f64, BigFraction, bigfrac_ge_f64, bigfrac_le_f64, bigfrac_gt_f64, bigfrac_lt_f64;
602    );
603
604    /// Check if a `BigFraction` is a multiple of another value
605    pub fn is_multiple_of_bigfrac(value: &BigFraction, multiple: &BigFraction) -> bool {
606        // Zero is a multiple of any non-zero number
607        if value.is_zero() {
608            return true;
609        }
610        // Division by zero is undefined, so return false
611        if multiple.is_zero() {
612            return false;
613        }
614        // A number is a multiple of another if division results in an integer
615        // (denominator of the result is 1)
616        (value / multiple).denom().is_none_or(fraction::One::is_one)
617    }
618}
619
620#[cfg(all(test, feature = "arbitrary-precision"))]
621mod tests {
622    use super::bignum;
623    use fraction::BigFraction;
624    use num_bigint::BigInt;
625    use serde_json::{Number, Value};
626    use std::cmp::Ordering;
627    use test_case::test_case;
628
629    fn number_from_str(raw: &str) -> Number {
630        match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
631            Value::Number(num) => num,
632            _ => unreachable!(),
633        }
634    }
635
636    #[test_case("18446744073709551616", u64::MAX, Ordering::Greater; "above u64 limit")]
637    fn compare_bigint_to_u64_limit(big: &str, limit: u64, expected: Ordering) {
638        let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
639        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
640    }
641
642    #[test_case("-18446744073709551616", i64::MIN, Ordering::Less; "below i64 limit")]
643    fn compare_bigint_to_i64_limit(big: &str, limit: i64, expected: Ordering) {
644        let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
645        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
646    }
647
648    // Infinity limits come from schema numbers beyond the exponent cap (e.g. `1e2000000`);
649    // limits without an exact integer form defer to the caller's f64 comparison.
650    #[test_case(f64::INFINITY, Some(Ordering::Less); "infinity limit")]
651    #[test_case(f64::NEG_INFINITY, Some(Ordering::Greater); "negative infinity limit")]
652    #[test_case(0.5, None; "no exact integer form")]
653    fn compare_bigint_to_f64_limit(limit: f64, expected: Option<Ordering>) {
654        let big = BigInt::parse_bytes(b"18446744073709551616", 10).unwrap();
655        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), expected);
656    }
657
658    #[test]
659    fn bigint_parses_scientific_integer() {
660        let num = number_from_str("1e19");
661        let parsed = bignum::try_parse_bigint(&num).expect("parsed bigint");
662        assert_eq!(
663            parsed,
664            BigInt::parse_bytes(b"10000000000000000000", 10).unwrap()
665        );
666    }
667
668    #[test]
669    fn bigint_rejects_non_integer_scientific() {
670        let num = number_from_str("1.25e1");
671        assert!(bignum::try_parse_bigint(&num).is_none());
672    }
673
674    #[test]
675    fn bigfraction_parses_scientific_decimal() {
676        let num = number_from_str("1.5e-5");
677        let parsed = bignum::try_parse_bigfraction(&num).expect("parsed bigfraction");
678        let expected =
679            BigFraction::from(BigInt::from(3)) / BigFraction::from(BigInt::from(200_000));
680        assert_eq!(parsed, expected);
681    }
682
683    #[test]
684    fn bigfraction_skips_scientific_integer() {
685        let num = number_from_str("3e4");
686        assert!(bignum::try_parse_bigfraction(&num).is_none());
687    }
688}