简体   繁体   中英

Ruby: How can I convert this array into this hash?

I have an array of arrays. Each item in the array contains three strings: a leg count, an animal and a sound.

a = [ ['4', 'dog', 'woof'] , ['4', 'cow', 'moo'], ['2', 'human', 'yo'] , ['2', 'yeti', 'wrarghh'] ]

I want to turn the array into this hash:

{ 
  '2' => [ { 'human' => 'yo' }, { 'yeti' => 'wrarghh'} ],
  '4' => [ { 'dog' => 'woof' }, { 'cow' => 'moo'} ]
}

I thought reduce would be the way to go but I'm not having much luck. My current stab looks like:

a.reduce({}) do |acc, item|
             acc[item.first] = [] unless acc.key? item.first
             acc[item.first] << { item[1] => item[2] }
           end

But it gets an error:

NoMethodError: undefined method `key?' for [{"dog"=>"woof"}]:Array

What is the best way to achieve this?

a.each_with_object({}) { |(kout, kin, val), h| (h[kout] ||= []) << { kin => val } }
  #=> {"4"=>[{"dog"=>"woof"}, {"cow"=>"moo"}], "2"=>[{"man"=>"yo"}, {"yeti"=>"wrarghh"}]}

We have

enum = a.each_with_object({})
  #=> #<Enumerator: [["4", "dog", "woof"], ["4", "cow", "moo"], ["2", "man", "yo"],
  #                  ["2", "yeti", "wrarghh"]]:each_with_object({})>

The first value is generated by this enumerator and passed to the block, and the block variables are assigned values:

(kout, kin, val), h = enum.next
  #=> [["4", "dog", "woof"], {}]

which is decomposed as follows.

kout
  #=> "4"
kin
  #=> "dog"
val
  #=> "woof"
h #=> {}

The block calculation is therefore

(h[kout] ||= []) << { kin => val }
  #=> (h[kout] = h[kout] || []) << { "dog" => "wolf" }
  #=> (h["4"] = h["4"] || []) << { "dog" => "wolf" }
  #=> (h["4"] = nil ||= []) << { "dog" => "wolf" }
  #=> (h["4"] = []) << { "dog" => "wolf" }
  #=> [] << { "dog" => "wolf" }
  #=> [{ "dog" => "wolf" }]

h["4"] || [] #=> [] h["4"] || [] #=> [] since h has no key "4" and therefore h["4"] #=> nil .

The next value of enum is passed to the block and the calculations are repeated.

(kout, kin, val), h = enum.next
  #=> [["4", "cow", "moo"], {"4"=>[{"dog"=>"woof"}]}]
kout
  #=> "4"
kin
  #=> "cow"
val
  #=> "moo"
h #=> {"4"=>[{"dog"=>"woof"}]}

(h[kout] ||= []) << { kin => val }
  #=> (h[kout] = h[kout] || []) << { "cow" => "moo" }
  #=> (h["4"] = h["4"] || []) << { "cow" => "moo" }
  #=> (h["4"] = [{"dog"=>"woof"}] ||= []) << { "cow" => "moo" }
  #=> (h["4"] = [{"dog"=>"woof"}]) << { "cow" => "moo" }
  #=> [{"dog"=>"woof"}] << { "cow" => "moo" }
  #=> [{ "dog" => "wolf" }, { "cow" => "moo" }]

This time h["4"] || [] #=> [{ "dog" => "wolf" }] h["4"] || [] #=> [{ "dog" => "wolf" }] because h now has a key "4" with a truthy value ( [{ "dog" => "wolf" }] ).

The remaining calculations are similar.

You way works, but, for reduce , the return value (ie, the last line) of the block becomes the next value for (in this case) acc , so all you need to change is:

a.reduce({}) do |acc, item|
  acc[item.first] = [] unless acc.key? item.first
  acc[item.first] << { item[1] => item[2] }

  acc # just add this line
end

Since the return value for Array#<< is the array itself, the second iteration gave acc as the array for the first element. There are, of course, lots of ways to do this, some arguably cleaner, but I find it's useful to know where I went wrong when something I think should work doesn't.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM