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