Cannot remove a String element in HashMap while iterating


#1

Hello,

What I try to achieve is the following:

I have a HashMap<String, u32>. Now I would like to remove characters from the String like the following:

  1. #someWord### -> someWord i.e. remove any ‘#’ as prefix or suffix
  2. ######### -> if String consists only of ‘#’ remove it from the HashMap
  3. some#Word -> some#Word, i.e do nothing if ‘#’ is in the middle

For option 1 I have found a nice method:

 let mut s = String::from("##dog#");
 s = s.trim_matches('#').to_string();

However, if I try to put all 3 checks in 1 piece of code it does not work - I am missing something:

As an example I started with:

for (word, _) in words.iter_mut() {
    
    word = &word.trim_matches('#').to_string(); // this is failing due to insufficient lifetime
    // some code
    words.remove(); // here it fails because I try to change words while iter_mut() is already doing it

Then I’ve tried t follow this suggestion Stack to have another collection created, but have some troubles with the filter method as it requires a bool expression:

let words: HashMap<_,_> = words.into_iter()
                                .filter(|&(ref word, _)|  // pseudocode -> trim word and if becomes empty, add nothing to the new HashMap )
                               .collect();

#2

You could resolve this by putting a “map” step before your “filter” step. The “map” step does the word-trimming and returns the (trimmed key, payload) tuple, then the “filter” step eliminates tuples with empty keys.


#3

Pff, yeah - with map in between into_iter() and filter() works as expected. Thanks.
A few more minutes and I would not need to post a new topic.


#4

You can filter and map in one closure: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map


#5

Thanks for pointing this out but in my case separating them will probably be more clear.

Initially I’ve tried with and was satisfied there were no errors from compiler:

.map(|(word, count)| (word.trim_matches(’#’).to_string(), count) )

However, after some tests I realized that it is not working correctly due to the HashMap property to have only 1 key value pair for the same key:
i.e. I cannot have

dog - 4
dog - 9

in a HashMap.

So what would you advise me:
Is it possible to use HashMap to achieve the following:

dog - 1
#dog# - 3

after command:

dog - 4

or should I use other constructors such as Vectors?


#6

Ah, indeed, I forgot about that.

A naive way to solve that would be to collect the (trimmed_key, value) pairs in a Vec, sort by key, merge the neighbouring elements and reconstruct the resulting HashMap.

But that has poor memory and computational efficiency, so I would personally prefer something like this:

  • Create a new HashMap
  • Loop over the (key, value) pairs of the old map
    • Trim the key
    • Check if the new map already contains an entry associated with the trimmed key
    • If so, add the value to that of the existing entry
    • If not, create a new entry for that key with the relevant value

HashMap has a nice API for this kind of use case. In addition, one extra advantage of this solution is you can avoid systematically converting each trimmed key to a heap-allocated String, and use just an &str slice in some cases instead.