Merging multi-dimensional hashes in Ruby - ruby-on-rails

I have two hashes which have a structure something similar to this:
hash_a = { :a => { :b => { :c => "d" } } }
hash_b = { :a => { :b => { :x => "y" } } }
I want to merge these together to produce the following hash:
{ :a => { :b => { :c => "d", :x => "y" } } }
The merge function will replace the value of :a in the first hash with the value of :a in the second hash. So, I wrote my own recursive merge function, which looks like this:
def recursive_merge( merge_from, merge_to )
merged_hash = merge_to
first_key = merge_from.keys[0]
if merge_to.has_key?(first_key)
merged_hash[first_key] = recursive_merge( merge_from[first_key], merge_to[first_key] )
else
merged_hash[first_key] = merge_from[first_key]
end
merged_hash
end
But I get a runtime error: can't add a new key into hash during iteration. What's the best way of going about merging these hashes in Ruby?

Ruby's existing Hash#merge allows a block form for resolving duplicates, making this rather simple. I've added functionality for merging multiple conflicting values at the 'leaves' of your tree into an array; you could choose to pick one or the other instead.
hash_a = { :a => { :b => { :c => "d", :z => 'foo' } } }
hash_b = { :a => { :b => { :x => "y", :z => 'bar' } } }
def recurse_merge(a,b)
a.merge(b) do |_,x,y|
(x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : [*x,*y]
end
end
p recurse_merge( hash_a, hash_b )
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
Or, as a clean monkey-patch:
class Hash
def merge_recursive(o)
merge(o) do |_,x,y|
if x.respond_to?(:merge_recursive) && y.is_a?(Hash)
x.merge_recursive(y)
else
[*x,*y]
end
end
end
end
p hash_a.merge_recursive hash_b
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}

You can do it in one line :
merged_hash = hash_a.merge(hash_b){|k,hha,hhb| hha.merge(hhb){|l,hhha,hhhb| hhha.merge(hhhb)}}
If you want to imediatly merge the result into hash_a, just replace the method merge by the method merge!
If you are using rails 3 or rails 4 framework, it is even easier :
merged_hash = hash_a.deep_merge(hash_b)
or
hash_a.deep_merge!(hash_b)

If you change the first line of recursive_merge to
merged_hash = merge_to.clone
it works as expected:
recursive_merge(hash_a, hash_b)
-> {:a=>{:b=>{:c=>"d", :x=>"y"}}}
Changing the hash as you move through it is troublesome, you need a "work area" to accumulate your results.

Try this monkey-patching solution:
class Hash
def recursive_merge(hash = nil)
return self unless hash.is_a?(Hash)
base = self
hash.each do |key, v|
if base[key].is_a?(Hash) && hash[key].is_a?(Hash)
base[key].recursive_merge(hash[key])
else
base[key]= hash[key]
end
end
base
end
end

