Skip to main content

jsonschema_value/
cmp.rs

1use num_cmp::NumCmp;
2use serde_json::{Map, Value};
3
4macro_rules! num_cmp {
5    ($left:expr, $right:expr) => {
6        if let Some(b) = $right.as_u64() {
7            NumCmp::num_eq($left, b)
8        } else if let Some(b) = $right.as_i64() {
9            NumCmp::num_eq($left, b)
10        } else {
11            #[cfg(feature = "arbitrary-precision")]
12            {
13                use crate::numeric::bignum;
14                use fraction::BigFraction;
15
16                let left_frac = BigFraction::from($left);
17
18                // Check BigInt/BigFraction BEFORE f64 to avoid precision loss
19                if let Some(right_bigint) = bignum::try_parse_bigint($right) {
20                    let right_frac = BigFraction::from(right_bigint);
21                    left_frac == right_frac
22                } else if let Some(right_frac) = bignum::try_parse_bigfraction($right) {
23                    left_frac == right_frac
24                } else if let Some(b) = $right.as_f64() {
25                    // Fallback to f64 for scientific notation or other cases
26                    left_frac == BigFraction::from(b)
27                } else {
28                    // Can't parse right - not equal
29                    false
30                }
31            }
32            #[cfg(not(feature = "arbitrary-precision"))]
33            {
34                if let Some(b) = $right.as_f64() {
35                    NumCmp::num_eq($left, b)
36                } else {
37                    unreachable!("Numbers always fit in u64/i64/f64 without arbitrary-precision")
38                }
39            }
40        }
41    };
42}
43
44/// Compare two JSON numbers for equality with arbitrary precision support
45#[inline]
46#[doc(hidden)]
47#[must_use]
48pub fn equal_numbers<L: crate::JsonNumber>(left: &L, right: &serde_json::Number) -> bool {
49    #[cfg(feature = "arbitrary-precision")]
50    {
51        use crate::numeric::bignum;
52        use fraction::BigFraction;
53
54        // Check BigInt/BigFraction first to avoid precision loss from f64 conversion
55        if let Some(left_bigint) = bignum::try_parse_bigint(&left.to_number()) {
56            if let Some(right_bigint) = bignum::try_parse_bigint(right) {
57                left_bigint == right_bigint
58            } else if let Some(b) = right.as_u64() {
59                left_bigint == num_bigint::BigInt::from(b)
60            } else if let Some(b) = right.as_i64() {
61                left_bigint == num_bigint::BigInt::from(b)
62            } else if let Some(right_frac) = bignum::try_parse_bigfraction(right) {
63                BigFraction::from(left_bigint) == right_frac
64            } else if let Some(b) = right.as_f64() {
65                BigFraction::from(left_bigint) == BigFraction::from(b)
66            } else {
67                unreachable!("Right is not parseable as any numeric type - should not happen for valid JSON numbers")
68            }
69        } else if let Some(left_frac) = bignum::try_parse_bigfraction(&left.to_number()) {
70            if let Some(right_frac) = bignum::try_parse_bigfraction(right) {
71                left_frac == right_frac
72            } else if let Some(right_bigint) = bignum::try_parse_bigint(right) {
73                left_frac == BigFraction::from(right_bigint)
74            } else if let Some(b) = right.as_u64() {
75                left_frac == BigFraction::from(b)
76            } else if let Some(b) = right.as_i64() {
77                left_frac == BigFraction::from(b)
78            } else if let Some(b) = right.as_f64() {
79                left_frac == BigFraction::from(b)
80            } else {
81                unreachable!("Right is not parseable as any numeric type - should not happen for valid JSON numbers")
82            }
83        } else if let Some(a) = left.as_u64() {
84            num_cmp!(a, right)
85        } else if let Some(a) = left.as_i64() {
86            num_cmp!(a, right)
87        } else if let Some(a) = left.as_f64() {
88            num_cmp!(a, right)
89        } else {
90            // Left is a number in scientific notation that doesn't fit in f64
91            // (e.g., 1e309, 1e400). With arbitrary-precision, these are stored as
92            // strings but can't be converted to any numeric type we support.
93            // Return false as we can't reliably compare them.
94            false
95        }
96    }
97    #[cfg(not(feature = "arbitrary-precision"))]
98    {
99        if let Some(a) = left.as_u64() {
100            num_cmp!(a, right)
101        } else if let Some(a) = left.as_i64() {
102            num_cmp!(a, right)
103        } else if let Some(a) = left.as_f64() {
104            num_cmp!(a, right)
105        } else {
106            // Past `f64`, with no exact arithmetic to place it against the literal.
107            false
108        }
109    }
110}
111
112/// Tests for two JSON values to be equal using the JSON Schema semantic.
113#[must_use]
114#[allow(clippy::missing_panics_doc)]
115pub fn equal(left: &Value, right: &Value) -> bool {
116    match (left, right) {
117        (Value::String(left), Value::String(right)) => left == right,
118        (Value::Bool(left), Value::Bool(right)) => left == right,
119        (Value::Null, Value::Null) => true,
120        (Value::Number(left), Value::Number(right)) => equal_numbers(left, right),
121        (Value::Array(left), Value::Array(right)) => equal_arrays(left, right),
122        (Value::Object(left), Value::Object(right)) => equal_objects(left, right),
123        (_, _) => false,
124    }
125}
126
127#[inline]
128#[must_use]
129pub fn equal_arrays(left: &[Value], right: &[Value]) -> bool {
130    left.len() == right.len() && {
131        let mut idx = 0_usize;
132        while idx < left.len() {
133            if !equal(&left[idx], &right[idx]) {
134                return false;
135            }
136            idx += 1;
137        }
138        true
139    }
140}
141
142#[inline]
143#[must_use]
144pub fn equal_objects(left: &Map<String, Value>, right: &Map<String, Value>) -> bool {
145    left.len() == right.len()
146        && left
147            .iter()
148            .zip(right)
149            .all(|((ka, va), (kb, vb))| ka == kb && equal(va, vb))
150}
151
152#[cfg(test)]
153mod tests {
154    use super::equal;
155    use serde_json::{json, Value};
156    use test_case::test_case;
157
158    #[test_case(&json!(1), &json!(1.0))]
159    #[test_case(&json!([2]), &json!([2.0]))]
160    #[test_case(&json!([-3]), &json!([-3.0]))]
161    #[test_case(&json!({"a": 1}), &json!({"a": 1.0}))]
162    fn are_equal(left: &Value, right: &Value) {
163        assert!(equal(left, right));
164    }
165
166    #[test_case(&json!(1), &json!(2.0))]
167    #[test_case(&json!([]), &json!(["foo"]))]
168    #[test_case(&json!([-3]), &json!([-4.0]))]
169    #[test_case(&json!({"a": 1}), &json!({"a": 1.0, "b": 2}))]
170    fn are_not_equal(left: &Value, right: &Value) {
171        assert!(!equal(left, right));
172    }
173
174    #[cfg(feature = "arbitrary-precision")]
175    mod arbitrary_precision {
176        use super::equal;
177        use serde_json::Value;
178        use test_case::test_case;
179
180        fn parse_json(s: &str) -> Value {
181            serde_json::from_str(s).unwrap()
182        }
183        #[test_case("0.1", "0.1", true; "exact decimal match")]
184        #[test_case("0.1", "0.10", true; "decimal with trailing zero")]
185        #[test_case("0.1", "0.100000", true; "decimal with many trailing zeros")]
186        #[test_case("0.1", "0.2", false; "different decimals")]
187        #[test_case("0.3", "0.30", true; "another trailing zero case")]
188        #[test_case("1.0", "1", true; "decimal vs integer")]
189        #[test_case("1.00", "1.0", true; "decimals with different trailing zeros")]
190        #[test_case(
191            "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.5",
192            "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.5",
193            true;
194            "huge decimal self equality"
195        )]
196        #[test_case(
197            "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.5",
198            "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.6",
199            false;
200            "huge decimals different"
201        )]
202        #[test_case(
203            "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.5",
204            "100",
205            false;
206            "huge decimal vs small integer"
207        )]
208        #[test_case("18446744073709551616", "18446744073709551616", true; "large integer self equality")]
209        #[test_case("18446744073709551616", "18446744073709551617", false; "large integers different")]
210        #[test_case("99999999999999999999999999999999999999", "99999999999999999999999999999999999999", true; "very large integer equality")]
211        #[test_case("99999999999999999999999999999999999999", "100", false; "very large vs small integer")]
212        #[test_case("100", "100.0", true; "small integer vs decimal")]
213        #[test_case("0.1", "1", false; "small decimal vs integer")]
214        #[test_case("1.0", "1", true; "decimal one vs integer one")]
215        #[test_case("18446744073709551616", "100.5", false; "large int vs decimal")]
216        #[test_case("18446744073709551616.0", "18446744073709551616", true; "large int as decimal vs large int")]
217        #[test_case("-0.1", "-0.1", true; "negative decimal equality")]
218        #[test_case("-0.1", "-0.10", true; "negative decimal trailing zero")]
219        #[test_case("-18446744073709551616", "-18446744073709551616", true; "negative large int")]
220        #[test_case("-18446744073709551616", "18446744073709551616", false; "negative vs positive large")]
221        #[test_case("-100.5", "-100.5", true; "negative decimal match")]
222        #[test_case("-100.5", "100.5", false; "negative vs positive decimal")]
223        #[test_case("0", "0.0", true; "zero integer vs decimal")]
224        #[test_case("0.0", "0.00", true; "zero decimals with different precision")]
225        #[test_case("-0.0", "0.0", true; "negative zero vs positive zero")]
226        #[test_case("1e10", "10000000000", true; "scientific notation vs integer")]
227        #[test_case("1e19", "10000000000000000000", true; "scientific integer beyond i64")]
228        #[test_case("1e19", "10000000000000000001", false; "scientific integer mismatch")]
229        #[test_case("1.5e2", "150", true; "decimal scientific vs integer")]
230        #[test_case("1.5e2", "150.0", true; "decimal scientific vs decimal")]
231        #[test_case(r"[0.1, 0.2, 0.3]", r"[0.1, 0.2, 0.3]", true; "array exact match")]
232        #[test_case(r"[0.1, 0.2]", r"[0.10, 0.20]", true; "array with trailing zeros")]
233        #[test_case(r"[18446744073709551616]", r"[18446744073709551616]", true; "array with large integer")]
234        #[test_case(r"[0.1, 0.2]", r"[0.1, 0.3]", false; "array different values")]
235        #[test_case(r#"{"value": 0.1}"#, r#"{"value": 0.1}"#, true; "object exact match")]
236        #[test_case(r#"{"value": 0.1}"#, r#"{"value": 0.10}"#, true; "object with trailing zero")]
237        #[test_case(r#"{"id": 18446744073709551616}"#, r#"{"id": 18446744073709551616}"#, true; "object with large integer")]
238        #[test_case(r#"{"value": 0.1}"#, r#"{"value": 0.2}"#, false; "object different values")]
239        #[test_case("18446744073709551616", "-1", false; "large positive bigint vs negative i64")]
240        #[test_case("18446744073709551616", "-100", false; "large positive bigint vs negative i64 small")]
241        #[test_case("-18446744073709551616", "-1", false; "large negative bigint vs small negative i64")]
242        #[test_case("18446744073709551616", "1e10", false; "large bigint vs scientific notation f64")]
243        #[test_case("10000000000", "1e10", true; "bigint vs scientific notation equal")]
244        #[test_case("-18446744073709551616", "-1.5e3", false; "negative bigint vs scientific notation")]
245        #[test_case("0.5", "5e-1", true; "bigfraction vs scientific notation equal")]
246        #[test_case("0.3", "3e-1", true; "bigfraction vs scientific equal exact")]
247        #[test_case("123.456", "1.23456e2", true; "bigfraction vs scientific notation")]
248        #[test_case("0.1", "1e-2", false; "bigfraction vs scientific not equal")]
249        #[test_case("1e309", "1e309", true; "huge scientific notation now handled")]
250        #[test_case("1e400", "1e400", true; "extreme scientific notation now handled")]
251        #[test_case("1e-400", "1e-400", true; "extreme small scientific notation self equality")]
252        #[test_case("1e309", "1", false; "huge scientific notation vs integer")]
253        fn arbitrary_precision_equality(left_str: &str, right_str: &str, should_equal: bool) {
254            let left = parse_json(left_str);
255            let right = parse_json(right_str);
256            assert_eq!(equal(&left, &right), should_equal);
257        }
258    }
259}