Problem
I have two arrays of variable lengths: people1
and people2
. I want to create an array of arrays where each inner array is a pair of elements from people1
and people2
The pairing should be based on a matching key. I’m using Ruby 2.2.0.
For example, given this input:
people1 = [{ name: :jon, id: 1 }, { name: :jay, id: 3 }, { name: :ray, id: 5 }]
people2 = [{ name: :jon, id: 2 }, { name: :ray, id: 7 }]
I would like this result:
pairs = [
[{ name: :jon, id: 1 }, { name: :jon, id: 2 }],
[{ name: :ray, id: 5 }, { name: :ray, id: 7 }]
]
Here’s what I came up with:
people1.collect do |p1|
if p2 = people2.find { |p2| p1[:name] == p2[:name] }
[p1, p2]
else
nil
end
end.compact
I looked at Array#zip, but it doesn’t seem to take a condition. Is there a more idiomatic way to do this?
Solution
First create a hash that maps names to the hashes for one list:
people2_by_name = Hash[people2.map { |h| [h[:name], h] }]
Filter the other list
people1.select { |h| people2_by_name.has_key?(h[:name]) }
.map { |h| [h, people2_by_name[h[:name]]] }
This will only work properly if there are no duplicate names in each, but it is O(n) in the number of hashes.
Veedrac’s answer will certainly work – this is just to present an alternative.
Namely, you could combine the arrays and use #group_by
:
(people1 + people2)
.group_by { |person| person[:name] }
.map { |name, people| people if people.count > 1 }
.compact
Or, instead of compact
ing nil
values, you could do:
(people1 + people2)
.group_by { |person| person[:name] }
.map(&:last)
.select { |people| people.count > 1 }
Same result.
I have assumed that, in both people1
and people2
, the values of :name
are unique.
Here’s one way to extract the desired pairs.
Code
def extract_pairs(people1, people2)
h2 = people2.each_with_object({}) { |g,h| h.update(g[:name]=>g) }
people1.each_with_object([]) do |g,a|
k = g[:name]
a << [g,h2[k]] if h2.key?(k)
end
end
Example
people1 = [{ name: :jon, id: 1 }, { name: :jay, id: 3 }, { name: :ray, id: 5 }]
people2 = [{ name: :jon, id: 2 }, { name: :ray, id: 7 }]
extract_pairs(people1, people2)
#=> [[{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}],
# [{:name=>:ray, :id=>5}, {:name=>:ray, :id=>7}]]
Explanation
For the above example:
enum0 = people2.each_with_object({})
#=> #<Enumerator: [{:name=>:jon, :id=>2}, {:name=>:ray, :id=>7}]:
# each_with_object({})>
g, h2 = enum0.next
#=> [{:name=>:jon, :id=>2}, {}]
g #=> {:name=>:jon, :id=>2}
h2 #=> {}
h2.update(g[:name]=>g)
#=> {}.update(:jon=>{:name=>:jon, :id=>2})
#=> {:jon=>{:name=>:jon, :id=>2}}
g, h2 = enum0.next
#=> [{:name=>:ray, :id=>7}, {:jon=>{:name=>:jon, :id=>2}}]
h2.update(g[:name]=>g)
#=> {:jon=>{:name=>:jon, :id=>2}}.update(:ray=>{:name=>:ray, :id=>7}
#=> {:jon=>{:name=>:jon, :id=>2}, :ray=>{:name=>:ray, :id=>7}}
enum1 = people1.each_with_object([])
#=> #<Enumerator: [{:name=>:jon, :id=>1}, {:name=>:jay, :id=>3},
# {:name=>:ray, :id=>5}]:each_with_object([])>
g, a = enum1.next
#=> [{:name=>:jon, :id=>1}, []]
k = g[:name]
#=> :jon
a << [g,h2[k]] if h2.key?(k)
# [] << [{:name=>:jon, :id=>1},
# {:jon=>{:name=>:jon, :id=>2}, :ray=>{:name=>:ray, :id=>7}}[:jon] if
# {:jon=>{:name=>:jon, :id=>2}, :ray=>{:name=>:ray, :id=>7}}.key?(:jon)
# [] << [{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}] if true
a #=> [{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}]
g, a = enum1.next
#=> [{:name=>:jay, :id=>3},
# [[{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}]]]
k = g[:name]
#=> :jay
a << [g,h2[k]] if h2.key?(k)
#=> nil
g, a = enum1.next
#=> [{:name=>:ray, :id=>5},
# [[{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}]]]
k = g[:name]
#=> :ray
a << [g,h2[k]] if h2.key?(k)
#=> [[{:name=>:jon, :id=>1}, {:name=>:jon, :id=>2}],
# [{:name=>:ray, :id=>5}, {:name=>:ray, :id=>7}]]
Here’s a one liner similar to Flambinos, but in my version, rather than mapping, producing unwanted nils, then compacting them, you are simply selecting the elements that you want. The intent is more transparent. It also allows you to use blocks with one argument only. These are subtle benefits, to be sure, and I like Flambino’s solution, but I thought the improvement was worth posting
(people1 + people2).group_by {|x| x[:name]}.values.select {|x| x.size > 1}