Traversing a Hash Recursively in Ruby

Iterate over a deeply nested level of hashes in Ruby

If I understand the goal, then you should be able to pass in the parent to your save method. For the top level, it will be nil. The following shows the idea where puts is used as a place holder for the "save".

def save_pair(parent, myHash)
myHash.each {|key, value|
value.is_a?(Hash) ? save_pair(key, value) :
puts("parent=#{parent.nil? ? 'none':parent}, (#{key}, #{value})")
}
end

Here is an example call to it:

hash = Hash.new
hash["key1"] = "value1"
hash["key2"] = "value2"
hash["key3"] = Hash.new
hash["key3"]["key4"] = "value4"
hash["key3"]["key5"] = "value5"
hash["key6"] = Hash.new
hash["key6"]["key7"] = "value7"
hash["key6"]["key8"] = Hash.new
hash["key6"]["key8"]["key9"] = "value9"

save_pair(nil, hash)

Traversing a Hash Recursively in Ruby

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.

How to iterate over deep nested hash without known depth in Ruby

So, you want to parse recursively until there are no more levels to parse into.

It’s super common in software and referred to as “recursion”. Have a search around to learn more about it - it’ll come up again and again in your journey. Welcome to ruby btw!

As for your actual current problem. Have a read of https://mrxpalmeiras.wordpress.com/2017/03/30/how-to-parse-a-nested-yaml-config-file-in-python-and-ruby/

But also, consider the i18n gem. See this answer https://stackoverflow.com/a/51216931/1777331 and the docs for the gem https://github.com/ruby-i18n/i18n This might fix your problem of handling internationalisation without you having to get into the details of handling yaml files.

Improve this recursive function for hash traversal in Ruby

Inspired by some of the other answers, here's a way to do it using recursive send() rather than eval():

def extract(obj, hash)
k, v = hash.to_a[0]
v.is_a?(Hash) ? extract(obj.send(k), v) : obj.send(k).send(v)
end

In the case mentioned in the question, extract(@user, {:club => :title}) would result in @user.send(:club).send(:title).

EDIT: as mentioned by zimbatm, an array like [:club, :title, :owner] might be cleaner. If you used that instead (and are running in an environment that supports Symbol#to_proc), you could just do:

def extract2(obj, method_array)
method_array.inject(obj, &:send)
end

How do I flatten a nested hash, recursively, into an array of arrays with a specific format?

As hinted at in the comments:

Looks pretty straightforward. Descend into hashes recursively, taking note of keys you visited in this branch. When you see an array, no need to recurse further. Append it to the list of keys and return

Tracking is easy, just pass the temp state down to recursive calls in arguments.

I meant something like this:

def tree_flatten(tree, path = [], &block)
case tree
when Array
block.call(path + tree)
else
tree.each do |key, sub_tree|
tree_flatten(sub_tree, path + [key], &block)
end
end
end

tree_flatten(tree_def) do |path|
p path
end

This code simply prints each flattened path as it gets one, but you can store it in an array too. Or even modify tree_flatten to return you a ready array, instead of yielding elements one by one.

Looping through a nested hash


{key1: "value1", key2: {key3: "value2", key4: "value3"}}

Your hash is not consistent hash of hashes, in the first iteration, value is value1 which is a string, and you cannot iterate over a string.

to avoid that, you can check beforehand like,

hash.each do |key,value|
p key
if value.is_a?(Hash)
value.each do |k,v|
p k
p v
end
else
p value
end
end

My goal is to access the values of key3 and key4. (I want to put them in a variable of some kind to be used elsewhere)

you can traverse a hash based on key associations. As per your need above, you can simply do:

hash[:key2][:key3]
#=> "value2"
hash[:key2][:key4]
#=> "value3"

Iterate nested hash that contains hash and/or Array

It is not entirely clear what you might want, but both Array and Hash implement each (which, in the case of Hash, is an alias for each_pair).

So to get exactly the output you would get if your method would work, you could implement it like this:

def iterate(h)
h.each do |k,v|
# If v is nil, an array is being iterated and the value is k.
# If v is not nil, a hash is being iterated and the value is v.
#
value = v || k

if value.is_a?(Hash) || value.is_a?(Array)
puts "evaluating: #{value} recursively..."
iterate(value)
else
# MODIFY HERE! Look for what you want to find in the hash here
# if v is nil, just display the array value
puts v ? "key: #{k} value: #{v}" : "array value #{k}"
end
end
end

DFS traversal of a deeply nested hash

If all values are hashes, this would work:

hash = {"." => {"foo" => {"hello" => {}, "world" => {}}, "bar" => {}}}

def traverse(hash, depth = 0)
hash.each { |key, value|
puts "#{depth} #{key}"
traverse(value, depth + 1)
}
end

traverse(hash)

Output:

0 .
1 foo
2 hello
2 world
1 bar


Related Topics



Leave a reply



Submit