How to transform "dot-notation" string keys in a Hash into a nested Hash?
How do I transform a Ruby Hash that looks like this:
{
:axis => [1,2],
:"coord.x" => [12,13],
:"coord.y" => [14,15],
}
Into this:
{
:axis => [1,2], #unchaged from input (ok)
:coord => #this has become a hash from coord.x and coord.y keys above
{
:x =&g开发者_如何学Ct; [12,13]
:y => [14,15]
}
}
I have no idea where to start!
# {"a.b.c"=>"v", "b.c.d"=>"c"} ---> {:a=>{:b=>{:c=>"v"}}, :b=>{:c=>{:d=>"c"}}}
def flat_keys_to_nested(hash)
hash.each_with_object({}) do |(key,value), all|
key_parts = key.split('.').map!(&:to_sym)
leaf = key_parts[0...-1].inject(all) { |h, k| h[k] ||= {} }
leaf[key_parts.last] = value
end
end
This code may need to be refactored but it works for the input you have given.
hash = {
:axis => [1,2],
"coord.x" => [12,13],
"coord.y" => [14,15],
}
new_hash = {}
hash.each do |key, val|
new_key, new_sub_key = key.to_s.split('.')
new_key = new_key.to_sym
unless new_sub_key.nil?
new_sub_key = new_sub_key.to_sym
new_hash[new_key] = {} if new_hash[new_key].nil?
new_hash[new_key].merge!({new_sub_key => val})
else
new_hash.store(key, val)
end
end
new_hash # => {:axis=>[1, 2], :coord=>{:x=>[12, 13], :y=>[14, 15]}}
The great thing about Ruby is that you can do things in different ways. Here is another (but as I measured - slightly slower, although this depends on the hash size) method:
hash = {
:axis => [1,2],
"coord.x" => [12,13],
"coord.y" => [14,15],
}
new_hash = Hash.new { |hash, key| hash[key] = {} }
hash.each do |key, value|
if key.respond_to? :split
key.split('.').each_slice(2) do |new_key, sub_key|
new_hash[new_key.to_sym].store(sub_key.to_sym, value)
end
next
end
new_hash[key] = value
end
puts new_hash # => {:axis=>[1, 2], :coord=>{:x=>[12, 13], :y=>[14, 15]}}
But, at least for me, it is easier and quicker to understand what is going on. So this is personal thing.
After doing some testing, I found that if you ever had a deeper structure, you'd end up running into issues with these algorithms because splitting the key only accounts for one dot ('.') in the key. If you were to have more a.b.c
, the algorithms would fail.
For example, given:
{
'a' => 'a',
'b.a' => 'b.a',
'b.b' => 'b.b',
'c.a.b.c.d' => 'c.a.b.c.d',
'c.a.b.c.e' => 'c.a.b.c.e'
}
you would expect:
{
'a' => 'a',
'b' => {'a' =>'b.a', 'b' => 'b.b'},
'c' => {
'a' => {
'b' => {
'c' => {
'd' => 'c.a.b.c.d',
'e' => 'c.a.b.c.e'
}
}
}
}
}
There are also issues if the data tries to overwrite a hash value with a scalar or vice versa:
{
'a3.b.c.d' => 'a3.b.c.d',
'a3.b' => 'a3.b'
}
or
{
'a4.b' => 'a4.b',
'a4.b.c.d' => 'a4.b.c.d'
}
Here's the final version. This one will raise an argument error if one of the bad cases occurs. Obviously you can catch the bad data versions and just echo back the original hash if that makes sense.
def convert_from_dotted_keys(hash)
new_hash = {}
hash.each do |key, value|
h = new_hash
parts = key.to_s.split('.')
while parts.length > 0
new_key = parts[0]
rest = parts[1..-1]
if not h.instance_of? Hash
raise ArgumentError, "Trying to set key #{new_key} to value #{value} on a non hash #{h}\n"
end
if rest.length == 0
if h[new_key].instance_of? Hash
raise ArgumentError, "Replacing a hash with a scalar. key #{new_key}, value #{value}, current value #{h[new_key]}\n"
end
h.store(new_key, value)
break
end
if h[new_key].nil?
h[new_key] = {}
end
h = h[new_key]
parts = rest
end
end
new_hash
end
In the spirit of modularity and reusability, I propose an alternative solution. On a first approach we could have written a go-backwards hash constructor:
input_hash.map do |main_key, main_value|
main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
{key.to_sym => value}
end
end
# [{:coord=>{:x=>[12, 13]}}, {:coord=>{:y=>[14, 15]}}, {:axis=>[1, 2]}]
Not quite what you wanted, but pretty close. Only if Ruby had a recursive merge for hashes we'd be done. Ruby has no such method, but no doubt other people has needed it and written some solutions. Pick the implementation you like the most and now simply write:
input_hash.map do |main_key, main_value|
main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
{key.to_sym => value}
end
end.inject(&:deep_merge)
# {:coord=>{:y=>[14, 15], :x=>[12, 13]}, :axis=>[1, 2]}
This is a slightly refactored version of @grosser answer:
def flatten(hash)
hash.each_with_object({}) do |(path,value), all|
*path, key = key.split('.').map!(&:to_sym)
leaf = path.inject(all) { |h, k| h[k] ||= {} }
leaf[key] = value
end
end
精彩评论