diff --git a/Cargo.toml b/Cargo.toml index 6148087b..532651d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ equivalent = { version = "1.0", default-features = false } hashbrown = { version = "0.16.1", default-features = false } arbitrary = { version = "1.0", optional = true, default-features = false } +approxim = { version = "0.6.6", optional = true } quickcheck = { version = "1.0", optional = true, default-features = false } serde_core = { version = "1.0.220", optional = true, default-features = false } rayon = { version = "1.9", optional = true } @@ -42,6 +43,7 @@ serde = { version = "1.0", default-features = false, features = ["derive"] } default = ["std"] std = [] serde = ["dep:serde_core", "dep:serde"] +approxim = ["dep:approxim", "std"] # for testing only, of course test_debug = [] @@ -59,7 +61,7 @@ features = ["arbitrary", "quickcheck", "serde", "borsh", "rayon", "sval"] rustdoc-args = ["--cfg", "docsrs"] [workspace] -members = ["test-nostd", "test-serde", "test-sval"] +members = ["test-approxim", "test-nostd", "test-serde", "test-sval"] [lints.rust] private-bounds = "deny" diff --git a/src/approxim.rs b/src/approxim.rs new file mode 100644 index 00000000..043899db --- /dev/null +++ b/src/approxim.rs @@ -0,0 +1,87 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "approxim")))] +//! Trait implementations from [`approxim`](https://docs.rs/approxim/latest/approxim/) crate +//! for [`IndexMap`]. +//! +//! Keys are compared using `PartialEq` and values are compared using the approximate comparison +//! traits. Insertion order is not taken into account for the comparison, just as with +//! [`PartialEq`]. + +use core::hash::BuildHasher; +use std::hash::Hash; + +use approxim::{AbsDiffEq, RelativeEq, UlpsEq}; + +use crate::IndexMap; + +impl AbsDiffEq> for IndexMap +where + K: Eq + Hash, + V1: AbsDiffEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, +{ + type Epsilon = V1::Epsilon; + + fn default_epsilon() -> Self::Epsilon { + V1::default_epsilon() + } + + fn abs_diff_eq(&self, other: &IndexMap, epsilon: Self::Epsilon) -> bool { + self.len() == other.len() + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.abs_diff_eq(v, epsilon)) + }) + } +} + +impl RelativeEq> for IndexMap +where + K: Eq + Hash, + V1: RelativeEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, +{ + fn default_max_relative() -> Self::Epsilon { + V1::default_max_relative() + } + + fn relative_eq( + &self, + other: &IndexMap, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.len() == other.len() + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.relative_eq(v, epsilon, max_relative)) + }) + } +} + +impl UlpsEq> for IndexMap +where + K: Eq + Hash, + V1: UlpsEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, +{ + fn default_max_ulps() -> u32 { + V1::default_max_ulps() + } + + fn ulps_eq(&self, other: &IndexMap, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.len() == other.len() + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.ulps_eq(v, epsilon, max_ulps)) + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b3a2551..b5be92da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,8 @@ //! dependency that arose between [`borsh`] and `indexmap`, `borsh v1.5.6` //! added an `indexmap` feature that should be used instead of enabling the //! feature here. +//! * `approxim`: Adds implementations for [`AbsDiffEq`], [`RelativeEq`] and [`UlpsEq`] +//! to [`IndexMap`]. //! //! _Note: only the `std` feature is enabled by default._ //! @@ -53,6 +55,9 @@ //! [`borsh`]: `::borsh` //! [`arbitrary::Arbitrary`]: `::arbitrary::Arbitrary` //! [`quickcheck::Arbitrary`]: `::quickcheck::Arbitrary` +//! [`AbsDiffEq`]: `::approxim::AbsDiffEq` +//! [`RelativeEq`]: `::approxim::RelativeEq` +//! [`UlpsEq`]: `::approxim::UlpsEq` //! //! ### Alternate Hashers //! @@ -105,6 +110,9 @@ extern crate alloc; #[macro_use] extern crate std; +#[cfg(feature = "approxim")] +#[cfg_attr(docsrs, doc(cfg(feature = "approxim")))] +mod approxim; mod arbitrary; #[macro_use] mod macros; diff --git a/test-approxim/Cargo.toml b/test-approxim/Cargo.toml new file mode 100644 index 00000000..d833bc6f --- /dev/null +++ b/test-approxim/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test-approxim" +version = "0.1.0" +publish = false +edition = "2021" + +[dependencies] + +[dev-dependencies] +approxim = { version = "0.6.6" } +indexmap = { path = "..", features = ["approxim"] } diff --git a/test-approxim/src/lib.rs b/test-approxim/src/lib.rs new file mode 100644 index 00000000..567f65ea --- /dev/null +++ b/test-approxim/src/lib.rs @@ -0,0 +1,77 @@ +#![cfg(test)] + +use approxim::{assert_abs_diff_eq, assert_relative_eq, assert_ulps_eq}; +use indexmap::indexmap; + +#[test] +fn test_abs_diff_eq() { + // check with non-zero epsilon + let left = indexmap! { 1 => 1000, 2 => 2000 }; + let right = indexmap! { 1 => 1001, 2 => 1999 }; + assert_abs_diff_eq!(left, right, epsilon = 1); + + // check different insertion order + let left = indexmap! { 1 => 2, 3 => 4 }; + let right = indexmap! { 3 => 4, 1 => 2, }; + assert_abs_diff_eq!(left, right); +} + +#[test] +#[should_panic] +fn test_abs_diff_eq_len() { + let left = indexmap! { 1 => 1000, 2 => 2000 }; + let right = indexmap! { 1 => 1000, 2 => 2000, 3 => 3000 }; + assert_abs_diff_eq!(left, right); +} + +#[test] +fn test_relative_eq() { + // check with non-default epsilon + let left = indexmap! { 1 => 1.71828182, 2 => 1.0 }; + let right = indexmap! { 1 => 1.718281815, 2 => 1.00000001 }; + assert_relative_eq!(left, right, epsilon = 1e-8); + + // check with non-default max relative value + let left = indexmap! { 1 => 1., 2 => 1. }; + let right = indexmap! { 1 => 1.2, 2 => 1.5 }; + assert_relative_eq!(left, right, max_relative = 0.34); + + // check different insertion order + let left = indexmap! { 1 => 2., 3 => 8. }; + let right = indexmap! { 3 => 8., 1 => 2., }; + assert_relative_eq!(left, right); +} + +#[test] +#[should_panic] +fn test_relative_eq_len() { + let left = indexmap! { 1 => 0., 2 => 1. }; + let right = indexmap! { 1 => 0., 2 => 1., 3 => 2. }; + assert_relative_eq!(left, right); +} + +#[test] +fn test_ulps_eq() { + // check with non-default epsilon + let left = indexmap! { 1 => 0., 2 => 0. }; + let right = indexmap! { 1 => 1e-41, 2 => 1e-40 }; + assert_ulps_eq!(left, right, epsilon = 1e-40); + + // check with non-default max ulps value + let left = indexmap! { 1 => 1., 2 => 1. }; + let right = indexmap! { 1 => 1. + 1e-16, 2 => 1. + 1e-15 }; + assert_ulps_eq!(left, right, max_ulps = 5); + + // check different insertion order + let left = indexmap! { 1 => 2., 3 => 8. }; + let right = indexmap! { 3 => 8., 1 => 2., }; + assert_ulps_eq!(left, right); +} + +#[test] +#[should_panic] +fn test_ulps_eq_len() { + let left = indexmap! { 1 => 0., 2 => 1. }; + let right = indexmap! { 1 => 0., 2 => 1., 3 => 2. }; + assert_ulps_eq!(left, right); +}