3
\$\begingroup\$

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?

\$\endgroup\$

4 Answers 4

4
\$\begingroup\$

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 \$\mathcal{O}(n)\$ in the number of hashes.

\$\endgroup\$
2
  • \$\begingroup\$ Disclaimer: I don't know Ruby. \$\endgroup\$
    – Veedrac
    Commented Jun 8, 2015 at 18:03
  • \$\begingroup\$ It's correct, nevertheless :) \$\endgroup\$
    – Flambino
    Commented Jun 8, 2015 at 19:34
4
\$\begingroup\$

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 compacting nil values, you could do:

(people1 + people2)
  .group_by { |person| person[:name] }
  .map(&:last)
  .select { |people| people.count > 1 }

Same result.

\$\endgroup\$
0
2
\$\begingroup\$

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}]] 
\$\endgroup\$
1
\$\begingroup\$

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}
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.