Skip to main content

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