Skip to content

Commit e2e0873

Browse files
committed
feat: add removal
1 parent dbc52cc commit e2e0873

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

src/lib.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,83 @@ where
288288
self.items.contains(item)
289289
}
290290

291+
/// Removes a value from the interner and returns the Handle and the Value.
292+
///
293+
/// # ⚠️ Performance Warning: O(n)
294+
///
295+
/// Unlike standard `HashMap` removal which is O(1), this operation is **O(n)**
296+
/// (linear time) because the interner is backed by a contiguous vector to
297+
/// preserve ordering.
298+
///
299+
/// When an item is removed, all subsequent items must be **shifted to the left**
300+
/// to fill the gap.
301+
///
302+
/// # ⚠️ Handle Invalidation
303+
///
304+
/// Because indices shift, **handles for items inserted after this one will change**.
305+
///
306+
/// ## Example Scenario
307+
///
308+
/// Imagine an interner with items `[A, B, C]` corresponding to handles `0, 1, 2`.
309+
///
310+
/// 1. You remove `B` (handle `1`).
311+
/// 2. `C` shifts left to fill the gap.
312+
/// 3. The storage is now `[A, C]`.
313+
///
314+
/// **The Consequence:**
315+
/// * Handle `0` (`A`) remains valid.
316+
/// * Handle `2` (which used to be `C`) is now out of bounds!
317+
/// * Handle `1` (which used to be `B`) now resolves to `C`.
318+
///
319+
/// # Handle Recovery
320+
///
321+
/// Since the shift is deterministic, you can "repair" your existing handles
322+
/// if you are tracking them.
323+
///
324+
/// * **Handles < removed:** Unaffected.
325+
/// * **Handles > removed:** Must be decremented by 1.
326+
///
327+
/// ```text
328+
/// if my_handle > removed_handle {
329+
/// my_handle -= 1;
330+
/// }
331+
/// ```
332+
pub fn remove<Q>(&mut self, item: &Q) -> Option<(H, T)>
333+
where
334+
T: Borrow<Q>,
335+
Q: Hash + Eq + ?Sized,
336+
{
337+
// shift_remove_full returns (index, value)
338+
// We use shift_remove to preserve the relative order of remaining items.
339+
let (idx, val) = self.items.shift_remove_full(item)?;
340+
341+
// The index returned by IndexSet is guaranteed to fit in usize.
342+
// We convert it back to H to return to the user.
343+
// We suppress the error here because if it was in the map, it had a valid handle.
344+
let handle = H::try_from(idx).ok()?;
345+
346+
Some((handle, val))
347+
}
348+
349+
/// Removes the item associated with the given `handle`.
350+
///
351+
/// # Returns
352+
///
353+
/// - `Some(T)`: The value that was removed, if the handle was valid.
354+
/// - `None`: If the handle was invalid (e.g. out of bounds).
355+
///
356+
/// # ⚠️ Performance & Invalidation
357+
///
358+
/// Like [`remove`](Self::remove), this operation is **O(n)** and will shift
359+
/// the indices of all subsequent items.
360+
///
361+
/// Any existing handle `h` where `h > handle` must be decremented by 1 to
362+
/// remain valid.
363+
pub fn remove_handle(&mut self, handle: H) -> Option<T> {
364+
let idx = usize::try_from(handle).ok()?;
365+
self.items.shift_remove_index(idx)
366+
}
367+
291368
/// Current capacity, in number of items.
292369
#[inline]
293370
pub fn capacity(&self) -> usize {
@@ -891,4 +968,85 @@ mod tests {
891968
let found = interner.lookup_handle("A").unwrap();
892969
assert_eq!(found, Some(h));
893970
}
971+
972+
#[test]
973+
fn test_remove_handle_shifts_indices() {
974+
let mut interner = create_string_interner();
975+
976+
// 1. Insert [A, B, C]
977+
let h_a = interner.intern_ref("A").unwrap(); // 0
978+
let h_b = interner.intern_ref("B").unwrap(); // 1
979+
let h_c = interner.intern_ref("C").unwrap(); // 2
980+
981+
assert_eq!(interner.len(), 3);
982+
983+
// 2. Remove "B" (index 1) using its handle
984+
let removed = interner.remove_handle(h_b);
985+
986+
assert_eq!(removed, Some("B".to_string()));
987+
assert_eq!(interner.len(), 2);
988+
989+
// 3. Verify the state of the remaining handles
990+
991+
// Handle 0 ("A") is unaffected because it was *before* the removal.
992+
assert_eq!(interner.resolve(h_a), Some(&"A".to_string()));
993+
994+
// Handle 2 ("C") is now BROKEN. It points to index 2, but the vector
995+
// is only length 2 (indices 0 and 1).
996+
assert_eq!(interner.resolve(h_c), None);
997+
998+
// "C" has actually shifted down to Handle 1.
999+
// (This simulates what happens if we reused the old 'B' handle)
1000+
assert_eq!(interner.resolve(h_b), Some(&"C".to_string()));
1001+
}
1002+
1003+
#[test]
1004+
fn test_remove_and_recover_handles() {
1005+
let mut interner = create_string_interner();
1006+
1007+
// 1. Setup handles: [0, 1, 2, 3]
1008+
// Items: ["A", "B", "C", "D"]
1009+
let mut handles = alloc::vec![
1010+
interner.intern_ref("A").unwrap(), // 0
1011+
interner.intern_ref("B").unwrap(), // 1
1012+
interner.intern_ref("C").unwrap(), // 2
1013+
interner.intern_ref("D").unwrap(), // 3
1014+
];
1015+
1016+
// 2. Remove "B" (index 1).
1017+
// usage: remove returns the handle of the item that was removed.
1018+
let (removed_handle, val) = interner.remove("B").unwrap();
1019+
1020+
assert_eq!(val, "B");
1021+
assert_eq!(removed_handle, 1);
1022+
1023+
// 3. The Recovery Loop
1024+
// We iterate over our local handles and patch them.
1025+
for h in &mut handles {
1026+
// Use strict greater-than (>).
1027+
// Handles < 1 stay the same.
1028+
// Handle == 1 is the one we just removed.
1029+
if *h > removed_handle {
1030+
*h -= 1;
1031+
}
1032+
}
1033+
1034+
// 4. Verification
1035+
1036+
// "A" (was 0) should still be 0
1037+
assert_eq!(interner.resolve(handles[0]), Some(&"A".to_string()));
1038+
1039+
// "B" (was 1) was removed. In our vector, `handles[1]` is still `1`.
1040+
// However, in the interner, index 1 has been filled by "C".
1041+
// This is expected behavior for the "removed" handle.
1042+
assert_eq!(interner.resolve(handles[1]), Some(&"C".to_string()));
1043+
1044+
// "C" (was 2) should have been patched to 1.
1045+
assert_eq!(handles[2], 1);
1046+
assert_eq!(interner.resolve(handles[2]), Some(&"C".to_string()));
1047+
1048+
// "D" (was 3) should have been patched to 2.
1049+
assert_eq!(handles[3], 2);
1050+
assert_eq!(interner.resolve(handles[3]), Some(&"D".to_string()));
1051+
}
8941052
}

0 commit comments

Comments
 (0)