I have a multi-dimensional array such as:
array = [["stop", "halt"],["stop", "red"],["go", "green"],["go","fast"],["caution","yellow"]]
And I want to turn it into a hash like this:
hash = {"stop" => ["halt","red"], "go" => ["green","fast"], "caution" => "yellow"}
However, when I array.to_h , the values overwrite one another and I get:
hash = {"stop" => "red", "go" => "fast", "caution" => "yellow"}
How do I get the desired array?
This is one way. It uses Enumerable#each_with_object and the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged.
array << ["stop", "or I'll fire!"]
array.each_with_object({}) { |(f,l),h|
h.update(f=>l) { |_,ov,nv| ov.is_a?(Array) ? ov << nv : [ov, nv] } }
#=> {"stop"=>["halt", "red", "or I'll fire!"],
# "go"=>["green", "fast"],
# "caution"=>"yellow"}
The code is simplified if you want all values in the returned hash to be arrays (i.e., "caution"=>["yellow"]), which is generally more convenient for subsequent calculations:
array.each_with_object({}) { |(f,l),h| h.update(f=>[l]) {|_,ov,nv| ov+nv }}
#=> {"stop"=>["halt", "red", "or I'll fire!"],
# "go"=>["green", "fast"],
# "caution"=>["yellow"]}
One way to do it:
array.inject({}) {|r, (k, v)| r[k] &&= [*r[k], v]; r[k] ||= v; r }
That's pretty messy though. Written out, it looks like this:
def to_hash_with_duplicates(arr)
{}.tap do |r|
arr.each do |k, v|
r[k] &&= [*r[k], v] # key already present, turn into array and add value
r[k] ||= v # key not present, simply store value
end
end
end
Edit: Thinking a bit more, @cary-swoveland's update-with-block solution is better, because it handles nil and false values correctly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With