Michael Lee
Michael Lee

Reputation: 458

Accessing values in nested hash

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

Answers (4)

Arjun
Arjun

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

tg_so
tg_so

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

the Tin Man
the Tin Man

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

osman
osman

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

Related Questions