Traversing a Hash Recursively in Ruby - 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')

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.

Related

Ruby: How do I remove nil/empty values from a nested hash and why aren't my attempts doing so?

I'm trying to write a method that removes all keys in a nested hash that point to nil recursively.
For example:
{:a=>nil, :b=>"b", :c=>nil, :d=>{:dd=>"dd", :ee=>nil, :ff=>"ff"}, :e=>{:gg=>nil, :hh=>nil}}
becomes:
{:b=>"b", :d=>{:dd=>"dd", :ff=>"ff"}}
I'm not having much luck though.
My most recent attempt looks like:
def deep_compact(hash)
hash.reject do |key, value|
deep_compact(value) if value.class == Hash
next true if value.nil? || value.empty?
end
end
Here I want to iterate over each key value pair in the hash. If the value is a hash, I want to do the same for that hash. I want to reject the pair if the value is nil or empty. Otherwise, I want to keep it.
The result isn't what I want:
#=> {:b=>"b", :d=>{:dd=>"dd", :ee=>nil, :ff=>"ff"}, :e=>{:gg=>nil, :hh=>nil}}
I have also tried:
def deep_compact(hash)
hash.compact.transform_values do |value|
deep_compact(value) if value.class == Hash
value
end
end
Again, I get the same result:
#=> {:b=>"b", :d=>{:dd=>"dd", :ee=>nil, :ff=>"ff"}, :e=>{:gg=>nil, :hh=>nil}}
This leaves me to believe that either I've missed something or my understanding of recursion is wrong.
Are any of my attempts close? What do I need to do to ensure I get the result I want: {:b=>"b", :d=>{:dd=>"dd", :ff=>"ff"}}?
The trick would be to recursively compact nested hashes and then to eliminate empty values.
compact = ->(hash) {
hash.is_a?(Hash) ?
hash.map { |k, v| [k, compact.(v)] }.
to_h.
delete_if { |_, v| v.nil? || v.respond_to?(:empty?) && v.empty? } :
hash
}
compact.(input)
#⇒ {:b=>"b", :d=>{:dd=>"dd", :ff=>"ff"}}
I discovered that by placing my recursive function call at the end of the block got me most of the way there. (Is this 'tail-end' recursion?)
I also call reject on the hash returned by transform_values to removed any empty pairs.
This achieves what I wanted:
def deep_compact(hash)
hash.compact.transform_values do |value|
next value unless value.class == Hash
deep_compact(value)
end.reject { |_k, v| v.empty? }
end
> h
=> {:a=>nil, :b=>"b", :c=>nil, :d=>{:dd=>"dd", :ee=>nil, :ff=>"ff"}, :e=>{:gg=>nil, :hh=>nil}}
> deep_compact h
=> {:b=>"b", :d=>{:dd=>"dd", :ff=>"ff"}}
Other option using Hash#reject!, it changes the original Hash:
def deep_compact(h)
h.each { |_, v| deep_compact(v) if v.is_a? Hash }.reject! { |_, v| v.nil? || v.empty? }
end
deep_compact(h)
#=> {:b=>"b", :d=>{:dd=>"dd", :ff=>"ff"}}

Put every Hash Element inside of an Array Ruby

Let's say I have a Hash like this:
my_hash = {"a"=>{"a1"=>"b1"}, "b"=>"b", "c"=>{"c1"=>{"c2"=>"c3"}}}
And I want to convert every element inside the hash that is also a hash to be placed inside of an Array.
For example, I want the finished Hash to look like this:
{"a"=>[{"a1"=>"b1"}], "b"=>"b", "c"=>[{"c1"=>[{"c2"=>"c3"}]}]}
Here is what I've tried so far, but I need it to work recursively and I'm not quite sure how to make that work:
my_hash.each do |k,v|
if v.class == Hash
my_hash[k] = [] << v
end
end
=> {"a"=>[{"a1"=>"b1"}], "b"=>"b", "c"=>[{"c1"=>{"c2"=>"c3"}}]}
You need to wrap your code into a method and call it recursively.
my_hash = {"a"=>{"a1"=>"b1"}, "b"=>"b", "c"=>{"c1"=>{"c2"=>"c3"}}}
def process(hash)
hash.each do |k,v|
if v.class == Hash
hash[k] = [] << process(v)
end
end
end
p process(my_hash)
#=> {"a"=>[{"a1"=>"b1"}], "b"=>"b", "c"=>[{"c1"=>[{"c2"=>"c3"}]}]}
Recurring proc is another way around:
h = {"a"=>{"a1"=>"b1"}, "b"=>"b", "c"=>{"c1"=>{"c2"=>"c3"}}}
h.map(&(p = proc{|k,v| {k => v.is_a?(Hash) ? [p[*v]] : v}}))
.reduce({}, &:merge)
# => {"a"=>[{"a1"=>"b1"}], "b"=>"b", "c"=>[{"c1"=>[{"c2"=>"c3"}]}]}
It can be done with single reduce, but that way things get even more obfuscated.

