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