In order to merge one into the other as the ticket suggested, you could modify #Phrogz function
def recurse_merge( merge_from, merge_to )
merge_from.merge(merge_to) do |_,x,y|
(x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : x
end
end
In case there is duplicate key, it will only use the content of merge_from hash

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 usage for answering the question. It's extremely easy:
hash_a = { :a => { :b => { :c => "d" } } }
hash_b = { :a => { :b => { :x => "y" } } }
puts hash_a.merge(hash_b) # Won't override hash_a
# output: { :a => { :b => { :x => "y" } } }
puts hash_a # hash_a is unchanged
# output: { :a => { :b => { :c => "d" } } }
hash_a.merge!(hash_b, recursive=true) # Will override hash_a
puts hash_a # hash_a was changed
# output: { :a => { :b => { :c => "d", :x => "y" } } }
For advanced example take a look at this answer.
Also take a look at my recursive version of Hash::each(Hash::each_pair) here.

Related

Plucking out all hash keys that has a specific word

How do you pluck out a hash key that has for example
Hash 1
{sample => {apple => 1, guest_email => my_email#example.com }}
Hash 2
{guest => {email => my_email#example.com}}
Lets say I want 1 method that will pluck out the email from either of those hashes, is there any way I can that like lets say hash.get_key_match("email")
You may use Hash#select to only return keypairs, matching the block:
h = { guest_email: 'some_mail', other_key: '123', apple: 1 }
h.select { |key, _value| key =~ /email/ }
#=> { guest_email: 'some_mail' }
I guess that you need the deep search.
There is no method from the box.
You need use recursion for this goal.
I suspect that your issue can be solved by:
class Hash
def deep_find(key, node = self)
searchable_key = key.to_s
matched_keys = node.keys.select { |e| e.to_s.match?(searchable_key) }
return node[matched_keys.first] if matched_keys.any?
node.values
.select { |e| e.respond_to?(:deep_find) }
.map { |e| e.deep_find(key, e) }
.flatten.first
end
end
h = {a_foo: {d: 4}, b: {foo: 1}}
p (h.deep_find(:foo))
# => {:d=>4}
h = {a: 2, c: {a_foo: :bar}, b: {foo: 1}}
p (h.deep_find(:foo))
# => :bar
you can use this
hash = { first_email: 'first_email', second_email: 'second_email' }
hash.select { |key, _value| key =~ /email/ }.map {|k, v| v}

Delete key from hash if value is empty in Ruby

Given:
data = {:a => 'A', :b => '', :c => 'C', :d => ''}
Objective: To remove key :c and :d if their value is an empty string. However, :a and :b should remain even if their value is an empty string. In the above example, end result should be:
{:a => 'A', :b => '', :c => 'C'}
Current approach:
if data[:c] == ''
data.delete(:c)
end
if data[:d] == ''
data.delete(:d)
end
Is there a better approach to get the same result? (In reality, key of type c and d is more than 10 which result in a long list of if checks.)
You can use Hash#delete_if method
unwanted_keys = [:c, :d]
hash = {:a => 'A', :b => '', :c => 'C', :d => ''}
hash.delete_if{ |k,v| unwanted_keys.include?(k) && v.blank? }
Hope that helps!
You can use Enumerable#each_with_object to build a new hash on the fly applying your conditions:
data.each_with_object({}) do |(k, v), h|
h[k] = v unless %i(c d).include?(k) && v == ''
end
#=> {:a=>"A", :b=>"", :c=>"C"}
As #user000001 suggested in comments, another good option is to use Enumerable#reject:
data.reject { |k, v| %i(c d).include?(k) && v == '' }
you can always just iterate through the list of keys
KEYS_TO_DELETE_IF_BLANK = %i[c d]
KEYS_TO_DELETE_IF_BLANK.each do |key|
data.delete(key) if data[key].blank?
end
You can also use delete if:
data = { :a => 'A', :b => '', :c => 'C', :d => '' }
data.delete_if { |k, value| %i(c d).include?(k) && value.to_s.strip == '' }
you can just use select to extract hash with keys that have values
data = {:a => 'A', :b => '', :c => 'C', :d => ''}
data.select() {|k, v| v.present? || k == :a || k == :b }
# => => {:a=>"A", :b=>"", :c=>"C"}

Method to check hash for values which may be a single string or an array of strings

I have a hash which contains values such as....
EXAMPLE = {
"101" => "SO01",
"102" => "SO02",
"103" => "SO03",
"105" => %w(S005 SO04)
}
I want to search the hash for say SO04 and get back "105", or "SO03" and get back "103". I'm sure there is a good way to do this but it's slipping my mind currently. I had been simply using EXAMPLE.key() but that's not going to cut it now that I realize there are arrays in there.
Try this:
>> EXAMPLE = {"101" => "SO01",
"102" => "SO02",
"103" => "SO03",
"105" => %w(SO05 SO04)}
>> EXAMPLE.find { |_,v| Array(v).include?('SO02') }.try(:first)
=> "102"
>> EXAMPLE.find { |_,v| Array(v).include?('SO05') }.try(:first)
=> "105"
>> EXAMPLE.find { |_,v| Array(v).include?('SO06') }.try(:first)
=> nil
.first is needed on the end because #find returns an array of [key, value].
If you are using Ruby 2.3 or later, you can use the safe navigation operator in place of try:
>> EXAMPLE.find { |_,v| Array(v).include?('SO05') }&.first
=> "105"
>> EXAMPLE.find { |_,v| Array(v).include?('SO06') }&.first
=> nil
EXAMPLE = {
"101" => "SO01",
"102" => "SO02",
"103" => "SO03",
"105" => ["S005", "SO04"]
}
def value_to_key(h, value)
h.keys.find { |k| Array(h[k]).include?(value) }
end
value_to_key(EXAMPLE, "SO02") #=> "102"
value_to_key(EXAMPLE, "SO04") #=> "105"
value_to_key(EXAMPLE, "cat") #=> nil
Note:
Array("SO03") #=> ["SO03"]
Array(["S005", "SO04"]) #=> ["S005", "SO04"]
An equivalent, but more memory-demanding, method is shown below. The reason for the difference in memory requirements is explained in the comments.
def value_to_key(h, value)
h.keys.find { |k| [*h[k]].include?(value) }
end
[*"SO03"] #=> ["SO03"]
[*["S005", "SO04"]] #=> ["S005", "SO04"]

Join Ruby hash keys into string

I'm trying to create a Ruby template on the fly with Chef attributes but I can't figure out how to map the attributes to output the way I need.
Example Hash:
a = {
"route" => {
"allocation" => {
"recovery" => {
"speed" => 5,
"timeout" => "30s"
},
"converge" => {
"timeout" => "1m"
}
}
}
}
Would turn into:
route.allocation.recovery.speed: 5
route.allocation.recovery.timeout: 30s
route.allocation.converge.timeout: 1m
Thanks for the help.
You can use recursion if your hash is not large enough to throw stack overflow exception. I don't know what are you trying to achieve, but this is example of how you can do it:
a = {
"route" => {
"allocation" => {
"recovery" => {
"speed" => 5,
"timeout" => "30s"
},
"converge" => {
"timeout" => "1m"
}
}
}
}
def show hash, current_path = ''
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path
else
puts "#{current_path}#{k} : #{v}"
end
end
end
show a
Output:
route.allocation.recovery.speed : 5
route.allocation.recovery.timeout : 30s
route.allocation.recovery.converge.timeout : 1m
I don't know Rails but I'm guessing that the following requires only a small tweak to give the result you want:
#result = []
def arrayify(obj, so_far=[])
if obj.is_a? Hash
obj.each { |k,v| arrayify(v, so_far+[k]) }
else
#result << (so_far+[obj])
end
end
arrayify(a)
#result
#=> [["route", "allocation", "recovery", "speed", 5],
# ["route", "allocation", "recovery", "timeout", "30s"],
# ["route", "allocation", "converge", "timeout", "1m"]]
EDIT: Did I completely misread your question - is the desired output a string? Oh dear.
I think this is a really good use case for OpenStruct:
require 'ostruct'
def build_structs(a)
struct = OpenStruct.new
a.each do |k, v|
if v.is_a? Hash
struct[k] = build_structs(v)
else
return OpenStruct.new(a)
end
end
struct
end
structs = build_structs(a)
output:
[2] pry(main)> structs.route.allocation.recovery.speed
=> 5
For anyone wanting to convert an entire hash with multi levels then here is the code I ended up using:
confHash = {
'elasticsearch' => {
'config' => {
'discovery' => {
'zen' => {
'ping' => {
'multicast' => {
'enabled' => false
},
'unicast' => {
'hosts' => ['127.0.0.1']
}
}
}
}
}
}
}
def generate_config( hash, path = [], config = [] )
hash.each do |k, v|
if v.is_a? Hash
path << k
generate_config( v, path, config )
else
path << k
if v.is_a? String
v = "\"#{v}\""
end
config << "#{path.join('.')}: #{v}"
end
path.pop
end
return config
end
puts generate_config(confHash['elasticsearch']['config'])
# discovery.zen.ping.multicast.enabled: false
# discovery.zen.ping.unicast.hosts: ["127.0.0.1"]

Flattening nested hash to a single hash with Ruby/Rails

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.
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"}
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"}
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.
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
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.
Just in case, that you want to keep their parent
def flatten_hash(param)
param.each_pair.reduce({}) do |a, (k, v)|
v.is_a?(Hash) ? a.merge({ k.to_sym => '' }, flatten_hash(v)) : a.merge(k.to_sym => v)
end
end
hash = {:foo=>"bar", :hello=>{:world=>"Hello World", :bro=>"What's up dude?"}, :a=>{:b=>{:c=>"d"}}}
flatten_hash(hash)
# {:foo=>"bar", :hello=>"", :world=>"Hello World", :bro=>"What's up dude?", :a=>"", :b=>"", :c=>"d"}

Resources