Given the following arrays:
arr1 = [
{ nested_test1: [1,2] },
{ nested_test2: [true, false] }
]
arr2 = [
{ test1: 'a', test2: 'b' }
]
I wanna loop over array arr2 and mutate it depending on the contents of arr1. The final result should look like this:
res_arr = [
{ test1: 'a', test2: 'b', nested_test1: 1, nested_test2: true },
{ test1: 'a', test2: 'b', nested_test1: 2, nested_test2: true },
{ test1: 'a', test2: 'b', nested_test1: 1, nested_test2: false },
{ test1: 'a', test2: 'b', nested_test1: 2, nested_test2: false },
]
So my initial setup looks like this:
res_arr = []
arr1.each do |hash1|
arr2.each do |hash2|
hash1.values.first.each do |value|
hash2[hash1.keys.first] = value
res_arr << hash2.clone
end
end
end
But this gives me the following:
res_arr = [
{:test1=>"a", :test2=>"b", :nested_test1=>1},
{:test1=>"a", :test2=>"b", :nested_test1=>2},
{:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>true},
{:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>false}
]
So what I wanna do then, is to use the mutated version for each iteration of arr2. How can this be achieved?
I'm not entirely clear what your goal here is. For instance, in arr2, can values of the hash be arrays? In arr1 can the hashes have more than one key value pair? Or what happens if arr2 has multiple hashes in it? Are they merged, or is the result expanded further somehow?
So it's hard to say exactly what the best course is.
Nonetheless, given the data you've given us and the result you say you want, you can get it like this using Array#product:
m = arr1.inject(&:merge)
m.values[0].product(m.values[1]).map {|a| Hash[m.keys.zip(a)].merge(arr2[0]) }
The intuition here is that I think you're trying to create all possible combinations of a couple of arrays, to generate all possible test cases? Depending on what you're trying to do, a more natural way to store the data and get the result might be:
m = {
test1: ['a'],
test2: ['b'],
nested_test1: [1, 2],
nested_test2: [true, false]
}
m.values.inject(&:product).map {|v| Hash[m.keys.zip(v.flatten)] }
Code
The following method permits its arguments to be arrays of arbitrary size.
def expand(arr1, arr2)
arr2.product(
arr1.map { |h| h.flatten.then { |k,v| v.map { |e| { k=>e } } } }.
then { |first, *rest| first.product(*rest) }
).map { |first, (*rest)| first.merge(*rest.flatten) }
end
Example
arr1 = [{ nested_test1: [1,2] }, { nested_test2: [true, false] }]
arr2 = [{ test1: 'a', test2: 'b' }, { test3: 'c', test4: 'd' }]
expand(arr1, arr2)
#=> [{:test1=>"a", :test2=>"b", :nested_test1=>1, :nested_test2=>true},
# {:test1=>"a", :test2=>"b", :nested_test1=>1, :nested_test2=>false},
# {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>true},
# {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>false},
# {:test3=>"c", :test4=>"d", :nested_test1=>1, :nested_test2=>true},
# {:test3=>"c", :test4=>"d", :nested_test1=>1, :nested_test2=>false},
# {:test3=>"c", :test4=>"d", :nested_test1=>2, :nested_test2=>true},
# {:test3=>"c", :test4=>"d", :nested_test1=>2, :nested_test2=>false}]
Explanation
The following steps are performed for the example.
Noting that:
arr1.map(&:flatten)
#=> [[:nested_test1, [1, 2]], [:nested_test2, [true, false]]]
the first step is the following:
a = arr1.map { |h| h.flatten.then { |k,v| v.map { |e| { k=>e } } }
#=> [[{:nested_test1=>1}, {:nested_test1=>2}],
# [{:nested_test2=>true}, {:nested_test2=>false}]]
See Hash#flatten and Object#then. The latter method made its debut in Ruby v2.6. It is an alias of Object#yield_self, which was new in v2.5.
Then:
b = a.then { |first, *rest| first.product(*rest) }
#=> [[{:nested_test1=>1}, {:nested_test2=>true}],
# [{:nested_test1=>1}, {:nested_test2=>false}],
# [{:nested_test1=>2}, {:nested_test2=>true}],
# [{:nested_test1=>2}, {:nested_test2=>false}]]
See Array#product. Here and especially below I made heavy use of Array decomposition. See also this article.
Continuing,
c = arr2.product(b)
#=> [[{:test1=>"a", :test2=>"b"}, [{:nested_test1=>1}, {:nested_test2=>true}]],
# [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>1}, {:nested_test2=>false}]],
# [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>2}, {:nested_test2=>true}]],
# [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>2}, {:nested_test2=>false}]],
# [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>1}, {:nested_test2=>true}]],
# [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>1}, {:nested_test2=>false}]],
# [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>2}, {:nested_test2=>true}]],
# [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>2}, {:nested_test2=>false}]]]
and lastly:
c.map { |first, (*rest)| first.merge(*rest.flatten) }
#=> <as shown above>
See Hash#merge.
Related
arr = [
{
:id=>2,
:start=> "3:30",
break: 30,
num_attendees: 14
},
{
id: 3,
start: "3: 40",
break: 40,
num_attendees: 4
},
{
id: 4,
start: "4: 40",
break: 10,
num_attendees: 40
}
]
When I do the following
arr.map do |hash|
[ hash[:id], hash[:start] ]
end
returns
#=> [[2, "3:30"], [3, "3: 40"], [4, "4: 40"]]
Is there an elegant and efficient way of passing an array like return_keys = [:id, :start] and get the same above values rather than hard coding inside the array.map
Would you consider the following elegant and efficient?
arr.map { |h| h.values_at(:id, :start) }
#=> [[2, "3:30"], [3, "3: 40"], [4, "4: 40"]]
or
arr.map { |h| h.values_at(*return_keys) }
#=> [[2, "3:30"], [3, "3: 40"], [4, "4: 40"]]
I find the following really expressive
keys = [:id, :start]
arr.map {|hash| hash.slice(*keys).values}
The slice method returns a hash only with the keys passed as parameters (which are preceded by the * operator to convert an array into keyword arguments and avoid hardcoding). Then, the values method gets just the values out of the hash
I have an array that looks something like this
array = [{ a: 123, b: 'foo', c: 'bar' }, { a: 456, b: 'baz', c: 'qux' }]
I would like to convert this to a hash whose keys are the values of :a in the hashes in array, and whose values consist of hashes with :b and :c.
{ 123 => { b: 'foo', c: 'bar' }, 456 => { b: 'baz', c: 'qux' } }
Is this doable using ruby?
array.each_with_object({}){|e, h| e = e.dup; h[e.delete(:a)] = e}
# => {123=>{:b=>"foo", :c=>"bar"}, 456=>{:b=>"baz", :c=>"qux"}}
If you don't care about side effects:
array.each_with_object({}){|e, h| h[e.delete(:a)] = e}
# => {123=>{:b=>"foo", :c=>"bar"}, 456=>{:b=>"baz", :c=>"qux"}}
I'm not sure what problem you're trying to solve. This is what I come out with:
def group_by_key(array, key)
array.map { |x| [x.delete(key), x] }.to_h
end
It works good on your example:
array = [{ a: 123, b: 'foo', c: 'bar' }, { a: 456, b: 'baz', c: 'qux' }]
group_by_key(array, :a)
#=> {123=>{:b=>"foo", :c=>"bar"}, 456=>{:b=>"baz", :c=>"qux"}}
Assuming that all elements of array (hashes) have a key :a and that array cannot be mutated, use Hash#reject:
array.each_with_object({}) { |g,h| h[g[:a]] = g.reject { |k,_| k == :a } }
#=> {123=>{:b=>"foo", :c=>"bar"}, 456=>{:b=>"baz", :c=>"qux"}}
Using Hash#select method.
array.map { |v| [v.fetch(:a), v.select { |k,_| k != :a }] }.to_h
Exact :a, :b & :c
array.map { |v| [v.fetch(:a), v.select { |k,_| [:b, :c].include? k }] }.to_h
Output
{123=>{:b=>"foo", :c=>"bar"}, 456=>{:b=>"baz", :c=>"qux"}}
I have an array of hashes:
[
{
"June" => { "A" => { 3 => 48.4 } }
},
{
"January" => { "C" => { 2 => 88.0} }
},
{
"January"=> { "B" => { 2 => 44.0} }
},
{
"January"=> { "C" => { 4 => 48.8} }
}
]
I need to group each similar hash key into an array of the subsequent values like the following:
{
"June" => [{ "A" => [{ 3 => 48.4 }]] },
"January" => [
{ "B" => [{ 2 => 44.0}],
{ "C" => [{ 2 => 88.0}, { 4 => 48.8}],
] }
}
I am looking for an efficient method of grouping these elements. Can anyone help me master this hash of hashes?
I am trying to avoid looping through the base array and grouping manually. I was hoping that map (or some other enumerable method) might give what I want. When I used reduce(Hash.new, :merge), it came close but it used the last hash for each month key instead of adding it to an array.
Note: I added the following after gaining a clearer understanding of the question. My original answer is below.
Here is the OP's array of hashes, modified slightly.
arr = [{ "June" =>{ "A"=>{ 3=>48.4 } } },
{ "January"=>{ "C"=>{ 2=>88.0 } } },
{ "January"=>{ "B"=>{ "D"=>{ 2=>44.0 } } } },
{ "January"=>{ "C"=>{ 2=>10.0 } } },
{ "January"=>{ "C"=>{ 4=>48.8 } } }]
The hash to be constructed appears to be the following.
{ "June" =>[{ "A"=>[{ 3=>48.4 }] }],
"January"=>[{ "B"=>[{ "D"=>[{ 2=>44.0 }] }] }],
"C"=>[{ 2=>98.0, 4=>48.8 }] }] }
Note that 88.0 + 10.0 #=> 98.0 in 2=>98.0.
Observe that all the arrays within arr contain a single element, a hash. That being the case, those arrays serve no useful purpose. I therefore suggest the following hash be constructed instead:
{ "June" =>{ "A"=>{ 3=>48.4 } },
"January"=>{ "B"=>{ "D"=>{ 2=>44.0 } } },
"C"=>{ 2=>98.0, 4=>48.8 } } }
This can be produced with the following recursive method.
def recurse(arr)
arr.map(&:flatten).
group_by(&:first).
each_with_object({}) do |(k,v),h|
o = v.map(&:last)
h.update(k=>o.first.is_a?(Hash) ? recurse(o) : o.sum )
end
end
recurse(arr)
#=> {"June"=>{"A"=>{3=>48.4}},
# "January"=>{"C"=>{2=>98.0, 4=>48.8}, "B"=>{"D"=>{2=>44.0}}}}
(Original answer follows)
Here are two ways to obtain the desired hash. I assume that arr is your array of hashes.
#1 Use the form of Hash::new that takes a block
arr.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
k, v = g.to_a.first
h[k] << v
end
# => {"June"=>[{"A"=>{3=>48.4}}],
# "January"=>[{"C"=>{2=>88.0}}, {"B"=>{2=>44.0}}, {"C"=>{4=>48.8}}]}
#2 Use Enumerable#group_by
arr.map(&:first).
group_by(&:first).
tap { |h| h.keys.each { |k| h[k] = h[k].map(&:last) } }
# => {"June"=>[{"A"=>{3=>48.4}}],
# "January"=>[{"C"=>{2=>88.0}}, {"B"=>{2=>44.0}}, {"C"=>{4=>48.8}}]}
The steps are as follows.
a = arr.map(&:first)
#=> [["June", {"A"=>{3=>48.4}}], ["January", {"C"=>{2=>88.0}}],
# ["January", {"B"=>{2=>44.0}}], ["January", {"C"=>{4=>48.8}}]]
b = a.group_by(&:first)
#=> {"June"=>[["June", {"A"=>{3=>48.4}}]],
# "January"=>[["January", {"C"=>{2=>88.0}}], ["January", {"B"=>{2=>44.0}}],
# ["January", {"C"=>{4=>48.8}}]]}
c = b.tap { |h| h.keys.each { |k| h[k] = h[k].map(&:last) } }
#=> {"June"=>[{"A"=>{3=>48.4}}],
# "January"=>[{"C"=>{2=>88.0}}, {"B"=>{2=>44.0}}, {"C"=>{=>48.8}}]}
Let me elaborate the last step. Inside tap's block, we compute the following.
h = b
d = h.keys
#=> ["June", "January"]
The first element of d is passed to each's block and the block variable is assigned to that element.
k = d.first
#=> "June"
The block calculation is as follows.
e = h[k]
#=> [["June", {"A"=>{3=>48.4}}]]
f = e.map(&:last)
#=> [{"A"=>{3=>48.4}}]
h[k] = f
#=> [{"A"=>{3=>48.4}}]
b #=> {"June"=>[{"A"=>{3=>48.4}}],
# "January"=>[["January", {"C"=>{2=>88.0}}],
# ["January", {"B"=>{2=>44.0}}],
# ["January", {"C"=>{4=>48.8}}]]}
Next, d[1] ("January") is passed to each's block and similar calculations are performed.
Rather than using Object#tap I could have written
h = arr.map(&:first).
group_by(&:first)
h.keys.each { |k| h[k] = h[k].map(&:last) }
h
tap merely avoids the creation of local variable h and the need to have a final line equal to h.
I have two arrays of hashes:
a = [
{
key: 1,
value: "foo"
},
{
key: 2,
value: "baz"
}
]
b = [
{
key: 1,
value: "bar"
},
{
key: 1000,
value: "something"
}
]
I want to merge them into one array of hashes, so essentially a + b except I want any duplicated key in b to overwrite those in a. In this case, both a and b contain a key 1 and I want the final result to have b's key value pair.
Here's the expected result:
expected = [
{
key: 1,
value: "bar"
},
{
key: 2,
value: "baz"
},
{
key: 1000,
value: "something"
}
]
I got it to work but I was wondering if there's a less wordy way of doing this:
hash_result = {}
a.each do |item|
hash_result[item[:key]] = item[:value]
end
b.each do |item|
hash_result[item[:key]] = item[:value]
end
result = []
hash_result.each do |k,v|
result << {:key => k, :value => v}
end
puts result
puts expected == result # prints true
uniq would work if you concatenate the arrays in reverse order:
(b + a).uniq { |h| h[:key] }
#=> [
# {:key=>1, :value=>"bar"},
# {:key=>1000, :value=>"something"},
# {:key=>2, :value=>"baz"}
# ]
It doesn't however preserve the order.
[a, b].map { |arr| arr.group_by { |e| e[:key] } }
.reduce(&:merge)
.flat_map(&:last)
Here we use hash[:key] as a key to build the new hash, then we merge them overriding everything with the last value and return values.
I would rebuild your data a bit, since there are redundant keys in hashes:
thin_b = b.map { |h| [h[:key], h[:value]] }.to_h
#=> {1=>"bar", 1000=>"something"}
thin_a = b.map { |h| [h[:key], h[:value]] }.to_h
#=> {1=>"bar", 1000=>"something"}
Then you can use just Hash#merge:
thin_a.merge(thin_b)
#=> {1=>"bar", 2=>"baz", 1000=>"something"}
But, if you want, you can get exactly result as mentioned in question:
result.map { |k, v| { key: k, value: v } }
#=> [{:key=>1, :value=>"bar"},
# {:key=>2, :value=>"baz"},
# {:key=>1000, :value=>"something"}]
using Enumerable#group_by and Enumerable#map
(b+a).group_by { |e| e[:key] }.values.map {|arr| arr.first}
If you need to merge two arrays of hashes that should be merged also and there is more than two keys, then next snippet should help:
[a, b].flatten
.compact
.group_by { |v| v[:key] }
.values
.map { |e| e.reduce(&:merge) }
I'm new in ruby. I have two hashes:
f = { "server"=>[{ "hostname"=>"a1", "ip"=>"10" }, {"hostname"=>"b1", "ip"=>"10.1" }] }
g = { "admin" =>[{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1" },
{ "name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]}
and I want to get another hash like this:
{ "data" => [{"hostname"=>"a1", "ip"=>"10", "name" =>"adam", "mail"=>"any"},
{"hostname"=>"b1", "ip"=>"10.1", "name" =>"mike", "mail"=>"id"}]}
The pairs "hostname"=>"something" always matches in hashes of both arrays. I have tried something like this:
data = server.merge(admin)
but it isn't so easy and as you expect it doesn't work. Could you help me merge these hashes and explain for the future how you did it?
A quick way that i can think of right now will look like:
servers = { "server" => [{"hostname"=>"a1", "ip"=>"10"}, {"hostname"=>"b1", "ip"=>"10.1"}]}
admins = { "data" => [{"hostname"=>"a1", "ip"=>"10", "name" =>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name" =>"mike", "mail"=>"id"}]}
# FYI: you can just use arrays for representing the above data, you don't necessarily need a hash.
list_of_entries = (servers.values + admins.values).flatten
grouped_by_hostname_entries = list_of_entries.group_by { |h| h['hostname'] }
grouped_by_hostname_entries.map { |_, values| values.inject({}, :merge) }
#=> [{"hostname"=>"a1", "ip"=>"10", "name"=>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name"=>"mike", "mail"=>"id"}]
As another variant you can try this
h1 = { "server" => [{"hostname"=>"a1", "ip"=>"10"}, {"hostname"=>"b1", "ip"=>"10.1"}]}
h2 = { "admin" => [{"name" =>"adam", "mail"=>"any", "hostname"=>"a1"}, {"name" =>"mike", "mail"=>"id", "hostname"=>"b1"}]}
h1['server'].zip(h2['admin']).map { |ar| ar.first.merge(ar.last) }
#=> [{"hostname"=>"a1", "ip"=>"10", "name"=>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name"=>"mike", "mail"=>"id"}]
zip let us iterate through two or more arrays at the same time.
We use map to return result.
In map block ar would be equal
[{"hostname"=>"a1", "ip"=>"10"}, {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}]
[{"hostname"=>"b1", "ip"=>"10.1"}, {"name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]
So ar.first would be {"hostname"=>"a1", "ip"=>"10"} and the ar.last would be {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}
Finally we use merge to combine two hashes.
Hope this will help.
Code and example
ff = f["server"].each_with_object({}) { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
{ "data"=>g["admin"].map { |h| h.merge(ff[h["hostname"]]) } }
#=> {"data"=>[{"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"},
# {"name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1"}]}
Explanation
We want to produce a hash
{ "data"=>arr }
where
arr #=> [{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10" },
# { "name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1" }]
so we need only compute arr.
First, we create the hash
ff = f["server"].each_with_object({}) { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
We have
enum = f["server"].each_with_object({})
#=> #<Enumerator: [{"hostname"=>"a1", "ip"=>"10"},
# {"hostname"=>"b1", "ip"=>"10.1"}]:each_with_object({})>
We can see the elements that will be generated by this enumerator (and passed to its block) by converting it to an array:
enum.to_a
#=> [[{"hostname"=>"a1", "ip"=>"10"}, {}],
# [{"hostname"=>"b1", "ip"=>"10.1"}, {}]]
Note
enum.each { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"},
# "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
each passes the first element of enum and assigns the block variables using parallel assignement (also call multiple assignment):
g,h = enum.next
#=> [{"hostname"=>"a1", "ip"=>"10"}, {}]
g #=> {"hostname"=>"a1", "ip"=>"10"}
h #=> {}
We may now perform the block calculation:
h[g["hostname"]] = g
#=> h["a1"] = {"hostname"=>"a1", "ip"=>"10"}
#=> {"hostname"=>"a1", "ip"=>"10"}
The return value is the new value of the block variable h. The second element of enum is then passed to the block and the block calculation is performed:
g,h = enum.next
#=> [{"hostname"=>"b1", "ip"=>"10.1"}, {"a1"=>{"hostname"=>"a1", "ip"=>"10"}}]
g #=> {"hostname"=>"b1", "ip"=>"10.1"}
h #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}}
Notice that the hash h has been updated.
h[g["hostname"]] = g
#=> {"hostname"=>"b1", "ip"=>"10.1"}
So now
h #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"},
# "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
and
ff #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
Now we can compute arr:
g["admin"].map { |h| h.merge(ff[h["hostname"]]) }
The first element of g["admin"] is passed to the block and assigned to the block variable:
h = g["admin"][0]
#=> {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}
and the block calculation is performed:
h.merge(ff[h["hostname"]])
#=> h.merge(ff["a1"])
#=> h.merge({"hostname"=>"a1", "ip"=>"10"})
#=> {"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"}
Then
h = g["admin"][1]
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"b1"}
h.merge(ff[h["hostname"]])
#=> h.merge(ff["b1"])
#=> h.merge({"hostname"=>"a2", "ip"=>"10"})
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"a2", "ip"=>"10"}
Therefore,
arr
#=> [{"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"},
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1"}]
is returned by the block and we are finished.
f = { "server"=>[{ "hostname"=>"a1", "ip"=>"10" },
{"hostname"=>"b1", "ip"=>"10.1" }] }
g = { "admin" =>[{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1" },
{ "name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]}
# manual way
host_admin_merge = []
host_admin_merge << f["server"].first.merge(g["admin"].first)
host_admin_merge << f["server"].last.merge(g["admin"].last)
# a bit more automated, iterate, test key's value, append to new array
host_admin_merge = []
f["server"].each do |host|
g["admin"].each do |admin|
if admin[:hostname] == host[:hostname]
host_admin_merge << host.merge(admin)
end
end
end
# assign the array to a hash with "data" as the key
host_admin_hash = {}
host_admin_hash["data"] = host_admin_merge
p host_admin_hash