2

I have a hash that looks like this:

{"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}

What I want to do is to make a hash of all existing values with an array of all keys that included it, e.g. turn the above into this:

{1 => ["a"], 2 => ["a", "d"], 3 => ["a", "c", "d"], 4 => ["b", "c"]}
Jikku Jose
  • 18,306
  • 11
  • 41
  • 61
A.V. Arno
  • 513
  • 2
  • 5
  • 12
  • @mikej Please again compare this question with the one you referenced. I do not understand the claim that this question is a duplicate of the former one. The previous question required a straightforward use of `invert`. That cannot be used here. – Cary Swoveland Feb 11 '15 at 20:09
  • 1
    @Cary, sorry, you're right. Have re-opened. I think the title of this question could be improved, but the example does make it clear. – mikej Feb 11 '15 at 20:17
  • "[Stack Overflow question checklist](https://meta.stackoverflow.com/questions/260648)" is a good read. We need more than requirements, we need to see an attempt to solve the problem. Questions containing requirements only feel like you're asking us to write the code for you, which is off-topic. – the Tin Man Jan 14 '20 at 00:25

3 Answers3

3

Try this:

module HashReverser
  def invert_map
    each_with_object({}) do |(key, value), result|
      value.each { |v| (result[v] ||= []) << key }
    end
  end
end

original = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5]}

original.extend(HashReverser).invert_map # => {1=>["a"], 2=>["a"], 3=>["a", "c"], 4=>["b", "c"], 5=>["b", "c"], 6=>["b"]}
Jikku Jose
  • 18,306
  • 11
  • 41
  • 61
  • 1
    `Object#extend` blows out the ruby method cache and is not recommended for runtime code. – Chris Heald Feb 11 '15 at 20:17
  • Very nice, JJ. Note you don't need `self.`. Perhaps worth noting that `...with_object(Hash.new { |h,k| h[k] = [] }`, `{ |v| result << key }` is an alternative. btw, I've found that `...(result[v] ||= []) << key` is generally the faster of the two. `include HashReverser` might be clearer. – Cary Swoveland Feb 11 '15 at 20:23
  • Please explain your code and why it's the recommended solution. We're trying to educate, not just solve the immediate problem with the code. – the Tin Man Jan 14 '20 at 00:27
2

I do prefer @Jikku's solution, but there's always another way. Here's one. (I see this is very close to @Chris's solution. I will leave it for the last line, which is a little different.)

Code

def inside_out(h)
  g = h.flat_map { |s,a| a.product([s]) }
       .group_by(&:first)
  g.merge(g) { |_,a| a.map(&:last) }
end

Example

h = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}

inside_out(h)
  #=> {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"],
  #    5=>["b", "c"], 6=>["b"], 7=>["d"]} 

Explanation

For h above:

a = h.flat_map { |s,a| a.product([s]) }
  #=> [[1, "a"], [2, "a"], [3, "a"], [4, "b"], [5, "b"], [6, "b"],
  #    [3, "c"], [4, "c"], [5, "c"], [7, "d"], [2, "d"], [3, "d"]] 
g = a.group_by(&:first)
  #=> {1=>[[1, "a"]], 2=>[[2, "a"], [2, "d"]],
  #    3=>[[3, "a"], [3, "c"], [3, "d"]],
  #    4=>[[4, "b"], [4, "c"]],
  #    5=>[[5, "b"], [5, "c"]],
  #    6=>[[6, "b"]],
  #    7=>[[7, "d"]]} 
g.merge(g) { |_,a| a.map(&:last) }
  #=> {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"],
  #    5=>["b", "c"], 6=>["b"], 7=>["d"]} 
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

An alternate solution:

# Given
h = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}

h.flat_map {|k, v| v.product [k]}.group_by(&:first).each_value {|v| v.map! &:last }

Or:

h.flat_map {|k, v| v.product [k]}.reduce({}) {|o, (k, v)| (o[k] ||= []) << v; o}

The idea here is that we use Array#product to create a list of inverted single key-value pairs:

product = h.flat_map {|k, v| v.product([k]) }
# => [[1, "a"], [2, "a"], [3, "a"], [4, "b"], [5, "b"], [6, "b"], [3, "c"], [4, "c"], [5, "c"], [7, "d"], [2, "d"], [3, "d"]]

Group them by the value of the first item in each pair:

groups = product.group_by(&:first)
# => {1=>[[1, "a"]], 2=>[[2, "a"], [2, "d"]], 3=>[[3, "a"], [3, "c"], [3, "d"]], 4=>[[4, "b"], [4, "c"]], 5=>[[5, "b"], [5, "c"]], 6=>[[6, "b"]], 7=>[[7, "d"]]}

And then convert the values to a list of the last values in each pair:

result = groups.each_value {|v| v.map! &:last }
# => {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"], 5=>["b", "c"], 6=>["b"], 7=>["d"]}
Chris Heald
  • 61,439
  • 10
  • 123
  • 137