ruby array of array with repeated values to hash of hash
I'm new to ruby and am having a hard time figuring out how to convert an array of arrays into a hash of a hash of an array.
for example, say I have:
[ [38, "s", "hum"],
[38, "t", "foo"],
[38, "t", "bar"],
[45, "s", "hum"],
[45, "t", "ram"],
[52, "s", "hum"],
[52, "t", "cat"],
[52, "t", "dog"]
]
I'm wanting in the end:
{38 => {"s" => ["hum"],
"t" => ["foo", "bar"]
开发者_如何学JAVA },
45 => {"s" => ["hum"],
"t" => ["ram"]
},
52 => {"s" => ["hum"],
"t" => ["cat", "dog"]
}
}
I've tried group_by and Hash, but neither is giving me what I'm looking for.
Maybe there's a more concise way of doing this, but I decided to just go the straightforward route:
input = [ [38, "s", "hum"],
[38, "t", "foo"],
[38, "t", "bar"],
[45, "s", "hum"],
[45, "t", "ram"],
[52, "s", "hum"],
[52, "t", "cat"],
[52, "t", "dog"]
]
output = {}
# I'll talk through the first iteration in the comments.
input.each do |outer_key, inner_key, value|
# Set output[38] to a new hash, since output[38] isn't set yet.
# If it were already set, this line would do nothing, so
# output[38] would keep its previous data.
output[outer_key] ||= {}
# Set output[38]["s"] to a new array, since output[38]["s"] isn't set yet.
# If it were already set, this line would do nothing, so
# output[38]["s"] would keep its previous data.
output[outer_key][inner_key] ||= []
# Add "hum" to the array at output[38]["s"].
output[outer_key][inner_key] << value
end
So, the part you'd actually use, all tidied up:
output = {}
input.each do |outer_key, inner_key, value|
output[outer_key] ||= {}
output[outer_key][inner_key] ||= []
output[outer_key][inner_key] << value
end
In cases like this, inject
(a.k.a. reduce
in 1.9) is a great tool:
input.inject({}) do |acc, (a, b, c)|
acc[a] ||= {}
acc[a][b] ||= []
acc[a][b] << c
acc
end
It will call the block once for each item in input
passing an accumulator and the item. The first time it passes the argument as the accumulator, and subsequent calls get the return value of the last call as accumulator.
This could be considered horrific or elegant, depending on your sensibilities:
input.inject(Hash.new {|h1,k1| h1[k1] = Hash.new {|h2,k2| h2[k2] = Array.new}}) {|hash,elem| hash[elem[0]][elem[1]].push(elem[2]); hash}
=> {38=>{"s"=>["hum"], "t"=>["foo", "bar"]}, 45=>{"s"=>["hum"], "t"=>["ram"]}, 52=>{"s"=>["hum"], "t"=>["cat", "dog"]}}
A more readable version of this would ideally be:
input.inject(Hash.new(Hash.new(Array.new))) {|hash,elem| hash[elem[0]][elem[1]].push(elem[2]); hash}
That is, start with an empty hash with default value equal to an empty hash with default value equal to an empty array. Then iterate over the input, storing the elements in the appropriate locations.
The problem with the latter syntax is that Hash.new(Hash.new(Array.new)) will cause all the hashes and arrays to have the same location in memory, and thus the values will be overwritten. The former syntax creates a new object each time and thus gives the desired result.
The example given in the question has a length of three for each element array, but the method below uses recursion, and can be used for an arbitrary length.
a = [ [38, "s", "hum", 1],
[38, "t", "foo", 2],
[38, "t", "bar", 3],
[45, "s", "hum", 1],
[45, "t", "ram", 1],
[52, "s", "hum", 3],
[52, "t", "cat", 3],
[52, "t", "dog", 2]
]
class Array
def rep
group_by{|k, _| k}.
each_value{|v| v.map!{|_, *args| args}}.
tap{|h| h.each{|k, v| h[k] = (v.first.length > 1 ? v.rep : v.flatten(1))}}
end
end
p a.rep
精彩评论