Forum: Ruby Flatten out Hash

Posted by Glenn Ritz (ritzg)
on 2010-03-09 13:31
Hi,

I would like to take a nested hash that looks like this:

{"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}

and turn it into an array of 2 element arrays like this:

[["a: b2: c2: ", 2], ["a: b: c: ", 1]]

Is there a simple way to do this?  I wrote a method that iterates
through the nested hashes recursively, but it's a bit cumbersome.
Posted by Jean G. (Guest)
on 2010-03-09 13:52
(Received via mailing list)
On Tue, Mar 9, 2010 at 8:31 PM, Glenn Ritz <glenn_ritz@yahoo.com> wrote:
> Hi,
>
> I would like to take a nested hash that looks like this:
>
> {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
>
> and turn it into an array of 2 element arrays like this:
>
> [["a: b2: c2: ", 2], ["a: b: c: ", 1]]
>

My method:

hash = {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
array = []

hash.keys.each do |k1|
    hash[k1].keys.each do |k2|
        hash[k1][k2].keys.each do |k3|
             array << ["#{k1}: #{k2}: #{k3}:",hash[k1][k2][k3]]
        end
    end
end
Posted by Glenn Ritz (ritzg)
on 2010-03-09 14:05
Jean G. wrote:
> On Tue, Mar 9, 2010 at 8:31 PM, Glenn Ritz <glenn_ritz@yahoo.com> wrote:
>> Hi,
>>
>> I would like to take a nested hash that looks like this:
>>
>> {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
>>
>> and turn it into an array of 2 element arrays like this:
>>
>> [["a: b2: c2: ", 2], ["a: b: c: ", 1]]
>>
> 
> My method:
> 
> hash = {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
> array = []
> 
> hash.keys.each do |k1|
>     hash[k1].keys.each do |k2|
>         hash[k1][k2].keys.each do |k3|
>              array << ["#{k1}: #{k2}: #{k3}:",hash[k1][k2][k3]]
>         end
>     end
> end

Thanks, Jean.

It's a good solution.  But it's dependent on there being 3 levels of 
hashes.  What if you didn't know how many levels of nesting there was 
before executing the code?  Suppose the hash looked like this instead:

{"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=> {"d2" => 2}}}}
Posted by Brian Candler (candlerb)
on 2010-03-09 14:15
Glenn Ritz wrote:
> Hi,
> 
> I would like to take a nested hash that looks like this:
> 
> {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
> 
> and turn it into an array of 2 element arrays like this:
> 
> [["a: b2: c2: ", 2], ["a: b: c: ", 1]]
> 
> Is there a simple way to do this?  I wrote a method that iterates
> through the nested hashes recursively, but it's a bit cumbersome.

I think that's the right way, if the nesting depth is variable. One 
example:

class Hash
  def flat_each(prefix="", &blk)
    each do |k,v|
      if v.is_a?(Hash)
        v.flat_each(prefix+k+": ", &blk)
      else
        yield prefix+k, v
      end
    end
  end
end

require 'enumerator'
h = {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
p h.to_enum(:flat_each).collect { |k,v| [k,v] }
Posted by Brian Candler (candlerb)
on 2010-03-09 14:25
Perhaps a better solution is to yield an array of keys (being the 'path' 
to reach the end node), because then the caller can choose how to 
combine them.

class Hash
  def flat_each(prefix=[], &blk)
    each do |k,v|
      if v.is_a?(Hash)
        v.flat_each(prefix+[k], &blk)
      else
        yield prefix+[k], v
      end
    end
  end
end

require 'enumerator'
h = {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=>2}}}
h = {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=> {"d2" => 2}}}}
p h.to_enum(:flat_each).collect { |k,v| [k.join(": "),v] }
h.flat_each do |k,v|
  puts "#{k.join("/")} => #{v}"
end
Posted by Robert Klemme (Guest)
on 2010-03-09 14:26
(Received via mailing list)
2010/3/9 Glenn Ritz <glenn_ritz@yahoo.com>:
>>> [["a: b2: c2: ", 2], ["a: b: c: ", 1]]
>>              array << ["#{k1}: #{k2}: #{k3}:",hash[k1][k2][k3]]
> {"a"=>{"b"=>{"c"=>1}, "b2"=>{"c2"=> {"d2" => 2}}}}
Basically you want to do a DFS and store the path to the root for
every leaf.  Something like http://gist.github.com/326558

Kind regards

robert
Posted by Richard Schneeman (snowmaninthesun)
on 2011-06-24 00:27
I coded up a way to do this in a method without needing to monkey-patch
Hash: http://forr.st/~zaG


# Takes in a hash in input and merges it with an output hash to produce
a hash that is only one dimensional
# i.e. there are no nested elements in the hash

def make_hash_one_dimensional(input = {}, output = {}, options = {})
  input.each do |key, value|
    key = options[:prefix].nil? ? "#{key}" :
"#{options[:prefix]}#{options[:delimiter]||"_"}#{key}"
    if value.is_a? Hash
      make_hash_one_dimensional(value, output, :prefix => key, 
:delimiter => "_")
    else
      output[key]  = value
    end
  end
  output
end

hash = {:a => {:b => {:c => {:d => {:e => "hi"}}}}, :f => "there"}
make_hash_one_dimensional(hash)
#=> {"f"=>"there", "a_b_c_d_e"=>"hi"}


From there it would be easy to turn this into an array.  Enjoy

--
@schneems
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.