Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
<!-- next-header -->
## Unreleased - ReleaseDate

### Added

- `from_iter_unique` constructors on `IdHashMap`, `BiHashMap`, and `TriHashMap`, matching the existing `IdOrdMap::from_iter_unique`. These build a map from an iterator and, rather than overwriting, return an error on the first item that conflicts with an already-inserted one.

Because a value in a `BiHashMap` or `TriHashMap` can conflict on more than one key at once, the error reports every distinct existing item it collides with (up to two for `BiHashMap` and up to three for `TriHashMap`).

### Changed

- MSRV updated to Rust 1.86.
Expand Down
105 changes: 101 additions & 4 deletions crates/iddqd/src/bi_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,91 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Clone + Allocator>
}
}

impl<T: BiHashItem, S: Default + Clone + BuildHasher, A: Allocator + Default>
BiHashMap<T, S, A>
{
/// Creates a new `BiHashMap` from an iterator of values, rejecting
/// duplicates.
///
/// A value conflicts when either of its keys matches an
/// already-inserted item, so a single value can collide with up to two
/// distinct existing items (one per key). On the first conflict, this
/// returns a [`DuplicateItem`] error containing the new value and every
/// conflicting item.
///
/// To overwrite duplicates instead, use [`BiHashMap::from_iter`].
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "default-hasher")] {
/// use iddqd::{BiHashItem, BiHashMap, bi_upcast};
///
/// #[derive(Debug, PartialEq, Eq)]
/// struct Item {
/// id: u32,
/// name: String,
/// value: i32,
/// }
///
/// impl BiHashItem for Item {
/// type K1<'a> = u32;
/// type K2<'a> = &'a str;
///
/// fn key1(&self) -> Self::K1<'_> {
/// self.id
/// }
/// fn key2(&self) -> Self::K2<'_> {
/// &self.name
/// }
/// bi_upcast!();
/// }
///
/// let items = vec![
/// Item { id: 1, name: "foo".to_string(), value: 42 },
/// Item { id: 2, name: "bar".to_string(), value: 99 },
/// ];
///
/// // Successful creation with unique keys.
/// let map: BiHashMap<Item> = BiHashMap::from_iter_unique(items).unwrap();
/// assert_eq!(map.len(), 2);
/// assert_eq!(map.get1(&1).unwrap().value, 42);
///
/// // Error with a duplicate key1.
/// let duplicate_items = vec![
/// Item { id: 1, name: "foo".to_string(), value: 42 },
/// Item { id: 1, name: "baz".to_string(), value: 99 },
/// ];
/// assert!(BiHashMap::<Item>::from_iter_unique(duplicate_items).is_err());
/// # }
/// ```
pub fn from_iter_unique<I: IntoIterator<Item = T>>(
iter: I,
) -> Result<Self, DuplicateItem<T>> {
let iter = iter.into_iter();
let mut map = Self::default();
map.reserve(iter.size_hint().0);
for value in iter {
if let Err((value, indexes)) =
map.insert_unique_or_dup_indexes(value)
{
// Removal produces owned duplicates, so that we don't need to
// specify `T: Clone` here.
let duplicates = indexes
.iter()
.map(|ix| {
map.remove_by_index(*ix)
.expect("duplicate index is present")
})
.collect();
return Err(DuplicateItem::__internal_new(value, duplicates));
}
}

Ok(map)
}
}

impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
/// Returns the hasher.
#[cfg(feature = "daft")]
Expand Down Expand Up @@ -2303,6 +2388,19 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
&mut self,
value: T,
) -> Result<ItemIndex, DuplicateItem<T, &T>> {
match self.insert_unique_or_dup_indexes(value) {
Ok(index) => Ok(index),
Err((value, duplicates)) => Err(DuplicateItem::__internal_new(
value,
duplicates.iter().map(|ix| &self.items[*ix]).collect(),
)),
}
}

fn insert_unique_or_dup_indexes(
&mut self,
value: T,
) -> Result<ItemIndex, (T, BTreeSet<ItemIndex>)> {
let mut duplicates = BTreeSet::new();

// Check for duplicates *before* inserting the new item, because we
Expand All @@ -2329,10 +2427,7 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
};

