Skip to content

Commit 793f6d1

Browse files
authored
Merge pull request #176 from cuviper/move_index
Add move_index to change the position of an entry
2 parents 6b425e4 + 3848768 commit 793f6d1

File tree

5 files changed

+137
-12
lines changed

5 files changed

+137
-12
lines changed

RELEASES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
- The new `IndexMap::shrink_to` and `IndexSet::shrink_to` methods shrink
2020
the capacity with a lower bound.
2121

22+
- The new `IndexMap::move_index` and `IndexSet::move_index` methods change
23+
the position of an item from one index to another, shifting the items
24+
between to accommodate the move.
25+
2226
- The new `map::Slice<K, V>` and `set::Slice<T>` offer a linear view of maps
2327
and sets, behaving a lot like normal `[(K, V)]` and `[T]` slices. Notably,
2428
comparison traits like `Eq` only consider items in order, rather than hash

src/map.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,19 @@ impl<K, V, S> IndexMap<K, V, S> {
876876
self.core.shift_remove_index(index)
877877
}
878878

879+
/// Moves the position of a key-value pair from one index to another
880+
/// by shifting all other pairs in-between.
881+
///
882+
/// * If `from < to`, the other pairs will shift down while the targeted pair moves up.
883+
/// * If `from > to`, the other pairs will shift up while the targeted pair moves down.
884+
///
885+
/// ***Panics*** if `from` or `to` are out of bounds.
886+
///
887+
/// Computes in **O(n)** time (average).
888+
pub fn move_index(&mut self, from: usize, to: usize) {
889+
self.core.move_index(from, to)
890+
}
891+
879892
/// Swaps the position of two key-value pairs in the map.
880893
///
881894
/// ***Panics*** if `a` or `b` are out of bounds.

