From 5ea3783c0ca94b9825e51a18f3b3a65df1d143fb Mon Sep 17 00:00:00 2001 From: Stan Papadopoulos Date: Wed, 26 Nov 2025 13:30:59 +0100 Subject: [PATCH 1/4] Implement traits from approxim --- Cargo.toml | 6 ++- src/approxim.rs | 94 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++++ test-approxim/Cargo.toml | 11 +++++ test-approxim/src/lib.rs | 38 ++++++++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/approxim.rs create mode 100644 test-approxim/Cargo.toml create mode 100644 test-approxim/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 6148087b..9b02cf46 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 } @@ -39,9 +40,10 @@ fnv = "1.0" serde = { version = "1.0", default-features = false, features = ["derive"] } [features] -default = ["std"] +default = ["std", "approxim"] 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..832efd2b --- /dev/null +++ b/src/approxim.rs @@ -0,0 +1,94 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "approxim")))] +//! Trait implementations from [`approxim`](https://docs.rs/approxim/latest/approxim/) crate. +//! +//! Considering [`IndexMap`] preserves insertion order, these trait implementations +//! take that into account. Meaning +//! ```should_panic +//! # use approxim::assert_abs_diff_eq; +//! # use indexmap::indexmap; +//! +//! let left = indexmap! { 1 => 2, 3 => 4 }; +//! let right = indexmap! { 3 => 4, 1 => 2, }; +//! +//! assert_abs_diff_eq!(left, right); // false +//! ``` +//! will result in relative inequality, despite the contents +//! being the same. + +use core::hash::BuildHasher; +use std::hash::Hash; + +use approxim::{AbsDiffEq, RelativeEq, UlpsEq}; + +use crate::IndexMap; + +impl AbsDiffEq for IndexMap +where + K: Eq + Hash, + V: AbsDiffEq, + V::Epsilon: Copy, + S: BuildHasher, +{ + type Epsilon = V::Epsilon; + + fn default_epsilon() -> Self::Epsilon { + V::default_epsilon() + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.len() == other.len() + && self.iter().zip(other.iter()).all( + |((key_left, val_left), (key_right, val_right))| { + key_left == key_right && val_left.abs_diff_eq(val_right, epsilon) + }, + ) + } +} + +/// RelativeEq implementation for V:RelativeEq +impl RelativeEq for IndexMap +where + K: Eq + Hash, + V: RelativeEq, + V::Epsilon: Copy, + S: BuildHasher, +{ + fn default_max_relative() -> Self::Epsilon { + V::default_max_relative() + } + + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.len() == other.len() + && self.iter().zip(other.iter()).all( + |((key_left, val_left), (key_right, val_right))| { + key_left == key_right && val_left.relative_eq(val_right, epsilon, max_relative) + }, + ) + } +} + +impl UlpsEq for IndexMap +where + K: Eq + Hash, + V: UlpsEq, + V::Epsilon: Copy, + S: BuildHasher, +{ + fn default_max_ulps() -> u32 { + V::default_max_ulps() + } + + fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.len() == other.len() + && self.iter().zip(other.iter()).all( + |((key_left, val_left), (key_right, val_right))| { + key_left == key_right && val_left.ulps_eq(val_right, epsilon, max_ulps) + }, + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b3a2551..62cc7aee 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`: Add 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..4887dea8 --- /dev/null +++ b/test-approxim/src/lib.rs @@ -0,0 +1,38 @@ +#![cfg(test)] + +use approxim::{assert_abs_diff_eq, assert_relative_eq}; +use indexmap::indexmap; + +#[test] +fn test_abs_diff_eq() { + let left = indexmap! { 1 => 1000, 2 => 2000 }; + let right = indexmap! { 1 => 1001, 2 => 1999 }; + assert_abs_diff_eq!(left, right, epsilon = 1); +} + +#[test] +#[should_panic] +fn test_abs_diff_eq_different_insertion_order() { + let left = indexmap! { 1 => 2, 3 => 4 }; + let right = indexmap! { 3 => 4, 1 => 2, }; + assert_abs_diff_eq!(left, right); +} + +#[test] +fn test_relative_eq() { + 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); + + 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); +} + +#[test] +#[should_panic] +fn test_relative_eq_different_insertion_order() { + let left = indexmap! { 1 => 2., 3 => 8. }; + let right = indexmap! { 3 => 8., 1 => 2., }; + assert_relative_eq!(left, right); +} From 592e14964870f796e38591e4ab24426b1dca03f8 Mon Sep 17 00:00:00 2001 From: Stan Papadopoulos Date: Mon, 1 Dec 2025 12:39:38 +0100 Subject: [PATCH 2/4] Generalised approxim trait implementations --- Cargo.toml | 2 +- src/approxim.rs | 91 +++++++++++++++++++--------------------- test-approxim/src/lib.rs | 13 +++--- 3 files changed, 48 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b02cf46..532651d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ fnv = "1.0" serde = { version = "1.0", default-features = false, features = ["derive"] } [features] -default = ["std", "approxim"] +default = ["std"] std = [] serde = ["dep:serde_core", "dep:serde"] approxim = ["dep:approxim", "std"] diff --git a/src/approxim.rs b/src/approxim.rs index 832efd2b..043899db 100644 --- a/src/approxim.rs +++ b/src/approxim.rs @@ -1,19 +1,10 @@ #![cfg_attr(docsrs, doc(cfg(feature = "approxim")))] -//! Trait implementations from [`approxim`](https://docs.rs/approxim/latest/approxim/) crate. +//! Trait implementations from [`approxim`](https://docs.rs/approxim/latest/approxim/) crate +//! for [`IndexMap`]. //! -//! Considering [`IndexMap`] preserves insertion order, these trait implementations -//! take that into account. Meaning -//! ```should_panic -//! # use approxim::assert_abs_diff_eq; -//! # use indexmap::indexmap; -//! -//! let left = indexmap! { 1 => 2, 3 => 4 }; -//! let right = indexmap! { 3 => 4, 1 => 2, }; -//! -//! assert_abs_diff_eq!(left, right); // false -//! ``` -//! will result in relative inequality, despite the contents -//! being the same. +//! 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; @@ -22,73 +13,75 @@ use approxim::{AbsDiffEq, RelativeEq, UlpsEq}; use crate::IndexMap; -impl AbsDiffEq for IndexMap +impl AbsDiffEq> for IndexMap where K: Eq + Hash, - V: AbsDiffEq, - V::Epsilon: Copy, - S: BuildHasher, + V1: AbsDiffEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, { - type Epsilon = V::Epsilon; + type Epsilon = V1::Epsilon; fn default_epsilon() -> Self::Epsilon { - V::default_epsilon() + V1::default_epsilon() } - fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + fn abs_diff_eq(&self, other: &IndexMap, epsilon: Self::Epsilon) -> bool { self.len() == other.len() - && self.iter().zip(other.iter()).all( - |((key_left, val_left), (key_right, val_right))| { - key_left == key_right && val_left.abs_diff_eq(val_right, epsilon) - }, - ) + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.abs_diff_eq(v, epsilon)) + }) } } -/// RelativeEq implementation for V:RelativeEq -impl RelativeEq for IndexMap +impl RelativeEq> for IndexMap where K: Eq + Hash, - V: RelativeEq, - V::Epsilon: Copy, - S: BuildHasher, + V1: RelativeEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, { fn default_max_relative() -> Self::Epsilon { - V::default_max_relative() + V1::default_max_relative() } fn relative_eq( &self, - other: &Self, + other: &IndexMap, epsilon: Self::Epsilon, max_relative: Self::Epsilon, ) -> bool { self.len() == other.len() - && self.iter().zip(other.iter()).all( - |((key_left, val_left), (key_right, val_right))| { - key_left == key_right && val_left.relative_eq(val_right, epsilon, max_relative) - }, - ) + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.relative_eq(v, epsilon, max_relative)) + }) } } -impl UlpsEq for IndexMap +impl UlpsEq> for IndexMap where K: Eq + Hash, - V: UlpsEq, - V::Epsilon: Copy, - S: BuildHasher, + V1: UlpsEq, + V1::Epsilon: Copy, + S1: BuildHasher, + S2: BuildHasher, { fn default_max_ulps() -> u32 { - V::default_max_ulps() + V1::default_max_ulps() } - fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + fn ulps_eq(&self, other: &IndexMap, epsilon: Self::Epsilon, max_ulps: u32) -> bool { self.len() == other.len() - && self.iter().zip(other.iter()).all( - |((key_left, val_left), (key_right, val_right))| { - key_left == key_right && val_left.ulps_eq(val_right, epsilon, max_ulps) - }, - ) + && self.iter().all(|(key, value)| { + other + .get(key) + .map_or(false, |v| value.ulps_eq(v, epsilon, max_ulps)) + }) } } diff --git a/test-approxim/src/lib.rs b/test-approxim/src/lib.rs index 4887dea8..ef5a6d82 100644 --- a/test-approxim/src/lib.rs +++ b/test-approxim/src/lib.rs @@ -5,14 +5,12 @@ 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); -} -#[test] -#[should_panic] -fn test_abs_diff_eq_different_insertion_order() { + // check different insertion order let left = indexmap! { 1 => 2, 3 => 4 }; let right = indexmap! { 3 => 4, 1 => 2, }; assert_abs_diff_eq!(left, right); @@ -20,18 +18,17 @@ fn test_abs_diff_eq_different_insertion_order() { #[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); -} -#[test] -#[should_panic] -fn test_relative_eq_different_insertion_order() { + // check different insertion order let left = indexmap! { 1 => 2., 3 => 8. }; let right = indexmap! { 3 => 8., 1 => 2., }; assert_relative_eq!(left, right); From 9fd9a2990bcabc5412a64053aea5506782ed9d94 Mon Sep 17 00:00:00 2001 From: Stan Papadopoulos Date: Mon, 1 Dec 2025 14:54:53 +0100 Subject: [PATCH 3/4] Extend testing with ulps_eq --- test-approxim/src/lib.rs | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/test-approxim/src/lib.rs b/test-approxim/src/lib.rs index ef5a6d82..567f65ea 100644 --- a/test-approxim/src/lib.rs +++ b/test-approxim/src/lib.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use approxim::{assert_abs_diff_eq, assert_relative_eq}; +use approxim::{assert_abs_diff_eq, assert_relative_eq, assert_ulps_eq}; use indexmap::indexmap; #[test] @@ -16,6 +16,14 @@ fn test_abs_diff_eq() { 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 @@ -33,3 +41,37 @@ fn test_relative_eq() { 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); +} From 329941eb3fab723b60d489dfa924543d1ec9af8b Mon Sep 17 00:00:00 2001 From: Stan Papadopoulos Date: Mon, 1 Dec 2025 14:59:25 +0100 Subject: [PATCH 4/4] Fix typo --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 62cc7aee..b5be92da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ //! 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`: Add implementations for [`AbsDiffEq`], [`RelativeEq`] and [`UlpsEq`] +//! * `approxim`: Adds implementations for [`AbsDiffEq`], [`RelativeEq`] and [`UlpsEq`] //! to [`IndexMap`]. //! //! _Note: only the `std` feature is enabled by default._