3

I'm new to ruby and am having a hard time figuring out how to convert an array of arrays into a hash of a hash of an array.

for example, say I have:

[ [38, "s", "hum"], 
  [38, "t", "foo"], 
  [38, "t", "bar"], 
  [45, "s", "hum"], 
  [45, "t", "ram"], 
  [52, "s", "hum"], 
  [52, "t", "cat"], 
  [52, "t", "dog"]
]

I'm wanting in the end:

{38 => {"s" => ["hum"],
        "t" => ["foo", "bar"]
       },
 45 => {"s" => ["hum"],
        "t" => ["ram"]
       },
 52 => {"s" => ["hum"],
        "t" => ["cat", "dog"]
       }
 }

I've tried group_by and Hash, but neither is giving me what I'm looking for.

4 Answers 4

2

Maybe there's a more concise way of doing this, but I decided to just go the straightforward route:

input = [ [38, "s", "hum"],
  [38, "t", "foo"],
  [38, "t", "bar"],
  [45, "s", "hum"],
  [45, "t", "ram"],
  [52, "s", "hum"],
  [52, "t", "cat"],
  [52, "t", "dog"]
]

output = {}

# I'll talk through the first iteration in the comments.

input.each do |outer_key, inner_key, value|
  # Set output[38] to a new hash, since output[38] isn't set yet.
  # If it were already set, this line would do nothing, so
  # output[38] would keep its previous data.
  output[outer_key] ||= {}

  # Set output[38]["s"] to a new array, since output[38]["s"] isn't set yet.
  # If it were already set, this line would do nothing, so
  # output[38]["s"] would keep its previous data.
  output[outer_key][inner_key] ||= []

  # Add "hum" to the array at output[38]["s"].
  output[outer_key][inner_key] << value
end

So, the part you'd actually use, all tidied up:

output = {}

input.each do |outer_key, inner_key, value|
  output[outer_key] ||= {}
  output[outer_key][inner_key] ||= []
  output[outer_key][inner_key] << value
end
Sign up to request clarification or add additional context in comments.

2 Comments

The compact version would be input.reduce({}) {|h,(x,y,z)| ((h[x] ||= {})[y] ||= []) << z; h}.
Thanks! This is producing what I'm looking for. I went with this solution (Matchu's) as it makes sense to me at this stage of my ruby knowledge.
1

In cases like this, inject (a.k.a. reduce in 1.9) is a great tool:

input.inject({}) do |acc, (a, b, c)|
  acc[a] ||= {}
  acc[a][b] ||= []
  acc[a][b] << c
  acc
end

It will call the block once for each item in input passing an accumulator and the item. The first time it passes the argument as the accumulator, and subsequent calls get the return value of the last call as accumulator.

1 Comment

See glenn's comment on Matchu's answer.
0

This could be considered horrific or elegant, depending on your sensibilities:

input.inject(Hash.new {|h1,k1| h1[k1] = Hash.new {|h2,k2| h2[k2] = Array.new}}) {|hash,elem| hash[elem[0]][elem[1]].push(elem[2]); hash}
=> {38=>{"s"=>["hum"], "t"=>["foo", "bar"]}, 45=>{"s"=>["hum"], "t"=>["ram"]}, 52=>{"s"=>["hum"], "t"=>["cat", "dog"]}}

A more readable version of this would ideally be:

input.inject(Hash.new(Hash.new(Array.new))) {|hash,elem| hash[elem[0]][elem[1]].push(elem[2]); hash}

That is, start with an empty hash with default value equal to an empty hash with default value equal to an empty array. Then iterate over the input, storing the elements in the appropriate locations.

The problem with the latter syntax is that Hash.new(Hash.new(Array.new)) will cause all the hashes and arrays to have the same location in memory, and thus the values will be overwritten. The former syntax creates a new object each time and thus gives the desired result.

1 Comment

Remember, if I want to use this hash at a later point in the code, I probably expect the hash's default behavior to be unmodified. I probably shouldn't do that, but it's risky to change my expectations.
0

The example given in the question has a length of three for each element array, but the method below uses recursion, and can be used for an arbitrary length.

a = [ [38, "s", "hum", 1], 
    [38, "t", "foo", 2],
    [38, "t", "bar", 3], 
    [45, "s", "hum", 1], 
    [45, "t", "ram", 1], 
    [52, "s", "hum", 3], 
    [52, "t", "cat", 3], 
    [52, "t", "dog", 2]
]

class Array
  def rep
    group_by{|k, _| k}.
    each_value{|v| v.map!{|_, *args| args}}.
    tap{|h| h.each{|k, v| h[k] = (v.first.length > 1 ? v.rep : v.flatten(1))}}
  end
end

p a.rep

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.