Merge nested hash without overwritting in Ruby - arrays

After checking this Ruby convert array to nested hash and other sites I am not able to achieve the following convertion:
I have this:
{"a"=>"text"}
{"b"=>{"x"=>"hola"}}
{"b"=>{"y"=>"pto"}
}
and I want to obtain:
{"a"=>text,
b=>{"x" => "hola",
"y" => "pto"
}
}
Until now the code seems like this:
tm =[[["a"],"text"],[["b","x"],"hola"],[["b","y"],"pto"]]
q = {}
tm.each do |l|
q = l[0].reverse.inject(l[1]) { |p, n| { n => p } }
i += 1
end
I tried with merge, but it overwrites the keys!. I tried also this How can I merge two hashes without overwritten duplicate keys in Ruby? but it keeps overwritting.
Update:
How can I do it for an undefined nested hash (level) ? hash[key1][key2][key3]... = "value"
{"a"=>"text"},
{"b"=>{"x"=>"hola"},
{"b"=>{"y"=>"pto"},
{"c"=>{"g"=>{"k" => "test1"}},
...
}

You could use merge with a block to tell Ruby how to handle duplicate keys:
a = {"a"=>"text"}
b = {"b"=>{"x"=>"hola"}}
c = {"b"=>{"y"=>"pto"}}
a.merge(b).merge(c) { |key, left, right| left.merge(right) }
#=> {"a"=>"text", "b"=>{"x"=>"hola", "y"=>"pto"}}

For Rails there is the deep_merge function for ActiveSupport that does exactly what you ask for.
You can implement the same for yourself as follows:
class ::Hash
def deep_merge(second)
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
self.merge(second, &merger)
end
end
Now,
h1 = {"a"=>"text"}
h2 = {"b"=>{"x"=>"hola"}}
h3 = {"b"=>{"y"=>"pto"}}
h1.deep_merge(h2).deep_merge(h3)
# => {"a"=>"text", "b"=>{"x"=>"hola", "y"=>"pto"}}

Related

Converting Ruby array of array into a hash

I have an array of arrays as below :
[
["2021-07-26T11:38:42.000+09:00", 1127167],
["2021-08-26T11:38:42.000+09:00", 1127170],
["2021-09-26T11:38:42.000+09:00", 1127161],
["2021-07-25T11:38:42.000+09:00", 1127177],
["2021-08-27T11:38:42.000+09:00", 1127104]
]
What i want to have as the output :
{
"2021-July" => [["2021-07-26T11:38:42.000+09:00", 1127167],["2021-07-25T11:38:42.000+09:00", 1127177]],
"2021-August" => [["2021-08-26T11:38:42.000+09:00", 112717],["2021-08-27T11:38:42.000+09:00", 112710]],
"2021-September" => ["2021-09-26T11:38:42.000+09:00", 112716]
}
I want to create the hash key year-month format based on the date value in each array element. What would be the easiest way to do this?
use group_by
date_array = [["2021-07-26T11:38:42.000+09:00", 1127167],["2021-08-26T11:38:42.000+09:00", 112717],["2021-09-26T11:38:42.000+09:00", 112716],["2021-07-25T11:38:42.000+09:00", 1127177],["2021-08-27T11:38:42.000+09:00", 112710]]
result = date_array.group_by{ |e| Date.parse(e.first).strftime("%Y-%B") }
date_array = [["2021-07-26T11:38:42.000+09:00", 1127167],["2021-08-26T11:38:42.000+09:00", 112717],["2021-09-26T11:38:42.000+09:00", 112716],["2021-07-25T11:38:42.000+09:00", 1127177],["2021-08-27T11:38:42.000+09:00", 112710]]
result_date_hash = Hash.new([])
date_array.each do |date|
formatted_date = Date.parse(date.first).strftime("%Y-%B")
result_date_hash[formatted_date] += date
end
Output:
puts result_date_hash
=>
{"2021-July"=>["2021-07-26T11:38:42.000+09:00", 1127167, "2021-07-25T11:38:42.000+09:00", 1127177],
"2021-August"=>["2021-08-26T11:38:42.000+09:00", 1127170, "2021-08-27T11:38:42.000+09:00", 1127104],
"2021-September"=>["2021-09-26T11:38:42.000+09:00", 1127161]}

Adding a count/tally to an array of hashes [duplicate]

I have an array of hashes:
[{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3>}, {"Dry Goods"=>2}]
I need to use inject here I think but I've really been struggling.
I want a new hash that reflects the sum of the previous hash's duplicate keys:
[{"Vegetable"=>15}, {"Dry Goods"=>5}]
I'm in control of the code that outputs this hash so I can modify it if necessary. The results were mainly hashes because this could end up nested any number of levels deep and then it's easy to call flatten on the array but not flatten the keys/values of the hash too:
def recipe_pl(parent_percentage=nil)
ingredients.collect do |i|
recipe_total = i.recipe.recipeable.total_cost
recipe_percentage = i.ingredient_cost / recipe_total
if i.ingredientable.is_a?(Purchaseitem)
if parent_percentage.nil?
{i.ingredientable.plclass => recipe_percentage}
else
sub_percentage = recipe_percentage * parent_percentage
{i.ingredientable.plclass => sub_percentage}
end
else
i.ingredientable.recipe_pl(recipe_percentage)
end
end
end
ar = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]
p ar.inject{|memo, el| memo.merge( el ){|k, old_v, new_v| old_v + new_v}}
#=> {"Vegetable"=>15, "Dry Goods"=>5}
Hash.merge with a block runs the block when it finds a duplicate; inject without a initial memo treats the first element of the array as memo, which is fine here.
Simply use:
array = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]
array.inject{|a,b| a.merge(b){|_,x,y| x + y}}
ar = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]
While the Hash.merge technique works fine, I think it reads better with an inject:
ar.inject({}) { |memo, subhash| subhash.each { |prod, value| memo[prod] ||= 0 ; memo[prod] += value } ; memo }
=> {"Dry Goods"=>5, "Vegetable"=>15}
Better yet, if you use Hash.new with a default value of 0:
ar.inject(Hash.new(0)) { |memo, subhash| subhash.each { |prod, value| memo[prod] += value } ; memo }
=> {"Dry Goods"=>5, "Vegetable"=>15}
Or if inject makes your head hurt:
result = Hash.new(0)
ar.each { |subhash| subhash.each { |prod, value| result[prod] += value } }
result
=> {"Dry Goods"=>5, "Vegetable"=>15}
I'm not sure that a hash is what you want here, because I don't multiple entries in each hash. so I'll start by changing your data representation a little.
ProductCount=Struct.new(:name,:count)
data = [ProductCount.new("Vegetable",10),
ProductCount.new("Vegetable",5),
ProductCount.new("Dry Goods",3),
ProductCount.new("Dry Goods",2)]
If the hashes can have multiple key-value pairs, then what you probably want to do is
data = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3>}, {"Dry Goods"=>2}]
data = data.map{|h| h.map{|k,v| ProductCount.new(k,v)}}.flatten
Now use the facets gem as follows
require 'facets'
data.group_by(&:name).update_values{|x| x.map(&:count).sum}
The result is
{"Dry Goods"=>5, "Vegetable"=>15}
If have two hashes with multiple keys:
h1 = { "Vegetable" => 10, "Dry Goods" => 2 }
h2 = { "Dry Goods" => 3, "Vegetable" => 5 }
details = {}
(h1.keys | h2.keys).each do |key|
details[key] = h1[key].to_i + h2[key].to_i
end
details

