Create a deep nested hash using loops in Ruby - ruby-on-rails

I want to create a nested hash using four values type, name, year, value. ie, key of the first hash will be type, value will be another hash with key name, then value of that one will be another hash with key year and value as value.
The array of objects I'm iterating looks like this:
elements = [
{
year: '2018',
items: [
{
name: 'name1',
value: 'value1',
type: 'type1',
},
{
name: 'name2',
value: 'value2',
type: 'type2',
},
]
},
{
year: '2019',
items: [
{
name: 'name3',
value: 'value3',
type: 'type2',
},
{
name: 'name4',
value: 'value4',
type: 'type1',
},
]
}
]
And I'm getting all values together using two loops like this:
elements.each do |element|
year = element.year
element.items.each |item|
name = item.name
value = item.value
type = item.type
# TODO: create nested hash
end
end
Expected output is like this:
{
"type1" => {
"name1" => {
"2018" => "value1"
},
"name4" => {
"2019" => "value4"
}
},
"type2" => {
"name2" => {
"2018" => "value2"
},
"name3" => {
"2019" => "value3"
}
}
}
I tried out some methods but it doesn't seems to work out as expected. How can I do this?

elements.each_with_object({}) { |g,h| g[:items].each { |f|
h.update(f[:type]=>{ f[:name]=>{ g[:year]=>f[:value] } }) { |_,o,n| o.merge(n) } } }
#=> {"type1"=>{"name1"=>{"2018"=>"value1"}, "name4"=>{"2019"=>"value4"}},
# "type2"=>{"name2"=>{"2018"=>"value2"}, "name3"=>{"2019"=>"value3"}}}
This uses the form of Hash#update (aka merge!) that employs a block (here { |_,o,n| o.merge(n) } to determine the values of keys that are present in both hashes being merged. See the doc for definitions of the three block variables (here _, o and n). Note that in performing o.merge(n) o and n will have no common keys, so a block is not needed for that operation.

Assuming you want to preserve the references (unlike in your desired output,) here you go:
elements = [
{
year: '2018',
items: [
{name: 'name1', value: 'value1', type: 'type1'},
{name: 'name2', value: 'value2', type: 'type2'}
]
},
{
year: '2019',
items: [
{name: 'name3', value: 'value3', type: 'type2'},
{name: 'name4', value: 'value4', type: 'type1'}
]
}
]
Just iterate over everything and reduce into the hash. On the structures of known shape is’s a trivial task:
elements.each_with_object(
Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } # for deep bury
) do |h, acc|
h[:items].each do |item|
acc[item[:type]][item[:name]][h[:year]] = item[:value]
end
end
#⇒ {"type1"=>{"name1"=>{"2018"=>"value1"},
# "name4"=>{"2019"=>"value4"}},
# "type2"=>{"name2"=>{"2018"=>"value2"},
# "name3"=>{"2019"=>"value3"}}}

Related

Merge the value of same key names in ruby on rails

