28

I am a Perl person and I have made Hashes like this for a while:

my %date;

#Assume the scalars are called with 'my' earlier

$date{$month}{$day}{$hours}{$min}{$sec}++

Now I am learning Ruby and I have so far found that using this tree is the way to do many keys and a value. Is there any way to use the simple format that I use with Perl using one line?

 @date = {                                                                                                                                                                      
        month => {                                                                                                                                                                 
          day => {                                                                                                                                                                 
           hours => {                                                                                                                                                              
              min => {                                                                                                                                                             
                sec => 1                                                                                                                                                           
              }                                                                                                                                                                    
            }                                                                                                                                                                      
          }                                                                                                                                                                        
        }                                                                                                                                                                                                                                                                                                                                                     
      }                   
brian d foy
  • 129,424
  • 31
  • 207
  • 592
BioDevMike
  • 315
  • 1
  • 3
  • 10
  • I don't know a way to do that out of the box. You could probably extend the Array or Hash class with a method that took in something like `[month, day, hours, min, sec, 1]` as an argument and converted it to that kind of hash for you, if it's something you think is useful. – Karl Jul 26 '10 at 21:22
  • It seems to me that Ruby folks use different way to store information inside a running script. I use hashes like crazy in perl to keep my data unique and categorized. Is there a better method to store data in this case? I am using this snippet in a log parsing script and more keys might be added for different log entries I need to monitor and keep categorized. – BioDevMike Jul 26 '10 at 21:35
  • 2
    Other “hash autovivification in Ruby” questions: http://stackoverflow.com/questions/170223/hashes-of-hashes-idiom-in-ruby http://stackoverflow.com/questions/3148747/is-auto-initialization-of-multi-dimensional-hash-array-possible-in-ruby-as-it-is and http://stackoverflow.com/questions/3172342/how-to-handle-combination-for-auto-vivifying-hash-in-ruby – Chris Johnsen Jul 26 '10 at 21:57
  • I wonder if someone ought to create a gem for multidimensional hashes and arrays. – Andrew Grimm Jul 26 '10 at 23:41
  • It seems wierd that autovivification of hashes isn't built in to ruby. I use this feature in perl all the time for storing parsed log entries. In ruby how do you guys log parse if you do not use molf's recursive method for your hashes? – BioDevMike Jul 27 '10 at 17:18
  • Does this answer your question? [Hashes of Hashes Idiom in Ruby?](https://stackoverflow.com/questions/170223/hashes-of-hashes-idiom-in-ruby) – brian d foy May 11 '21 at 08:25

8 Answers8

53

Unfortunately, there is no simple, practical way. A Ruby equivalent would be an ugly, ugly beast like:

((((@date[month] ||= {})[day] ||= {})[hours] ||= {})[min] ||= {})[sec] = 1

There is a way to assign default values for missing keys in hashes, though:

@date = Hash.new { |hash, key| hash[key] = {} }

# @date[:month] is set to a new, empty hash because the key is missing.
@date[:month][:day] = 1

Unfortunately this does not work recursively.

...unless you create it yourself; hooray for Ruby!

class Hash
  def self.recursive
    new { |hash, key| hash[key] = recursive }
  end
end

@date = Hash.recursive
@date[month][day][hours][min][sec] = 1
# @date now equals {month=>{day=>{hours=>{min=>{sec=>1}}}}}

Keep in mind, though, that all unset values are now {} rather than nil.

molf
  • 73,644
  • 13
  • 135
  • 118
  • Thanks for a great answer. One more thing. How would you go about storing parsed data? I use hashes in perl what is your take on storing categorized parsed data? (My other comment to my question post has more) – BioDevMike Jul 26 '10 at 21:43
  • If you need the data available by month, by month + day, by month + day + hour, etc., then your approach is ok. If you need to do something else with it, then it really depends on your use case. You might even be fine just using an array like `[month, day, hours, min, sec]` or a simple string as hash key; but I can't say without knowing more. – molf Jul 26 '10 at 21:56
  • I also found I could stick in symbols easily by doing: @date[month.to_sym][day.to_sym][hours.to_sym][min.to_sym][sec.to_sym] =+ 1 Anyway to stick that into that recursive function to auto sym keys? – BioDevMike Jul 28 '10 at 17:32
  • @ThomasG33K, not really; you could subclass `Hash` and auto-convert your keys to symbols, or you can use `ActiveSupport::HashWithIndifferentAccess` if you can afford a dependency on activesupport (a Rails component). Sticking it in the recursive method above will only work when the `Hash` is created, not when you access its keys. – molf Jul 29 '10 at 10:03
  • 1
    is there a way to apply this to an existing hash? I mean define `hsh = {a: 'A', b: 'B'}` and then bolt on this recursive definition functionality later? The reason is I have a very large / deep hash defined and it would be a lot of code to write it all out as `hsh[:a][:b][:c][:d] = 'D'; hsh[:a][:b][:c][:e] = 'E'` – austinheiman Sep 29 '16 at 18:42
  • Come over here and give me a big hug! That just saved me a lot of headaches – Nate Uni Nov 21 '16 at 03:05
22

Here's a couple of options similar to the answer given by @molf but without the monkey patch.

Using a factory method:

  def hash_tree
    Hash.new do |hash, key|
      hash[key] = hash_tree
    end
  end

  @date = hash_tree
  @date[month][day][hours][min][sec] = 1

With a custom class:

  class HashTree < Hash
    def initialize
      super do |hash, key|
        hash[key] = HashTree.new
      end
    end
  end

  @date = HashTree.new
  @date[month][day][hours][min][sec] = 1
opsb
  • 29,325
  • 19
  • 89
  • 99
20

Compared to the lambda expression given above, this is simpler and also in one line:

Hash.new {|h,k| h[k] = Hash.new(&h.default_proc) }
Horst
  • 201
  • 2
  • 2
18
->f{f[f]}[->f{Hash.new{|h,k|h[k]=f[f]}}]

Obviously.

Xavier Shay
  • 4,067
  • 1
  • 30
  • 54
  • 11
    Ideal for those coming from perl – artemave Jul 11 '13 at 10:17
  • 1
    It took me 20 minutes to understand it. I will not be able to reproduce it. But it is cool. – artemave Jul 11 '13 at 10:49
  • 1
    It's the y-combinator, golfed using new Ruby 1.9 stabby lambda (lambda literal) syntax. http://rhnh.net/2007/12/20/understanding-the-y-combinator and http://verboselogging.com/2011/09/20/proc-block-and-two-smoking-lambdas are respective explanations. – Xavier Shay Jul 11 '13 at 12:36
2

Using symbols seemed to work:

ree-1.8.7-2009.10 > @date = {:month =>{:day => {:hours => {:min => {:sec => 1 } } } } }
 => {:month=>{:day=>{:hours=>{:min=>{:sec=>1}}}}} 

I can then retrieve the val like this:

ree-1.8.7-2009.10 > @date[:month][:day]
 => {:hours=>{:min=>{:sec=>1}}}
nicholasklick
  • 1,212
  • 10
  • 14
  • The question could use an edit, for clarity... in perl-land, `$month` probably isn't anything like the name "month" (as a symbol, string, or anything else), and is probably more like `5` or `"05"` or maybe `"May"`. A final hash might look like `{5 => {10 => {13 => {1 => {41 => 1, 42 => 1}}}}}` if I'd incremented for each of two seconds in time: 13:01:41 and 13:01:42 on May 10th. But, this is a fault of how the question is presented, so I'll skip giving a down-vote. – lindes May 10 '19 at 20:04
1

It doesn't look like Ruby can do autovivification from the start, but you can easily add in that functionality. A search for "ruby autovivification" on Google gives:

http://t-a-w.blogspot.com/2006/07/autovivification-in-ruby.html

Which contains a decent example of how to create a hash that will work the way you are looking for.

ruby hash autovivification (facets) might also be helpful.

Community
  • 1
  • 1
Eric Strom
  • 39,821
  • 2
  • 80
  • 152
1

You can use the Facets gem's Hash.autonew to do the same thing as the recursive function given in Molf's answer.

Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
1

1) Refinements

