Merge Ruby nested hashes with same keys - ruby
I have several hashes in Ruby which have nested hashes inside of them an share very similar structure. They look something like this:
a = {
"year_1": {
"sub_type_a": {
"label1": value1
}
},
"year_2": {
"sub_type_a": {
"label2": value2
}
}
}
b = {
"year_1": {
"sub_type_a": {
"label3": value3
}
},
"year_2": {
"sub_type_a": {
"label4": value4
}
}
}
c = {
"year_1": {
"sub_type_a": {
"label5": value5
}
},
"year_2": {
"sub_type_a": {
"label6": value6
}
}
}
I want to combine them in one single hash which would have the nested data combined where possible without overwriting other values like this:
result = {
"year_1": {
"sub_type_a": {
"label1": value1,
"label3": value3,
"label5": value5
}
},
"year_2": {
"sub_type_a": {
"label2": value2,
"label4": value4,
"label6": value6
}
}
}
There could also be several sub types instead of just one but that's the general idea.
If I use the merge function it just overwrites the label-value data inside the sub_type hashes and I am left with only one record.
Is there a simple way to achieve this? I can write a function that iterates the hashes recursively and figure out inside what to add where but it feels like that there should be a simpler way.
Something similar.
Combine each_with_object, each and merge so you can iterate trough each hash and assign the merged values when they exist to a temporal new one:
[a, b, c].each_with_object({}) do |years_data, hash|
years_data.each do |year, data|
hash[year] = (hash[year] || {}).merge(data) { |_, oldval, newval| oldval.merge(newval) }
end
end
# {
# :year_1 => {
# :sub_type_a => {
# :label1 => :value1,
# :label3 => :value3,
# :label5 => :value5
# }
# },
# :year_2 => {
# :sub_type_a => {
# :label2 => :value2,
# :label4 => :value4,
# :label6 => :value6
# }
# }
# }
If you are using Rails (or ActiveSupport) you might want to look at deep_merge, which handles merging of nested hashes for you
We are given the following.
a = {:year_1=>{:sub_type_a=>{:label1=>"value1"}},
:year_2=>{:sub_type_a=>{:label2=>"value2"}}}
b = {:year_1=>{:sub_type_a=>{:label3=>"value3"}},
:year_2=>{:sub_type_a=>{:label4=>"value4"}}}
c = {:year_1=>{:sub_type_a=>{:label5=>"value5"}},
:year_2=>{:sub_type_a=>{:label6=>"value6"}}}
arr = [a, b, c]
We may construct the desired hash as follows.
arr.each_with_object({}) do |g,h|
g.each do |yr,v|
k,f = v.first
h.update(yr=>{ k=>f }) { |_,o,n| { k=>o[k].merge(n[k]) } }
end
end
#=> {:year_1=>{:sub_type_a=>{:label1=>"value1", :label3=>"value3",
# :label5=>"value5"}},
# :year_2=>{:sub_type_a=>{:label2=>"value2", :label4=>"value4",
# :label6=>"value6"}}}
This uses the form of Hash#update (a.k.a merge!) that employs a block to determine the values of keys that are present in both hashes being merged. See the link for an explanation of that block's three block variables. I've used an underscore (a valid local variable) for the first block variable, the common key, to signal to the reader that it is not used in the block calculation. That is a common convention.
For anyone interested in the gory detail of the calculations (the one sure-fire way to understand what's going on) I will execute the code with some puts statements added.
arr.each_with_object({}) do |g,h|
puts "g=#{g}"
puts "h=#{h}"
g.each do |yr,v|
puts " yr=#{yr}"
puts " v=#{v}"
k,f = v.first
puts " k=#{k}"
puts " f=#{f}"
puts " yr=>{ k=>f } = #{yr}=>#{v} = #{{ yr=>v }}"
h.update(yr=>{ k=>f }) do |_,o,n|
puts " _=#{_}"
puts " o=#{o}"
puts " n=#{n}"
puts " { k=>o[k].merge(n[k]) }"
puts " => { #{k}=>#{o[k]}.merge(#{n[k]}) }"
{ k=>o[k].merge(n[k]) }.tap { |e| puts " => #{e}" }
end
end
end
The following is displayed.
g={:year_1=>{:sub_type_a=>{:label1=>"value1"}},
:year_2=>{:sub_type_a=>{:label2=>"value2"}}}
h={}
yr=year_1
v={:sub_type_a=>{:label1=>"value1"}}
k=sub_type_a
f={:label1=>"value1"}
yr=>{ k=>f } = year_1=>{:sub_type_a=>{:label1=>"value1"}} =
{:year_1=>{:sub_type_a=>{:label1=>"value1"}}}
yr=year_2
v={:sub_type_a=>{:label2=>"value2"}}
k=sub_type_a
f={:label2=>"value2"}
yr=>{ k=>f } = year_2=>{:sub_type_a=>{:label2=>"value2"}} =
{:year_2=>{:sub_type_a=>{:label2=>"value2"}}}
g={:year_1=>{:sub_type_a=>{:label3=>"value3"}},
:year_2=>{:sub_type_a=>{:label4=>"value4"}}}
h={:year_1=>{:sub_type_a=>{:label1=>"value1"}},
:year_2=>{:sub_type_a=>{:label2=>"value2"}}}
yr=year_1
v={:sub_type_a=>{:label3=>"value3"}}
k=sub_type_a
f={:label3=>"value3"}
yr=>{ k=>f } = year_1=>{:sub_type_a=>{:label3=>"value3"}} =
{:year_1=>{:sub_type_a=>{:label3=>"value3"}}}
_=year_1
o={:sub_type_a=>{:label1=>"value1"}}
n={:sub_type_a=>{:label3=>"value3"}}
{ k=>o[k].merge(n[k]) }
=> { sub_type_a=>{:label1=>"value1"}.
merge({:label3=>"value3"}) }
=> {:sub_type_a=>{:label1=>"value1", :label3=>"value3"}}
yr=year_2
v={:sub_type_a=>{:label4=>"value4"}}
k=sub_type_a
f={:label4=>"value4"}
yr=>{ k=>f } = year_2=>{:sub_type_a=>{:label4=>"value4"}} =
{:year_2=>{:sub_type_a=>{:label4=>"value4"}}}
_=year_2
o={:sub_type_a=>{:label2=>"value2"}}
n={:sub_type_a=>{:label4=>"value4"}}
{ k=>o[k].merge(n[k]) }
=> { sub_type_a=>{:label2=>"value2"}.
merge({:label4=>"value4"}) }
=> {:sub_type_a=>{:label2=>"value2", :label4=>"value4"}}
g={:year_1=>{:sub_type_a=>{:label5=>"value5"}},
:year_2=>{:sub_type_a=>{:label6=>"value6"}}}
h={:year_1=>{:sub_type_a=>{:label1=>"value1", :label3=>"value3"}},
:year_2=>{:sub_type_a=>{:label2=>"value2", :label4=>"value4"}}}
yr=year_1
v={:sub_type_a=>{:label5=>"value5"}}
k=sub_type_a
f={:label5=>"value5"}
yr=>{ k=>f } = year_1=>{:sub_type_a=>{:label5=>"value5"}} =
{:year_1=>{:sub_type_a=>{:label5=>"value5"}}}
_=year_1
o={:sub_type_a=>{:label1=>"value1", :label3=>"value3"}}
n={:sub_type_a=>{:label5=>"value5"}}
{ k=>o[k].merge(n[k]) }
=> { sub_type_a=>{:label1=>"value1", :label3=>"value3"}.
merge({:label5=>"value5"}) }
=> {:sub_type_a=>{:label1=>"value1", :label3=>"value3",
:label5=>"value5"}}
yr=year_2
v={:sub_type_a=>{:label6=>"value6"}}
k=sub_type_a
f={:label6=>"value6"}
yr=>{ k=>f } = year_2=>{:sub_type_a=>{:label6=>"value6"}} =
{:year_2=>{:sub_type_a=>{:label6=>"value6"}}}
_=year_2
o={:sub_type_a=>{:label2=>"value2", :label4=>"value4"}}
n={:sub_type_a=>{:label6=>"value6"}}
{ k=>o[k].merge(n[k]) }
=> { sub_type_a=>{:label2=>"value2", :label4=>"value4"}.
merge({:label6=>"value6"}) }
=> {:sub_type_a=>{:label2=>"value2", :label4=>"value4",
:label6=>"value6"}}
=> {:year_1=>{:sub_type_a=>{:label1=>"value1", :label3=>"value3",
:label5=>"value5"}},
:year_2=>{:sub_type_a=>{:label2=>"value2", :label4=>"value4",
:label6=>"value6"}}}
Hash#merge takes an optional conflict resolution block, which will be called any time a key is present in both the subject and the parameter.
You can use this to e.g. recursively merge your hashes.
Related
Building a data structure in Ruby
Am am trying to build a data structure by looping over an array. This is what I have a = ['a1', 'a2'] def func(name) {name => {'key1'=> 'value2', 'key2'=> 'value2' } } end content = {'root'=> a.each do | v | func(v) end } Which gives me {"root"=>["a1","a2"]} I am after this { "r"=> { "a1"=> { "key1"=> "value2", "key2"=> "value2" }, "a2"=> { "key1"=> "value2", "key2"=> "value2" } } } Can someone show me where I am going wrong?
You're using each, which only return the array itself. Instead of that, you need to use map: content = { 'root' => a.map do | v | func(v) end } Or in a better syntax, since the block is one line: content = { 'root' => a.map { |v| func(v) } }
It still needs some refactoring but you will get the idea. If you use map it will return array and resulted "r" will be array and not hash. {"r": a.each_with_object(Hash.new(0)) { |h1, h2| h2[h1]={"key1"=>"value2", "key2"=>"value2"} } } it will return exactly {:r=>{ "a1"=>{"key1"=>"value2", "key2"=>"value2"}, "a2"=>{"key1"=>"value2","key2"=>"value2"} } }
How do I use an array of named indices (keys) to set a value in a nested hash?
Given any nested hash, for example: { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil}}, united_states: { district_of_coloumbia: { washington: nil}}} how can I use any array of keys [:canada, :ontario, :ottawa] or [:united_states, :district_of_columbia, :washington] to get or set a value. Basically, my problem is how do I change [:canada, :ontario, :ottawa] into a getter or setter of the format hash[:canada][:ontario][:ottawa] when I don't know the length of the array of keys. so I can do something like: hash[:canada][:ontario][:ottawa] = nil hash[:canada][:manitoba][:winnipeg] = :me I made a getter using recursion: def reindex(h, index_array) i = index_array.shift result = index_array.empty? ? h[i] : reindex(h[i], index_array) result end But I feel like I'm over thinking this and there should be a simpler way.
Much simpler approach(in my opinion) is to access elements successively with :[]: keys = [:canada, :ontario, :ottawa] hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } } # get p keys.inject(hash) { |h, k| h.public_send(:[], k) } #=> :me # set last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) } last.public_send(:[]=, keys[-1], 'other') p hash #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}} Wrapped in methods: def get_by_keys(hash, keys) keys.inject(hash) { |h, k| h.public_send(:[], k) } end def set_by_keys(hash, keys, v) last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) } last.public_send(:[]=, keys[-1], v) hash end keys = [:canada, :ontario, :ottawa] hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } } p get_by_keys(hash, keys) #=> :me p set_by_keys(hash, keys, 'other') #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
class Hash def deep_fetch(*path) path.reduce(self) do |mem, key| mem[key] if mem end end def deep_assign(*path, val) key = path.shift if path.empty? self[key] = val else if self[key].is_a?(Hash) self[key].deep_assign(*path, val) else self[key] = path.reverse.inject(val) { |a, n| {n => a} } end end self end end
countries = {:canada=>{:ontario=>{:ottawa=>:me}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}} hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } hash.merge!(countries) hash[:canada][:ontario][:ottawa] = nil hash[:canada][:manitoba][:winnipeg] = :me hash => {:canada=>{:ontario=>{:ottawa=>nil}, :manitoba=>{:winnipeg=>:me}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
Yes, recursion is an option. Here's how it could be implemented. Code def get(hash, arr) case arr.size when 1 then hash[arr.first] else get(hash[arr.first], arr[1..-1]) end end def set(hash, arr, val) case arr.size when 1 then hash[arr.first] = val else set(hash[arr.first], arr[1..-1], val) end end Example hash = { canada: { ontario: { ottawa: :me }, manitoba: { winnipeg: nil } }, united_states: { district_of_columbia: { washington: nil } } } arr_can = [:canada, :ontario, :ottawa] arr_us = [:united_states, :district_of_columbia, :washington] get(hash, arr_can) #=> :me get(hash, arr_us) #=> nil set(hash, arr_can, 'cat') set(hash, arr_us, 'dog') hash # => {:canada=>{:ontario=> {:ottawa=>"cat"}, # :manitoba=>{:winnipeg=>nil}}, # :united_states=> # {:district_of_columbia=>{:washington=>"dog"}} # }
I think recursion is your best option. I wouldn't consider it "overthinking" I'd do: def getter(hash, array) return hash[array[0]] if array.count == 1 getter(hash[array[0]], array[1..-1], item) end def setter(hash, array, item) return hash[array[0]] = item if array.count == 1 setter(hash[array[0]], array[1..-1], item) end
Use user and pass based on banner in telnet class of ruby
I want to write a script that does a login with username and password based on the output of the banner from a connection made by Net::Telnet in ruby: code to login and execute a few commands (commands are in file "commands"): USER = "myuser" PASS = "mypass" USER2 = "my2ndUser" PASS2 = "my2ndPass" ENABLEPASS = "myenablepass" ENABLEPASS2 = "my2ndEnablePass" tn = Net::Telnet::new("Host" => line, "Timeout" => 10, "Waittime" => 0.1, "Prompt" => /[#>:]/n) { |resp| print "==> "+resp} tn.cmd(USER) { |c| print c } tn.cmd(PASS) { |c| print c } tn.cmd("terminal length 0") { |c| print c } tn.cmd("en") { |c| print c } tn.cmd(ENABLEPASS) { |c| print c } tn.cmd("\n") { |c| print c } File.open('commands').each do |l| tn.cmd(l) { |c| print c } end tn.cmd("\n") { |c| print c } tn.cmd("\n") { |c| print c } tn.close Desired: same script but uses the 2nd parameters when it says "my2ndrouter" in the banner/MOTD of the router that is outputted when a login attempt is done (just before you have to insert your user ans pass)
If I got the scenario right, you can use waitfor to syphon in the data until you get the prompt to receive the user name. Then process the data in waitfor block to detect which of the two sets of parameters to use. routers = [ { "user" => "myuser", "pass" => "mypass", "enablepass" => "myendablepass" }, { "user" => "my2ndUser", "pass" => "my2ndPass", "enablepass" => "my2ndEndablePass" } ] tn = Net::Telnet::new("Host" => line, "Timeout" => 10, "Waittime" => 0.1, "Prompt" => /[#>:]/n) { |resp| print "==> "+resp} my_router = routers[0] # Replace /login:/ with whatever regexp that matches your login prompt, e.g. your catch-all /[#>:]/n tn.waitfor(/login:/) { |banner| my_router = routers[1] if banner.match("my2ndrouter") } tn.cmd(my_router['user']) { |c| print c } tn.cmd(my_router['pass']) { |c| print c } tn.cmd("terminal length 0") { |c| print c } tn.cmd("en") { |c| print c } tn.cmd(my_router['enablepass']) { |c| print c } tn.cmd("\n") { |c| print c } File.open('commands').each do |l| tn.cmd(l) { |c| print c } end tn.cmd("\n") { |c| print c } tn.cmd("\n") { |c| print c } tn.close
Merge two hashes on a particular value
I am checking whether the hash hash_volumes below has a key whose instance_id matches with a key of hash hash_instance. hash_volumes = { :"vol-d16d12b8" => { :instance_id => "i-4e4ba679", }, } hash_instance = { :"i-4e4ba679" => { :arch => "x86_64", }, } If it does, then I need to merge it to the hash_instance. I find that vol-d16d12b8 matches with the instance i-4e4ba679 and hence I want to merge it with hash_instance so that the final hash_instance will look like below: hash_instance = { :"i-4e4ba679" => { :arch => "x86_64", :volume => "vol-d16d12b8" # this is new entry to `hash_instance` }, } I am not able to merge these two hashes as explained above. I suspect my if statement is wrong. Please take a look at my code below: hash_volumes.each_key do |x| hash_instance.each_key do |y| if hash_volumes[x][:instance_id] == y ## I think this line is the problem hash_instance[y][:volume] = x end end end hash_instance Output: { :"i-4e4ba679" => { :arch => "x86_64" } } The code above gives hash_instance without adding volume to it. I tried as below, but none worked: if hash_volumes[x][:instance_id] == "#{y}" # => this if statement gives me syntax error ..... if hash_volumes[x][:instance_id] =~ /"#{y}"/ # => this if statement does not make any changes to above output.
hash_volumes = { :"vol-d16d12b8" => { :instance_id => "i-4e4ba679", }, } hash_instance = { :"i-4e4ba679" => { :arch => "x86_64", }, } hash_volumes.each do |key, val| id = val[:instance_id] #returns nil if the there is no :instance_id key if id id_as_sym = id.to_sym if hash_instance.has_key? id_as_sym hash_instance[id_as_sym][:volume] = id end end end --output:-- {:"i-4e4ba679"=>{:arch=>"x86_64", :volume=>"i-4e4ba679"}}
A simple implementation would be this: hash_instance.each do |k1, v1| next unless k = hash_volumes.find{|k2, v2| v2[:instance_id].to_sym == k1} v1[:volume] = k.first end
How do I add a value to a nested hash?
I have a nested hash: hash = { "a" => "a", "b" => { "c" => "c", "d" => { "e" => "e" } } } and I have a hash: new_value = { "b.d.e" => "new value" } I need some sort of "magical" function that replaces the value of the hash at hash["b"]["d"]["e"], like: magical_function(hash, new_value) #=> hash = { "a" => "a", "b" => { "c" => "c", "d" => { "e" => "new value" } } } I have no idea how. Can someone help please?
It's not magical if it's implemented in a straight-forward manner: merge_hash.each do |key, value| parts = key.split('.') leaf = parts.pop target = parts.inject(hash) do |h, k| h[k] ||= { } end target[leaf] = value end
Here's another solution: class Hash def replace_value(*keys, value) current = self current = current[keys.shift] while keys.size > 1 current[keys.last] = value end end Called by invoking hash.replace_value("b","d","e", "new_value").