Flattening nested hash to a single hash with Ruby/Rails

16,336

Solution 1

You could do this:

def flatten_hash(hash)
  hash.each_with_object({}) do |(k, v), h|
    if v.is_a? Hash
      flatten_hash(v).map do |h_k, h_v|
        h["#{k}.#{h_k}".to_sym] = h_v
      end
    else 
      h[k] = v
    end
   end
end

flatten_hash(:foo => "bar",
  :hello => {
    :world => "Hello World",
    :bro => "What's up dude?",
  },
  :a => {
    :b => {
      :c => "d"
    }
  })
# => {:foo=>"bar", 
# =>  :"hello.world"=>"Hello World", 
# =>  :"hello.bro"=>"What's up dude?", 
# =>  :"a.b.c"=>"d"} 

Solution 2

Because I love Enumerable#reduce and hate lines apparently:

def flatten_hash(param, prefix=nil)
  param.each_pair.reduce({}) do |a, (k, v)|
    v.is_a?(Hash) ? a.merge(flatten_hash(v, "#{prefix}#{k}.")) : a.merge("#{prefix}#{k}".to_sym => v)
  end
end

irb(main):118:0> flatten_hash(hash)
=> {:foo=>"bar", :"hello.world"=>"Hello World", :"hello.bro"=>"What's up dude?", :"a.b.c"=>"d"}

Solution 3

The top voted answer here will not flatten the object all the way, it does not flatten arrays. I've corrected this below and have offered a comparison:

x = { x: 0, y: { x: 1 }, z: [ { y: 0, x: 2 }, 4 ] }

def top_voter_function ( hash )
  hash.each_with_object( {} ) do |( k, v ), h|
    if v.is_a? Hash
      top_voter_function( v ).map do |h_k, h_v|
        h[ "#{k}.#{h_k}".to_sym ] = h_v
      end
    else
      h[k] = v
    end
  end
end

def better_function ( a_el, a_k = nil )
  result = {}

  a_el = a_el.as_json

  a_el.map do |k, v|
    k = "#{a_k}.#{k}" if a_k.present?
    result.merge!( [Hash, Array].include?( v.class ) ? better_function( v, k ) : ( { k => v } ) )
  end if a_el.is_a?( Hash )

  a_el.uniq.each_with_index do |o, i|
    i = "#{a_k}.#{i}" if a_k.present?
    result.merge!( [Hash, Array].include?( o.class ) ? better_function( o, i ) : ( { i => o } ) )
  end if a_el.is_a?( Array )

  result
end

top_voter_function( x ) #=> {:x=>0, :"y.x"=>1, :z=>[{:y=>0, :x=>2}, 4]}
better_function( x ) #=> {"x"=>0, "y.x"=>1, "z.0.y"=>0, "z.0.x"=>2, "z.1"=>4} 

I appreciate that this question is a little old, I went looking online for a comparison of my code above and this is what I found. It works really well when used with events for an analytics service like Mixpanel.

Solution 4

In my case I was working with the Parameters class so none of the above solutions worked for me. What I did to resolve the problem was to create the following function:

def flatten_params(param, extracted = {})
    param.each do |key, value|
        if value.is_a? ActionController::Parameters
            flatten_params(value, extracted)
        else
            extracted.merge!("#{key}": value)
        end
    end
    extracted
end

Then you can use it like flatten_parameters = flatten_params(params). Hope this helps.

Solution 5

Or if you want a monkey-patched version or Uri's answer to go your_hash.flatten_to_root:

class Hash
  def flatten_to_root
    self.each_with_object({}) do |(k, v), h|
      if v.is_a? Hash
        v.flatten_to_root.map do |h_k, h_v|
          h["#{k}.#{h_k}".to_sym] = h_v
        end
      else
        h[k] = v
      end
    end
  end
end
Share:
16,336

Related videos on Youtube

yerforkferchips
Author by

yerforkferchips

Updated on June 24, 2022

Comments

  • yerforkferchips
    yerforkferchips almost 2 years

    I want to "flatten" (not in the classical sense of .flatten) down a hash with varying levels of depth, like this:

    {
      :foo => "bar",
      :hello => {
        :world => "Hello World",
        :bro => "What's up dude?",
      },
      :a => {
        :b => {
          :c => "d"
        }
      }
    }
    

    down into a hash with one single level, and all the nested keys merged into one string, so it would become this:

    {
      :foo => "bar",
      :"hello.world" => "Hello World",
      :"hello.bro" => "What's up dude?",
      :"a.b.c" => "d"
    }
    

    but I can't think of a good way to do it. It's a bit like the deep_ helper functions that Rails adds to Hashes, but not quite the same. I know recursion would be the way to go here, but I've never written a recursive function in Ruby.

    • kiddorails
      kiddorails almost 10 years
      Will it be useful if you convert your hash in OpenStruct object (the way Rails do it)? Edit: I now realized that it won't perform it recursively in object. But recursive-open-struct seems like a solution.
  • yerforkferchips
    yerforkferchips almost 10 years
    Brilliant, this saves me a day of headaches. And it's nice and simple.
  • Oscar
    Oscar over 7 years
    As a tip, I recommend using the gem 'humanize' to make the numbers more readable.
  • yerforkferchips
    yerforkferchips over 7 years
    Thank you. My question was specifically for flattening hashes, though, and I actually didn't want arrays to be flattened for my use case back then.
  • Oscar
    Oscar about 7 years
    No worries. Actually, there's no mention of arrays in the original question at all
  • Dan R
    Dan R almost 7 years
    Thanks for submitting, I found this very useful!
  • jgomo3
    jgomo3 about 4 years
    Excellent. Probably you want to change the map for an each.
  • Mr. Demetrius Michael
    Mr. Demetrius Michael almost 2 years
    doesn't work with nested array of hashes