tags:

views:

83

answers:

1

I'm having a problem with this function that traverses a Hash. The Hash may contain an Array of Hashes. I want the method to search for an id and then return just the nested hash it finds.

It seems to work for the traversal, but it returns the original value passed in.

require 'rubygems'
require 'ruby-debug'

def find_by_id(node, find_this="")
  if node.is_a?(Hash)
    node.each do |k,v|
      if v.is_a?(Array)
        v.each do |elm|
          if elm["_id"] == find_this && !find_this.empty?
            return elm      # THIS IS WHAT I WANT!
          else
            find_by_id(elm, find_this)
          end
        end
      end
    end
  end
end

x = {"name" => "first", "_id"=>'4c96a9a56f831b0eb9000005', "items"=>["name" => "second", "_id"=>'4c96a9af6f831b0eb9000009', "others"=>[{"name" => "third", "_id"=>'4c96a9af6f831b0eb9000007'}, {"name" => "fourth", "_id"=>'4c96a9af6f831b0eb9000008'}] ] }

find_by_id(x, '4c96a9af6f831b0eb9000008')
+2  A: 

When you invoke find_by_id recursively, you're not doing anything with the return value. You need to check whether it found something and if so return that, i.e.:

result = find_by_id(elm, find_this)
return result if result

You also need to return nil at the end of the method (after the each loop), so it returns nil if nothing was found. If you don't, it'll return the return value of each which is the hash that you iterated over.

Edit:

Here's the full code with the changes I outlined:

def find_by_id(node, find_this="")
  if node.is_a?(Hash)
    node.each do |k,v|
      if v.is_a?(Array)
        v.each do |elm|
          if elm["_id"] == find_this && !find_this.empty?
            return elm      # THIS IS WHAT I WANT!
          else
            result = find_by_id(elm, find_this)
            return result if result
          end
        end
      end
    end
  end
  # Return nil if no match was found
  nil
end

Edit2:

An alternative approach, that I find cleaner, is to separate the logic for iterating the structure from the logic for finding the element with the right id:

def dfs(hsh, &blk)
  return enum_for(:dfs, hsh) unless blk

  yield hsh
  hsh.each do |k,v|
    if v.is_a? Array
      v.each do |elm|
        dfs(elm, &blk)
      end
    end
  end
end

def find_by_id(hsh, search_for)
  dfs(hsh).find {|node| node["_id"] == search_for }
end

By making dfs return an Enumerable we can use the Enumerable#find method, which makes the code a bit simpler.

This also enables code reuse if you ever need to write another method that needs to iterate through the hash recursively, as you can just reuse the dfs method.

sepp2k
I tried that, it returns the wrong value. `return elm ` does get reached though when it is supposed to.
Dex
@Dex: Right. You also need to return nil if you don't find anything. Edited to add that.
sepp2k
I'd like to point out that it's not the recursion to blame, but rather `each` operator, from inside of which last operator doesn't return its value from the original function.
Pavel Shved
Perhaps someone can post the full code? I'm still having issues.
Dex
@Dex: By just putting `nil` at the end of the method (right before the last `end`) it works for me.
sepp2k
Me too! Thanks, this was driving me nuts!
Dex