Given two arrays (for instance a list of people and a list of subjects), I need to initialise a Hash
with all zeros for all possible combinations of these two arrays. I can manage to do that with the code on the bottom, but it seems like there should be a less convoluted, more elegant way to do this.
people = %w(tom mary rob)
subjects = %w(math english)
Hash[people.map { |person| [person, Hash[subjects.map { |subject| [subject, 0] }]]}]
# output: => {"tom"=>{"math"=>0, "english"=>0},
# "mary"=>{"math"=>0, "english"=>0},
# "rob"=>{"math"=>0, "english"=>0}}
3 Answers 3
Conceptually, that's the way to go. I'd just propose some small changes:
- Use Array#to_h instead of Hash#[]
- This line is too long, split it in multiple lines.
So I'd write:
scores = people.map do |person|
person_scores = subjects.map { |subject| [subject, 0] }.to_h
[person, person_scores]
end.to_h
While you can do this in one line, there's really no need. At least you can break the line into do..end
blocks to keep things more readable.
But moreso, I'd consider dup
ing the zeros hash (presumably grades for the subjects so I'll call it that) once you've built it, rather than build and re-build it for each person.
Also, you can use each_with_object
(a sort of reduce
) instead of wrapping a mapped array in Hash[..]
.
For instance,
grades = subjects.each_with_object({}) { |subject, hash| hash[subject] = 0 }
people.each_with_object({}) { |name, hash| hash[name] = grades.dup }
But there are many ways to go about this. While it's a little outside the task, you could simply initialize a Hash with default value of zero for unknown keys:
grades = Hash.new(0)
grades["math"] #=> 0
grades["no a real subject"] #=> 0
Of course, as the last line shows, you'll get zero for any key, even if it doesn't really make sense. Usually you'd of course just get a nil
. You'll also want to be careful with this approach in general, if the default value is an object, since it won't automatically dup
itself; it'll be the same object (same object reference) for every key. It's not a problem with numbers though.
Still, it'd condense things to one line again:
people.each_with_object({}) { |name, hash| hash[name] = Hash.new(0) }
A more precise alternative, that only creates the required keys, could be:
grades = Hash[ subjects.zip(Array.new(subjects.size, 0)) ]
And of course, recent versions of Ruby have a to_h
method that lets you chain the call, rather than wrap an array in it:
grades = subjects.map { |subject| [subject, 0] }.to_h
people.map { |name| [name, grades.dup] }.to_h
Which is close to what you started with, just using dup
.
There are even more ways to do things - this is just off the top of my head.
I did start with this:
~/ (main) > notes = Array.new(subjects.size,0)
=> [0,0]
~/ (main) > Hash[[people, Array.new(people.size, Hash[[subjects, notes].transpose])].transpose]
=> {
"tom" => {
"math" => 0,
"english" => 0
},
"mary" => {
"math" => 0,
"english" => 0
},
"rob" => {
"math" => 0,
"english" => 0
}
}
But there is a pattern: make a hash with keys from a supplied array and all with the same value. Then a lambda could help:
~/ (main) > head_val_to_h = ->(head, val) { Hash[[head, Array.new(head.size, val)].transpose]}
=> #<Proc:0x00000008bce460@(pry):55 (lambda)>
~/ (main) > head_val_to_h[people, head_val_to_h[subjects, 0]]
=> {
"tom" => {
"math" => 0,
"english" => 0
},
"mary" => {
"math" => 0,
"english" => 0
},
"rob" => {
"math" => 0,
"english" => 0
}
}