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").

Resources