how to merge hash of hash with same keys in ruby [duplicate] - ruby

I would like to merge a nested hash.
a = {:book=>
[{:title=>"Hamlet",
:author=>"William Shakespeare"
}]}
b = {:book=>
[{:title=>"Pride and Prejudice",
:author=>"Jane Austen"
}]}
I would like the merge to be:
{:book=>
[{:title=>"Hamlet",
:author=>"William Shakespeare"},
{:title=>"Pride and Prejudice",
:author=>"Jane Austen"}]}
What is the nest way to accomplish this?

For rails 3.0.0+ or higher version there is the deep_merge function for ActiveSupport that does exactly what you ask for.

I found a more generic deep-merge algorithm here, and used it like so:
class ::Hash
def deep_merge(second)
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
self.merge(second, &merger)
end
end
a.deep_merge(b)

To add on to Jon M and koendc's answers, the below code will handle merges of hashes, and :nil as above, but it will also union any arrays that are present in both hashes (with the same key):
class ::Hash
def deep_merge(second)
merger = proc { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
merge(second.to_h, &merger)
end
end
a.deep_merge(b)

For variety's sake - and this will only work if you want to merge all the keys in your hash in the same way - you could do this:
a.merge(b) { |k, x, y| x + y }
When you pass a block to Hash#merge, k is the key being merged, where the key exists in both a and b, x is the value of a[k] and y is the value of b[k]. The result of the block becomes the value in the merged hash for key k.
I think in your specific case though, nkm's answer is better.

A little late to answer your question, but I wrote a fairly rich deep merge utility awhile back that is now maintained by Daniel Deleo on Github: https://github.com/danielsdeleo/deep_merge
It will merge your arrays exactly as you want. From the first example in the docs:
So if you have two hashes like this:
source = {:x => [1,2,3], :y => 2}
dest = {:x => [4,5,'6'], :y => [7,8,9]}
dest.deep_merge!(source)
Results: {:x => [1,2,3,4,5,'6'], :y => 2}
It won't merge :y (because int and array aren't considered mergeable) - using the bang (!) syntax causes the source to overwrite.. Using the non-bang method will leave dest's internal values alone when an unmergeable entity is found. It will add the arrays contained in :x together because it knows how to merge arrays. It handles arbitrarily deep merging of hashes containing whatever data structures.
Lots more docs on Daniel's github repo now..

All answers look to me overcomplicated. Here's what I came up with eventually:
# #param tgt [Hash] target hash that we will be **altering**
# #param src [Hash] read from this source hash
# #return the modified target hash
# #note this one does not merge Arrays
def self.deep_merge!(tgt_hash, src_hash)
tgt_hash.merge!(src_hash) { |key, oldval, newval|
if oldval.kind_of?(Hash) && newval.kind_of?(Hash)
deep_merge!(oldval, newval)
else
newval
end
}
end
P.S. use as public, WTFPL or whatever license

