Reputation: 458
I'm trying to filter nested hashes and pull various keys and values. Here is the hash I'm looking at:
exp = {
fam: {cty: "bk", ins: 3},
spec: {cty: "man", ins: 2},
br: {cty: "qns", ins: 1},
aha: {cty: "man", ins: 0}
}
I'm trying to find all the hash keys where cty
is "man"
. I'd like to run something where the result is the following hash:
e = {
spec: {cty: "man", ins: 2},
aha: {cty: "man", ins: 0}
}
I tried this and it seems like it almost works:
exp.each do |e, c, value|
c = :cty.to_s
value = "man"
if e[c] == value
puts e
end
end
But the result I get is:
=> true
Instead of what I'm looking for:
e = {
spec: {cty: "man", ins: 2},
aha: {cty: "man", ins: 0}
}
Upvotes: 2
Views: 5771
Reputation: 823
Simplest way to digging into Nested hash is:-
class Hash
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.is_a? Enumerable
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
Hash.deep_find(key)
Upvotes: 0
Reputation: 496
As the Tin Man pointed out, there are two parameters you can pass in to a block (the code between do
and end
in this case) when iterating through a hash --- one for its key and the other for its value.
To iterate though a hash (and print out its values)
h = { a: "hello", b: "bonjour", c: "hola" }
using .each
method, you can do:
h.each do |key, value|
puts value
end
The result will be:
hello
bonjour
hola
=> {:a=>"hello", :b=>"bonjour", :c=>"hola"}
Please note that the value "returned" is the hash we iterated through, which evaluates to true
in ruby. (Anything other than nil
or false
will evaluate to true
in Ruby. See What evaluates to false in Ruby?)
This is important because the reason you got true
in your code (which in fact should be {:fam=>{:cty=>"bk", :ins=>3}, :spec=>{:cty=>"man", :ins=>2}, :br=>{:cty=>"qns", :ins=>1}, :aha=>{:cty=>"man", :ins=>0}}
), rather than the parsed hash you wanted is that the value returned by .each
method for a hash is the hash itself (which evaluates to true
).
Which is why osman created an empty hash e = {}
so that, during each iteration of the hash, we can populate the newly created hash e
with the key and value we want.
This explains why he can do:
e = exp.select do |k,v|
v[:cty] == "man"
end
Here the code depends upon select
method being able to return a new hash with the key and value we want (rather than the original hash as is the case for .each
method).
But if you do
e = exp.each do |k,v|
v[:cty] == "man"
end
The variable e
will be assigned the original hash exp
itself, which is not what we want. Therefore it's very important to understand what the returned value is when applying a method.
For more information about return values (and Ruby in general), I highly recommend the free e-book from LaunchSchool "Introduction to Programming with Ruby" (https://launchschool.com/books/ruby). This not only helped me recognise the importance of return values, but also gave me a solid foundation on Ruby prigramming in general, which is really useful if you are planning to learn Ruby on Rails (which I'm doing now :)).
Upvotes: 0
Reputation: 160631
To start, you need to understand what iterating over a hash will give you.
Consider this:
exp = {
fam: {cty: "bk", ins: 3},
spec: {cty: "man", ins: 2},
br: {cty: "qns", ins: 1},
aha: {cty: "man", ins: 0}
}
exp.map { |e, c, value| [e, c, value] }
# => [[:fam, {:cty=>"bk", :ins=>3}, nil], [:spec, {:cty=>"man", :ins=>2}, nil], [:br, {:cty=>"qns", :ins=>1}, nil], [:aha, {:cty=>"man", :ins=>0}, nil]]
This is basically what you're doing as you loop and Ruby passes the block the key/value pairs. You're telling Ruby to give you the current hash key in e
, the current hash value in c
and, since there's nothing else being passed in, the value
parameter becomes nil
.
Instead, you need a block variable for the key, one for the value:
exp.map { |k, v| [k, v] }
# => [[:fam, {:cty=>"bk", :ins=>3}], [:spec, {:cty=>"man", :ins=>2}], [:br, {:cty=>"qns", :ins=>1}], [:aha, {:cty=>"man", :ins=>0}]]
Notice that the nil values are gone.
Rewriting your code taking that into account, plus refactoring it for simplicity:
exp = {
fam: {cty: 'bk', ins: 3},
spec: {cty: 'man', ins: 2},
br: {cty: 'qns', ins: 1},
aha: {cty: 'man', ins: 0}
}
exp.each do |k, v|
if v[:cty] == 'man'
puts k
end
end
# >> spec
# >> aha
Now it's returning the keys you want, so it becomes easy to grab the entire hashes. select
is the appropriate method to use when you're trying to locate specific things:
exp = {
fam: {cty: 'bk', ins: 3},
spec: {cty: 'man', ins: 2},
br: {cty: 'qns', ins: 1},
aha: {cty: 'man', ins: 0}
}
e = exp.select { |k, v| v[:cty] == 'man' }
# => {:spec=>{:cty=>"man", :ins=>2}, :aha=>{:cty=>"man", :ins=>0}}
Older versions of Ruby didn't maintain hash output from the hash iterators so we'd have to coerce back to a hash:
e = exp.select { |k, v| v[:cty] == 'man' }.to_h
# => {:spec=>{:cty=>"man", :ins=>2}, :aha=>{:cty=>"man", :ins=>0}}
Upvotes: 3
Reputation: 2459
e = {}
exp.each do |k,v|
if v[:cty] == "man"
e[k] = v
end
end
p e
or even
e = exp.select do |k,v|
v[:cty] == "man"
end
Upvotes: 2