Convertir un hash anidado en un hash plano

Esta pregunta es la inversa de esta pregunta.

Given a nested hash like

{
    :a => {
       :b => {:c => 1, :d => 2},
       :e => 3,
    },
    :f => 4,
}

what is the best way to convert it into a flat hash like

{
    [:a, :b, :c] => 1,
    [:a, :b, :d] => 2,
    [:a, :e] => 3,
    [:f] => 4,
}

preguntado el 10 de marzo de 12 a las 16:03

So you want an array of keys in order that lead to a value? -

8 Respuestas

De otra manera:

def flat_hash(h,f=[],g={})
  return g.update({ f=>h }) unless h.is_a? Hash
  h.each { |k,r| flat_hash(r,f+[k],g) }
  g
end

h = { :a => { :b => { :c => 1,
                      :d => 2 },
              :e => 3 },
      :f => 4 }

flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

contestado el 27 de mayo de 14 a las 02:05

I know Ruby doesn't necessarily support TCO out of the box, but if you didn't return g here, would this be tail-call optimized? - oxidado

I've heard the term "tail-call optimized" but I have no idea what it means. Perhaps a computer scientist can answer your question. - cary swoveland

You can't optimize the tail call here IMO since you have a cascaded recursion. - Stefan Majewski

Very similar to Adiel Mittmann's solution

def flat_hash(h, k = [])
  new_hash = {}
  h.each_pair do |key, val|
    if val.is_a?(Hash)
      new_hash.merge!(flat_hash(val, k + [key]))
    else
      new_hash[k + [key]] = val
    end
  end
  new_hash
end

Edit: Refactored for elegance. Should be almost as fast.

def flat_hash(hash, k = [])
  return {k => hash} unless hash.is_a?(Hash)
  hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
end

respondido 10 mar '12, 19:03

This one ran the fastest. Thanks. - sawa

@sawa: Just a tip for the future: If you want a fast solution, mention this in the question next time. Usually the main criterion in dynamic languages like Python or Ruby is elegancy and conciseness. If you specifically ask for performance as well, you could get much better suited answers :) - Niklas B.

@sawa: refactored for elegance. - Kyle

@Kyle One thing that was good about your earlier code was that it can be redefined as a method on Hash. The rewritten one cannot. - sawa

@Niklas, perhaps, but most Ruby-readers know sawa is a speed-freak. - cary swoveland

Mi intento:

def flatten_hash(h)
  return { [] => h } unless h.is_a?(Hash)
  Hash[h.map { |a,v1| flatten_hash(v1).map { |b,v2| [[a] + b, v2] } }.flatten(1)]
end

Sorry for the bad variables names, had to fit it in one line.

respondido 10 mar '12, 17:03

This is not an attempt to give you the el albergue mejor calificado way to do it, but it is a way :P

def flatten(hash)
  return {[] => hash} if !hash.is_a?(Hash)
  map = {}
  hash.each_pair do |key1, value1|
    flatten(value1).each_pair do |key2, value2|
      map[[key1] + key2] = value2
    end
  end
  return map
end

It works for your example, producing this result:

{[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

It may not produce the result you expect if there are empty hashes.

respondido 10 mar '12, 17:03

A functional approach (see the historia for an alternative implementations):

def recursive_flatten(hash)
  hash.flat_map do |key, value|
    if value.is_a?(Hash)
      recursive_flatten(value).map { |ks, v| [[key] + ks, v] } 
    else
      [[[key], value]]
    end
  end.to_h
end

contestado el 23 de mayo de 17 a las 13:05

Inspired by @cary-swoveland way, but in Hash class :

class Hash
  def deep_flatten(previous_key=[])
    flat_hash = {}
    self.each do |key, value|
      next_key = previous_key+[key]
      flat_hash.update(value.is_a?(Hash) ? value.deep_flatten(next_key) : {next_key=>value})
    end
    return flat_hash
  end
end

h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }

h.deep_flatten #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

Respondido el 23 de Septiembre de 15 a las 09:09

A declarative solution using ProfundoEnumerable:

require 'deep_enumerable'

h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }

h.deep_each.map do |k, v|
  [DeepEnumerable.deep_key_to_array(k), v]
end.to_h

or, for those who prefer point-free style

h.deep_each.to_h.shallow_map_keys(&DeepEnumerable.method(:deep_key_to_array))

Respondido 24 Abr '16, 16:04

Array support / readable names / no update for speed / stringified results keys

def flat_hash(input, base = nil, all = {})
  if input.is_a?(Array)
    input = input.each_with_index.to_a.each(&:reverse!)
  end

  if input.is_a?(Hash) || input.is_a?(Array)
    input.each do |k, v|
      flat_hash(v, base ? "#{base}.#{k}" : k, all)
    end
  else
    all[base] = input
  end

  all
end

respondido 12 mar '17, 02:03

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.