Here is even better solution for recursive merging that uses refinements and has bang method alongside with block support. This code does work on pure Ruby.
module HashRecursive
refine Hash do
def merge(other_hash, recursive=false, &block)
if recursive
block_actual = Proc.new {|key, oldval, newval|
newval = block.call(key, oldval, newval) if block_given?
[oldval, newval].all? {|v| v.is_a?(Hash)} ? oldval.merge(newval, &block_actual) : newval
}
self.merge(other_hash, &block_actual)
else
super(other_hash, &block)
end
end
def merge!(other_hash, recursive=false, &block)
if recursive
self.replace(self.merge(other_hash, recursive, &block))
else
super(other_hash, &block)
end
end
end
end
using HashRecursive
After using HashRecursive was executed you can use default Hash::merge and Hash::merge! as if they haven't been modified. You can use blocks with these methods as before.
The new thing is that you can pass boolean recursive (second argument) to these modified methods and they will merge hashes recursively.
Example for simple usage is written at this answer. Here is an advanced example.
The example in this question is bad because it got nothing to do with recursive merging. Following line would meet question's example:
a.merge!(b) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
Let me give you a better example to show the power of the code above. Imagine two rooms, each have one bookshelf in it. There are 3 rows on each bookshelf and each bookshelf currently have 2 books. Code:
room1 = {
:shelf => {
:row1 => [
{
:title => "Hamlet",
:author => "William Shakespeare"
}
],
:row2 => [
{
:title => "Pride and Prejudice",
:author => "Jane Austen"
}
]
}
}
room2 = {
:shelf => {
:row2 => [
{
:title => "The Great Gatsby",
:author => "F. Scott Fitzgerald"
}
],
:row3 => [
{
:title => "Catastrophe Theory",
:author => "V. I. Arnol'd"
}
]
}
}
We are going to move books from the shelf in the second room to the same rows on the shelf in the first room. First we will do this without setting recursive flag, i.e. same as using unmodified Hash::merge!:
room1.merge!(room2) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1
The output will tell us that the shelf in the first room would look like this:
room1 = {
:shelf => {
:row2 => [
{
:title => "The Great Gatsby",
:author => "F. Scott Fitzgerald"
}
],
:row3 => [
{
:title => "Catastrophe Theory",
:author => "V. I. Arnol'd"
}
]
}
}
As you can see, not having recursive forced us to throw out our precious books.
Now we will do the same thing but with setting recursive flag to true. You can pass as second argument either recursive=true or just true:
room1.merge!(room2, true) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1
Now the output will tell us that we actually moved our books:
room1 = {
:shelf => {
:row1 => [
{
:title => "Hamlet",
:author => "William Shakespeare"
}
],
:row2 => [
{
:title => "Pride and Prejudice",
:author => "Jane Austen"
},
{
:title => "The Great Gatsby",
:author => "F. Scott Fitzgerald"
}
],
:row3 => [
{
:title => "Catastrophe Theory",
:author => "V. I. Arnol'd"
}
]
}
}
That last execution could be rewritten as following:
room1 = room1.merge(room2, recursive=true) do |k, v1, v2|
if v1.is_a?(Array) && v2.is_a?(Array)
v1+v2
else
v2
end
end
puts room1
or
block = Proc.new {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
room1.merge!(room2, recursive=true, &block)
puts room1
That's it. Also take a look at my recursive version of Hash::each(Hash::each_pair) here.

I think Jon M's answer is the best, but it fails when you merge in a hash with a nil/undefined value.
This update solves the issue:
class ::Hash
def deep_merge(second)
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
self.merge(second, &merger)
end
end
a.deep_merge(b)

a[:book] = a[:book] + b[:book]
Or
a[:book] << b[:book].first

Related

Remove nil values from hash

I am looking to remove keys from hash that have nil value. article is a class storing each article, and attributes method stores the article as hash.
Expected Result:
{"articles":[{"results":[{"author":null,"title":"Former bar manager jailed for preying on homeless 14-year-old girl","summary":"<p><img src=\"http://images.theage.com.au/2015/08/24/6790912/Thumbnail999662740gisd08image.related.thumbnail.320x214.gj68pg.png1440386418031.jpg-90x60.jpg\" width=\"90\" height=\"60\" style=\"float:left;margin:4px;border:0px\"/></p>A man who preyed on a 14-year-old girl he came across living on the streets of Wodonga has been jailed for nine months.","images":null,"source":null,"date":"Mon, 24 Aug 2015 03:20:21 +0000","guid":"<guid isPermaLink=\"false\">gj68pg</guid>","link":"http://www.theage.com.au/victoria/former-bar-manager-jailed-for-preying-on-homeless-14yearold-girl-20150824-gj68pg.html","section":null,"item_type":null,"updated_date":null,"created_date":null,"material_type_facet":null,"abstract":null,"byline":null,"kicker":null}]}]}
Looking to remove null values from the above output.
def attributes
hash = {
"author" => #author,
"title" => #title,
"summary" => #summary,
"images" => #images,
"source" => #source,
"date" => #date
}
hash = {}
count = 0
article.attributes.each do |key,value|
if value == nil
hash[count] = article.attributes.delete(key)
count += 1
end
end
hash.to_json
The result is as below:
{"0":null,"1":null,"2":null,"3":null,"4":null,"5":null,"6":null,"7":null,"8":null,"9":null,"10":null}
If you're on Ruby >= 2.4.6, you can use compact.
https://apidock.com/ruby/v2_4_6/Hash/compact
Example:
hash = {:foo => nil, :bar => "bar"}
=> {:foo=>nil, :bar=>"bar"}
hash.compact
=> {:bar=>"bar"}
hash
=> {:foo=>nil, :bar=>"bar"}
There's also compact! which removes nil values, or nil if no values in the hash were nil (from version >= 2.5.5).
https://apidock.com/ruby/v2_4_6/Hash/compact%21
Example:
hash
=> {:foo=>nil, :bar=>"bar"}
hash.compact!
=> {:bar=>"bar"}
hash
=> {:bar=>"bar"}
hash.compact!
=> nil
How about trying:
hash = article.attributes.select {|k, v| v }
If the value is false or nil, the attribute will be ignored.
If you want to keep the false value and only eliminate nil, you could run:
hash = article.attributes.select {|k, v| !v.nil? }
You can remove all the nil values from a hash namely "h":
h.delete_if { |k, v| v.nil? }
and you can use this one to remove empty values, too:
h.delete_if { |k, v| v.nil? || v.empty? }
In my case I use refinements to Hash class
module HashUtil
refine Hash do
# simple but can not adapt to nested hash
def squeeze
select{|_, v| !v.nil? }
end
# complex but can adapt to nested hash
def squeeze_deep
each_with_object({}) do |(k, v), squeezed_hash|
if v.is_a?(Hash)
squeezed_hash[k] = v.squeeze
else
squeezed_hash[k] = v unless v.nil?
end
end
end
end
end
class Article
using HashUtil
def attributes
hash = {
"author" => #author,
"title" => #title,
"summary" => #summary,
"images" => #images,
"source" => #source,
"date" => #date
}
hash.squeeze
end
end
The given input is valid JSON, and the question has the JSON tag, so I'd like to offer a generic solution to the problem of recursively eliminating keys in JSON objects, wherever they occur, and no matter what the input JSON may be.
The solution is written in a new JSON-oriented programming language, jq, but even if you cannot use jq, the solution is so brief and elegant that it may suggest an implementation to the general problem in your language of choice.
Here it is - a one-liner:
walk( if type == "object" then with_entries( select(.value != null) ) else . end)
This presupposes jq version 1.5 (see https://stedolan.github.io/jq/).
If you have an older version of jq, the definition of walk/1 can easily be added. (See e.g. Transforming the name of key deeper in the JSON structure with jq)

how can I programmatically identify which keys have sub key-value-pairs in a JSON doc? [duplicate]

This question already has answers here:
Flattening nested hash to a single hash with Ruby/Rails
(6 answers)
Closed 8 years ago.
I fetch a JSON document and need to programmatically "flatten" the keys for another third-party service.
What this means is, if my JSON doc comes back with the following:
{'first_name' => "Joe", 'hoffman' => {'patterns' => ['negativity', 'self-sabotage'], 'right_road' => 'happy family'}, 'mbti' => 'INTJ'}
I need to be able to know to create a "flat" key-value pair for a third-party service like this:
first_name = "Joe"
hoffman.patterns = "negativity, self-sabotage"
hoffman.right_road = "happy family"
mbti = "INTJ"
Once I know there's a sub-document, the parsing I think I have figured out just appending the sub-keys with key + '.' + "{subkey}" but right now, don't know which ones are straight key-value and which one's have sub-documents.
Question:
a) How can I parse the JSON to know which keys have sub-documents (additional key-values)?
b) Suggestions on ways to create a string from an array
You could also monkey patch Hash to do this on it's own like so:
class Hash
def flatten_keys(prefix=nil)
each_pair.map do |k,v|
key = [prefix,k].compact.join(".")
v.is_a?(Hash) ? v.flatten_keys(key) : [key,v.is_a?(Array) ? v.join(", ") : v]
end.flatten.each_slice(2).to_a
end
def to_flat_hash
Hash[flatten_keys]
end
end
Then it would be
require 'json'
h = JSON.parse(YOUR_JSON_RESPONSE)
#=> {'first_name' => "Joe", 'hoffman' => {'patterns' => ['negativity', 'self-sabotage'], 'right_road' => 'happy family'}, 'mbti' => 'INTJ'}
h.to_flat_hash
#=> {"first_name"=>"Joe", "hoffman.patterns"=>"negativity, self-sabotage", "hoffman.right_road"=>"happy family", "mbti"=>"INTJ"}
Will work with additional nesting too
h = {"first_name"=>"Joe", "hoffman"=>{"patterns"=>["negativity", "self-sabotage"], "right_road"=>"happy family", "wrong_road"=>{"bad_choices"=>["alcohol", "heroin"]}}, "mbti"=>"INTJ"}
h.to_flat_hash
#=> {"first_name"=>"Joe", "hoffman.patterns"=>"negativity, self-sabotage", "hoffman.right_road"=>"happy family", "hoffman.wrong_road.bad_choices"=>"alcohol, heroin", "mbti"=>"INTJ"}
Quick and dirty recursive proc:
# assuming you've already `JSON.parse` the incoming json into this hash:
a = {'first_name' => "Joe", 'hoffman' => {'patterns' => ['negativity', 'self-sabotage'], 'right_road' => 'happy family'}, 'mbti' => 'INTJ'}
# define a recursive proc:
flatten_keys = -> (h, prefix = "") do
#flattened_keys ||= {}
h.each do |key, value|
# Here we check if there's "sub documents" by asking if the value is a Hash
# we also pass in the name of the current prefix and key and append a . to it
if value.is_a? Hash
flatten_keys.call value, "#{prefix}#{key}."
else
# if not we concatenate the key and the prefix and add it to the #flattened_keys hash
#flattened_keys["#{prefix}#{key}"] = value
end
end
#flattened_keys
end
flattened = flatten_keys.call a
# => "first_name"=>"Joe", "hoffman.patterns"=>["negativity", "self-sabotage"], "hoffman.right_road"=>"happy family", "mbti"=>"INTJ"}
And then, to turn the arrays into strings just join them:
flattened.inject({}) do |hash, (key, value)|
value = value.join(', ') if value.is_a? Array
hash.merge! key => value
end
# => {"first_name"=>"Joe", "hoffman.patterns"=>"negativity, self-sabotage", "hoffman.right_road"=>"happy family", "mbti"=>"INTJ"}
Another way, inspired by this post:
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 }
result = {}
flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
.each{ |k, v| result[k.join('.')] = v } #=> {"a.b.c"=>1, "a.b.d"=>2, "a.e"=>3, "f"=>4}

