2

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?

JQuack
  • 21
  • 4
  • 1
    show that super slow method. – Amit Joki Feb 22 '15 at 06:34
  • 2
    Ruby 1.8.4 was released almost [ten years ago](https://www.ruby-lang.org/de/news/2005/12/24/ruby-184-released/). You should consider to upgrade to a newer version. It will be hard to find gems that still working with this outdated version. – spickermann Feb 22 '15 at 07: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! – JQuack Feb 22 '15 at 07:52
  • 1
    Ruby 1.8.7 is seven years old... – spickermann Feb 22 '15 at 10:21

3 Answers3

4

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"]} 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Is there a way which works for Ruby 1.8.4? cause "flat_map" doesn't seem to work. – JQuack Feb 22 '15 at 06:47
  • 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. – Cary Swoveland Feb 22 '15 at 06:51
  • 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` ? – JQuack Feb 22 '15 at 06:57
  • My apologies for disappearing for a few minutes. I had misread the question (hard to do, I know), so had to fix my answer. – Cary Swoveland Feb 22 '15 at 07:09
  • no worries! thank you for the swift reply. I tried running it though (the latest one you posted), but i received this error : undefined method `each_with_object' for # (NoMethodError) – JQuack Feb 22 '15 at 07:13
  • 1
    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](http://ruby-doc.org/core-2.2.0/Enumerable.html#method-i-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? – Cary Swoveland Feb 22 '15 at 07:40
  • I decided it was easier to just initialize the hash as a separate step, and change `each_with_object` to `each`. Ruby has had `each` since the big bang, so no problem there with 1.8.7. – Cary Swoveland Feb 22 '15 at 08:07
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"]}
Rustam Gasanov
  • 15,290
  • 8
  • 59
  • 72
  • 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 – rajat banerjee Oct 30 '17 at 01:31
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"]}
spickermann
  • 100,941
  • 9
  • 101
  • 131