I want to merge the ids of same name values
hashes = [{
id: 3456824,
name: 'John'
},{
id: 6578954,
name: 'Vicky'
},{
id: 987456,
name: 'John'
}]
Expected:
[{
id: [3456824,987456],
name: 'John'
},{
id: 6578954,
name: 'Vicky'
}]
how I can achieve this in ruby on rails?
Here is a one liner:
hashes = [{
id: 3456824,
name: 'John'
},{
id: 6578954,
name: 'Vicky'
},{
id: 987456,
name: 'John'
}]
result = hashes.group_by{|h| h[:name] }.map{|k, v| {id: v.map{|x| x[:id]}, name: k}}
puts result
Check this repl: https://repl.it/repls/ShadowyCornyVideogames
Here are two ways to compute the desired result.
Use the form of Hash::new that takes a block
hashes.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
h[g[:name]] << g[:id]
end.map { |name,id| { id: id, name: name } }
#=> [{:id=>[3456824, 987456], :name=>"John"},
# {:id=>[6578954], :name=>"Vicky"}]
The first step of this calculation1 is
hashes.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
h[g[:name]] << g[:id]
end
#=> {"John"=>[3456824, 987456], "Vicky"=>[6578954]}
If a hash is defined
h = Hash.new { |h,k| h[k] = [] }
and (possibly after having added key-value pairs) h has no key k, h[k] in
h[k] << v
causes the block { |h,k| h[k] = [] } to be executed, resulting in the key value pair k=>[] being added to h, then << v is executed, changing h[k] from [] to [k].
Notice that this returns :id=>[6578954], rather than :id=>6578954, which was asked for by the question. Having all values of :id return an array avoids the need to check if :id returns an array or integer in subsequent code that processes the return value of this operation.
If :id=>6578954, were desired, one could write
hashes.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
h[g[:name]] << g[:id]
end.transform_values { |v| v.size==1 ? v.first : v }.
map { |name,id| { id: id, name: name } }
#=> [{:id=>[3456824, 987456], :name=>"John"},
# {:id=>6578954, :name=>"Vicky"}]
See Hash#transform_values.
Use 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
arr.each_with_object({}) do |g,h|
h.update(g[:name]=>[g[:id]]) { |_,o,n| o+n }
end.map { |name,id| { id: id, name: name } }
#=> [{:id=>[3456824, 987456], :name=>"John"},
# {:id=>[6578954], :name=>"Vicky"}]
If :id=>6578954, rather than :id=>[6578954], were desired:
arr.each_with_object({}) do |g,h|
h.update(g[:name]=>g[:id]) { |_,o,n| [*o,n] }
end.map { |name,id| { id: id, name: name } }
#=> [{:id=>[3456824, 987456], :name=>"John"},
# {:id=>6578954, :name=>"Vicky"}]
Notice that here update's argument is g[:name]=>g[:id] whereas it was previously g[:name]=>[g[:id]].
The first step is as follows.
arr.each_with_object({}) do |g,h|
h.update(g[:name]=>g[:id]) { |_,o,n| [*o,n] }
end
#=> {"John"=>[3456824, 987456], "Vicky"=>6578954}
In general, one or both of these approaches can be taken when Enumerable#group_by can be used. The reverse if often true also. The choice among these methods is a matter of personal taste.
1. A variant of the first part of this calculation is hashes.each_with_object({}) { |g,h| (h[g[:name]] ||= []) << g[:id] } #=> {"John"=>[3456824, 987456], "Vicky"=>[6578954]}.
You can do it like this:
args = [{
id: 3456824,
name: 'John'
},{
id: 6578954,
name: 'Vicky'
},{
id: 987456,
name: 'John'
}]
value_pairs = args.map { |h| h.values_at(:name, :id) }
grouped_by_name = value_pairs.group_by(&:first).transform_values { |arr| arr.map(&:last) }
as_hashes = grouped_by_name.map { |name, ids| { id: ids, name: name } }
One more possible solution is:
array = [
{
id: 3456824,
name: "John"
},
{
id: 6578954,
name: "Vicky"
},
{
id: 987456,
name: "John"
}
]
grouped_by_name = array.each_with_object(Hash.new {|h,k| h[k] = [] }) do |hash, result|
result[hash[:name]] << hash[:id]
end
=> {"John"=>[3456824, 987456], "Vicky"=>[6578954]}
grouped_by_name.map do |grouped_hash|
{
id: grouped_hash.last,
name: grouped_hash.first
}
end
=> [{:id=>[3456824, 987456], :name=>"John"}, {:id=>[6578954], :name=>"Vicky"}]

How to map ruby hashes correctly based on key provided