Create array of objects from hash keys and values

I have a collection of product codes in an array: #codes. I then check to see how many instances of each product I have:
#popular = Hash.new(0)
#codes.each do |v|
#popular[v] += 1
end
This produces a hash like { code1 => 5, code2 => 12}. What I really need is a nice array of the form:
[ {:code => code1, :frequency => 5}, {:code => code2, :frequency => 12} ]
How do I build an array like that from the hashes I'm producing? Alternatively, is there a more direct route? The objects in question are ActiveModel objects with :code as an attribute. Thanks in advance!
#popular.map { |k, v| { code: k, frequency: v } }
This will produce an array of Hashes. If you need an array of models, replace the inner {...} with an appropriate constructor.
Change your code to
#codes.each_with_object([]) do
|code, a|
if h = a.find{|h| h[:code] == code}
h[:frequency] += 1
else
a.push(code: code, frequency: 0)
end
end
For speed:
#codes.group_by{|e| e}.map{|k, v| {code: k, frequency: v.length}}
Not the most efficient, but this is another way:
def counts(codes)
codes.uniq.map { |e| { code: e, frequency: codes.count(e) } }
end
codes = %w{code5 code12 code5 code3 code5 code12 code7}
#=> ["code5", "code12", "code5", "code3", "code5", "code12", "code7"]
counts(codes)
#=> [{:code=>"code5", :frequency=>3}, {:code=>"code12", :frequency=>2},
# {:code=>"code3", :frequency=>1}, {:code=>"code7" , :frequency=>1}]

