diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e05a93..a8407060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Ignore `prefixItems` under Draft 2019-09 as it was introduced in Draft 2020-12. +### Fixed + +- Numbers with zero fraction incorrectly handled in `uniqueItems`. + ### Performance - Speedup `apply`. diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 55dcdb1f..d4fe5714 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -11,6 +11,10 @@ - Ignore `prefixItems` under Draft 2019-09 as it was introduced in Draft 2020-12. +### Fixed + +- Numbers with zero fraction incorrectly handled in `uniqueItems`. + ### Performance - Speedup `apply`. diff --git a/crates/jsonschema/src/keywords/unique_items.rs b/crates/jsonschema/src/keywords/unique_items.rs index 53ec0c8b..bef08a07 100644 --- a/crates/jsonschema/src/keywords/unique_items.rs +++ b/crates/jsonschema/src/keywords/unique_items.rs @@ -13,9 +13,14 @@ use std::hash::{Hash, Hasher}; // Based on implementation proposed by Sven Marnach: // https://stackoverflow.com/questions/60882381/what-is-the-fastest-correct-way-to-detect-that-there-are-no-duplicates-in-a-json -#[derive(PartialEq)] pub(crate) struct HashedValue<'a>(&'a Value); +impl PartialEq for HashedValue<'_> { + fn eq(&self, other: &Self) -> bool { + equal(self.0, other.0) + } +} + impl Eq for HashedValue<'_> {} impl Hash for HashedValue<'_> { @@ -24,12 +29,12 @@ impl Hash for HashedValue<'_> { Value::Null => state.write_u32(3_221_225_473), // chosen randomly Value::Bool(ref item) => item.hash(state), Value::Number(ref item) => { - if let Some(number) = item.as_u64() { + if let Some(number) = item.as_f64() { + number.to_bits().hash(state) + } else if let Some(number) = item.as_u64() { number.hash(state); } else if let Some(number) = item.as_i64() { number.hash(state); - } else if let Some(number) = item.as_f64() { - number.to_bits().hash(state) } } Value::String(ref item) => item.hash(state), @@ -165,6 +170,8 @@ mod tests { #[test_case(&[json!(1), json!(1)] => false; "two non-unique elements")] #[test_case(&[json!(1), json!(2), json!(3)] => true; "three unique elements")] #[test_case(&[json!(1), json!(2), json!(1)] => false; "three non-unique elements")] + #[test_case(&[json!(1), json!(2), json!(3), json!(4), json!(5), json!(6), json!(7), json!(8), json!(9), json!(10), json!(11), json!(12), json!(13), json!(14), json!(15), json!(1.0)] => false; "positive numbers with fractions")] + #[test_case(&[json!(-1), json!(-2), json!(-3), json!(-4), json!(-5), json!(-6), json!(-7), json!(-8), json!(-9), json!(-10), json!(-11), json!(-12), json!(-13), json!(-14), json!(-15), json!(-1.0)] => false; "negative numbers with fractions")] #[test_case(&[json!(1), json!("string"), json!(true), json!(null), json!({"key": "value"}), json!([1, 2, 3])] => true; "mixed types")] #[test_case(&[json!({"a": 1, "b": 1}), json!({"a": 1, "b": 2}), json!({"a": 1, "b": 3})] => true; "complex objects unique")] #[test_case(&[json!({"a": 1, "b": 2}), json!({"b": 2, "a": 1}), json!({"a": 1, "b": 2})] => false; "complex objects non-unique")]