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    #[cfg(feature = "arbitrary-precision")]
195    if let Some(decimal) = bignum::past_digit_budget(&value.as_str()) {
196        return bignum::Decimal::of_limit(multiple)
197            .is_some_and(|multiple| decimal.is_multiple_of(&multiple));
198    }
199    if let Some(value_f64) = value.as_f64() {
200        // Zero is a multiple of any non-zero number, but a magnitude below the smallest
201        // subnormal reads as zero without being one.
202        if value_f64.is_zero() {
203            return decimal_is_zero(&value.as_str());
204        }
205        if value_f64.abs() < multiple {
206            return false;
207        }
208        // From the JSON Schema spec
209        //
210        // > A numeric instance is valid only if division by this keyword's value results in an integer.
211        //
212        // For fractions, integers have denominator equal to one.
213        //
214        // Ref: https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1
215        if let Some(answer) = divides_exactly(value_f64, multiple) {
216            return answer;
217        }
218        (BigFraction::from(value_f64) / BigFraction::from(multiple))
219            .denom()
220            .is_none_or(One::is_one)
221    } else {
222        // This branch is only possible for large floats in scientific notation, we don't really
223        // support it
224        false
225    }
226}
227
228/// The maximum integer that can be exactly represented in f64.
229/// Beyond this value, f64 loses precision and arithmetic operations become unreliable.
230const MAX_SAFE_INTEGER: u64 = 1u64 << 53;
231
232/// Whether the number written in `text` is exactly zero: `0e5` is, `1e-400` is not.
233fn decimal_is_zero(text: &str) -> bool {
234    let mantissa = text.split(['e', 'E']).next().unwrap_or(text);
235    mantissa
236        .bytes()
237        .all(|byte| !byte.is_ascii_digit() || byte == b'0')
238}
239
240/// Whether the plain decimal integer in `text` divides by `divisor`, by long division over its
241/// digits; exact at any width. `None` for anything else.
242#[cfg(not(feature = "arbitrary-precision"))]
243pub(crate) fn decimal_is_multiple_of(text: &str, divisor: u64) -> Option<bool> {
244    if text.contains(['e', 'E']) {
245        return None;
246    }
247    let text = text.strip_prefix('-').unwrap_or(text);
248    let (digits, fraction) = match text.split_once('.') {
249        Some((digits, fraction)) => (digits, Some(fraction)),
250        None => (text, None),
251    };
252    // A fraction of nothing but zeros leaves the value whole.
253    if fraction.is_some_and(|fraction| !fraction.bytes().all(|byte| byte == b'0')) {
254        return Some(false);
255    }
256    // `remainder` stays below `divisor`, at most 2^53, so the step cannot overflow.
257    let mut remainder = 0_u64;
258    for byte in digits.bytes() {
259        remainder = (remainder * 10 + u64::from(byte - b'0')) % divisor;
260    }
261    Some(remainder == 0)
262}
263
264pub fn is_multiple_of_integer<N: crate::JsonNumber>(value: &N, multiple: f64) -> bool {
265    // Integer instances use integer modulo directly: it is exact and avoids the slower float
266    // `fract()` + `%`. The divisor guard keeps it exact - divisors above 2^53 may already have
267    // lost precision when converted to f64 during schema compilation, and `multiple > 0.0` avoids
268    // a divide-by-zero panic on the integer modulo. Non-integer or huge instances fall through to
269    // the f64 path below.
270    let divisor_ok =
271        multiple > 0.0 && multiple <= MAX_SAFE_INTEGER as f64 && multiple.fract() == 0.0;
272    if divisor_ok {
273        if let Some(v) = value.as_u64() {
274            return (v % (multiple as u64)) == 0;
275        }
276        if let Some(v) = value.as_i64() {
277            return (v % (multiple as i64)) == 0;
278        }
279        // An integer past `u64` still divides exactly, and `as_f64` below would answer about the
280        // value it rounds to instead.
281        #[cfg(feature = "arbitrary-precision")]
282        if let Some(decimal) = bignum::past_digit_budget(&value.as_str()) {
283            return bignum::Decimal::of_limit(multiple)
284                .is_some_and(|multiple| decimal.is_multiple_of(&multiple));
285        }
286        #[cfg(feature = "arbitrary-precision")]
287        if let Some(big_value) = bignum::try_parse_bigint(&value.to_number()) {
288            let divisor = num_bigint::BigInt::from(multiple as i64);
289            return bignum::is_multiple_of_bigint(&big_value, &divisor);
290        }
291        // Only where `f64` cannot answer exactly, so the literal is not rendered on the hot path.
292        #[cfg(not(feature = "arbitrary-precision"))]
293        if value
294            .as_f64()
295            .is_none_or(|value_f64| value_f64.abs() >= MAX_SAFE_INTEGER as f64)
296        {
297            if let Some(answer) = decimal_is_multiple_of(&value.as_str(), multiple as u64) {
298                return answer;
299            }
300        }
301    }
302
303    if let Some(value_f64) = value.as_f64() {
304        // A magnitude below the smallest subnormal underflows to zero, which the modulo below
305        // would read as the divisor dividing evenly; such a value is a proper fraction of any
306        // whole divisor.
307        if value_f64 == 0.0 && !decimal_is_zero(&value.as_str()) {
308            return false;
309        }
310        // As the divisor has its fractional part as zero, then any value with a non-zero
311        // fractional part can't be a multiple of this divisor, therefore it is short-circuited
312        value_f64.fract() == 0. && (value_f64 % multiple) == 0.
313    } else {
314        // Number doesn't fit in f64 - must be huge with arbitrary_precision
315        #[cfg(feature = "arbitrary-precision")]
316        {
317            // Try parsing as BigInt first for large integers
318            if let Some(big_value) = bignum::try_parse_bigint(&value.to_number()) {
319                use num_bigint::BigInt;
320                // Convert the multiple to BigInt.
321                // Note: For large divisors beyond i64/u64 range, the schema compilation
322                // should have created a MultipleOfBigIntValidator instead, which stores
323                // the divisor as BigInt directly. This path handles the case where the
324                // instance is huge but the divisor fits in f64.
325                // Since we know multiple is an integer (checked before calling this function),
326                // we can safely convert via i64 for divisors in the i64 range.
327                // For divisors beyond i64 but representable in f64, precision may be lost,
328                // but that's inherent to f64 representation.
329                let multiple_int = BigInt::from(multiple as i64);
330                return bignum::is_multiple_of_bigint(&big_value, &multiple_int);
331            }
332            // Not an integer - can't be a multiple of an integer divisor
333            false
334        }
335        // Exponent forms past `f64` are placed only by `arbitrary-precision`.
336        #[cfg(not(feature = "arbitrary-precision"))]
337        {
338            false
339        }
340    }
341}
342
343#[cfg(feature = "arbitrary-precision")]
344pub mod bignum {
345    use fraction::BigFraction;
346    use num_bigint::BigInt;
347    use num_traits::{ToPrimitive, Zero};
348    use serde_json::Number;
349    use std::str::FromStr;
350
351    /// The most digits `BigInt` and `BigFraction` materialise for one number. Their arithmetic
352    /// grows quadratically with the digit count and a short literal like `1e-1000000` writes a
353    /// million, so numbers past this budget take [`Decimal`], which is linear in the written text.
354    const MAX_DIGITS: u64 = 10_000;
355
356    /// Whether materialising the number written in `text` takes more than `MAX_DIGITS` digits:
357    /// the digits it writes plus the zeros its exponent shifts in.
358    #[inline]
359    pub fn exceeds_digit_budget(text: &str) -> bool {
360        exceeds_digit_budget_with(text, has_exponent(text))
361    }
362
363    fn has_exponent(text: &str) -> bool {
364        memchr::memchr2(b'e', b'E', text.as_bytes()).is_some()
365    }
366
367    fn exceeds_digit_budget_with(text: &str, has_exponent: bool) -> bool {
368        // Without an exponent the text is at least as long as the digits it materialises.
369        if !has_exponent {
370            return text.len() as u64 > MAX_DIGITS;
371        }
372        let mut digits: u64 = 0;
373        let mut fraction: i64 = 0;
374        let mut in_fraction = false;
375        let mut exponent: i64 = 0;
376        let mut exponent_negative = false;
377        let mut in_exponent = false;
378        for byte in text.bytes() {
379            match byte {
380                b'0'..=b'9' if in_exponent => {
381                    exponent = exponent
382                        .saturating_mul(10)
383                        .saturating_add(i64::from(byte - b'0'));
384                }
385                b'0'..=b'9' => {
386                    digits += 1;
387                    fraction += i64::from(in_fraction);
388                }
389                b'.' => in_fraction = true,
390                b'e' | b'E' => in_exponent = true,
391                b'-' if in_exponent => exponent_negative = true,
392                _ => {}
393            }
394        }
395        let exponent = if exponent_negative {
396            -exponent
397        } else {
398            exponent
399        };
400        digits.saturating_add(exponent.saturating_sub(fraction).unsigned_abs()) > MAX_DIGITS
401    }
402
403    /// The number written in `text` as a [`Decimal`] when it [`exceeds_digit_budget`].
404    pub fn past_digit_budget(text: &str) -> Option<Decimal> {
405        exceeds_digit_budget(text).then(|| Decimal::parse(text))
406    }
407
408    /// A decimal number, `digits * 10^exponent`, with no leading or trailing zeros in `digits`;
409    /// zero has no digits. Exact at any magnitude, with work linear in the written text.
410    #[derive(Debug, Clone, PartialEq, Eq)]
411    pub struct Decimal {
412        negative: bool,
413        digits: Vec<u8>,
414        exponent: i64,
415    }
416
417    impl Decimal {
418        /// The number written in `text`, a JSON number literal.
419        // ponytail: exponents saturate at `i64`, so numbers written past `1e9223372036854775807`
420        // compare equal to each other.
421        pub fn parse(text: &str) -> Self {
422            let mut negative = false;
423            let mut digits = Vec::new();
424            let mut fraction: i64 = 0;
425            let mut in_fraction = false;
426            let mut exponent: i64 = 0;
427            let mut exponent_negative = false;
428            let mut in_exponent = false;
429            for byte in text.bytes() {
430                match byte {
431                    b'0'..=b'9' if in_exponent => {
432                        exponent = exponent
433                            .saturating_mul(10)
434                            .saturating_add(i64::from(byte - b'0'));
435                    }
436                    b'0'..=b'9' => {
437                        if !(digits.is_empty() && byte == b'0') {
438                            digits.push(byte);
439                        }
440                        fraction += i64::from(in_fraction);
441                    }
442                    b'.' => in_fraction = true,
443                    b'e' | b'E' => in_exponent = true,
444                    b'-' if in_exponent => exponent_negative = true,
445                    b'-' => negative = true,
446                    _ => {}
447                }
448            }
449            let trailing = digits
450                .iter()
451                .rev()
452                .take_while(|&&byte| byte == b'0')
453                .count();
454            digits.truncate(digits.len() - trailing);
455            if digits.is_empty() {
456                return Self {
457                    negative: false,
458                    digits,
459                    exponent: 0,
460                };
461            }
462            let exponent = if exponent_negative {
463                -exponent
464            } else {
465                exponent
466            };
467            Self {
468                negative,
469                digits,
470                exponent: exponent
471                    .saturating_sub(fraction)
472                    .saturating_add(trailing as i64),
473            }
474        }
475
476        /// A primitive limit as the decimal it prints as; `None` for infinities and NaN.
477        pub fn of_limit<T: Copy + ToPrimitive>(limit: T) -> Option<Self> {
478            let float = limit.to_f64()?;
479            if float.fract() == 0.0 {
480                if let Some(integer) = limit.to_i64() {
481                    return Some(Self::parse(&integer.to_string()));
482                }
483                if let Some(integer) = limit.to_u64() {
484                    return Some(Self::parse(&integer.to_string()));
485                }
486            }
487            float
488                .is_finite()
489                .then(|| Self::parse(zmij::Buffer::new().format_finite(float)))
490        }
491
492        pub fn of_integer(value: &BigInt) -> Self {
493            Self::parse(&value.to_string())
494        }
495
496        /// `None` when no finite decimal writes `value`.
497        pub fn of_fraction(value: &BigFraction) -> Option<Self> {
498            use num_bigint::BigUint;
499            use num_traits::One;
500
501            let (numerator, denominator) = (value.numer()?, value.denom()?);
502            // Scaling by ten clears one factor of two and one of five, so a finite decimal exists
503            // exactly when those are the denominator's only prime factors.
504            let twos = denominator.trailing_zeros().unwrap_or(0);
505            let mut rest = denominator >> twos;
506            let five = BigUint::from(5_u8);
507            let mut fives = 0_u64;
508            while (&rest % &five).is_zero() {
509                rest /= &five;
510                fives += 1;
511            }
512            if !rest.is_one() {
513                return None;
514            }
515            let places = twos.max(fives);
516            let digits = numerator
517                * BigUint::from(2_u8).pow(u32::try_from(places - twos).ok()?)
518                * five.pow(u32::try_from(places - fives).ok()?);
519            let sign = if value.is_sign_negative() { "-" } else { "" };
520            Some(Self::parse(&format!("{sign}{digits}e-{places}")))
521        }
522
523        pub fn is_integer(&self) -> bool {
524            self.exponent >= 0
525        }
526
527        pub fn is_multiple_of(&self, multiple: &Self) -> bool {
528            use num_bigint::BigUint;
529
530            if self.digits.is_empty() {
531                return true;
532            }
533            if multiple.digits.is_empty() {
534                return false;
535            }
536            // `self / multiple` is `self.digits / (multiple.digits * 10^-shift)`. A negative shift
537            // asks for `self.digits` to divide by ten, which a value with no trailing zero never does.
538            let Ok(shift) =
539                u64::try_from(i128::from(self.exponent) - i128::from(multiple.exponent))
540            else {
541                return false;
542            };
543            let divisor = digit_chunks(&multiple.digits)
544                .fold(BigUint::zero(), |value, (scale, chunk)| {
545                    value * scale + chunk
546                });
547            let remainder = digit_chunks(&self.digits)
548                .fold(BigUint::zero(), |value, (scale, chunk)| {
549                    (value * scale + chunk) % &divisor
550                });
551            let scale = BigUint::from(10_u8).modpow(&BigUint::from(shift), &divisor);
552            (remainder * scale % &divisor).is_zero()
553        }
554    }
555
556    /// `digits` in chunks of 19, each within `u64`, paired with the power of ten it shifts by.
557    fn digit_chunks(digits: &[u8]) -> impl Iterator<Item = (num_bigint::BigUint, u64)> + '_ {
558        digits.chunks(19).map(|chunk| {
559            let value = chunk
560                .iter()
561                .fold(0_u64, |value, byte| value * 10 + u64::from(byte - b'0'));
562            (
563                num_bigint::BigUint::from(10_u64.pow(chunk.len() as u32)),
564                value,
565            )
566        })
567    }
568
569    impl PartialOrd for Decimal {
570        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
571            Some(self.cmp(other))
572        }
573    }
574
575    impl Ord for Decimal {
576        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
577            let sign = |value: &Self| match (value.digits.is_empty(), value.negative) {
578                (true, _) => 0,
579                (false, true) => -1,
580                (false, false) => 1,
581            };
582            // The position of the leading digit orders magnitudes first; with no trailing zeros,
583            // digit strings sharing that position order lexicographically.
584            let leading = |value: &Self| value.digits.len() as i128 + i128::from(value.exponent);
585            sign(self).cmp(&sign(other)).then_with(|| {
586                let magnitude = leading(self)
587                    .cmp(&leading(other))
588                    .then_with(|| self.digits.cmp(&other.digits));
589                if self.negative {
590                    magnitude.reverse()
591                } else {
592                    magnitude
593                }
594            })
595        }
596    }
597
598    #[derive(Debug, Clone)]
599    struct DecimalComponents {
600        negative: bool,
601        digits: String,
602        fraction_digits: usize,
603        exponent: i64,
604    }
605
606    impl DecimalComponents {
607        fn parse(num_str: &str) -> Option<Self> {
608            let bytes = num_str.as_bytes();
609            if bytes.is_empty() {
610                return None;
611            }
612
613            let mut idx = 0;
614            let negative = if bytes[idx] == b'-' {
615                idx += 1;
616                true
617            } else {
618                false
619            };
620
621            if idx >= bytes.len() {
622                return None;
623            }
624
625            let mut digits = String::with_capacity(bytes.len());
626            let int_start = idx;
627            while idx < bytes.len() && bytes[idx].is_ascii_digit() {
628                idx += 1;
629            }
630            if int_start == idx {
631                return None;
632            }
633            digits.push_str(&num_str[int_start..idx]);
634
635            let mut fraction_digits = 0usize;
636            if idx < bytes.len() && bytes[idx] == b'.' {
637                idx += 1;
638                let frac_start = idx;
639                while idx < bytes.len() && bytes[idx].is_ascii_digit() {
640                    idx += 1;
641                }
642                if frac_start == idx {
643                    return None;
644                }
645                digits.push_str(&num_str[frac_start..idx]);
646                fraction_digits = idx - frac_start;
647            }
648
649            let mut exponent: i64 = 0;
650            if idx < bytes.len() && (bytes[idx] == b'e' || bytes[idx] == b'E') {
651                idx += 1;
652                if idx >= bytes.len() {
653                    return None;
654                }
655                let mut exp_sign: i64 = 1;
656                if bytes[idx] == b'+' {
657                    idx += 1;
658                } else if bytes[idx] == b'-' {
659                    exp_sign = -1;
660                    idx += 1;
661                }
662                let exp_start = idx;
663                while idx < bytes.len() && bytes[idx].is_ascii_digit() {
664                    idx += 1;
665                }
666                if exp_start == idx {
667                    return None;
668                }
669                let exp_value = num_str[exp_start..idx].parse::<i64>().ok()?;
670                exponent = exp_value.checked_mul(exp_sign)?;
671            }
672
673            if idx != bytes.len() {
674                return None;
675            }
676
677            Some(Self {
678                negative,
679                digits,
680                fraction_digits,
681                exponent,
682            })
683        }
684
685        #[inline]
686        fn decimal_shift(&self) -> i64 {
687            self.exponent - self.fraction_digits as i64
688        }
689    }
690
691    fn digits_are_zero(s: &str) -> bool {
692        s.bytes().all(|b| b == b'0')
693    }
694
695    fn trailing_zero_count(s: &str) -> usize {
696        s.as_bytes()
697            .iter()
698            .rev()
699            .take_while(|b| **b == b'0')
700            .count()
701    }
702
703    fn append_zeros(target: &mut String, count: usize) -> Option<()> {
704        let new_len = target.len().checked_add(count)?;
705        target.reserve(count);
706        target.extend(std::iter::repeat_n('0', count));
707        debug_assert_eq!(target.len(), new_len);
708        Some(())
709    }
710
711    fn pow10_bigint(exp: usize) -> Option<BigInt> {
712        if exp == 0 {
713            return Some(BigInt::from(1));
714        }
715        let exp_u32 = u32::try_from(exp).ok()?;
716        Some(BigInt::from(10).pow(exp_u32))
717    }
718
719    /// The rational a whole number denotes. `BigFraction::from` writes the integer out in decimal
720    /// and reads it back one digit at a time, while a magnitude over one is already the reduced form.
721    pub fn whole(value: BigInt) -> BigFraction {
722        let (sign, magnitude) = value.into_parts();
723        let fraction = BigFraction::new_raw(magnitude, num_bigint::BigUint::from(1_u8));
724        if sign == num_bigint::Sign::Minus {
725            -fraction
726        } else {
727            fraction
728        }
729    }
730
731    /// Try to parse a Number as `BigInt` if it's outside i64 range or for compile-time
732    /// schema values that need exact representation
733    pub fn try_parse_bigint(num: &Number) -> Option<BigInt> {
734        use super::MAX_SAFE_INTEGER;
735
736        let num_str = num.as_str();
737
738        // Parse as BigInt if it's beyond 2^53 (where f64 loses precision).
739        // Values beyond 2^53 need BigInt for accurate arithmetic even if they fit in i64/u64.
740        // Note: If as_i64() fails but as_u64() succeeds, the value is in [2^63, 2^64-1],
741        // which is always > 2^53, so no additional check needed for u64.
742        if let Some(v) = num.as_i64() {
743            if v.unsigned_abs() <= MAX_SAFE_INTEGER {
744                return None;
745            }
746        }
747
748        let has_exponent = has_exponent(num_str);
749        if exceeds_digit_budget_with(num_str, has_exponent) {
750            return None;
751        }
752
753        if !has_exponent && !num_str.as_bytes().contains(&b'.') {
754            return BigInt::from_str(num_str).ok();
755        }
756
757        let mut components = DecimalComponents::parse(num_str)?;
758        let mut shift = components.decimal_shift();
759
760        if shift < 0 {
761            let needed = (-shift) as usize;
762            if digits_are_zero(&components.digits) {
763                components.digits.clear();
764                components.digits.push('0');
765                shift = 0;
766            } else {
767                let zeros = trailing_zero_count(&components.digits);
768                if zeros < needed {
769                    return None;
770                }
771                let new_len = components.digits.len() - needed;
772                components.digits.truncate(new_len);
773                shift = 0;
774            }
775        }
776
777        if shift > 0 {
778            append_zeros(&mut components.digits, shift as usize)?;
779        }
780
781        let digits_trimmed = components.digits.trim_start_matches('0');
782        let digits_ref = if digits_trimmed.is_empty() {
783            "0"
784        } else {
785            digits_trimmed
786        };
787        let mut value = BigInt::from_str(digits_ref).ok()?;
788        if components.negative && !value.is_zero() {
789            value = -value;
790        }
791        Some(value)
792    }
793
794    /// Try to parse a Number as `BigFraction` for arbitrary precision decimal support
795    ///
796    /// Returns Some for numbers requiring exact decimal precision:
797    /// - Decimals with a decimal point (e.g., `0.1`, `123.456`)
798    /// - Scientific notation decimals that can't be represented exactly as f64
799    ///
800    /// Returns None for:
801    /// - Integers that fit in i64 (handled by standard numeric path)
802    /// - Large integers including u64 beyond `i64::MAX` (handled by `try_parse_bigint`)
803    pub fn try_parse_bigfraction(num: &Number) -> Option<BigFraction> {
804        // Skip integers that fit in i64 - they don't need BigFraction
805        if num.as_i64().is_some() {
806            return None;
807        }
808
809        let num_str = num.as_str();
810
811        let has_exponent = has_exponent(num_str);
812        if !has_exponent && !num_str.as_bytes().contains(&b'.') {
813            return None;
814        }
815
816        if exceeds_digit_budget_with(num_str, has_exponent) {
817            return None;
818        }
819
820        if !has_exponent {
821            return BigFraction::from_str(num_str).ok();
822        }
823
824        let components = DecimalComponents::parse(num_str)?;
825        let shift = components.decimal_shift();
826
827        // A number with exponent that still resolves to an integer is handled by BigInt.
828        if shift >= 0 {
829            return None;
830        }
831
832        let denom_power = (-shift) as usize;
833        let denominator = pow10_bigint(denom_power)?;
834        let mut numerator = BigInt::from_str(&components.digits).ok()?;
835        if components.negative && !numerator.is_zero() {
836            numerator = -numerator;
837        }
838        Some(whole(numerator) / whole(denominator))
839    }
840
841    /// Exact ordering of a big-integer instance against a numeric limit.
842    ///
843    /// Integer-representable limits compare via `BigInt`; infinite limits (schema numbers
844    /// beyond the exponent cap) order every finite instance. `None` means the limit has no
845    /// exact integer form and the caller should fall back to `f64` comparison.
846    pub(crate) fn compare_bigint_to_limit<T>(big: &BigInt, limit: T) -> Option<std::cmp::Ordering>
847    where
848        T: Copy + ToPrimitive,
849    {
850        use std::cmp::Ordering;
851
852        let limit_f64 = limit.to_f64()?;
853        if limit_f64.fract() == 0.0 {
854            // `to_i64`/`to_u64` are exact for u64/i64 limits and for integer-valued f64 limits.
855            if let Some(limit_int) = limit.to_i64() {
856                return Some(big.cmp(&BigInt::from(limit_int)));
857            }
858            if let Some(limit_int) = limit.to_u64() {
859                return Some(big.cmp(&BigInt::from(limit_int)));
860            }
861        }
862        if limit_f64 == f64::INFINITY {
863            return Some(Ordering::Less);
864        }
865        if limit_f64 == f64::NEG_INFINITY {
866            return Some(Ordering::Greater);
867        }
868        None
869    }
870
871    /// The limit as an exact fraction, for instances that only have a rational form.
872    ///
873    /// `None` where the limit itself is not an exact integer, leaving the caller on `f64`.
874    fn limit_as_bigfraction<T>(limit: T) -> Option<BigFraction>
875    where
876        T: Copy + ToPrimitive,
877    {
878        if limit.to_f64()?.fract() != 0.0 {
879            return None;
880        }
881        if let Some(limit_int) = limit.to_i64() {
882            return Some(BigFraction::from(limit_int));
883        }
884        limit.to_u64().map(BigFraction::from)
885    }
886
887    /// Exact ordering of a JSON number literal against a numeric limit.
888    ///
889    /// `None` means no exact form is available on one side and the caller should fall back to
890    /// `f64` comparison.
891    pub(crate) fn compare_to_limit<T>(num: &Number, limit: T) -> Option<std::cmp::Ordering>
892    where
893        T: Copy + ToPrimitive,
894    {
895        if let Some(value) = past_digit_budget(num.as_str()) {
896            return Some(value.cmp(&Decimal::of_limit(limit)?));
897        }
898        if let Some(big) = try_parse_bigint(num) {
899            return compare_bigint_to_limit(&big, limit);
900        }
901        let value = try_parse_bigfraction(num)?;
902        value.partial_cmp(&limit_as_bigfraction(limit)?)
903    }
904
905    macro_rules! define_bigint_cmp {
906        ($($fn_name:ident, $prim_type:ty, $to_prim:ident, $op:tt, $overflow_sign:expr);* $(;)?) => {
907            $(
908                pub fn $fn_name(bigint: &BigInt, value: $prim_type) -> bool {
909                    if let Some(converted) = bigint.$to_prim() {
910                        converted $op value
911                    } else {
912                        bigint.sign() == $overflow_sign
913                    }
914                }
915            )*
916        };
917    }
918
919    define_bigint_cmp!(
920        bigint_ge_u64, u64, to_u64, >=, num_bigint::Sign::Plus;
921        bigint_le_u64, u64, to_u64, <=, num_bigint::Sign::Minus;
922        bigint_gt_u64, u64, to_u64, >, num_bigint::Sign::Plus;
923        bigint_lt_u64, u64, to_u64, <, num_bigint::Sign::Minus;
924        bigint_ge_i64, i64, to_i64, >=, num_bigint::Sign::Plus;
925        bigint_le_i64, i64, to_i64, <=, num_bigint::Sign::Minus;
926        bigint_gt_i64, i64, to_i64, >, num_bigint::Sign::Plus;
927        bigint_lt_i64, i64, to_i64, <, num_bigint::Sign::Minus;
928        bigint_ge_f64, f64, to_f64, >=, num_bigint::Sign::Plus;
929        bigint_le_f64, f64, to_f64, <=, num_bigint::Sign::Minus;
930        bigint_gt_f64, f64, to_f64, >, num_bigint::Sign::Plus;
931        bigint_lt_f64, f64, to_f64, <, num_bigint::Sign::Minus;
932    );
933
934    // Generate reverse comparison functions (primitive op BigType -> BigType op primitive)
935    macro_rules! define_reverse_cmp {
936        ($($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);* $(;)?) => {
937            $(
938                pub fn $rev_ge(value: $prim_type, big: &$big_type) -> bool {
939                    $fwd_le(big, value)
940                }
941
942                pub fn $rev_le(value: $prim_type, big: &$big_type) -> bool {
943                    $fwd_ge(big, value)
944                }
945
946                pub fn $rev_gt(value: $prim_type, big: &$big_type) -> bool {
947                    $fwd_lt(big, value)
948                }
949
950                pub fn $rev_lt(value: $prim_type, big: &$big_type) -> bool {
951                    $fwd_gt(big, value)
952                }
953            )*
954        };
955    }
956
957    define_reverse_cmp!(
958        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;
959        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;
960        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;
961    );
962
963    /// Check if a Number (as `BigInt`) is a multiple of another `BigInt`
964    pub fn is_multiple_of_bigint(value: &BigInt, multiple: &BigInt) -> bool {
965        // Zero is a multiple of any non-zero number
966        // Mathematically: 0 = k * multiple for k = 0
967        if value.is_zero() {
968            return true;
969        }
970
971        // Note: multiple.is_zero() case is not handled here because JSON Schema
972        // validation rejects schemas with "multipleOf: 0" during compilation
973        // (exclusiveMinimum constraint requires multipleOf > 0).
974        // The modulo operation below would panic if multiple is zero, but this
975        // is prevented by schema validation.
976
977        (value % multiple).is_zero()
978    }
979
980    // BigFraction comparison functions
981    macro_rules! define_bigfraction_cmp {
982        ($($fn_name:ident, $prim_type:ty, $op:tt);* $(;)?) => {
983            $(
984                pub fn $fn_name(bigfrac: &BigFraction, value: $prim_type) -> bool {
985                    let value_frac = BigFraction::from(value);
986                    *bigfrac $op value_frac
987                }
988            )*
989        };
990    }
991
992    define_bigfraction_cmp!(
993        bigfrac_ge_u64, u64, >=;
994        bigfrac_le_u64, u64, <=;
995        bigfrac_gt_u64, u64, >;
996        bigfrac_lt_u64, u64, <;
997        bigfrac_ge_i64, i64, >=;
998        bigfrac_le_i64, i64, <=;
999        bigfrac_gt_i64, i64, >;
1000        bigfrac_lt_i64, i64, <;
1001        bigfrac_ge_f64, f64, >=;
1002        bigfrac_le_f64, f64, <=;
1003        bigfrac_gt_f64, f64, >;
1004        bigfrac_lt_f64, f64, <;
1005    );
1006
1007    define_reverse_cmp!(
1008        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;
1009        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;
1010        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;
1011    );
1012
1013    /// Check if a `BigFraction` is a multiple of another value
1014    pub fn is_multiple_of_bigfrac(value: &BigFraction, multiple: &BigFraction) -> bool {
1015        // Zero is a multiple of any non-zero number
1016        if value.is_zero() {
1017            return true;
1018        }
1019        // Division by zero is undefined, so return false
1020        if multiple.is_zero() {
1021            return false;
1022        }
1023        // A number is a multiple of another if division results in an integer
1024        // (denominator of the result is 1)
1025        (value / multiple).denom().is_none_or(fraction::One::is_one)
1026    }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::{decimal_parts, divides_exactly};
1032    use test_case::test_case;
1033
1034    #[test_case(0.1, Some((1, 1)); "leading zero")]
1035    #[test_case(2.675, Some((2675, 3)); "three decimals")]
1036    #[test_case(-0.25, Some((-25, 2)); "negative")]
1037    // `zmij` always writes a fractional part, so an integral value scales by ten.
1038    #[test_case(7.0, Some((70, 1)); "integral")]
1039    #[test_case(1e-7, Some((1, 7)); "negative exponent")]
1040    #[test_case(1e300, Some((1, -300)); "positive exponent")]
1041    #[test_case(f64::NAN, None; "not a number")]
1042    #[test_case(f64::INFINITY, None; "infinite")]
1043    fn decimal_parts_reads_the_printed_decimal(value: f64, expected: Option<(i128, i32)>) {
1044        assert_eq!(decimal_parts(value), expected);
1045    }
1046
1047    // `BigFraction::from(f64)` rounds these into non-multiples; the decimal reading does not.
1048    #[test_case(1_070_468.14, 0.01, true; "large amount of cents")]
1049    #[test_case(1_070_468.13, 0.01, true; "another large amount of cents")]
1050    #[test_case(1_070_468.145, 0.01, false; "large amount of half cents")]
1051    #[test_case(19.99, 0.01, true; "small amount of cents")]
1052    #[test_case(0.0075, 0.0001, true; "fourth decimal place")]
1053    #[test_case(5.35, 2.675, true; "fractional divisor")]
1054    #[test_case(505_661.899_999_999_97, 0.1, false; "seventeen significant digits")]
1055    fn divides_exactly_answers(value: f64, multiple: f64, expected: bool) {
1056        assert_eq!(divides_exactly(value, multiple), Some(expected));
1057    }
1058
1059    #[test_case(1e300; "too large to scale into i128")]
1060    #[test_case(1e-300; "too small to scale into i128")]
1061    fn divides_exactly_defers_out_of_range(value: f64) {
1062        assert_eq!(divides_exactly(value, 0.01), None);
1063    }
1064}
1065
1066#[cfg(all(test, feature = "arbitrary-precision"))]
1067mod bignum_tests {
1068    use crate::numeric::bignum;
1069    use fraction::BigFraction;
1070    use num_bigint::BigInt;
1071    use serde_json::{Number, Value};
1072    use std::cmp::Ordering;
1073    use test_case::test_case;
1074
1075    fn number_from_str(raw: &str) -> Number {
1076        match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
1077            Value::Number(num) => num,
1078            _ => unreachable!(),
1079        }
1080    }
1081
1082    #[test_case("18446744073709551616", u64::MAX, Ordering::Greater; "above u64 limit")]
1083    fn compare_bigint_to_u64_limit(big: &str, limit: u64, expected: Ordering) {
1084        let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
1085        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
1086    }
1087
1088    #[test_case("-18446744073709551616", i64::MIN, Ordering::Less; "below i64 limit")]
1089    fn compare_bigint_to_i64_limit(big: &str, limit: i64, expected: Ordering) {
1090        let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
1091        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
1092    }
1093
1094    // Infinity limits come from schema numbers beyond the exponent cap (e.g. `1e2000000`);
1095    // limits without an exact integer form defer to the caller's f64 comparison.
1096    #[test_case(f64::INFINITY, Some(Ordering::Less); "infinity limit")]
1097    #[test_case(f64::NEG_INFINITY, Some(Ordering::Greater); "negative infinity limit")]
1098    #[test_case(0.5, None; "no exact integer form")]
1099    fn compare_bigint_to_f64_limit(limit: f64, expected: Option<Ordering>) {
1100        let big = BigInt::parse_bytes(b"18446744073709551616", 10).unwrap();
1101        assert_eq!(bignum::compare_bigint_to_limit(&big, limit), expected);
1102    }
1103
1104    #[test]
1105    fn bigint_parses_scientific_integer() {
1106        let num = number_from_str("1e19");
1107        let parsed = bignum::try_parse_bigint(&num).expect("parsed bigint");
1108        assert_eq!(
1109            parsed,
1110            BigInt::parse_bytes(b"10000000000000000000", 10).unwrap()
1111        );
1112    }
1113
1114    #[test]
1115    fn bigint_rejects_non_integer_scientific() {
1116        let num = number_from_str("1.25e1");
1117        assert!(bignum::try_parse_bigint(&num).is_none());
1118    }
1119
1120    #[test]
1121    fn bigfraction_parses_scientific_decimal() {
1122        let num = number_from_str("1.5e-5");
1123        let parsed = bignum::try_parse_bigfraction(&num).expect("parsed bigfraction");
1124        let expected =
1125            BigFraction::from(BigInt::from(3)) / BigFraction::from(BigInt::from(200_000));
1126        assert_eq!(parsed, expected);
1127    }
1128
1129    #[test]
1130    fn bigfraction_skips_scientific_integer() {
1131        let num = number_from_str("3e4");
1132        assert!(bignum::try_parse_bigfraction(&num).is_none());
1133    }
1134}
1135
1136#[cfg(all(test, feature = "arbitrary-precision"))]
1137mod exact_multiple_of_tests {
1138    use super::is_multiple_of_integer;
1139    use serde_json::{Number, Value};
1140    use test_case::test_case;
1141
1142    fn number(raw: &str) -> Number {
1143        match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
1144            Value::Number(num) => num,
1145            _ => unreachable!(),
1146        }
1147    }
1148
1149    // Integers past `u64` still divide exactly; rounding them into `f64` first answers about a
1150    // different number.
1151    #[test_case("135107988821114880000000000000", 3.0, true; "multiple of three")]
1152    #[test_case("135107988821114880000000000001", 3.0, false; "one past a multiple of three")]
1153    #[test_case("135107988821114880000000000002", 3.0, false; "two past a multiple of three")]
1154    #[test_case("18446744073709551617", 2.0, false; "odd just past u64")]
1155    #[test_case("18446744073709551618", 2.0, true; "even just past u64")]
1156    #[test_case("1e30", 3.0, false; "scientific not a multiple")]
1157    #[test_case("1e30", 2.0, true; "scientific is a multiple")]
1158    fn exact_beyond_u64(value: &str, divisor: f64, expected: bool) {
1159        assert_eq!(is_multiple_of_integer(&number(value), divisor), expected);
1160    }
1161}