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