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