2

Suppose I have:

a = Hash.new
a['google.com'][:traffic] << 50
a['google.com'][:traffic] << 20
a['google.com'][:popular] << 1
a['google.com'][:popular] << 2
3a['yahoo.com'][:anythinghere] << 3

needs to produce something like this:

a = { 'google.com' => {traffic: [50,20], popular: [1,2]}, 'yahoo.com' => { anythinghere: [3,4] } }

So far I've tried something of this kind in hope that it would produce this result:

a= Hash.new(Hash.new(Array.new))

For example, a['google.com'] would produce a new hash while a['google.com'][:anythinghere] would produce a new array. When I try to execute the above insertions, however, a is empty? No idea what's going on, I'm pretty sure I'm missing something fundamental here. Take a look:

a = stats = Hash.new(Hash.new(Array.new))
a['google.com'][:traffic] << 5
a['google.com'][:traffic] << 6
p a['google.com'][:traffic] #=> [5,6]
p a['google.com'] #=> empty hash???
p a #=> empty hash???
daremkd
  • 8,244
  • 6
  • 40
  • 66

3 Answers3

4

The reason why you're getting this unexpected behavior is because when you pass a default value to a Hash as an argument to new, that one object is the default value for all keys. For example:

s = "but"
a = Hash.new(s)
a['x'].concat 't'
puts a['y']
# butt
puts s
# butt

This makes sense since Ruby passes objects using pointers, so whenever you get the default value it's really a pointer to the original object you passed.

To work around this, you can set the default value in a block. That way the Hash has to reevaluate the block every time it needs the default value:

a = Hash.new { "but" }
a['x'].concat 't'
puts a['x']
# but
puts a.size
# 0

The next gotcha is that when Ruby gets the default value, it doesn't assign it to any keys. That's why the size of the Hash is still 0 after accessing a key in the previous example. This problem can also be worked around since Ruby supplies the hash itself and the missing key to the default value block, so we can do the assignment there:

a = Hash.new { |hash, key| hash[key] = "but" }
a['x'].concat 't'
puts a['x']
# butt
puts a['y']
# but
puts a.size
# 2
Max
  • 21,123
  • 5
  • 49
  • 71
1

You could use this line: Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }

a = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
a['google.com'][:traffic] << 50
a['google.com'][:traffic] << 20
a['google.com'][:popular] << 1
a['google.com'][:popular] << 2
a['yahoo.com'][:anythinghere] << 3
a
# => {"google.com"=>{:traffic=>[50, 20], :popular=>[1, 2]}, "yahoo.com"=>{:anythinghere=>[3]}}

I find the block supplied to Hash.new a bit messy, so it could be cleaned up like this:

def Hash.default_set(&block)
   Hash.new { |h, k| h[k] = block.call }
end
a = Hash.default_set { Hash.default_set { [] } }
# ...
August
  • 12,410
  • 3
  • 35
  • 51
0

Another way:

Code

def hashify(arr)
  arr.each_with_object({}) do |e,h|
    *hash_keys, array_key, value = e
    g = hash_keys.reduce(h) { |g,k| g[k] ||= {} }
    (g[array_key] ||= []) << value
  end
end  

Examples

a = [['google.com', :traffic, 50],
     ['google.com', :traffic, 20],
     ['google.com', :popular, 1],
     ['google.com', :popular, 2],
     ['yahoo.com',  :anythinghere, 3],
     ['yahoo.com',  :anythinghere, 4]]

hashify(a)  
  #=> {"google.com"=>{:traffic=>[50, 20],
  #    :popular=>[1, 2]},
  #    "yahoo.com"=>{:anythinghere=>[3, 4]}}

a = [['google.com', :traffic, 50],
     ['google.com', :traffic1, :cat, :dog, 20],
     ['google.com', :traffic1, :cat, :dog, 30],
     ['google.com', :popular, 1],
     ['google.com', :popular, 2],
     ['yahoo.com',  :anythinghere, 3],
     ['yahoo.com',  :anythinghere, 4]]

hashify(a)  
  #=> {"google.com"=>{:traffic=>[50], :traffic1=>{:cat=>{:dog=>[20, 30]}},
  #    :popular=>[1, 2]},
  #    "yahoo.com"=>{:anythinghere=>[3, 4]}}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100