Skip to main content

jsonschema_value/
numeric_check.rs

1#![allow(clippy::must_use_candidate)]
2
3use serde_json::Number;
4
5use super::numeric;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum BoundOp {
9    Lt,
10    Lte,
11    Gt,
12    Gte,
13}
14
15impl BoundOp {
16    pub fn from_u8(value: u8) -> Option<Self> {
17        match value {
18            0 => Some(Self::Lt),
19            1 => Some(Self::Lte),
20            2 => Some(Self::Gt),
21            3 => Some(Self::Gte),
22            _ => None,
23        }
24    }
25}
26
27// An `f64` limit is exact only up to 2^53, so an integer limit keeps its own width and compares
28// against the instance in exact arithmetic.
29#[derive(Clone, Debug, PartialEq)]
30pub enum CompiledBound {
31    F64 {
32        op: BoundOp,
33        limit: f64,
34    },
35    #[cfg(not(feature = "arbitrary-precision"))]
36    I64 {
37        op: BoundOp,
38        limit: i64,
39    },
40    #[cfg(not(feature = "arbitrary-precision"))]
41    U64 {
42        op: BoundOp,
43        limit: u64,
44    },
45    #[cfg(feature = "arbitrary-precision")]
46    BigInt {
47        op: BoundOp,
48        limit: num_bigint::BigInt,
49    },
50    #[cfg(feature = "arbitrary-precision")]
51    BigFrac {
52        op: BoundOp,
53        limit: fraction::BigFraction,
54    },
55    /// A limit with more digits than exact arithmetic takes on.
56    #[cfg(feature = "arbitrary-precision")]
57    Decimal {
58        op: BoundOp,
59        limit: numeric::bignum::Decimal,
60    },
61}
62
63// Codegen inlines every multipleOf that fits f64 and only routes arbitrary-precision
64// divisors through `compile_multiple_of`, so only the big variants are ever constructed.
65#[derive(Clone, Debug, PartialEq)]
66pub enum CompiledMultipleOf {
67    #[cfg(feature = "arbitrary-precision")]
68    BigInt(num_bigint::BigInt),
69    #[cfg(feature = "arbitrary-precision")]
70    BigFrac(fraction::BigFraction),
71    #[cfg(feature = "arbitrary-precision")]
72    Decimal(numeric::bignum::Decimal),
73    Unsupported,
74}
75
76#[inline]
77fn check_primitive_bound<T>(op: BoundOp, value: &Number, limit: T) -> bool
78where
79    T: Copy + num_traits::ToPrimitive,
80    u64: num_cmp::NumCmp<T>,
81    i64: num_cmp::NumCmp<T>,
82    f64: num_cmp::NumCmp<T>,
83{
84    match op {
85        BoundOp::Lt => numeric::lt(value, limit),
86        BoundOp::Lte => numeric::le(value, limit),
87        BoundOp::Gt => numeric::gt(value, limit),
88        BoundOp::Gte => numeric::ge(value, limit),
89    }
90}
91
92#[cfg(feature = "arbitrary-precision")]
93#[inline]
94fn infinity_cmp(op: BoundOp, is_negative: bool) -> bool {
95    match op {
96        BoundOp::Gte | BoundOp::Gt => !is_negative,
97        BoundOp::Lte | BoundOp::Lt => is_negative,
98    }
99}
100
101#[cfg(feature = "arbitrary-precision")]
102fn holds(op: BoundOp, ordering: std::cmp::Ordering) -> bool {
103    match op {
104        BoundOp::Lt => ordering.is_lt(),
105        BoundOp::Lte => ordering.is_le(),
106        BoundOp::Gt => ordering.is_gt(),
107        BoundOp::Gte => ordering.is_ge(),
108    }
109}
110
111#[cfg(feature = "arbitrary-precision")]
112#[inline]
113fn check_bigint_bound(op: BoundOp, limit: &num_bigint::BigInt, value: &Number) -> bool {
114    if let Some(instance) = numeric::bignum::past_digit_budget(value.as_str()) {
115        return holds(
116            op,
117            instance.cmp(&numeric::bignum::Decimal::of_integer(limit)),
118        );
119    }
120    if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
121        return match op {
122            BoundOp::Lt => instance_bigint < *limit,
123            BoundOp::Lte => instance_bigint <= *limit,
124            BoundOp::Gt => instance_bigint > *limit,
125            BoundOp::Gte => instance_bigint >= *limit,
126        };
127    }
128
129    if let Some(v) = value.as_u64() {
130        return match op {
131            BoundOp::Lt => numeric::bignum::u64_lt_bigint(v, limit),
132            BoundOp::Lte => numeric::bignum::u64_le_bigint(v, limit),
133            BoundOp::Gt => numeric::bignum::u64_gt_bigint(v, limit),
134            BoundOp::Gte => numeric::bignum::u64_ge_bigint(v, limit),
135        };
136    }
137
138    if let Some(v) = value.as_i64() {
139        return match op {
140            BoundOp::Lt => numeric::bignum::i64_lt_bigint(v, limit),
141            BoundOp::Lte => numeric::bignum::i64_le_bigint(v, limit),
142            BoundOp::Gt => numeric::bignum::i64_gt_bigint(v, limit),
143            BoundOp::Gte => numeric::bignum::i64_ge_bigint(v, limit),
144        };
145    }
146
147    if let Some(v) = value.as_f64() {
148        return match op {
149            BoundOp::Lt => numeric::bignum::f64_lt_bigint(v, limit),
150            BoundOp::Lte => numeric::bignum::f64_le_bigint(v, limit),
151            BoundOp::Gt => numeric::bignum::f64_gt_bigint(v, limit),
152            BoundOp::Gte => numeric::bignum::f64_ge_bigint(v, limit),
153        };
154    }
155
156    if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
157        let limit_frac = numeric::bignum::whole(limit.clone());
158        return match op {
159            BoundOp::Lt => instance_bigfrac < limit_frac,
160            BoundOp::Lte => instance_bigfrac <= limit_frac,
161            BoundOp::Gt => instance_bigfrac > limit_frac,
162            BoundOp::Gte => instance_bigfrac >= limit_frac,
163        };
164    }
165
166    infinity_cmp(op, value.as_str().starts_with('-'))
167}
168
169#[cfg(feature = "arbitrary-precision")]
170#[inline]
171fn check_bigfrac_bound(op: BoundOp, limit: &fraction::BigFraction, value: &Number) -> bool {
172    if let Some(instance) = numeric::bignum::past_digit_budget(value.as_str()) {
173        if let Some(limit) = numeric::bignum::Decimal::of_fraction(limit) {
174            return holds(op, instance.cmp(&limit));
175        }
176    }
177    if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
178        return match op {
179            BoundOp::Lt => instance_bigfrac < *limit,
180            BoundOp::Lte => instance_bigfrac <= *limit,
181            BoundOp::Gt => instance_bigfrac > *limit,
182            BoundOp::Gte => instance_bigfrac >= *limit,
183        };
184    }
185
186    if let Some(v) = value.as_u64() {
187        return match op {
188            BoundOp::Lt => numeric::bignum::u64_lt_bigfrac(v, limit),
189            BoundOp::Lte => numeric::bignum::u64_le_bigfrac(v, limit),
190            BoundOp::Gt => numeric::bignum::u64_gt_bigfrac(v, limit),
191            BoundOp::Gte => numeric::bignum::u64_ge_bigfrac(v, limit),
192        };
193    }
194
195    if let Some(v) = value.as_i64() {
196        return match op {
197            BoundOp::Lt => numeric::bignum::i64_lt_bigfrac(v, limit),
198            BoundOp::Lte => numeric::bignum::i64_le_bigfrac(v, limit),
199            BoundOp::Gt => numeric::bignum::i64_gt_bigfrac(v, limit),
200            BoundOp::Gte => numeric::bignum::i64_ge_bigfrac(v, limit),
201        };
202    }
203
204    // An integer past i64 is exact as a BigInt, while `f64` rounds it onto limits a fractional
205    // digit separates it from, e.g. `-10000000000000000000000000` against the same with `.1`.
206    if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
207        let instance_frac = numeric::bignum::whole(instance_bigint);
208        return match op {
209            BoundOp::Lt => instance_frac < *limit,
210            BoundOp::Lte => instance_frac <= *limit,
211            BoundOp::Gt => instance_frac > *limit,
212            BoundOp::Gte => instance_frac >= *limit,
213        };
214    }
215
216    if let Some(v) = value.as_f64() {
217        return match op {
218            BoundOp::Lt => numeric::bignum::f64_lt_bigfrac(v, limit),
219            BoundOp::Lte => numeric::bignum::f64_le_bigfrac(v, limit),
220            BoundOp::Gt => numeric::bignum::f64_gt_bigfrac(v, limit),
221            BoundOp::Gte => numeric::bignum::f64_ge_bigfrac(v, limit),
222        };
223    }
224
225    // Dynamic BigFraction validators treat this branch as valid because
226    // extremely large scientific notation cannot be compared reliably.
227    true
228}
229
230pub fn compile_bound(op: BoundOp, limit: &Number) -> CompiledBound {
231    #[cfg(feature = "arbitrary-precision")]
232    {
233        if let Some(limit) = numeric::bignum::past_digit_budget(limit.as_str()) {
234            return CompiledBound::Decimal { op, limit };
235        }
236        if let Some(value) = numeric::bignum::try_parse_bigint(limit) {
237            return CompiledBound::BigInt { op, limit: value };
238        }
239        if let Some(value) = numeric::bignum::try_parse_bigfraction(limit) {
240            return CompiledBound::BigFrac { op, limit: value };
241        }
242    }
243
244    #[cfg(not(feature = "arbitrary-precision"))]
245    {
246        if let Some(value) = limit.as_i64() {
247            return CompiledBound::I64 { op, limit: value };
248        }
249        if let Some(value) = limit.as_u64() {
250            return CompiledBound::U64 { op, limit: value };
251        }
252    }
253
254    if let Some(value) = limit.as_f64() {
255        return CompiledBound::F64 { op, limit: value };
256    }
257
258    // Past `f64`; reachable without `arbitrary-precision` once another crate in the build enables
259    // `serde_json/arbitrary_precision`.
260    let limit = if crate::JsonNumber::as_str(limit).starts_with('-') {
261        f64::NEG_INFINITY
262    } else {
263        f64::INFINITY
264    };
265    CompiledBound::F64 { op, limit }
266}
267
268pub fn check_bound(compiled: &CompiledBound, value: &Number) -> bool {
269    match compiled {
270        CompiledBound::F64 { op, limit } => check_primitive_bound(*op, value, *limit),
271        #[cfg(not(feature = "arbitrary-precision"))]
272        CompiledBound::I64 { op, limit } => check_primitive_bound(*op, value, *limit),
273        #[cfg(not(feature = "arbitrary-precision"))]
274        CompiledBound::U64 { op, limit } => check_primitive_bound(*op, value, *limit),
275        #[cfg(feature = "arbitrary-precision")]
276        CompiledBound::BigInt { op, limit } => check_bigint_bound(*op, limit, value),
277        #[cfg(feature = "arbitrary-precision")]
278        CompiledBound::BigFrac { op, limit } => check_bigfrac_bound(*op, limit, value),
279        #[cfg(feature = "arbitrary-precision")]
280        CompiledBound::Decimal { op, limit } => holds(
281            *op,
282            numeric::bignum::Decimal::parse(value.as_str()).cmp(limit),
283        ),
284    }
285}
286
287#[cfg(feature = "arbitrary-precision")]
288pub fn compile_multiple_of(multiple_of: &Number) -> CompiledMultipleOf {
289    #[cfg(feature = "arbitrary-precision")]
290    {
291        if let Some(value) = numeric::bignum::past_digit_budget(multiple_of.as_str()) {
292            return CompiledMultipleOf::Decimal(value);
293        }
294        if let Some(value) = numeric::bignum::try_parse_bigint(multiple_of) {
295            return CompiledMultipleOf::BigInt(value);
296        }
297        if let Some(value) = numeric::bignum::try_parse_bigfraction(multiple_of) {
298            return CompiledMultipleOf::BigFrac(value);
299        }
300    }
301
302    CompiledMultipleOf::Unsupported
303}
304
305#[cfg(feature = "arbitrary-precision")]
306pub fn check_multiple_of(compiled: &CompiledMultipleOf, value: &Number) -> bool {
307    use numeric::bignum::Decimal;
308
309    if let Some(instance) = numeric::bignum::past_digit_budget(value.as_str()) {
310        let multiple = match compiled {
311            CompiledMultipleOf::BigInt(multiple) => Some(Decimal::of_integer(multiple)),
312            CompiledMultipleOf::BigFrac(multiple) => Decimal::of_fraction(multiple),
313            CompiledMultipleOf::Decimal(multiple) => Some(multiple.clone()),
314            CompiledMultipleOf::Unsupported => return true,
315        };
316        if let Some(multiple) = multiple {
317            return instance.is_multiple_of(&multiple);
318        }
319    }
320    match compiled {
321        #[cfg(feature = "arbitrary-precision")]
322        CompiledMultipleOf::BigInt(multiple) => {
323            use num_bigint::BigInt;
324
325            if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
326                return numeric::bignum::is_multiple_of_bigint(&instance_bigint, multiple);
327            }
328
329            if let Some(v) = value.as_u64() {
330                let v_bigint = BigInt::from(v);
331                return numeric::bignum::is_multiple_of_bigint(&v_bigint, multiple);
332            }
333
334            if let Some(v) = value.as_i64() {
335                let v_bigint = BigInt::from(v);
336                return numeric::bignum::is_multiple_of_bigint(&v_bigint, multiple);
337            }
338
339            false
340        }
341        #[cfg(feature = "arbitrary-precision")]
342        CompiledMultipleOf::BigFrac(multiple) => {
343            use num_traits::ToPrimitive;
344
345            if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
346                return numeric::bignum::is_multiple_of_bigfrac(&instance_bigfrac, multiple);
347            }
348
349            if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
350                let value_frac = numeric::bignum::whole(instance_bigint);
351                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
352            }
353
354            if let Some(v) = value.as_u64() {
355                let value_frac = fraction::BigFraction::from(v);
356                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
357            }
358
359            if let Some(v) = value.as_i64() {
360                let value_frac = fraction::BigFraction::from(v);
361                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
362            }
363
364            let multiple_f64 = multiple.to_f64().unwrap_or(f64::INFINITY);
365            numeric::is_multiple_of_float(value, multiple_f64)
366        }
367        CompiledMultipleOf::Decimal(multiple) => {
368            Decimal::parse(value.as_str()).is_multiple_of(multiple)
369        }
370        CompiledMultipleOf::Unsupported => true,
371    }
372}
373
374/// Which arithmetic the validator uses for a `multipleOf` divisor. A rewrite that moves a divisor
375/// between kinds can change verdicts, so only same-kind rewrites preserve membership.
376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
377pub enum DivisorKind {
378    /// Integer instances take exact integer modulo.
379    Whole,
380    /// A whole divisor past the exact-modulo guard, where instances go through `f64` remainder.
381    WholeLossy,
382    /// Every instance goes through rational division.
383    Fractional,
384}
385
386/// The arithmetic `divisor` selects, mirroring how `multipleOf` compiles.
387pub fn divisor_kind(divisor: &Number) -> DivisorKind {
388    #[cfg(feature = "arbitrary-precision")]
389    {
390        if numeric::bignum::exceeds_digit_budget(divisor.as_str()) {
391            return DivisorKind::Fractional;
392        }
393        if numeric::bignum::try_parse_bigint(divisor).is_some() {
394            return DivisorKind::Whole;
395        }
396        if numeric::bignum::try_parse_bigfraction(divisor).is_some() {
397            return DivisorKind::Fractional;
398        }
399    }
400    match divisor.as_f64() {
401        Some(value) if value.fract() != 0. => DivisorKind::Fractional,
402        // `is_multiple_of_integer` keeps exact modulo only while the divisor itself is exact.
403        Some(value) if value.abs() <= MAX_SAFE_INTEGER_F64 => DivisorKind::Whole,
404        // A divisor with no `f64` form only arises under arbitrary precision, where the exact
405        // parses above have already classified it.
406        _ => DivisorKind::WholeLossy,
407    }
408}
409
410const MAX_SAFE_INTEGER_F64: f64 = 9_007_199_254_740_992.0;
411
412/// Whether `value` satisfies `multipleOf: divisor`, deciding it the way the validator does.
413pub fn satisfies_multiple_of(divisor: &Number, value: &Number) -> bool {
414    #[cfg(feature = "arbitrary-precision")]
415    {
416        let compiled = compile_multiple_of(divisor);
417        if compiled != CompiledMultipleOf::Unsupported {
418            return check_multiple_of(&compiled, value);
419        }
420    }
421    // A divisor with no `f64` is one the validator skips, so every instance passes there.
422    divisor.as_f64().is_none_or(|limit| {
423        if limit.fract() == 0. {
424            numeric::is_multiple_of_integer(value, limit)
425        } else {
426            numeric::is_multiple_of_float(value, limit)
427        }
428    })
429}