0

I have a map: HashSet<String, String> and want to conditionally mutate each of the values.

I want to lookup the new values in the same HashSet that I am mutating.

How can I do it without borrow checker problems?

fn main() {
    let mut map = std::collections::HashMap::new();
    map.insert("alien".to_string(), "".to_string());
    map.insert("covid".to_string(), "virus".to_string());
    map.insert("mammal".to_string(), "animal".to_string());
    map.insert("cat".to_string(), "mammal".to_string());
    map.insert("lion".to_string(), "cat".to_string());

    println!("{:#?}", map);

    // Replace values by lookup in self
    loop {
        let mut done = true;
        for (key, val) in map {
            if let Some(new) = map.get(val) {
                if let Some(old) = map.insert(key.clone(), new.clone()) {
                    if &old != new {
                        done = false;
                    }
                }
            }
        }
        if done {
            break;
        }
    }

    let mut result = std::collections::HashMap::new();
    result.insert("alien".to_string(), "".to_string());
    result.insert("covid".to_string(), "virus".to_string());
    result.insert("mammal".to_string(), "animal".to_string());
    result.insert("cat".to_string(), "animal".to_string());
    result.insert("lion".to_string(), "animal".to_string());

    println!("{:#?}", result);

    assert_eq!(map, result);
}

Playground

Adamarla
  • 739
  • 9
  • 20

1 Answers1

0

After spending quite some time, this is what I've come up with, using @Stargateur's suggestion.

I use a second HashMap that will store what you're looking for - a root of each node in the tree (or more specifically forest of trees) defined by your map. To prevent traversing these trees completely, I check if a root for a parent is already found. If so, then no need to look for root, just use it. Otherwise I traverse the tree until I either find a root or a root is already found in reducedMap, and as I traverse I store which nodes I visited - the root we'll find will be their root too.

As root is found, I assign this root to all visited nodes.

We finish with converting the reducedMap, which is a map of references, to the result map, for that we clone strings

    // map of shortcuts to root, just references, no string copying
    let mut reducedMap = std::collections::HashMap::<&std::string::String, &std::string::String>::new();

    for (key, mut val) in map.iter() {
        let mut intermediateKeys = vec![]; // all intermediate keys as we go to root
        loop { 
            intermediateKeys.push(key); // remember the intermediate key to update it's shortcut too
            if let Some(shortcut) = reducedMap.get(val) { 
                val = shortcut; // if we know the shortcut, take it and leave 
                break;
            } 
            if let Some(parent) = map.get(val) {
                val = parent; // while not root, go deeper
            } else {
                break; // reached the root
            }
        }
        // insert or update shortcuts for all intermediate keys (root is in `val`)
        intermediateKeys.drain(..) // take items and clear the vector
            .for_each(|k| *reducedMap.entry(k).or_insert(val) = val);
    }

    map = reducedMap.iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect();  // move result back, now actually cloning strings

Playground

Alexey S. Larionov
  • 6,555
  • 1
  • 18
  • 37