You can use the Ruby Refinements instead of Monkey Patching.

It is way more secure than MonkeyPatching the entire Hash class and the usage part is still fancy

Setup ‍

# Use Hash.dynamic_keys to create keys dynamically on a Hash
module HashDynamicKeysRefinements
  refine Hash.singleton_class do
    def dynamic_keys
      new { |hash, key| hash[key] = dynamic_keys }
    end
  end
end

Usage

using HashDynamicKeysRefinements

dynamic_hash = Hash.dynamic_keys

dynamic_hash[:where][:requests][:id] = 3

dynamic_hash
=> {:where=>{:requests=>{:id=>3}}}

2) New Class

Or you can create a new class to implement this feature on the Hash class, the usage part is also awesome.

Setup ‍

  • /lib/hash_dynamic.rb
# Enables dynamic keys creation on a Hash
#   query = Hash.dynamic_keys
#   query[:where][:requests][:id] = 3
#   puts query
#     {:where=>{:requests=>{:id=>3}}}
class HashDynamic < Hash
  def self.new
    Hash.new { |hash, key| hash[key] = new }
  end
end

Usage

require 'hash_dynamic.rb'

dynamic_hash = HashDynamic.new

dynamic_hash[:a][:b][:c] = 5

dynamic_hash
=> {:a=>{:b=>{:c=>5}}}
Victor
  • 1,904
  • 18
  • 18