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}
56
57// Codegen inlines every multipleOf that fits f64 and only routes arbitrary-precision
58// divisors through `compile_multiple_of`, so only the big variants are ever constructed.
59#[derive(Clone, Debug, PartialEq)]
60pub enum CompiledMultipleOf {
61    #[cfg(feature = "arbitrary-precision")]
62    BigInt(num_bigint::BigInt),
63    #[cfg(feature = "arbitrary-precision")]
64    BigFrac(fraction::BigFraction),
65    Unsupported,
66}
67
68#[inline]
69fn check_primitive_bound<T>(op: BoundOp, value: &Number, limit: T) -> bool
70where
71    T: Copy + num_traits::ToPrimitive,
72    u64: num_cmp::NumCmp<T>,
73    i64: num_cmp::NumCmp<T>,
74    f64: num_cmp::NumCmp<T>,
75{
76    match op {
77        BoundOp::Lt => numeric::lt(value, limit),
78        BoundOp::Lte => numeric::le(value, limit),
79        BoundOp::Gt => numeric::gt(value, limit),
80        BoundOp::Gte => numeric::ge(value, limit),
81    }
82}
83
84#[cfg(feature = "arbitrary-precision")]
85#[inline]
86fn infinity_cmp(op: BoundOp, is_negative: bool) -> bool {
87    match op {
88        BoundOp::Gte | BoundOp::Gt => !is_negative,
89        BoundOp::Lte | BoundOp::Lt => is_negative,
90    }
91}
92
93#[cfg(feature = "arbitrary-precision")]
94#[inline]
95fn check_bigint_bound(op: BoundOp, limit: &num_bigint::BigInt, value: &Number) -> bool {
96    use fraction::BigFraction;
97
98    if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
99        return match op {
100            BoundOp::Lt => instance_bigint < *limit,
101            BoundOp::Lte => instance_bigint <= *limit,
102            BoundOp::Gt => instance_bigint > *limit,
103            BoundOp::Gte => instance_bigint >= *limit,
104        };
105    }
106
107    if let Some(v) = value.as_u64() {
108        return match op {
109            BoundOp::Lt => numeric::bignum::u64_lt_bigint(v, limit),
110            BoundOp::Lte => numeric::bignum::u64_le_bigint(v, limit),
111            BoundOp::Gt => numeric::bignum::u64_gt_bigint(v, limit),
112            BoundOp::Gte => numeric::bignum::u64_ge_bigint(v, limit),
113        };
114    }
115
116    if let Some(v) = value.as_i64() {
117        return match op {
118            BoundOp::Lt => numeric::bignum::i64_lt_bigint(v, limit),
119            BoundOp::Lte => numeric::bignum::i64_le_bigint(v, limit),
120            BoundOp::Gt => numeric::bignum::i64_gt_bigint(v, limit),
121            BoundOp::Gte => numeric::bignum::i64_ge_bigint(v, limit),
122        };
123    }
124
125    if let Some(v) = value.as_f64() {
126        return match op {
127            BoundOp::Lt => numeric::bignum::f64_lt_bigint(v, limit),
128            BoundOp::Lte => numeric::bignum::f64_le_bigint(v, limit),
129            BoundOp::Gt => numeric::bignum::f64_gt_bigint(v, limit),
130            BoundOp::Gte => numeric::bignum::f64_ge_bigint(v, limit),
131        };
132    }
133
134    if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
135        let limit_frac = BigFraction::from(limit.clone());
136        return match op {
137            BoundOp::Lt => instance_bigfrac < limit_frac,
138            BoundOp::Lte => instance_bigfrac <= limit_frac,
139            BoundOp::Gt => instance_bigfrac > limit_frac,
140            BoundOp::Gte => instance_bigfrac >= limit_frac,
141        };
142    }
143
144    infinity_cmp(op, value.as_str().starts_with('-'))
145}
146
147#[cfg(feature = "arbitrary-precision")]
148#[inline]
149fn check_bigfrac_bound(op: BoundOp, limit: &fraction::BigFraction, value: &Number) -> bool {
150    if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
151        return match op {
152            BoundOp::Lt => instance_bigfrac < *limit,
153            BoundOp::Lte => instance_bigfrac <= *limit,
154            BoundOp::Gt => instance_bigfrac > *limit,
155            BoundOp::Gte => instance_bigfrac >= *limit,
156        };
157    }
158
159    if let Some(v) = value.as_u64() {
160        return match op {
161            BoundOp::Lt => numeric::bignum::u64_lt_bigfrac(v, limit),
162            BoundOp::Lte => numeric::bignum::u64_le_bigfrac(v, limit),
163            BoundOp::Gt => numeric::bignum::u64_gt_bigfrac(v, limit),
164            BoundOp::Gte => numeric::bignum::u64_ge_bigfrac(v, limit),
165        };
166    }
167
168    if let Some(v) = value.as_i64() {
169        return match op {
170            BoundOp::Lt => numeric::bignum::i64_lt_bigfrac(v, limit),
171            BoundOp::Lte => numeric::bignum::i64_le_bigfrac(v, limit),
172            BoundOp::Gt => numeric::bignum::i64_gt_bigfrac(v, limit),
173            BoundOp::Gte => numeric::bignum::i64_ge_bigfrac(v, limit),
174        };
175    }
176
177    // An integer past i64 is exact as a BigInt, while `f64` rounds it onto limits a fractional
178    // digit separates it from, e.g. `-10000000000000000000000000` against the same with `.1`.
179    if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
180        let instance_frac = fraction::BigFraction::from(instance_bigint);
181        return match op {
182            BoundOp::Lt => instance_frac < *limit,
183            BoundOp::Lte => instance_frac <= *limit,
184            BoundOp::Gt => instance_frac > *limit,
185            BoundOp::Gte => instance_frac >= *limit,
186        };
187    }
188
189    if let Some(v) = value.as_f64() {
190        return match op {
191            BoundOp::Lt => numeric::bignum::f64_lt_bigfrac(v, limit),
192            BoundOp::Lte => numeric::bignum::f64_le_bigfrac(v, limit),
193            BoundOp::Gt => numeric::bignum::f64_gt_bigfrac(v, limit),
194            BoundOp::Gte => numeric::bignum::f64_ge_bigfrac(v, limit),
195        };
196    }
197
198    // Dynamic BigFraction validators treat this branch as valid because
199    // extremely large scientific notation cannot be compared reliably.
200    true
201}
202
203pub fn compile_bound(op: BoundOp, limit: &Number) -> CompiledBound {
204    #[cfg(feature = "arbitrary-precision")]
205    {
206        if let Some(value) = numeric::bignum::try_parse_bigint(limit) {
207            return CompiledBound::BigInt { op, limit: value };
208        }
209        if let Some(value) = numeric::bignum::try_parse_bigfraction(limit) {
210            return CompiledBound::BigFrac { op, limit: value };
211        }
212    }
213
214    #[cfg(not(feature = "arbitrary-precision"))]
215    {
216        if let Some(value) = limit.as_i64() {
217            return CompiledBound::I64 { op, limit: value };
218        }
219        if let Some(value) = limit.as_u64() {
220            return CompiledBound::U64 { op, limit: value };
221        }
222    }
223
224    if let Some(value) = limit.as_f64() {
225        return CompiledBound::F64 { op, limit: value };
226    }
227
228    #[cfg(feature = "arbitrary-precision")]
229    {
230        let limit = if limit.as_str().starts_with('-') {
231            f64::NEG_INFINITY
232        } else {
233            f64::INFINITY
234        };
235        CompiledBound::F64 { op, limit }
236    }
237
238    #[cfg(not(feature = "arbitrary-precision"))]
239    {
240        unreachable!("non-arbitrary-precision serde_json::Number always has an f64 representation");
241    }
242}
243
244pub fn check_bound(compiled: &CompiledBound, value: &Number) -> bool {
245    match compiled {
246        CompiledBound::F64 { op, limit } => check_primitive_bound(*op, value, *limit),
247        #[cfg(not(feature = "arbitrary-precision"))]
248        CompiledBound::I64 { op, limit } => check_primitive_bound(*op, value, *limit),
249        #[cfg(not(feature = "arbitrary-precision"))]
250        CompiledBound::U64 { op, limit } => check_primitive_bound(*op, value, *limit),
251        #[cfg(feature = "arbitrary-precision")]
252        CompiledBound::BigInt { op, limit } => check_bigint_bound(*op, limit, value),
253        #[cfg(feature = "arbitrary-precision")]
254        CompiledBound::BigFrac { op, limit } => check_bigfrac_bound(*op, limit, value),
255    }
256}
257
258#[cfg(feature = "arbitrary-precision")]
259pub fn compile_multiple_of(multiple_of: &Number) -> CompiledMultipleOf {
260    #[cfg(feature = "arbitrary-precision")]
261    {
262        if let Some(value) = numeric::bignum::try_parse_bigint(multiple_of) {
263            return CompiledMultipleOf::BigInt(value);
264        }
265        if let Some(value) = numeric::bignum::try_parse_bigfraction(multiple_of) {
266            return CompiledMultipleOf::BigFrac(value);
267        }
268    }
269
270    CompiledMultipleOf::Unsupported
271}
272
273#[cfg(feature = "arbitrary-precision")]
274pub fn check_multiple_of(compiled: &CompiledMultipleOf, value: &Number) -> bool {
275    match compiled {
276        #[cfg(feature = "arbitrary-precision")]
277        CompiledMultipleOf::BigInt(multiple) => {
278            use num_bigint::BigInt;
279
280            if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
281                return numeric::bignum::is_multiple_of_bigint(&instance_bigint, multiple);
282            }
283
284            if let Some(v) = value.as_u64() {
285                let v_bigint = BigInt::from(v);
286                return numeric::bignum::is_multiple_of_bigint(&v_bigint, multiple);
287            }
288
289            if let Some(v) = value.as_i64() {
290                let v_bigint = BigInt::from(v);
291                return numeric::bignum::is_multiple_of_bigint(&v_bigint, multiple);
292            }
293
294            false
295        }
296        #[cfg(feature = "arbitrary-precision")]
297        CompiledMultipleOf::BigFrac(multiple) => {
298            use num_traits::ToPrimitive;
299
300            if let Some(instance_bigfrac) = numeric::bignum::try_parse_bigfraction(value) {
301                return numeric::bignum::is_multiple_of_bigfrac(&instance_bigfrac, multiple);
302            }
303
304            if let Some(instance_bigint) = numeric::bignum::try_parse_bigint(value) {
305                let value_frac = fraction::BigFraction::from(instance_bigint);
306                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
307            }
308
309            if let Some(v) = value.as_u64() {
310                let value_frac = fraction::BigFraction::from(v);
311                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
312            }
313
314            if let Some(v) = value.as_i64() {
315                let value_frac = fraction::BigFraction::from(v);
316                return numeric::bignum::is_multiple_of_bigfrac(&value_frac, multiple);
317            }
318
319            let multiple_f64 = multiple.to_f64().unwrap_or(f64::INFINITY);
320            numeric::is_multiple_of_float(value, multiple_f64)
321        }
322        CompiledMultipleOf::Unsupported => true,
323    }
324}
325
326/// Which arithmetic the validator uses for a `multipleOf` divisor. A rewrite that moves a divisor
327/// between kinds can change verdicts, so only same-kind rewrites preserve membership.
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
329pub enum DivisorKind {
330    /// Integer instances take exact integer modulo.
331    Whole,
332    /// A whole divisor past the exact-modulo guard, where instances go through `f64` remainder.
333    WholeLossy,
334    /// Every instance goes through rational division.
335    Fractional,
336}
337
338/// The arithmetic `divisor` selects, mirroring how `multipleOf` compiles.
339pub fn divisor_kind(divisor: &Number) -> DivisorKind {
340    #[cfg(feature = "arbitrary-precision")]
341    {
342        if numeric::bignum::try_parse_bigint(divisor).is_some() {
343            return DivisorKind::Whole;
344        }
345        if numeric::bignum::try_parse_bigfraction(divisor).is_some() {
346            return DivisorKind::Fractional;
347        }
348    }
349    match divisor.as_f64() {
350        Some(value) if value.fract() != 0. => DivisorKind::Fractional,
351        // `is_multiple_of_integer` keeps exact modulo only while the divisor itself is exact.
352        Some(value) if value.abs() <= MAX_SAFE_INTEGER_F64 => DivisorKind::Whole,
353        // A divisor with no `f64` form only arises under arbitrary precision, where the exact
354        // parses above have already classified it.
355        _ => DivisorKind::WholeLossy,
356    }
357}
358
359const MAX_SAFE_INTEGER_F64: f64 = 9_007_199_254_740_992.0;
360
361/// Whether `value` satisfies `multipleOf: divisor`, deciding it the way the validator does.
362pub fn satisfies_multiple_of(divisor: &Number, value: &Number) -> bool {
363    #[cfg(feature = "arbitrary-precision")]
364    {
365        let compiled = compile_multiple_of(divisor);
366        if compiled != CompiledMultipleOf::Unsupported {
367            return check_multiple_of(&compiled, value);
368        }
369    }
370    // A divisor with no `f64` is one the validator skips, so every instance passes there.
371    divisor.as_f64().is_none_or(|limit| {
372        if limit.fract() == 0. {
373            numeric::is_multiple_of_integer(value, limit)
374        } else {
375            numeric::is_multiple_of_float(value, limit)
376        }
377    })
378}