How do I replace one element in an array with potentially multiple elements?

I want to replace an item in an array:
arr = ["55", "4.ARTHUR", "masddf"]
with potentially multiple items based on whether it matches a regular expression. I would like to have the result:
["55", "4.", "ARTHUR", "masddf"]
I tried:
arr.map { |o| o =~ /\d+\./ ? o.split(/^(\d+\.)/).reject { |c| c.empty? } : o }
# => ["55", ["4.", "ARTHUR"], "masddf"]
arr.map { |o| o =~ /\d+\./ ? o.split(/^(\d+\.)/).reject { |c| c.empty? }.flatten : o }
# => ["55", ["4.", "ARTHUR"], "masddf"]
I can't seem to get the elements outside of the array they got split into.
Any ideas?
Use flat_map instead:
arr = ["55", "4.ARTHUR", "masddf"]
arr.flat_map { |o| o =~ /\d+\./ ? o.split(/^(\d+\.)/).reject { |c| c.empty? } : o }
# => ["55", "4.", "ARTHUR", "masddf"]
See it on repl.it: https://repl.it/F90V
By the way, a simpler way to solve this problem is to use String#scan:
arr.flat_map {|o| o.scan(/^\d+\.|.+/) }
See it on repl.it: https://repl.it/F90V/1

How to array_wrap an hash of hashes

