Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Traversing a Hash Recursively in Ruby

Tags:

ruby

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')
like image 878
Dex Avatar asked Dec 17 '22 21:12

Dex


1 Answers

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.

like image 88
sepp2k Avatar answered Dec 30 '22 03:12

sepp2k