My data is like:
h = { themes_data: {
Marketing: [
{
id: 68,
projectno: "15",
}
],
Produktentwicklung: [
{
id: 68,
projectno: "15",
},
{
id: 4,
projectno: "3",
}
],
Marketing_summary: [
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
Produktentwicklung_summary: [
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
}
And my output should be like:
{ "marketing" => [
{
id: 68,
projectno: "15",
},
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
"Produktentwicklung" => [
{
id: 68,
projectno: "15"
},
{
id: 4,
projectno: "3",
},
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
Code:
def year_overview_theme
branch_hash = {}
#themes_data.each do |td|
arr = []
td[1].map do |dt|
arr << [{content: dt[:projectno], size: 5, align: :right, background_color: 'D8E5FF'}]
end
branch_hash["#{td[0]}"] = arr
end
branch_hash
end
The problem is that it does not iterate for right hash key.
For example, i want like:
marketing + marketing_summary as 1 hash and similarly
Produktentwicklung = Produktentwicklung_summary as one hash but there is some problem in my logic.
Is there a way that I can check like after 2 iteration,
it should do arr << data with branch_hash["#{td[0]}"] = arr ?
The desired hash can be constructed as follows.
h[:themes_data].each_with_object({}) { |(k,v),g|
g.update(k.to_s[/[^_]+/]=>v) { |_,o,n| o+n } }
#=> { "Marketing"=>[
# {:id=>68, :projectno=>"15"},
# {:ges=>{:result=>"47.6"}, :theme=>"Marketing"}
# ],
# "Produktentwicklung"=>[
# {:id=>68, :projectno=>"15"},
# {:id=>4, :projectno=>"3"},
# {:ges=>{:result=>"87.7"}, :theme=>"Produktentwicklung"}
# ]
# }
This uses the form of Hash#update (aka merge) that employs a block to determine the values of keys that are present in both hashes being merged. Here that block is:
{ |_,o,n| o+n }
The first block variable, _, is the common key. I have represented it with an underscore (a valid local variable) to tell the reader that it is not used in the block calculation. That is common practice. The values of the other two block variables, o and n, are explained at the link for the method update.
The regular expression /[^_]+/, matches one or more characters from the start of the string that are not (^) underscores. When used with the method String#[], we obtain:
"Marketing"[/[^_]+/] #=> "Marketing"
"Marketing_summary"[/[^_]+/] #=> "Marketing"
Let me start with a note: This looks to me like something that should rather be solved in SQL (if it's coming from SQL) instead of Ruby.
With that out of the way, here's a solution that should work:
output = {}
themes_data.each do |theme, projects|
projects.each do |project|
key = project[:theme] || theme.to_s
output[key] ||= [] # make sure the target is initialized
output[key] << project
end
end
There would probably be more elegant solutions using reduce or each_with_object but this works and it's simple enough.
keys = themes_data.keys
summary_keys = themes_data.keys.grep(/_summary/)
result = {}.tap do |hash|
(keys - summary_keys).each do |key|
hash[key] = themes_data[key] + themes_data["#{key}_summary".to_sym]
end
end

Merge a single array into nested array in rails

I have a list of objects something like this:
a = [
{
id: 0,
name: "ABC",
work: "ABC2"
},
{
id: 0,
name: "XYZ",
work: "XYZ2"
},
{
id: 1,
name: "ACD",
work: "ACD2"
}
]
And I want to convert it into something like this:
b = [
{
id: 0,
fields: [
{
name: "ABC",
work: "ABC2"
},
{
name: "XYZ",
work: "XYZ2"
}
]
},
{
id: 1,
fields: [
{
name: "ACD",
work: "ACD2"
}
]
}
]
The idea is to group the objects (by id) in one array.
The approach I tried is:
b = []
rest_object = []
a.each_with_index do |aa, idx|
aa.delete(:id)
rest_object << aa
if idx == 0
next
end
puts a[idx][:id], a[idx-1][:id]
if a[idx][:id] != a[idx-1][:id]
b << {id: a[idx-1][:id], names: rest_object}
rest_object = []
end
end
But I am getting an empty output.
Also, if it is possible to achieve the same in some cleaner way.
That would be helpful.
Something like this does the job. This deletes the :id key-value pair from each hash and uses the value to group the remainder of the hash. Then map the resulting hash to created an array and transform the data into {id: ..., fields: ...} format.
a = [{id: 0, name: "ABC", work: "ABC2"}, {id: 0, name: "XYZ", work: "XYZ2"}, {id: 1, name: "ACD", work: "ACD2"}]
b = a.group_by { |hash| hash.delete(:id) }
.map { |id, fields| {id: id, fields: fields} }
#=> [{:id=>0, :fields=>[{:name=>"ABC", :work=>"ABC2"}, {:name=>"XYZ", :work=>"XYZ2"}]}, {:id=>1, :fields=>[{:name=>"ACD", :work=>"ACD2"}]}]
Note: This mutates the hashes in the a array. If you don't want this change a.group_by to a.map(&:dup).group_by. Which first duplicates all hashes before doing any mutations.
Try following,
required_keys = a[0].except(:id).keys
b = a.group_by { |x| x[:id] }
b = b.inject([]) do |m,(k,v)|
arr = { id: k }
required_keys.each do |key|
arr[key.to_s.pluralize.to_sym] = v.map { |z| z.slice(key) }
end
m << arr
end
# => [{:id=>0, :names=>[{:name=>"ABC"}, {:name=>"XYZ"}]}, {:id=>1, :names=>[{:name=>"ACD"}]}]
a = [
{
id: 0,
name: "ABC"
},
{
id: 0,
name: "XYZ"
},
{
id: 1,
name: "ACD"
}
]
ary = []
a.each do|val|
ary[val[:id]] = {id: val[:id]} unless ary[val[:id]]
ary[val[:id]][:names] = [] unless ary[val[:id]][:names]
ary[val[:id]][:names].push({name: val[:name]})
end
If I understand, given a more general array like this:
a = [ { id: 0, name: "ABC", etc: "01" },
{ id: 0, name: "XYZ", etc: "02" },
{ id: 1, name: "ACD", etc: "11" } ]
The first idea I came up with is doing something like this:
require 'active_support/inflector' # for using pluralize
res = a.group_by{ |h| h[:id] }.values.map do |ary|
h = Hash.new{ |h,k| h[k] = [] }
ary.each { |hh| hh.each { |k,v| h[k] << v } }
h[:id] = h[:id].first
h.transform_keys do |k|
unless k == :id
k.to_s.pluralize.to_sym
else
k
end
end
end
res #=> [{:id=>0, :names=>["ABC", "XYZ"], :etcs=>["01", "02"]}, {:id=>1, :names=>["ACD"], :etcs=>["11"]}]
This is not exactly the format you require, but to get that format, just change this line
ary.each { |hh| hh.each { |k,v| h[k] << v } }
to
ary.each { |hh| hh.each { |k,v| h[k] << { k => v } } }

How to create loop within a JSON request object?

I need to create multiple "items" to be used within a json request .
Here's a request that works:
customs_info = EasyPost::CustomsInfo.create(
eel_pfc: 'NOEEI 30.37(a)',
customs_certify: true,
customs_signer: "first_name last_name",
contents_type: 'merchandise',
contents_explanation: '',
restriction_type: 'none',
restriction_comments: '',
# non_delivery_option: 'abandon',
customs_items: [
{
description: "asdf",
quantity: 1,
weight: 23,
value: 23,
# hs_tariff_number: '654321',
origin_country: 'US'
},
{
description: "1234568",
quantity: 1,
weight: 23,
value: 23,
# hs_tariff_number: '654321',
origin_country: 'US'
}
]
)
What I need is to not need to manually set the customs_items.
I tried:
customs_info = EasyPost::CustomsInfo.create(
eel_pfc: 'NOEEI 30.37(a)',
customs_certify: true,
customs_signer: "#{shipping_address.shipping_address_final.first_name} #{shipping_address.shipping_address_final.last_name}",
contents_type: 'merchandise',
contents_explanation: '',
restriction_type: 'none',
restriction_comments: '',
# non_delivery_option: 'abandon',
customs_items: [
vendor_line_items.map do |li|
{
description: "#{li.shop_product.product.item.title}",
quantity: li.quantity,
weight: li.shop_product.product.weight,
value: li.shop_product.price,
# hs_tariff_number: '654321',
origin_country: 'US'
}
end
]
)
Error Statement: Parameters to create Custom Item(s) invalid or missing
How can I create the loop to work with the JSON request and work like the first example that works manually?
If you remove the [] that is surrounding the vendor_line_itmes.map code, you will be good to go.
customs_info = EasyPost::CustomsInfo.create(
# ...
customs_items: vendor_line_items.map do |li|
{
description: "#{li.shop_product.product.item.title}",
quantity: li.quantity,
# ...
}
end
)
The map operation returns an array so the json you are currently generating would look like (note the array of arrays in customs_info):
{
"eel_pfc": "NOEEI 30.37(a)",
...
"customs_info": [
[
{
"description": "...",
"quantity": 5,
...
}
]
]
}

Rails: How to merge two hashes if a specific key has the same value?

I'm trying to merge hashes if a specific key has the same value.
here is the array
[{
id: 77,
member_phone: "9876543210",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 77,
member_phone: "123456789",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 78,
member_phone: "12345",
created_at: "2017-05-03T11:06:03.000Z",
name: "XYZ"
}]
and the required output:
[{
id: 77,
member_phone: "123456789,9876543210",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 78,
member_phone: "12345",
created_at: "2017-05-03T11:06:03.000Z",
name: "XYZ"
}]
here's the code I tried:
merge_users.group_by { |h1| h1["id"] }.map do |k,v|
{ "id" => k, :member_phone => v.map { |h2| h2[:member_phone] }.join(", ") }
end
how can I do it?
The following code would work for your given example.
code
result = arr.group_by {|h| h[:id]}.values.map do |arr|
arr.reduce do |h1, h2|
h1.merge(h2) do |k, ov, nv|
ov.eql?(nv) ? ov : [ov, nv].join(",")
end
end
end
p result
#=>[{:id=>77, :member_phone=>"9876543210,123456789", :created_at=>"2017-05-03T11:06:03.000Z", :name=>"Sure"}, {:id=>78, :member_phone=>"12345", :created_at=>"2017-05-03T11:06:03.000Z", :name=>"XYZ"}]
How about:
grouped = data.group_by do |item|
item[:id]
end
combined = grouped.map do |_id, hashes|
hashes.inject({}) do |memo, hash|
memo.merge(hash)
end
end
It works in two passes:
First group all hashes by the value of the :id key
This returns a Hash with the id as key, and an array (of all the hashes with this id) as value.
In a second pass all the hashes are merged and mapped to an array again.
arr = [
{ id: 77, phone: "9876543210", name: "Sure" },
{ id: 77, phone: "123456789", name: "Sure" },
{ id: 78, phone: "12345", name: "XYZ" }
]
You could use the form of Hash#update (aka merge!) that uses a block to compute the values of keys that are present in both hashes being merged.
arr.each_with_object({}) { |g,h| h.update(g[:id]=>g) { |_,o,n|
o.merge(phone: "#{o[:phone]}#{n[:phone]}") } }.values
#=> [{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# {:id=>78, :phone=>"12345", :name=>"XYZ"}]
Note that the receiver of Hash#values is the following.
#=> {77=>{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# 78=>{:id=>78, :phone=>"12345", :name=>"XYZ"}}
See the doc for Hash#update for definitions of the three block variables _, o and n. I used an underscore for the first variable (a valid name for a local variable) to signify that it is not used in the block calculation (a common practice).
Note that Hash#update can almost always be used when Enumerable#group_by can be used, and vice-versa.
Here's one way to use Hash#group_by here.
arr.group_by { |h| h[:id] }.
map { |_,a| a.first.merge(phone: a.map { |h| h[:phone] }.join) }
#=> [{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# {:id=>78, :phone=>"12345", :name=>"XYZ"}]
Note that
arr.group_by { |h| h[:id] }
#=> {77=>[{:id=>77, :phone=>"9876543210", :name=>"Sure"},
# {:id=>77, :phone=>"123456789", :name=>"Sure"}],
# 78=>[{:id=>78, :phone=>"12345", :name=>"XYZ"}]}

Resources