src/map/core.rs

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,29 +283,77 @@ impl<K, V> IndexMapCore<K, V> {
283283
///
284284
/// The index should already be removed from `self.indices`.
285285
fn shift_remove_finish(&mut self, index: usize) -> (K, V) {
286-
// use Vec::remove, but then we need to update the indices that point
287-
// to all of the other entries that have to move
286+
// Correct indices that point to the entries that followed the removed entry.
287+
self.decrement_indices(index + 1, self.entries.len());
288+
289+
// Use Vec::remove to actually remove the entry.
288290
let entry = self.entries.remove(index);
291+
(entry.key, entry.value)
292+
}
289293

290-
// correct indices that point to the entries that followed the removed entry.
291-
// use a heuristic between a full sweep vs. a `find()` for every shifted item.
292-
let raw_capacity = self.indices.buckets();
293-
let shifted_entries = &self.entries[index..];
294-
if shifted_entries.len() > raw_capacity / 2 {
295-
// shift all indices greater than `index`
294+
/// Decrement all indices in the range `start..end`.
295+
///
296+
/// The index `start - 1` should not exist in `self.indices`.
297+
/// All entries should still be in their original positions.
298+
fn decrement_indices(&mut self, start: usize, end: usize) {
299+
// Use a heuristic between a full sweep vs. a `find()` for every shifted item.
300+
let shifted_entries = &self.entries[start..end];
301+
if shifted_entries.len() > self.indices.buckets() / 2 {
302+
// Shift all indices in range.
296303
for i in self.indices_mut() {
297-
if *i > index {
304+
if start <= *i && *i < end {
298305
*i -= 1;
299306
}
300307
}
301308
} else {
302-
// find each following entry to shift its index
303-
for (i, entry) in (index + 1..).zip(shifted_entries) {
309+
// Find each entry in range to shift its index.
310+
for (i, entry) in (start..end).zip(shifted_entries) {
304311
update_index(&mut self.indices, entry.hash, i, i - 1);
305312
}
306313
}
314+
}
307315

308-
(entry.key, entry.value)
316+
/// Increment all indices in the range `start..end`.
317+
///
318+
/// The index `end` should not exist in `self.indices`.
319+
/// All entries should still be in their original positions.
320+
fn increment_indices(&mut self, start: usize, end: usize) {
321+
// Use a heuristic between a full sweep vs. a `find()` for every shifted item.
322+
let shifted_entries = &self.entries[start..end];
323+
if shifted_entries.len() > self.indices.buckets() / 2 {
324+
// Shift all indices in range.
325+
for i in self.indices_mut() {
326+
if start <= *i && *i < end {
327+
*i += 1;
328+
}
329+
}
330+
} else {
331+
// Find each entry in range to shift its index, updated in reverse so
332+
// we never have duplicated indices that might have a hash collision.
333+
for (i, entry) in (start..end).zip(shifted_entries).rev() {
334+
update_index(&mut self.indices, entry.hash, i, i + 1);
335+
}
336+
}
337+
}
338+
339+
pub(super) fn move_index(&mut self, from: usize, to: usize) {
340+
let from_hash = self.entries[from].hash;
341+
if from != to {
342+
// Use a sentinal index so other indices don't collide.
343+
update_index(&mut self.indices, from_hash, from, usize::MAX);
344+
345+
// Update all other indices and rotate the entry positions.
346+
if from < to {
347+
self.decrement_indices(from + 1, to + 1);
348+
self.entries[from..=to].rotate_left(1);
349+
} else if to < from {
350+
self.increment_indices(to, from);
351+
self.entries[to..=from].rotate_right(1);
352+
}
353+
354+
// Change the sentinal index to its final position.
355+
update_index(&mut self.indices, from_hash, usize::MAX, to);
356+
}
309357
}
310358

311359
/// Remove an entry by swapping it with the last

src/set.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,19 @@ impl<T, S> IndexSet<T, S> {
731731
self.map.shift_remove_index(index).map(|(x, ())| x)
732732
}
733733

734+
/// Moves the position of a value from one index to another
735+
/// by shifting all other values in-between.
736+
///
737+
/// * If `from < to`, the other values will shift down while the targeted value moves up.
738+
/// * If `from > to`, the other values will shift up while the targeted value moves down.
739+
///
740+
/// ***Panics*** if `from` or `to` are out of bounds.
741+
///
742+
/// Computes in **O(n)** time (average).
743+
pub fn move_index(&mut self, from: usize, to: usize) {
744+
self.map.move_index(from, to)
745+
}
746+
734747
/// Swaps the position of two values in the set.
735748
///
736749
/// ***Panics*** if `a` or `b` are out of bounds.

tests/quick.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,53 @@ quickcheck_limit! {
216216
map[&key] == value && map[i] == value
217217
})
218218
}
219+
220+
// Use `u8` test indices so quickcheck is less likely to go out of bounds.
221+
fn swap_indices(vec: Vec<u8>, a: u8, b: u8) -> TestResult {
222+
let mut set = IndexSet::<u8>::from_iter(vec);
223+
let a = usize::from(a);
224+
let b = usize::from(b);
225+
226+
if a >= set.len() || b >= set.len() {
227+
return TestResult::discard();
228+
}
229+
230+
let mut vec = Vec::from_iter(set.iter().cloned());
231+
vec.swap(a, b);
232+
233+
set.swap_indices(a, b);
234+
235+
// Check both iteration order and hash lookups
236+
assert!(set.iter().eq(vec.iter()));
237+
assert!(vec.iter().enumerate().all(|(i, x)| {
238+
set.get_index_of(x) == Some(i)
239+
}));
240+
TestResult::passed()
241+
}
242+
243+
// Use `u8` test indices so quickcheck is less likely to go out of bounds.
244+
fn move_index(vec: Vec<u8>, from: u8, to: u8) -> TestResult {
245+
let mut set = IndexSet::<u8>::from_iter(vec);
246+
let from = usize::from(from);
247+
let to = usize::from(to);
248+
249+
if from >= set.len() || to >= set.len() {
250+
return TestResult::discard();
251+
}
252+
253+
let mut vec = Vec::from_iter(set.iter().cloned());
254+
let x = vec.remove(from);
255+
vec.insert(to, x);
256+
257+
set.move_index(from, to);
258+
259+
// Check both iteration order and hash lookups
260+
assert!(set.iter().eq(vec.iter()));
261+
assert!(vec.iter().enumerate().all(|(i, x)| {
262+
set.get_index_of(x) == Some(i)
263+
}));
264+
TestResult::passed()
265+
}
219266
}
220267

221268
use crate::Op::*;

0 commit comments

Comments
 (0)