Extending a ruby class (hash) with new function (recursive_merge) - ruby

If I want to recursively merge 2 hashes, I can do so with the following function:
def recursive_merge(a,b)
a.merge(b) {|key,a_item,b_item| recursive_merge(a_item,b_item) }
end
This works great, in that I can now do:
aHash = recursive_merge(aHash,newHash)
But I'd like to add this as a self-updating style method similar to merge!. I can add in the returning function:
class Hash
def recursive_merge(newHash)
self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
end
end
But am not sure how to re-create the bang function that updates the original object without association.
class Hash
def recursive_merge!(newHash)
self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
# How do I set "self" to this new hash?
end
end
edit example as per comments.
h={:a=>{:b => "1"}
h.recursive_merge!({:a=>{:c=>"2"})
=> {:a=>{:b=>"1", :c="2"}}
The regular merge results in :b=>"1" being overwritten by :c="2"

Use merge! rather than attempt to update self. I don't believe it makes sense to use merge! anywhere but at the top level, so I wouldn't call the bang version recursively. Instead, use merge! at the top level, and call the non-bang method recursively.
It may also be wise to check both values being merged are indeed hashes, otherwise you may get an exception if you attempt to recursive_merge on a non-hash object.
#!/usr/bin/env ruby
class Hash
def recursive_merge(other)
self.merge(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
end
def recursive_merge!(other)
self.merge!(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
end
end
h1 = { a: { b:1, c:2 }, d:1 }
h2 = { a: { b:2, d:4 }, d:2 }
h3 = { d: { b:1, c:2 } }
p h1.recursive_merge(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1.recursive_merge(h3) # => {:a=>{:b=>1, :c=>2}, :d=>{:b=>1, :c=>2}}
p h1.recursive_merge!(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1 # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
If you have a specific reason to fully merge in place, possibly for speed, you can experiment with making the second function call itself recursively, rather than delegate the recursion to the first function. Be aware that may produce unintended side effects if the hashes store shared objects.
Example:
h1 = { a:1, b:2 }
h2 = { a:5, c:9 }
h3 = { a:h1, b:h2 }
h4 = { a:h2, c:h1 }
p h3.recursive_merge!(h4)
# Making recursive calls to recursive_merge
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>1, :b=>2}}
# Making recursive calls to recursive_merge!
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>5, :b=>2, :c=>9}}
As you can see, the second (shared) copy of h1 stored under the key :c is updated to reflect the merge of h1 and h2 under the key :a. This may be surprising and unwanted. Hence why I recommend using recursive_merge for the recursion, and not recursive_merge!.

Related

Accessing nested hashes with accessors in ruby

So I have a hash:
a = {
foo: {
bar: 1
}
}
Now I can access value 1 with a[:foo][:bar].
How would I go on about generating methods from this automatically so I could access the value with a.foo.bar?
Is this even possible? If it is how could I generate this for a predetermined hash?
This is doable with https://ruby-doc.org/stdlib-3.0.0/libdoc/ostruct/rdoc/OpenStruct.html from the standard library.
To make this work recursively we can use:
require 'ostruct'
def to_os(obj)
case obj
when Hash
OpenStruct.new(obj.transform_values { |h| to_os(h) })
when Array
obj.map { |o| to_os(o) }
else
obj
end
end
a = { foo: { bar: 1 } }
b = to_os(a)
puts b[:foo]
puts b.foo
puts b[:foo][:bar]
But as noted in the comments this becomes an OpenStruct, which means that the output of the upper code is:
#<OpenStruct bar=1>
#<OpenStruct bar=1>
1
So what we deduce from this that the nested hash gets lost.

Nested Dictionary Navigation

I have a nested dictionary/hash structure in Ruby like this:
d = {
:foo => {
:hoo => nil,
:baz => {
# ...
},
},
:bar => {
:doo => {
# ...
},
:loo => nil,
},
# ...
}
Every individual dictionary is of unknown size. Its keys are symbols and its values are either nil or another dictionary. I am trying to write two methods:
down() takes the current dictionary and a valid key as arguments and returns a pointer to that value (either nil or another dictionary).
up() takes the current dictionary and returns a pointer to the outer dictionary it is contained within.
down() is straightforward (it is the same as d[:foo]), but I am hesitant to just use d[:foo] because up() is basically impossible without storing some additional structural/navigation information about d as down() is called.
The return values cannot be a copy of anything in d, they must be pointers. I am still confused about which situations are pass-by-value vs pass-by-ref in Ruby, but I do know all of its "effectively a pointer" variables are mutable.
Update:
When I say "takes the current dictionary" I am just referring to some variable dict such that dict = d or dict = (some child of d). dict is either a dictionary or nil. For example:
d = # ...
dict = down(d, :foo)
# dict is now the current dictionary and points to {:hoo=>nil,:baz=>{}} in d
dict2 = down(dict, :baz)
# down() "takes the current dictionary" (dict)
# then returns a new current dictionary (dict2)
In my attempts at a solution, it has seemed essential that some additional entity is keeping track of the level at which the current dictionary resides.
I'm not certain I fully understand the question but perhaps you are looking for something like the following, which is just a linked list and not a nested hash of dictionaries, but I think this is more typical of what is done in Ruby.
class Dictionary
attr_accessor :name, :up, :down
def initialize(name, up)
#name = name
#up = up
#down = []
up.down << self unless up.nil?
end
end
We begin by creating the top Dictionary.
top = Dictionary.new("top", nil)
#=> #<Dictionary:0x000056dd361bcd48 #name="top", #up=nil, #down=[]>
Now let's create a child of top.
top_foo = Dictionary.new("foo", top)
#=> #<Dictionary:0x000056dd361ce458 #name="foo",
# #up=#<Dictionary:0x000056dd361bcd48 #name="top", #up=nil,
# #down=[#<Dictionary:0x000056dd361ce458 ...>]>,
# #down=[]>
Look at return value carefully. The value of top_foo's instance variable #up is the instance of Dictionary named top. We can see this more clearly as follows:
top_foo.up.name
#=> "top"
top_foo.down
#=> []
Let's see how top.down (formerly an empty array) has changed.
top.down.map(&:name)
#=> ["foo"]
Now create another child of top.
top_goo = Dictionary.new("goo", top)
#=> #<Dictionary:0x000056dd3611a480 #name="goo",
# #up=#<Dictionary:0x000056dd35eed180 #name="top",...
# #down=[]>
top_goo.up.name
#=> "top"
top_goo.down
#=> []
top.down.map(&:name)
#=> ["foo", "goo"]
Now create a child of top_foo:
top_foo_who = Dictionary.new("who", top_foo)
#=> #<Dictionary:0x000056dd36171488 #name="who",
# #up=#<top_foo instance>
top_foo_who.name
#=> "who"
top_foo_who.up.name
#=> "foo"
top_foo_who.down
#=> []
top_foo.down.map(&:name)
#=> "foo"
Suppose we wish to go from top_foo_who to top.
start = top_foo_who
dict = loop do
break start if start.up.nil?
start = start.up
end
#=> #<Dictionary:...>
dict.name
#=> "top"
dict.down.map(&:name)
#=> ["foo", "goo"]
If we wanted to go from top to top_foo, by the name "foo", we could write:
idx = top.down.map(&:name).index("foo")
#=> 0
dict = top.down[idx]
#=> #<Dictionary...>
dict.name
#=> "foo"

How to merge the hash this way

I have written a huge code something like below
headers, *data_rows = #testCaseSheet
local = headers.zip(*data_rows)
local = local[1..-1].map {|dataRow| local[0].zip(dataRow).to_h}
testCaseHash = {}
local.each do |value|
testCaseHash[value["Locator"]] = value.tap {|hs| hs.delete("Locator")}
end
#testCaseSheet = []
p testCaseHash
[h["Test Name"], testCaseHash],
which output me this as below, now I need to merge this action with each test, I don't know how to do this.
hash= {"Action"=>{"css=#entityType"=>"Type", "id=idNumber"=>"TypeAndWait", "id=shortName"=>"TypeAndTab", "id=FirstName"=>"TypeTabAndWait", nil=>nil},
"Test1"=>{"css=#entityType"=>"Individual", "id=idNumber"=>"2323", "id=shortName"=>"M", "id=FirstName"=>"Abc", "id=lastName"=>"Gg"},
"Test2"=>{"css=#entityType"=>"Legal", "id=idNumber"=>"2323", "id=shortName"=>"Z", "id=FirstName"=>"Xyz", "id=lastName"=>"Gg"}}
Now I want to merge this action with the followings tests for an example,
hash= { "Test1"=>{"css=#entityType"=>["Individual","Type"], "id=idNumber"=>["2323","TypeAndWait"], "id=shortName"=>["M","TypeAndTab"], "id=FirstName"=>["Abc","TypeTabAndWait"]},
"Test2"=>{"css=#entityType"=>["Legal""Type"], "id=idNumber"=>["2323","TypeAndWait"], "id=shortName"=>["Z","TypeAndTab"], "id=FirstName"=>["Xyz","TypeTabAndWait"]}}
I don't know how to merge this way, Can anyone help me?
If I understand you want something like this
hash_1 = {a: "a1", b: "b1", c: "c1"}
hash_2 = {a: "a2", b: "b2", d: "d1"}
p hash_1.merge(hash_2) { |k, v1, v2| v1 = [v1, v2] }
# => {:a=>["a1", "a2"], :b=>["b1", "b2"], :c=>"c1", :d=>"d1"}
Which in your case can be:
test_1_value = my_hash['Test1'].merge(my_hash['Action']) { |k, v1, v2| v1 = [v1, v2] }
# => {"css=#entityType"=>["Individual", "Type"], "id=idNumber"=>["2323", "TypeAndWait"], "id=shortName"=>["M", "TypeAndTab"], "id=FirstName"=>["Abc", "TypeTabAndWait"], "id=\"lastName"=>"Gg", nil=>nil}
This is a general solution, you can manipulate furthermore removing the unwanted keys ad apply to fit your needs.
Edit - picking up comments
Remove unwanted keys and simplified merge block:
keys_to_remove = ["id=lastName", "whatever", nil]
test_1_value = my_hash['Test1'].merge(my_hash['Action']) { |k, *vs| vs }.delete_if{ |k, _| keys_to_remove.include? k }
# => {"css=#entityType"=>["Individual", "Type"], "id=idNumber"=>["2323", "TypeAndWait"], "id=shortName"=>["M", "TypeAndTab"], "id=FirstName"=>["Abc", "TypeTabAndWait"]}
I want to expand on iGians answer. Although the answer describes how the issue should be solved, it didn't use any iteration. You can iterate over the tests in the following way:
hash = {
"Action"=>{"css=#entityType"=>"Type", "id=idNumber"=>"TypeAndWait", "id=shortName"=>"TypeAndTab", "id=FirstName"=>"TypeTabAndWait", nil=>nil},
"Test1"=>{"css=#entityType"=>"Individual", "id=idNumber"=>"2323", "id=shortName"=>"M", "id=FirstName"=>"Abc", "id=lastName"=>"Gg"},
"Test2"=>{"css=#entityType"=>"Legal", "id=idNumber"=>"2323", "id=shortName"=>"Z", "id=FirstName"=>"Xyz", "id=lastName"=>"Gg"},
}
action = hash.delete 'Action'
tests = hash
tests.each_value do |test|
action_with_test_keys = action.select { |key, _value| test.key? key }
test.merge!(action_with_test_keys) { |_key, *values| values } # values = [old, new]
end
This assumes that 'Action' is the only non-test key in the hash and all other values should be merged with the 'Action' value. Keep in mind that this approach mutates the hash variable. If you don't want this you should simply #dup the hash beforehand or look for a non-mutating approach.
Optimizations:
If you use Ruby 2.5.0 or higher you can use #slice instead of #select.
action.select { |key, _value| test.key? key }
# is replaced with
action.slice(*test.keys)
If you are 100% sure that each test in tests contains the same keys and there is always at least one test present, you could move the action_with_test_keys assignment out of the #each_value block to save resources.
tests = hash # anchor point in the above solution
action_with_test_keys = action.slice(*tests.values.first.keys) # added
References:
Hash#delete to remove the 'Action' key from the hash variable.
Hash#each_value to iterate over each value of tests.
Hash#select to select only the action keys that are present on test.
Hash#key? to check if the given key is present.
Hash#merge! to merge action_with_test_keys and update the test variable.
Hash#slice replacement for Hash#select if you use Ruby 2.5.0 or higher.
Generally speaking, it might be a good idea to build up the desired data structure while dealing with the underlaying data objects. However, if you need to transform you hash afterwards, here is one way to do that:
hash = {
"Action"=>{"css=#entityType"=>"Type", "id=idNumber"=>"TypeAndWait", "id=shortName"=>"TypeAndTab", "id=FirstName"=>"TypeTabAndWait", nil=>nil},
"Test1"=>{"css=#entityType"=>"Individual", "id=idNumber"=>"2323", "id=shortName"=>"M", "id=FirstName"=>"Abc", "id=lastName"=>"Gg"},
"Test2"=>{"css=#entityType"=>"Legal", "id=idNumber"=>"2323", "id=shortName"=>"Z", "id=FirstName"=>"Xyz", "id=lastName"=>"Gg"}
}
action = hash['Action']
tests = hash.reject { |k, v| k == 'Action' }
mapping = tests.map do |name, test|
groups = (action.to_a + test.to_a).group_by(&:first)
no_keys = groups.map { |k, v| [k, v.each(&:shift).flatten] }
no_keys.reject! { |k, v| v.length == 1 }
[name, Hash[no_keys]]
end
Hash[mapping]
# => {"Test1"=>{"css=#entityType"=>["Type", "Individual"], "id=idNumber"=>["TypeAndWait", "2323"], "id=shortName"=>["TypeAndTab", "M"], "id=FirstName"=>["TypeTabAndWait", "Abc"]},
# "Test2"=>{"css=#entityType"=>["Type", "Legal"], "id=idNumber"=>["TypeAndWait", "2323"], "id=shortName"=>["TypeAndTab", "Z"], "id=FirstName"=>["TypeTabAndWait", "Xyz"]}}
I hope you find that useful.

Ruby: Merge Hash containing Arrays

I am trying to merge two Hashes.I got the code from here. As you can see I want to add one more hobby to my list.
There are many hobbies. When h1 is formed ,only one hobby was available. When the second hobby arrived I wanted to merge it with the previous hash.
The structure of the Hashes are :
h1 = {"students"=>[{"name"=>"bobby", "hobbies"=>[{"outdoor"=>"cycling"}]}]}
h2 = {"students"=>[{"name"=>"bobby", "hobbies"=>[{"indoor"=>"reading"}]}]}
I ran this code:
res = h1.merge(h2) {|key,val1,val2| val1.merge val2}
The error I got was:
undefined method `merge' for [{"name"=>"bobby", "hobbies"=>[{"outdoor"=>"cycling"}]}]:Array (NoMethodError)
So since array is involved, I don't know what to do. Any Help.
Required Output:
{"students"=>[{"name"=>"bobby", "hobbies"=>[{"outdoor"=>"cycling", "indoor"=>"reading"}]}]}
The built-in marge method cannot do what you want.
You should write the original code to merge these structure like this.
def recursive_merge(h1, h2)
case h1
when Hash
(h1.keys + h2.keys).each do |key|
if h1.include?(key) && h2.include?(key) then
recursive_merge(h1[key], h2[key])
elsif h2.include?(key)
h1[key] = h2[key]
end
end
when Array
for i in 0...([h1.count, h2.count].max)
if i < h1.count && i < h2.count then
recursive_merge(h1[i], h2[i])
elsif i < h2.count then
h1[i] = h2[i]
end
end
end
end
recursive_merge(h1, h2)
puts h1
This method results the follwing output.
Please note that this method overwrites h1 itself.
{"students"=>[{"name"=>"bobby", "hobbies"=>[{"outdoor"=>"cycling", "indoor"=>"reading"}]}]}
The answer on your question might be:
h1.merge(h2) { |k, v1, v2|
v1.map.with_index { |v1e, idx|
v1e['hobbies'].first.merge! v2[idx]['hobbies'].first
v1e
}
v1
}
#⇒ {
# "students" => [
# [0] {
# "hobbies" => [
# [0] {
# "indoor" => "reading",
# "outdoor" => "cycling"
# }
# ],
# "name" => "bobby"
# }
# ]
# }
But I would definitely redesign hobbies to be a hash, not an array of hashes, eliminating weird first calls:
v1e['hobbies'].merge! v2[idx]['hobbies']
Hope it helps.

Ruby library function to transform Enumerable to Hash

Consider this extension to Enumerable:
module Enumerable
def hash_on
h = {}
each do |e|
h[yield(e)] = e
end
h
end
end
It is used like so:
people = [
{:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
]
people_hash = people.hash_on { |person| person[:name] }
p people_hash['fred'] # => {:age=>32, :name=>"fred"}
p people_hash['barney'] # => {:age=>42, :name=>"barney"}
Is there a built-in function which already does this, or close enough to it that this extension is not needed?
Enumerable.to_h converts a sequence of [key, value]s into a Hash so you can do:
people.map {|p| [p[:name], p]}.to_h
If you have multiple values mapped to the same key this keeps the last one.
[ {:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
].group_by { |person| person[:name] }
=> {"fred"=>[{:name=>"fred", :age=>32}],
"barney"=>[{:name=>"barney", :age=>42}]}
Keys are in form of arrays to have a possibility to have a several Freds or Barneys, but you can use .map to reconstruct if you really need.

Resources