if !duplicates.is_empty() {
return Err(DuplicateItem::__internal_new(
value,
duplicates.iter().map(|ix| &self.items[*ix]).collect(),
));
return Err((value, duplicates));
}

let next_index = self.items.assert_can_grow().insert(value);
Expand Down Expand Up @@ -2789,6 +2884,8 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> IntoIterator
/// The `FromIterator` implementation for `BiHashMap` overwrites duplicate
/// items.
///
/// To reject duplicates, use [`BiHashMap::from_iter_unique`].
///
/// # Examples
///
/// ```
Expand Down
77 changes: 77 additions & 0 deletions crates/iddqd/src/id_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,81 @@ impl<T: IdHashItem, S: BuildHasher, A: Clone + Allocator> IdHashMap<T, S, A> {
}
}

impl<T: IdHashItem, S: Default + Clone + BuildHasher, A: Allocator + Default>
IdHashMap<T, S, A>
{
/// Creates a new `IdHashMap` from an iterator of values, rejecting
/// duplicates.
///
/// To overwrite duplicates instead, use [`IdHashMap::from_iter`].
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "default-hasher")] {
/// use iddqd::{IdHashItem, IdHashMap, id_upcast};
///
/// #[derive(Debug, PartialEq, Eq, Hash)]
/// struct Item {
/// id: String,
/// value: u32,
/// }
///
/// impl IdHashItem for Item {
/// type Key<'a> = &'a str;
/// fn key(&self) -> Self::Key<'_> {
/// &self.id
/// }
/// id_upcast!();
/// }
///
/// let items = vec![
/// Item { id: "foo".to_string(), value: 42 },
/// Item { id: "bar".to_string(), value: 99 },
/// ];
///
/// // Successful creation with unique keys.
/// let map: IdHashMap<Item> = IdHashMap::from_iter_unique(items).unwrap();
/// assert_eq!(map.len(), 2);
/// assert_eq!(map.get("foo").unwrap().value, 42);
///
/// // Error with duplicate keys.
/// let duplicate_items = vec![
/// Item { id: "foo".to_string(), value: 42 },
/// Item { id: "foo".to_string(), value: 99 },
/// ];
/// assert!(IdHashMap::<Item>::from_iter_unique(duplicate_items).is_err());
/// # }
/// ```
pub fn from_iter_unique<I: IntoIterator<Item = T>>(
iter: I,
) -> Result<Self, DuplicateItem<T>> {
let iter = iter.into_iter();
let mut map = Self::default();
map.reserve(iter.size_hint().0);
for value in iter {
// It would be nice to use insert_unique here, but that would return
// a `DuplicateItem<T, &T>`, which can only be converted into an
// owned value if T: Clone. Doing this via the Entry API means we
// can return a `DuplicateItem<T>` without requiring T to be Clone.
match map.entry(value.key()) {
Entry::Occupied(entry) => {
let duplicate = entry.remove();
return Err(DuplicateItem::__internal_new(
value,
vec![duplicate],
));
}
Entry::Vacant(entry) => {
entry.insert_known_unique(value);
}
}
}

Ok(map)
}
}

impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IdHashMap<T, S, A> {
#[cfg(feature = "daft")]
pub(crate) fn hasher(&self) -> &S {
Expand Down Expand Up @@ -1816,6 +1891,8 @@ impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IntoIterator
/// The `FromIterator` implementation for `IdHashMap` overwrites duplicate
/// items.
///
/// To reject duplicates, use [`IdHashMap::from_iter_unique`].
///
/// # Examples
///
/// ```
Expand Down
126 changes: 121 additions & 5 deletions crates/iddqd/src/tri_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,110 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Clone + Allocator>
}
}

