0

I have several vars such as year, month, day, hour, minute. I need to do some operations on a nested hash with these values like hash[2012][12][12][4][3]. When meet a new date, I need to extend the hash.

I can do this by judging each level one by one, and create a hash if not exists. But I'm wondering there is a convenience way to do it.

In perl, I can just call $hash{$v1}{$v2}{$v3}..{$vN} = something because hash not defined can be created by default.

Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
akawhy
  • 1,558
  • 1
  • 11
  • 18

2 Answers2

1

I agree with sawa's comment. You will probably have many other problems with such approach. However, what you want is possible.

Search for "ruby hash default value".

By default, the default value of Hash entry is nil. You can change that, ie:

h = Hash.new(5)

and now the h when asked for non-existing key will return 5, not nil. You can order it to return a new empty array, or new empty hash in a similar way.

But, be careful. It is easy to accidentally SHARE the default instance through all entries.

h = Hash.new([]) // default value = Array object, let's name it X
one = h[:dad]  // returns THE SAME object X
two = h[:mom]  // returns THE SAME object X

You must be careful to not use the shared-default-instance, and to use operations that will not mutate it. You cannot just

h[:mom] << 'thing'

as the h[:brandnewone] will now return mutated default instance with "thing" inside.

See here for a good explanation and proper usage examples

or, even better: example of autovivifying hash

Community
  • 1
  • 1
quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
  • 1
    A common way to avoid the problem you mention is to use a block: `h = Hash.new{[]}`. But your answer is not necessarily clear on how that extends to an arbitrary depth. – sawa Dec 14 '13 at 12:30
  • true. I've forgot about the block option. I've just finally found [this answer about auto-vivification](http://stackoverflow.com/a/10130862/717732) what explains how to use that option to produce new hashes recursively. – quetzalcoatl Dec 14 '13 at 12:36
1

You could add a helper method, which you might find useful in other contexts:

def mfetch(hash, *keys)
  return nil if (keys.empty? || !hash[keys.first]) 
  return hash[keys.first] if keys.size == 1
  k = keys.shift
  raise ArgumentError, "Too many keys" unless hash[k].is_a? Hash
  return mfetch(hash[k], *keys)
end

h = {cat: {dog: {pig: 'oink'}}}    # => {:cat=>{:dog=>{:pig=>"oink"}}} 
mfetch(h, :cat, :dog, :pig)        # => "oink"
mfetch(h, :cat, :dog)              # => {:pig=>"oink"}  
mfetch(h, :cat)                    # => {:dog=>{:pig=>"oink"}} 
mfetch(h, :cow)                    # => nil 
mfetch(h, :cat, :cow)              # => nil 
mfetch(h, :cat, :dog, :cow)        # => nil 
mfetch(h, :cat, :dog, :pig, :cow)  # => ArgumentError: Too many keys

If you preferred, you could instead add the method to the Hash class:

class Hash
  def mfetch(*keys)
    return nil if (keys.empty? || !hash[keys.first]) 
    return self[keys.first] if keys.size == 1
    k = keys.shift
    raise ArgumentError, "Too many keys" unless self[k].is_a? Hash
    return self[k].mfetch(*keys)
  end
end

h.mfetch(:cat, :dog, :pig)         # => "oink"

or if you are using Ruby 2.0, replace class Hash with refine Hash do to limit the addition to the current class. It might be convenient to put it in a module to be included.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100