Parslet subtree doesn't fire

Resume (I shrinked down the following long story to the simple problem)
tree = {:properties => [{:a => 'b'}, {:c => 'd'}]}
big_tree = {:properties => [{:a => 'b'}, {:c => 'd'}], :moves => [{:a => 'b'}, {:c => 'd'}]}
trans = Parslet::Transform.new do
rule(:properties => subtree(:nested)) do
out = {}
nested.each {|pair| out = out.merge pair}
{:properties => out}
end
end
pp tree
pp trans.apply(tree)
pp big_tree
pp trans.apply(big_tree)
# OUTPUT
{:properties=>[{:a=>"b"}, {:c=>"d"}]}
{:properties=>{:a=>"b", :c=>"d"}} # Worked with small tree
{:properties=>[{:a=>"b"}, {:c=>"d"}], :moves=>[{:a=>"b"}, {:c=>"d"}]}
{:properties=>[{:a=>"b"}, {:c=>"d"}], :moves=>[{:a=>"b"}, {:c=>"d"}]} # Didn't work with bigger tree
=========================FULL STORY (Not so relevant after the header)
I am making an SGF files parser with Parslet.
Now I am at the stage to make a Transformer.
From the Parser I already get the structs like that:
[{:properties=>
[{:name=>"GM"#2, :values=>[{:value=>"1"#5}]},
{:name=>"FF"#7, :values=>[{:value=>"4"#10}]},
{:name=>"SZ"#12, :values=>[{:value=>"19"#15}]},
{:name=>"AP"#18, :values=>[{:value=>"SmartGo Kifu:2.2"#21}]},
{:name=>"GN"#40, :values=>[{:value=>"2013-05-11g"#43}]},
{:name=>"PW"#57, :values=>[{:value=>"Dahan"#60}]},
{:name=>"PB"#68, :values=>[{:value=>"SmartGo"#71}]},
{:name=>"DT"#81, :values=>[{:value=>"2013-05-11"#84}]},
{:name=>"KM"#97, :values=>[{:value=>"6.5"#100}]},
{:name=>"RE"#106, :values=>[{:value=>"W+R"#109}]},
{:name=>"RU"#115, :values=>[{:value=>"AGA (Area)"#118}]},
{:name=>"ID"#129, :values=>[{:value=>"ch0"#132}]}],
:moves=>
[{:player=>"B"#137, :place=>"oq"#139},
{:player=>"W"#143, :place=>"dd"#145},
{:player=>"B"#149, :place=>"oo"#151},
...etc...
The ruleset I am using to Transform:
# Rewrite player: COLOR, place: X to COLOR: X
rule( player: simple(:p), place: simple(:pl)) do
if p == 'W'
{ white: pl }
elsif p == 'B'
{ black: pl }
end
end
# Un-nest single-value hash
rule( value: simple(:v)) { v }
# Rewrite name: KEY, values: SINGLE_VALUE to KEY: SINGLE_VALUE
rule( name: simple(:n), values: [ simple(:v) ]) { {n.to_sym => v} }
# A Problem!!!
rule( properties: subtree(:props) ) do
out = {}
props.each {|pair| pair.each {|k, v| out[k] = v}}
{ properties: out }
end
With such rules I get the following struct:
[{:properties=>
[{:GM=>"1"#5},
{:FF=>"4"#10},
{:SZ=>"19"#15},
{:AP=>"SmartGo Kifu:2.2"#21},
{:GN=>"2013-05-11g"#43},
{:PW=>"Dahan"#60},
{:PB=>"SmartGo"#71},
{:DT=>"2013-05-11"#84},
{:KM=>"6.5"#100},
{:RE=>"W+R"#109},
{:RU=>"AGA (Area)"#118},
{:ID=>"ch0"#132}],
:moves=>
[{:black=>"oq"#139},
{:white=>"dd"#145},
{:black=>"oo"#151},
...etc...
Everything is perfect.
The only Problem of mine is that :properties Array of Hashes.
In the end I want to have
[{:properties=>
{:GM=>"1"#5,
:FF=>"4"#10,
:SZ=>"19"#15,
:AP=>"SmartGo Kifu:2.2"#21,
:GN=>"2013-05-11g"#43,
:PW=>"Dahan"#60,
:PB=>"SmartGo"#71,
:DT=>"2013-05-11"#84,
:KM=>"6.5"#100,
:RE=>"W+R"#109,
:RU=>"AGA (Area)"#118,
:ID=>"ch0"#132},
:moves=>
[{:black=>"oq"#139},
{:white=>"dd"#145},
{:black=>"oo"#151},
...etc...
You see? Merge all arrayed hashes inside :properties, because after the previous transformations they now have unique keys. Also flatten the struct a bit.
Hey! I can do it manually. I mean to run a separate method like
merged_stuff = {}
tree.first[:properties].each {|pair| pair.each {|k, v| merged_stuff[k] = v}}
tree.first[:properties] = merged_stuff
But Why I Cannot Do That With The Neat Transform Rules, To Have All Transformation Logic In One Place?
The point is that rule( properties: subtree(:props) ) does not get fired at all. Even if I just return nil from the block, it doesn't change anything. So, seems, that this subtree doesn't catch the things, or I don't.
The problem is that :properties and :moves are keys in the same hash, and subtree apparently doesn't want to match part of a hash. If you remove :moves, the rule will be executed. It is kinda explained in the documentation:
A word on patterns
Given the PORO hash
{
:dog => 'terrier',
:cat => 'suit' }
one might assume that the following rule matches :dog and replaces it by 'foo':
rule(:dog => 'terrier') { 'foo' }
This is frankly impossible. How would 'foo' live besides :cat => 'suit'
inside the hash? It cannot. This is why hashes are either matched completely,
cats n’ all, or not at all.
though I must admit it's not a really clear example.
So the problem rule should look like this:
rule( properties: subtree(:props), moves: subtree(:m) ) do
out = {}
props.each {|pair| pair.each {|k, v| out[k] = v}}
{ properties: out , moves: m}
end
Transform rules match a whole node and replace it, so you need to match the whole hash, not just one key.
rule( properties: subtree(:props), moves: subtree(:moves) )
If you converted the {:name=>"GM", :values=>[{:value=>"1"}]} type things into objects (using OpenStruct say) then you don't need to use subtree, you can use sequence.

How do I convert a Ruby hash so that all of its keys are symbols?

I have a Ruby hash which looks like:
{ "id" => "123", "name" => "test" }
I would like to convert it to:
{ :id => "123", :name => "test" }
hash = {"apple" => "banana", "coconut" => "domino"}
Hash[hash.map{ |k, v| [k.to_sym, v] }]
#=> {:apple=>"banana", :coconut=>"domino"}
#mu is too short: Didn't see word "recursive", but if you insist (along with protection against non-existent to_sym, just want to remind that in Ruby 1.8 1.to_sym == nil, so playing with some key types can be misleading):
hash = {"a" => {"b" => "c"}, "d" => "e", Object.new => "g"}
s2s =
lambda do |h|
Hash === h ?
Hash[
h.map do |k, v|
[k.respond_to?(:to_sym) ? k.to_sym : k, s2s[v]]
end
] : h
end
s2s[hash] #=> {:d=>"e", #<Object:0x100396ee8>=>"g", :a=>{:b=>"c"}}
If you happen to be in Rails then you'll have symbolize_keys:
Return a new hash with all keys converted to symbols, as long as they respond to to_sym.
and symbolize_keys! which does the same but operates in-place. So, if you're in Rails, you could:
hash.symbolize_keys!
If you want to recursively symbolize inner hashes then I think you'd have to do it yourself but with something like this:
def symbolize_keys_deep!(h)
h.keys.each do |k|
ks = k.to_sym
h[ks] = h.delete k
symbolize_keys_deep! h[ks] if h[ks].kind_of? Hash
end
end
You might want to play with the kind_of? Hash to match your specific circumstances; using respond_to? :keys might make more sense. And if you want to allow for keys that don't understand to_sym, then:
def symbolize_keys_deep!(h)
h.keys.each do |k|
ks = k.respond_to?(:to_sym) ? k.to_sym : k
h[ks] = h.delete k # Preserve order even when k == ks
symbolize_keys_deep! h[ks] if h[ks].kind_of? Hash
end
end
Note that h[ks] = h.delete k doesn't change the content of the Hash when k == ks but it will preserve the order when you're using Ruby 1.9+. You could also use the [(key.to_sym rescue key) || key] approach that Rails uses in their symbolize_keys! but I think that's an abuse of the exception handling system.
The second symbolize_keys_deep! turns this:
{ 'a' => 'b', 'c' => { 'd' => { 'e' => 'f' }, 'g' => 'h' }, ['i'] => 'j' }
into this:
{ :a => 'b', :c => { :d => { :e => 'f' }, :g => 'h' }, ['i'] => 'j' }
You could monkey patch either version of symbolize_keys_deep! into Hash if you really wanted to but I generally stay away from monkey patching unless I have very good reasons to do it.
If you are using Rails >= 4 you can use:
hash.deep_symbolize_keys
hash.deep_symbolize_keys!
or
hash.deep_stringify_keys
hash.deep_stringify_keys!
see http://apidock.com/rails/v4.2.1/Hash/deep_symbolize_keys
Just in case you are parsing JSON, from the JSON docs you can add the option to symbolize the keys upon parsing:
hash = JSON.parse(json_data, symbolize_names: true)
Victor Moroz provided a lovely answer for the simple recursive case, but it won't process hashes that are nested within nested arrays:
hash = { "a" => [{ "b" => "c" }] }
s2s[hash] #=> {:a=>[{"b"=>"c"}]}
If you need to support hashes within arrays within hashes, you'll want something more like this:
def recursive_symbolize_keys(h)
case h
when Hash
Hash[
h.map do |k, v|
[ k.respond_to?(:to_sym) ? k.to_sym : k, recursive_symbolize_keys(v) ]
end
]
when Enumerable
h.map { |v| recursive_symbolize_keys(v) }
else
h
end
end
Try this:
hash = {"apple" => "banana", "coconut" => "domino"}
# => {"apple"=>"banana", "coconut"=>"domino"}
hash.tap do |h|
h.keys.each { |k| h[k.to_sym] = h.delete(k) }
end
# => {:apple=>"banana", :coconut=>"domino"}
This iterates over the keys, and for each one, it deletes the stringified key and assigns its value to the symbolized key.
If you're using Rails (or just Active Support):
{ "id" => "123", "name" => "test" }.symbolize_keys
Starting with Ruby 2.5 you can use the transform_key method.
So in your case it would be:
h = { "id" => "123", "name" => "test" }
h.transform_keys!(&:to_sym) #=> {:id=>"123", :name=>"test"}
Note: the same methods are also available on Ruby on Rails.
Here's a Ruby one-liner that is faster than the chosen answer:
hash = {"apple" => "banana", "coconut" => "domino"}
#=> {"apple"=>"banana", "coconut"=>"domino"}
hash.inject({}){|h,(k,v)| h[k.intern] = v; h}
#=> {:apple=>"banana", :coconut=>"domino"}
Benchmark results:
n = 100000
Benchmark.bm do |bm|
bm.report { n.times { hash.inject({}){|h,(k,v)| h[k.intern] = v; h} } }
bm.report { n.times { Hash[hash.map{ |k, v| [k.to_sym, v] }] } }
end
# => user system total real
# => 0.100000 0.000000 0.100000 ( 0.107940)
# => 0.120000 0.010000 0.130000 ( 0.137966)
I'm partial to:
irb
ruby-1.9.2-p290 :001 > hash = {"apple" => "banana", "coconut" => "domino"}
{
"apple" => "banana",
"coconut" => "domino"
}
ruby-1.9.2-p290 :002 > hash.inject({}){ |h, (n,v)| h[n.to_sym] = v; h }
{
:apple => "banana",
:coconut => "domino"
}
This works because we're iterating over the hash and building a new one on the fly. It isn't recursive, but you could figure that out from looking at some of the other answers.
hash.inject({}){ |h, (n,v)| h[n.to_sym] = v; h }
You can also extend core Hash ruby class placing a /lib/hash.rb file :
class Hash
def symbolize_keys_deep!
new_hash = {}
keys.each do |k|
ks = k.respond_to?(:to_sym) ? k.to_sym : k
if values_at(k).first.kind_of? Hash or values_at(k).first.kind_of? Array
new_hash[ks] = values_at(k).first.send(:symbolize_keys_deep!)
else
new_hash[ks] = values_at(k).first
end
end
new_hash
end
end
If you want to make sure keys of any hash wrapped into arrays inside your parent hash are symbolized, you need to extend also array class creating a "array.rb" file with that code :
class Array
def symbolize_keys_deep!
new_ar = []
self.each do |value|
new_value = value
if value.is_a? Hash or value.is_a? Array
new_value = value.symbolize_keys_deep!
end
new_ar << new_value
end
new_ar
end
end
This allows to call "symbolize_keys_deep!" on any hash variable like this :
myhash.symbolize_keys_deep!
def symbolize_keys(hash)
new={}
hash.map do |key,value|
if value.is_a?(Hash)
value = symbolize_keys(value)
end
new[key.to_sym]=value
end
return new
end
puts symbolize_keys("c"=>{"a"=>2,"k"=>{"e"=>9}})
#{:c=>{:a=>2, :k=>{:e=>9}}}
Here's my two cents,
my version of symbolize_keys_deep! uses the original symbolize_keys! provided by rails and just makes a simple recursive call to Symbolize sub hashes.
def symbolize_keys_deep!(h)
h.symbolize_keys!
h.each do |k, v|
symbolize_keys_deep!(v) if v.is_a? Hash
end
end
Facets' Hash#rekey is also a worth mentioning.
Sample:
require 'facets/hash/rekey'
{ "id" => "123", "name" => "test" }.deep_rekey
=> {:id=>"123", :name=>"test"}
There is also a recursive version:
require 'facets/hash/deep_rekey'
{ "id" => "123", "name" => {"first" => "John", "last" => "Doe" } }.deep_rekey
=> {:id=>"123", :name=>{:first=>"John", :last=>"Doe"}}
Here's a little recursive function to do a deep symbolization of the keys:
def symbolize_keys(hash)
Hash[hash.map{|k,v| v.is_a?(Hash) ? [k.to_sym, symbolize_keys(v)] : [k.to_sym, v] }]
end

Resources