multi-dimensional container of any size and converts it into a one dimensional associative array

Write a function that accepts a multi-dimensional container of any size and converts it into a one dimensional associative array whose keys are strings representing their value's path in the original container.
So { 'one' => {'two' => 3, 'four' => [ 5,6,7]}, 'eight'=> {'nine'=> {'ten'=>11}}}
would become
:
"{'one/two' => 3,'one/four/0' => 5, 'one/four/1' => 6, 'one/four/2' => 7, 'eight/nine/ten' : 11}"
I've gotten this so far... But am having a lot of issues. Any pointers to things I am overlooking?
def oneDimHash(hash)
if hash.is_a?(Fixnum)
puts "AHHH"
else
hash.each_pair do |key,value|
if value.is_a?(Hash)
#temp_key << key << '/'
oneDimHash(value)
elsif value.is_a?(Array)
value.each_with_index do |val,index|
puts index
#temp_key << "#{index}"
oneDimHash(val)
end
else
#temp_key << key
#result["#{#temp_key}"] = "#{value}"
#temp_key = ''
end
end
end
end
It's immediately suspect to me that you are using instance variables instead of method arguments / local variables. Very likely that is producing messed-up keys, at least. Supposing that the method signature cannot be modified, you can work around the need for additional arguments by delegating to a helper function. Perhaps I'd try an approach along these lines:
def oneDimHash(o)
oneDimHashInternal("", o, {})
end
def oneDimHashInternal(keyStem, o, hash)
if o.is_a? Hash
o.each_pair do |key, value|
oneDimHashInternal("#{keystem}/#{key}", value, hash)
end
elsif o.is_a? Array
# Work this out for yourself
else
# Store the (non-container) object in hash
# Work this out for yourself
end
hash
end
Note also that there are Enumerables that are neither Arrays nor Hashes. I don't know whether you need to account for such.
How about this?
def oneDimHash(obj,parent="")
unless obj.is_a?(Hash)
puts "AHHH" # or may be better: raise "AHHH"
else
obj.flat_map do |key,value|
combined_key = [parent,key.to_s].join '/'
case value
when Hash then oneDimHash(value,combined_key).to_a
when Array then value.each_with_index.map { |v,i| [combined_key+"/#{i}",v] }
else [ [combined_key,value] ]
end
end.to_h
end
end

Ruby search for super nested key from json response