impl<T: TriHashItem, S: Default + Clone + BuildHasher, A: Allocator + Default>
TriHashMap<T, S, A>
{
/// Creates a new `TriHashMap` from an iterator of values, rejecting
/// duplicates.
///
/// A value conflicts when any of its keys matches an already-inserted
/// item, so a single value can collide with up to three distinct
/// existing items (one per key). On the first conflict, this returns a
/// [`DuplicateItem`] error containing the new value and every
/// conflicting item.
///
/// To overwrite duplicates instead, use [`TriHashMap::from_iter`].
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "default-hasher")] {
/// use iddqd::{TriHashItem, TriHashMap, tri_upcast};
///
/// #[derive(Debug, PartialEq, Eq)]
/// struct Item {
/// id: u32,
/// name: String,
/// email: String,
/// }
///
/// impl TriHashItem for Item {
/// type K1<'a> = u32;
/// type K2<'a> = &'a str;
/// type K3<'a> = &'a str;
/// fn key1(&self) -> Self::K1<'_> {
/// self.id
/// }
/// fn key2(&self) -> Self::K2<'_> {
/// &self.name
/// }
/// fn key3(&self) -> Self::K3<'_> {
/// &self.email
/// }
/// tri_upcast!();
/// }
///
/// let items = vec![
/// Item {
/// id: 1,
/// name: "foo".to_string(),
/// email: "foo@example.com".to_string(),
/// },
/// Item {
/// id: 2,
/// name: "bar".to_string(),
/// email: "bar@example.com".to_string(),
/// },
/// ];
///
/// // Successful creation with unique keys.
/// let map: TriHashMap<Item> = TriHashMap::from_iter_unique(items).unwrap();
/// assert_eq!(map.len(), 2);
/// assert_eq!(map.get1(&1).unwrap().name, "foo");
///
/// // Error with a duplicate key3.
/// let duplicate_items = vec![
/// Item {
/// id: 1,
/// name: "foo".to_string(),
/// email: "dup@example.com".to_string(),
/// },
/// Item {
/// id: 2,
/// name: "bar".to_string(),
/// email: "dup@example.com".to_string(),
/// },
/// ];
/// assert!(TriHashMap::<Item>::from_iter_unique(duplicate_items).is_err());
/// # }
/// ```
pub fn from_iter_unique<I: IntoIterator<Item = T>>(
iter: I,
) -> Result<Self, DuplicateItem<T>> {
let iter = iter.into_iter();
let mut map = Self::default();
map.reserve(iter.size_hint().0);
for value in iter {
if let Err((value, indexes)) =
map.insert_unique_or_dup_indexes(value)
{
// Removal produces owned duplicates, so that we don't need to
// specify `T: Clone` here.
let duplicates = indexes
.iter()
.map(|ix| {
map.remove_by_index(*ix)
.expect("duplicate index is present")
})
.collect();
return Err(DuplicateItem::__internal_new(value, duplicates));
}
}

Ok(map)
}
}

impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
/// Returns the hasher.
#[cfg(feature = "daft")]
Expand Down Expand Up @@ -1489,6 +1593,19 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
&mut self,
value: T,
) -> Result<(), DuplicateItem<T, &T>> {
match self.insert_unique_or_dup_indexes(value) {
Ok(_) => Ok(()),
Err((value, duplicates)) => Err(DuplicateItem::__internal_new(
value,
duplicates.iter().map(|ix| &self.items[*ix]).collect(),
)),
}
}

fn insert_unique_or_dup_indexes(
&mut self,
value: T,
) -> Result<ItemIndex, (T, BTreeSet<ItemIndex>)> {
let mut duplicates = BTreeSet::new();

// Check for duplicates *before* inserting the new item, because we
Expand Down Expand Up @@ -1522,10 +1639,7 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
};

if !duplicates.is_empty() {
return Err(DuplicateItem::__internal_new(
value,
duplicates.iter().map(|ix| &self.items[*ix]).collect(),
));
return Err((value, duplicates));
}

let next_index = self.items.assert_can_grow().insert(value);
Expand All @@ -1535,7 +1649,7 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
e2.unwrap().insert(next_index);
e3.unwrap().insert(next_index);

Ok(())
Ok(next_index)
}

/// Returns true if the map contains a single item that matches all three
Expand Down Expand Up @@ -3191,6 +3305,8 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> IntoIterator
/// The `FromIterator` implementation for `TriHashMap` overwrites duplicate
/// items.
///
/// To reject duplicates, use [`TriHashMap::from_iter_unique`].
///
/// # Examples
///
/// ```
Expand Down
Loading
Loading