Generate nested hashes from strings and deep merging in ruby
To generate nested hash:
hash = {}
"one.two.three".split('.').reduce(hash) { |h,m| h[m] = {} }
puts hash #=> {"one"=>{"two"=>{"three"=>{}}}}
If you don't have rails installed then install activesupport gem:
gem install activesupport
Then include it into your file:
require 'active_support/core_ext/hash/deep_merge'
hash = {
"one" => {
"two" => {
"three" => {}
}
}
}.deep_merge(another_hash)
The access to the internals would be:
hash['one']['two']['three'] #=> {}
Merge nested hash without overwritting in Ruby
For Rails there is the deep_merge
function for ActiveSupport
that does exactly what you ask for.
You can implement the same for yourself as follows:
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
Now,
h1 = {"a"=>"text"}
h2 = {"b"=>{"x"=>"hola"}}
h3 = {"b"=>{"y"=>"pto"}}
h1.deep_merge(h2).deep_merge(h3)
# => {"a"=>"text", "b"=>{"x"=>"hola", "y"=>"pto"}}
Merging nested hash with deep_merge using the same key
If you are using this [deep_merge][1]
or [deep_merge!][2]
method, then try following
collection.deep_merge(new_collection) { |key, this_val, other_val|
this_val.class == String ? this_val : this_val + other_val
}
Note:- We have to add this_val.class == String
otherwise we'll get provider=>"totaltotal"
& also assuming each hash key will have value of same data type.
[13] pry(main)> collection = {:foo=>{:amount_gross=>1886.0}, :bar=>{:amount_gross=>184.0}, :total=>{:provider=>"total", :amount_gross=>2070.0}}
=> {:foo=>{:amount_gross=>1886.0}, :bar=>{:amount_gross=>184.0}, :total=>{:provider=>"total", :amount_gross=>2070.0}}
[14] pry(main)> new_collection = {:foo=>{:amount_gross=>100.0}, :bar=>{:amount_gross=>100.0}, :total=>{:provider=>"total", :amount_gross=>200.0}}
=> {:foo=>{:amount_gross=>100.0}, :bar=>{:amount_gross=>100.0}, :total=>{:provider=>"total", :amount_gross=>200.0}}
[15] pry(main)> collection.deep_merge!(new_collection) { |key, this_val, other_val| this_val.class == String ? this_val : this_val + other_val}
=> {:foo=>{:amount_gross=>1986.0}, :bar=>{:amount_gross=>284.0}, :total=>{:provider=>"total", :amount_gross=>2270.0}}
Ruby: Nested hash loses keys and values on merging with another nested hash
The way Hash#merge
works is consistent: if the key exists in both, then the key value of the merged-in hash replaces the the first one in all cases. You'll need a recursive merge.
There is a deep_merge
available in Rails. But if you're not using Rails, or if it doesn't suit your requirements, you can roll your own fairly easily.
Hash#merge
does support a block which can help:
h1.merge(h2) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
old.merge(new) : new }
This will work if you just have one layer deep of embedded hashes. If you have an arbitrary depth of nested hashes, you can monkey patch Hash
with a deep_merge
:
class Hash
def deep_merge(h)
self.merge(h) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
old.deep_merge(new) : new }
end
end
Or something like that... :) This will recursively merge if there is a hash in both cases, otherwise, it replaces as usual. You can modify it to your taste.
Trying this out for your case:
2.6.1 :008 > class Hash
2.6.1 :009?> def deep_merge(h)
2.6.1 :010?> self.merge(h) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
2.6.1 :011 > old.deep_merge(new) : new }
2.6.1 :012?> end
2.6.1 :013?> end
=> :deep_merge
2.6.1 :014 > h1 = {"a"=>{"x"=>"1","y"=>"2"}, "b"=>"z"}
=> {"a"=>{"x"=>"1", "y"=>"2"}, "b"=>"z"}
2.6.1 :015 > h2 = {"a"=>{"x"=>"3"}, "b"=>"q"}
=> {"a"=>{"x"=>"3"}, "b"=>"q"}
2.6.1 :016 > h1.deep_merge(h2)
=> {"a"=>{"x"=>"3", "y"=>"2"}, "b"=>"q"}
2.6.1 :017 >
hash deep_merge with arrays
If h1
and h2
are the two hashes you wish to merge, and h3
is the desired result, I think the following should work:
@merge_key1, @merge_key2 = "key-one", "key-two"
def merge_em(g1, g2)
case g1
when Array
g1.size.times {|i| merge_em(g1[i], g2[i])}
when Hash
g1.keys.each do |k|
v = g1[k]
if (Hash === v || (Array === v && !v.empty?))
merge_em(v, g2[k])
else
g1[k] = {@merge_key1 => v, @merge_key2 => g2[k]}
end
end
end
end
h = Marshal.load(Marshal.dump(h1))
merge_em(h, h2)
p (h == h3) # => true
A few notes:
- This solution depends on the hash structures; it may not work if the structures are changed.
Marshal.load(Marshal.dump(h1))
is used to make a "deep copy" ofh1
, so thath1
is not modified. Ifh1
can be modified,merge_em(h1, h2)
is sufficient, withh1
being the merged hash.- The gem awesome_print gives you a nicely-formatted display of complex structures like these. All you'd need to do here is
require 'ap'
followed byap h
. Try it!
Related Topics
Error Install Rubyracer with Error "Invalid Gem: Package Is Corrupt"
How to Remove Duplicates in a Hash in Ruby on Rails
How to Fix an Accidental 'Sudo Bundle Install Dir_Name'
Using Compass from Ruby (Not Shell)
What Does +@ Mean as a Method in Ruby
How to Redefine Fixnum's + (Plus) Method in Ruby and Keep Original + Functionality
How to Write an Rspec Test for a Ruby Method That Contains "Gets.Chomp"
Get Chromes Console Log via Ruby Webdriver
Make Rails Ignore Daylight Saving Time When Displaying a Date
Rails 4 How to Call Accessible_Attributes from Model
How to Convert Ppt to Images in Ruby
How to Mark a Cucumber Scenario as Pending
How to Convert Boolean Values to Integers
Rails 5 Db Migration: How to Fix Activerecord::Concurrentmigrationerror
How to Give a Sub-Module the Same Name as a Top-Level Class
Command for Displaying a Gem's Dependencies
Scaffolding Activerecord: Two Columns of the Same Data Type
How to More Elegantly Remove Duplicate Items Across All Elements of a Ruby Array