4

Looking for an answer that works on Ruby 1.8.7 :

For example lets say I have a hash like this:

{"Book Y"=>["author B", "author C"], "Book X"=>["author A", "author B", "author C"]}

and I want to get this:

{ 
    "author A" => ["Book X"],
    "author B" => ["Book Y", "Book X"],
    "author C" => ["Book Y", "Book X"] 
}

I wrote a really long method for it, but with large datasets, it is super slow.

Any elegant solutions?

4
  • 1
    show that super slow method. Commented Feb 22, 2015 at 6:34
  • 2
    Ruby 1.8.4 was released almost ten years ago. You should consider to upgrade to a newer version. It will be hard to find gems that still working with this outdated version. Commented Feb 22, 2015 at 7:33
  • Already updated the question to mention that its for Ruby 1.8.7. Anyway the two answers that worked for this question would be by spickermann and Rustam A. Gasanov (below). The answer that the rest provided (Cory and sawa) work for newer versions of Ruby. Thank you everyone! Commented Feb 22, 2015 at 7:52
  • 1
    Ruby 1.8.7 is seven years old... Commented Feb 22, 2015 at 10:21

3 Answers 3

5

This is one way:

g = {"Book Y"=>["author B", "author C"],
     "Book X"=>["author A", "author B", "author C"]}

g.each_with_object({}) do |(book,authors),h|
  authors.each { |author| (h[author] ||= []) << book }
end
  #=> {"author B"=>["Book Y", "Book X"],
  #    "author C"=>["Book Y", "Book X"],
  #    "author A"=>["Book X"]} 

The steps:

enum = g.each_with_object({})
  #=> #<Enumerator: {"Book Y"=>["author B", "author C"],
  #   "Book X"=>["author A", "author B", "author C"]}:each_with_object({})> 

We can see the elements of enum, which it will pass into the block, by converting it to an array:

enum.to_a
  #=> [[["Book Y", ["author B", "author C"]], {}],
  #    [["Book X", ["author A", "author B", "author C"]], {}]]

The first element of enum passed to the block and assigned to the block variables is:

(book,authors),h = enum.next
  #=> [["Book Y", ["author B", "author C"]], {}] 
book
  #=> "Book Y" 
authors
  #=> ["author B", "author C"] 
h
  #=> {} 

enum1 = authors.each
  #=> #<Enumerator: ["author B", "author C"]:each>
author = enum1.next
  #=> "author B"
(h[author] ||= []) << book
  #=> (h["author B"] ||= []) << "Book Y"
  #=> (h["author B"] = h["author B"] || []) << "Book Y"
  #=> (h["author B"] = nil || []) << "Book Y"
  #=> h["author B"] = ["Book Y"]
  #=> ["Book Y"]
h #=> {"author B"=>["Book Y"]} 

Next:

author = enum1.next
  #=> "author C" 
(h[author] ||= []) << book
h #=> {"author B"=>["Book Y", "Book Y"], "author C"=>["Book Y"]} 

Having finished with "Book X",

(book,authors),h = enum.next
  #=> [["Book X", ["author A", "author B", "author C"]],
  #    {"author B"=>["Book Y", "Book Y"], "author C"=>["Book Y"]}]
book
  #=> "Book X" 
authors
  #=> ["author A", "author B", "author C"] 
h
  #=> {"author B"=>["Book Y", "Book Y"], "author C"=>["Book Y"]} 

We now repeat the same calculations as as we did for "Book X". The only difference is that when we encounter:

(h[author] ||= []) << book

which is equivalent to

(h[author] = h[author] || []) << book

in most case h[author] on the right of the equals sign will not be nil (e.g., it may be ["Book X"], in which case the above expression reduces to:

h[author] << book

Addendum

For versions of Ruby before the war (e.g., 1.8.7), just initialize the hash first and use each instead of each_with_object (we got the latter with 1.9. I was too young for 1.8.7, but I often wonder how people got along without it.) You just need to remember to return h at the end, as each just returns its receiver.

So change it to:

h = {}
g.each do |book,authors|
  authors.each { |author| (h[author] ||= []) << book }
end
h
  #=> {"author B"=>["Book Y", "Book X"],
  #    "author C"=>["Book Y", "Book X"],
  #    "author A"=>["Book X"]} 
Sign up to request clarification or add additional context in comments.

7 Comments

Is there a way which works for Ruby 1.8.4? cause "flat_map" doesn't seem to work.
Generally you can deal with that with a well place flatten. If you still need help with that I'll look at it after I've finished writing the explanation.
hmmm i've not used the flatten method before. It looks like its for arrays and not hashes. How would the final code look like if I were to use flatten ?
My apologies for disappearing for a few minutes. I had misread the question (hard to do, I know), so had to fix my answer.
Oh, right, you are using an ancient version of Ruby. We were give each_with_object in Ruby 1.9. You can use Enumerable#reduce (aka inject) instead. I'll edit to show you. btw, is there some reason you can't use a more modern version of Ruby?
|
3
h = {"Book Y"=>["author B", "author C"], "Book X"=>["author A", "author B", "author C"]}

p h.inject(Hash.new([])) { |memo,(key,values)|
  values.each { |value| memo[value] += [key] }
  memo
}
# => {"author B"=>["Book Y", "Book X"], "author C"=>["Book Y", "Book X"], "author A"=>["Book X"]}

1 Comment

This is still useful, thanks! Ruby lacks a good multimap (that I can find) so inverting a hash of arrays is a hack but effective way to get around value lookups
0

I would do something like this in Ruby 1.8:

hash = {"Book Y"=>["author B", "author C"], "Book X"=>["author A", "author B", "author C"]}

library = Hash.new { |h, k| h[k] = [] }

hash.each do |book, authors|
  authors.each { |author| library[author] << book }
end

puts library 
#=> {"author B"=>["Book Y", "Book X"], "author C"=>["Book Y", "Book X"], "author A"=>["Book X"]}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.