I have a terribly nested Json response.
[[{:test=>[{:id=>1, :b=>{id: '2'}}]}]]
There's more arrays than that but you get the idea.
Is there a way to recursively search through and find all the items that have a key I need?
I tried using this function extract_list() but it doesn't handle arrays well.
def nested_find(obj, needed_keys)
return {} unless obj.is_a?(Array) || obj.is_a?(Hash)
obj.inject({}) do |hash, val|
if val.is_a?(Hash) && (tmp = needed_keys & val.keys).length > 0
tmp.each { |key| hash[key] = val[key] }
elsif val.is_a?(Array)
hash.merge!(obj.map { |v| nested_find(v, needed_keys) }.reduce(:merge))
end
hash
end
end
Example
needed_keys = [:id, :another_key]
nested_find([ ['test', [{id:1}], [[another_key: 5]]]], needed_keys)
# {:id=>1, :another_key=>5}
The following is not what I'd suggest, but just to give a brief alternative to the other solutions provided:
2.1.1 :001 > obj = [[{:test=>[{:id=>1, :b=>{id: '2'}}]}]]
=> [[{:test=>[{:id=>1, :b=>{:id=>"2"}}]}]]
2.1.1 :002 > key = :id
=> :id
2.1.1 :003 > obj.inspect.scan(/#{key.inspect}=>([^,}]*)[,}]/).flatten.map {|s| eval s}
=> [1, "2"]
Note: use of eval here is just for an example. It would fail/produce incorrect results on anything whose inspect value was not eval-able back to the same instance, and it can execute malicious code:
You'll need to write your own recursive handler. Assuming that you've already converted your JSON to a Ruby data structure (via JSON.load or whatnot):
def deep_find_value_with_key(data, desired_key)
case data
when Array
data.each do |value|
if found = deep_find_value_with_key value, desired_key
return found
end
end
when Hash
if data.key?(desired_key)
data[desired_key]
else
data.each do |key, val|
if found = deep_find_value_with_key(val, desired_key)
return found
end
end
end
end
return nil
end
The general idea is that given a data structure, you check it for the key (if it's a hash) and return the matching value if found. Otherwise, you iterate it (if it's an Array or Hash) and perform the same check on each of it's children.
This will find the value for the first occurrence of the given key, or nil if the key doesn't exist in the tree. If you need to find all instances then it's slightly different - you basically need to pass an array that will accumulate the values:
def deep_find_value_with_key(data, desired_key, hits = [])
case data
when Array
data.each do |value|
deep_find_value_with_key value, desired_key, hits
end
when Hash
if data.key?(desired_key)
hits << data[desired_key]
else
data.each do |key, val|
deep_find_value_with_key(val, desired_key)
end
end
end
return hits
end

'Hash#keys' in helper

I have this method in helper:
def get_hash_keys(hash)
hash.delete_if{|k, v| v.class != Hash}
return hash.keys
end
When I call get_hash_keys from another method in the same helper, it returns a blank array :
def datas_size
sum = 0
new_hash = {title: "This is a test", datas: {firstname: "foo", lastname: "bar"}}
new_hash.each do |k, v|
sum += v.size if get_hash_keys(new_hash).includes? k
end
return sum
end
I tested to change return hash.keys with fixed array, and I get it. Only keys function seems not to work. I also double checked my array in params.
Is there some specifications I ignore working inside helpers ?
Edit for #DRSE :
I start with hash that contains others hashes. I need to know size of each children hash. The point is when I call this function (get_hash_keys) from the views, it fails (return blank array), but from console it works (return keys).
More investigation this morning drive me to conclude may be this is a wrong usage of delete_if. My ununderstood solution is to replace :
def get_hash_keys(hash)
hash.delete_if{|k, v| v.class != Hash}
return hash.keys
end
with
def get_hash_keys(hash)
tmp_hash = hash.clone
tmp_hash.delete_if{|k, v| v.class != Hash}
return tmp_hash.keys
end
Nothing is wrong with hash.keys:
def get_new_keys(hash)
hash.delete_if{|k,v| k == :bar}
return hash.keys
end
get_new_keys({qux: :bar, bar: :baz}) # => [:qux]
This, on the other hand, is busted.
def display_keys(hash)
return "foo " + get_new_keys(hash)
end
Are you expecting an array to be returned? If so, that's not the way to go about it at all. You could do something like:
def display_keys(hash)
return [:foo].concat(get_new_keys(hash))
end
In which case, it'd do something like:
def display_keys(hash)
return [:foo].concat(get_new_keys(hash))
end
display_keys({qux: :bar, bar: :baz}) # => [:foo, :qux]
The .keys method returns and Array of keys in the Hash.
Your question is impossible to answer for two reasons:
You have not included the condition in your delete_if block. My
guess is that you are deleting all keys in the hash, so .keys is
returning [].
You have not described the output you are expecting.
On a side note, your method display_keys does not work in ruby 2.3. You cannot concatenate an Array and a String like that. It will throw a TypeError: no implicit conversion of Array into String
But taking my best guess at it the following code works just fine:
def get_new_keys(hash)
hash.delete_if{ |k,v| v % 2 == 0 }
return hash.keys
end
def display_keys(hash)
return ["foo"] + get_new_keys(hash)
end
hash = {
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
seven: 7,
eight: 8,
nine: 9,
ten: 10
}
Outputs:
pry(main)> display_keys( hash )
=> ["foo", :one, :three, :five, :seven, :nine]

Resources