inv = {"C"=>{"CPS"=>{"CP"=>{"name"=>"a"}}}} is my object
I want
inv = {"C"=>{"CPS"=>{"CP"=>[{"name"=>"a"}]}}}
I tried
inv["C"]["CPS"].inject({}) do |result, (k, v)|
k = Array.wrap(v)
end
=> [{"name"=>"a"}]
but still inv={"C"=>{"CPS"=>{"CP"=>{"name"=>"a"}}}}
tries map also
Another option is to use tap
inv["C"]["CPS"].tap do |h|
h["CP"] = [h["CP"]] #or Array.wrap(h["CP"]) in rails
end
inv
#=> {"C"=>{"CPS"=>{"CP"=>[{"name"=>"a"}]}}}
tap will yield the current object so you can modify it in place.
Update
Inspired by #CarySwoveland's broader application you could use something like this as well.
class HashWrapper
attr_reader :original_hash
attr_accessor :target_keys
def initialize(h,*target_keys)
#original_hash = h
#target_keys = target_keys
end
def wrapped_hash
#wrapped_hash ||= {}
end
def wrap_me
original_hash.each do |k,v|
value = v.is_a?(Hash) ? HashWrapper.new(v,*target_keys).wrap_me : v
wrapped_hash[k] = wrap(k,value)
end
wrapped_hash
end
private
def wrap(k,v)
target_keys.include?(k) ? [v] : v
end
end
Then implementation is as follows
wrapper = HashWrapper.new(inv,"CP")
wrapper.wrap_me
#=> {"C"=>
{"CPS"=>
{"CP"=>
[
{"name"=>"a"}
]
}
}
}
new_wrapper = HashWrapper.new(inv,"CP","CPS")
new_wrapper.wrap_me
#=> {"C"=>
{"CPS"=>
[
{"CP"=>
[
{"name"=>"a"}
]
}
]
}
}
This assumes unique keys all the way through the hierarchy otherwise nested keys of the same name will be wrapped in the same fashion from the bottom up.
e.g.
inv = {"C"=>{"CPS"=>{"CP"=>{"name"=>"a"}},"CP" => "higher level"}}
HashWrapper.new(inv,"CP").wrap_me
#=> {"C"=>
{"CPS"=>
{"CP"=>
[
{"name"=>"a"}
]
},
"CP"=>
[
"higher level"
]
}
}
This should do it:
hash = {"C"=>{"CPS"=>{"CP"=>{"name"=>"a"}}}}
val = hash["C"]["CPS"]["CP"]
val_as_arr = [val] # can optionally call flatten here
hash["C"]["CPS"]["CP"] = val_as_arr
puts hash
# => {"C"=>{"CPS"=>{"CP"=> [{"name" => "a"}] }}}
basically
get the value
convert to array
set the value
There is no iteration required here i.e. map or reduce
I suggest you use recursion, in the form of a compact and easily readable method that has broader application than solutions that only work with your specific hash.
def wrap_it(h)
h.each { |k,v| h[k] = v.is_a?(Hash) ? wrap_it(v) : [v] }
h
end
h = { "C"=>{ "CPS"=>{ "CP"=>{ "name"=>"a" } } } }
wrap_it(h)
#=> {"C"=>{"CPS"=>{"CP"=>{"name"=>["a"]}}}}
h = { "C"=>{ "CPS"=>{ "CP"=>{ "CPPS"=> { "name"=>"cat" } } } } }
wrap_it(h)
#=> {"C"=>{"CPS"=>{"CP"=>{"CPPS"=>{"name"=>["cat"]}}}}}
h = { "C"=>{ "CPS"=>{ "CP"=>{ "CPPS"=> { "name"=>"cat" } },
"DP"=>{ "CPPPS"=>"dog" } } } }
wrap_it(h)
#=> {"C"=>{"CPS"=>{"CP"=>{"CPPS"=>{"name"=>["cat"]}}, "DP"=>{"CPPPS"=>["dog"]}}}}

How do I change the first elements of a 2 dimensional array?

I have an array
[[-20,23],[-80,65], ... []]
and I need
[["20",23],["80",65], ... []]
I have no idea how to deal with it.
Here is my code:
#posts = Post.featured_post.where(new_follow: true)
posts = (#posts.map { |post| "-#{ post[:ss_group_id] }_#{ post[:post_id] }" }).join(',') # here make parameters for request
posts = '"' + posts + '"'
posts_response = get_request(code_constructor('API.get', { posts: posts },[])) # here is response from API
noexist_posts = #posts.pluck(:vk_group_id, :post_id) - (posts_response[0].map { |h| h.values_at('owner_id', 'id') })
.map { |a| [a[0].abs.to_s, a[1]] } # here is what I want
I tried to find which posts don't exist.
A more inefficient, but cooler looking alternative to regular reassignment:
x.map { |first, *rest| [first.abs.to_s, *rest] }
You could do something like this
result = your_array.map{|a| [a[0].abs.to_s),a[1]]}
For some reason noone has posted the "regular reassignment" yet, so here it is (assuming you want to change the array in place):
x.each { |numbers| numbers[0] = numbers.first.abs.to_s }

Resources