Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a ruby nested hash with array as inner value

Tags:

ruby

I am trying to create a nested hash where the inner values are arrays. For example

{"monday"=>{"morning"=>["John", "Katie", "Dave"],"afternoon"=>["Anne", "Charlie"]}, 
"tuesday"=>{"morning"=>["Joe"],"afternoon"=>["Chris","Tim","Melissa"]}}

I tried

h = Hash.new( |hash, key| hash[key] = Hash.new([]) }

When I try

h["monday"]["morning"].append("Ben")

and look at h, I get

{"monday" => {}}

rather than

{"monday" => {"morning"=>["Ben"]}}

I'm pretty new to Ruby, any suggestions for getting the functionality I want?

like image 207
amelia2440 Avatar asked Jul 13 '20 18:07

amelia2440


3 Answers

Close, you'll have to initialise a new hash as the value of the initial key, and set an Array as the value of the nested hash:

h = Hash.new { |hash, key| hash[key] = Hash.new { |k, v| k[v] = Array.new } }

h["monday"]["morning"] << "Ben"

{"monday"=>{"morning"=>["Ben"]}} 

This way you will not have to initialise an array every time you want to push a value. The key will be as you set in the initial parameter, the second parameter will create a nested hash where the value will be an array you can push to with '<<'. Is this a solution to use in live code? No, it’s not very readable but explains a way of constructing data objects to fit your needs.

like image 70
benjessop Avatar answered Nov 16 '22 04:11

benjessop


Refactored for Explicitness

While it's possible to create a nested initializer using the Hash#new block syntax, it's not really very readable and (as you've seen) it can be hard to debug. It may therefore be more useful to construct your nested hash in steps that you can inspect and debug as you go.

In addition, you already know ahead of time what your keys will be: the days of the week, and morning/afternoon shifts. For this use case, you might as well construct those upfront rather than relying on default values.

Consider the following:

require 'date'

# initialize your hash with a literal
schedule = {}

# use constant from Date module to initialize your
# lowercase keys
Date::DAYNAMES.each do |day|
    # create keys with empty arrays for each shift
    schedule[day.downcase] = { 
      "morning"   => [], 
      "afternoon" => [], 
    }   
end

This seems more explicit and readable to me, but that's admittedly subjective. Meanwhile, calling pp schedule will show you the new data structure:

{"sunday"=>{"morning"=>[], "afternoon"=>[]},
 "monday"=>{"morning"=>[], "afternoon"=>[]},
 "tuesday"=>{"morning"=>[], "afternoon"=>[]},
 "wednesday"=>{"morning"=>[], "afternoon"=>[]},
 "thursday"=>{"morning"=>[], "afternoon"=>[]},
 "friday"=>{"morning"=>[], "afternoon"=>[]},
 "saturday"=>{"morning"=>[], "afternoon"=>[]}}

The new data structure can then have its nested array values assigned as you currently expect:

schedule["monday"]["morning"].append("Ben")
#=> ["Ben"]

As a further refinement, you could append to your nested arrays in a way that ensures you don't duplicate names within a scheduled shift. For example:

schedule["monday"]["morning"].<<("Ben").uniq!
schedule["monday"]
#=> {"morning"=>["Ben"], "afternoon"=>[]}
like image 30
Todd A. Jacobs Avatar answered Nov 16 '22 02:11

Todd A. Jacobs


There are many ways to create the hash. One simple way is as follows.

days      = [:monday,  :tuesday]
day_parts = [:morning, :afternoon]

h = days.each_with_object({}) do |d,h|
  h[d] = day_parts.each_with_object({}) { |dp,g| g[dp] = [] }
end
  #=> {:monday=>{:morning=>[], :afternoon=>[]},
  #    :tuesday=>{:morning=>[], :afternoon=>[]}}

Populating the hash will of course depend on the format of the data. For example, if the data were as follows:

people = { "John"   =>[:monday,  :morning],
           "Katie"  =>[:monday,  :morning],
           "Dave"   =>[:monday,  :morning],
           "Anne"   =>[:monday,  :afternoon],
           "Charlie"=>[:monday,  :afternoon],
           "Joe"    =>[:tuesday, :morning],
           "Chris"  =>[:tuesday, :afternoon],
           "Tim"    =>[:tuesday, :afternoon],
           "Melissa"=>[:tuesday, :afternoon]}

we could build the hash as follows.

people.each { |name,(day,day_part)| h[day][day_part] << name }
  #=> {
  #     :monday=>{
  #       :morning=>["John", "Katie", "Dave"],
  #       :afternoon=>["Anne", "Charlie"]
  #     },
  #     :tuesday=>{
  #       :morning=>["Joe"],
  #       :afternoon=>["Chris", "Tim", "Melissa"]
  #     }
  #   }
like image 33
Cary Swoveland Avatar answered Nov 16 '22 04:11

Cary Swoveland