65

I've got two Arrays:

members     = ["Matt Anderson", "Justin Biltonen", "Jordan Luff", "Jeremy London"]
instruments = ["guitar, vocals", "guitar", "bass", "drums"]

What I would like to do is combine these so that the resulting data structure is a Hash like so:

{"Matt Anderson"=>["guitar", "vocals"], "Justin Biltonen"=>"guitar", "Jordan Luff"=>"bass", "Jeremy London"=>"drums"}

Note the value for "Matt Anderson" is now an Array instead of a string. Any Ruby wizards care to give this a shot?

I know Hash[*members.zip(instruments).flatten] combines them almost the way I want, but what about turning the "guitars, vocals" string into an array first?

Peter DeWeese
  • 18,141
  • 8
  • 79
  • 101
Michael Irwin
  • 3,119
  • 5
  • 24
  • 40
  • 2
    the `*` and `.flatten` in the example in the question are not needed. `Hash[members.zip(instruments)]` gives the same result as `Hash[*members.zip(instruments).flatten]` – nohat Mar 03 '11 at 00:04
  • 1
    I would turn it into `{... "Justin Biltonen" => ["guitar"] ...}` as it will make work later easier -- in which case map/zip/split/Hash will give a nice solution. –  Mar 03 '11 at 00:20

8 Answers8

128

As Rafe Kettler posted, using zip is the way to go.

Hash[members.zip(instruments)] 
Raj
  • 1,764
  • 2
  • 13
  • 14
  • 8
    Simplest solution I've seen. – Venkat D. Oct 05 '12 at 02:45
  • 3
    This answer is wrong. OP asked for `{"Matt Anderson"=>["guitar", "vocals"], ...}`, but this answer returns `{"Matt Anderson"=>"guitar, vocals", ...} ` – nohat Jan 28 '14 at 02:53
  • How can you do the same thing but naming the keys? i.e. `{ [:member => 'Jeremy London, :instrument => 'drums'], [:member => 'other guy', :instrument=> 'guitar']...` – kakubei Apr 08 '14 at 11:57
  • I think in the simplified case (where you don't have multiple instruments per member), you can just do ```members.zip(instruments).map{|m, i| {:member => m, :instrument => i} }``` – Raj Jun 09 '14 at 17:47
  • 1
    This does not answer the question, but it is a great way to combine 2 arrays into one new hash. For instance, k = [*:a..:f] then v = [*'a'..'f'] finally h = Hash[k.zip(v)] gives you a very nice hash # => {:a=>"a", :b=>"b", :c=>"c", :d=>"d", :e=>"e", :f=>"f"} – jasonleonhard Nov 16 '15 at 22:28
  • This answers the question implied in the title, which is what I was looking for. – dansalmo Oct 11 '18 at 21:27
  • 3
    You can also refactor to: `members.zip(instruments).to_h` – Mateus Luiz Jun 16 '20 at 17:26
52

Use map and split to convert the instrument strings into arrays:

instruments.map {|i| i.include?(',') ? (i.split /, /) : i}

Then use Hash[] and zip to combine members with instruments:

Hash[members.zip(instruments.map {|i| i.include?(',') ? (i.split /, /) : i})]

to get

{"Jeremy London"=>"drums",
 "Matt Anderson"=>["guitar", "vocals"],
 "Jordan Luff"=>"bass",
 "Justin Biltonen"=>"guitar"}

If you don't care if the single-item lists are also arrays, you can use this simpler solution:

Hash[members.zip(instruments.map {|i| i.split /, /})]

which gives you this:

{"Jeremy London"=>["drums"],
 "Matt Anderson"=>["guitar", "vocals"],
 "Jordan Luff"=>["bass"],
 "Justin Biltonen"=>["guitar"]}
nohat
  • 7,113
  • 10
  • 40
  • 43
  • +1 But I can't imagine what your first solution is good for. Why does the hash returns a string or an array? I'd choose your second solution anytime.( "steenslag"=>[]) – steenslag Mar 03 '11 at 01:29
  • 7
    @steenslag it's good for being exactly what the question asked for :-) – nohat Mar 03 '11 at 14:32
12

Example 01

k = ['a', 'b', 'c']
v = ['aa', 'bb']
h = {}

k.zip(v) { |a,b| h[a.to_sym] = b } 
# => nil

p h 
# => {:a=>"aa", :b=>"bb", :c=>nil}

Example 02

k = ['a', 'b', 'c']
v = ['aa', 'bb', ['aaa','bbb']]
h = {}

k.zip(v) { |a,b| h[a.to_sym] = b }
p h 
# => {:a=>"aa", :b=>"bb", :c=>["aaa", "bbb"]}
coolesting
  • 1,451
  • 5
  • 18
  • 22
3

This is the best and cleanest way to do what you want.

Hash[members.zip(instruments.map{|i| i.include?(',') ? i.split(',') : i})]

Enjoy!

1
h = {}
members.each_with_index do |el,ix|
    h[el] = instruments[ix].include?(",") ? instruments[ix].split(",").to_a : instruments[ix]
end
h
Zabba
  • 64,285
  • 47
  • 179
  • 207
  • Actually, my is wrong , you do need to check for the , if you need the string for a single entry rather than a array containing one string – macarthy Mar 02 '11 at 23:50
1
members.inject({}) { |m, e| t = instruments.delete_at(0).split(','); m[e] = t.size > 1 ? t : t[0]; m }

If you don't care about 1-element arrays in the result, you can use:

members.inject({}) { |m, e| m[e] = instruments.delete_at(0).split(','); m }
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
0
h = {}

members.each_with_index {|item, index|
     h.store(item,instruments[index].split)
}
macarthy
  • 3,074
  • 2
  • 23
  • 24
0

When approaching a programming task like this, it helps to think in terms of step-wise data transformations.

You have two arrays: 1) band members and 2) instruments they play. BUT sometimes a member plays more than one instrument. This means there's another entity implicitly lurking in there.

This entity can be made explicit by introducing a logical condition:

if member plays multiple parts:
  return an array containing those parts
 else:
  return the instrument

Once you've transformed your original instruments array according to this condition, you're ready to merge it into a Hash with members:

members     = ["Matt Anderson", "Justin Biltonen", "Jordan Luff", "Jeremy London"]
instruments = ["guitar, vocals", "guitar", "bass", "drums"]

def band(members, instruments)
 roles = instruments.map do |ins|
  arr = ins.split(", ")
  arr.length > 1 ?  arr : ins
 end
 Hash[members.zip(roles)]
end


require 'minitest/spec'
require 'minitest/autorun'

describe "creates a band out of members and instruments" do
 it "combines arrays into a hash" do

  band(members, instruments).must_equal({
     "Matt Anderson"   => ["guitar", "vocals"],
     "Justin Biltonen" => "guitar",
     "Jordan Luff"     => "bass",
     "Jeremy London"   => "drums"
    })

 end
end
fullstackplus
  • 1,061
  • 3
  • 17
  • 31