开发者

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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