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").
Related
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.
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"}
}
}
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
Is there a simpler way to do this in ruby?
ops_schema_name = "ops"
tables.each do |table|
table.schema_name = ops_schema_name
end
When reading properties, is it as simple as tables.collect(&:schema_name)?
I am assuming there is a shortcut for setters.
You could do
schema_name_updater = -> table { table.schema_name = 'ops' }
tables.each(&schema_name_updater)
Based on the good Jörg's answer, I came up with this generic solution, for any kind of objects and any number of attributes:
attrs_setter = -> obj, attrs { attrs.each { |k, v| obj.send("#{k}=", v) } }
$> tables.each { |table| attrs_setter.call(table, { :name => 'ops' }) }
$> Obj = Struct.new(:a, :b)
# => Obj
$> objs = Array.new(2) { |i| i = Obj.new('hi', 'there') }
# => [#<struct Obj a="hi", b="there">, #<struct Obj a="hi", b="there">]
$> objs.each { |obj| attrs_setter.call(obj, { a: 'good', b: 'bye' } ) }
# => [#<struct Obj a="good", b="bye">, #<struct Obj a="good", b="bye">]
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