Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Improve this recursive function for hash traversal in Ruby

Tags:

ruby

recursion

I've written a method to turn a hash (nested if necessary) of values into a chain that can be used with eval to dynamically return values from an object.

E.g. passed a hash like { :user => { :club => :title }}, it will return "user.club.title", which I can then eval. (The point of this is to write a method for views that will allow me to dump the contents of objects rapidly, by passing in the object and a list of attributes that I want to display, e.g.: item_row(@user, :name, :email, { :club => :title })

Here's what I've got. It works, but I know it can be improved. Curious to see how you'd improve it.

# hash = { :user => { :club => :title }}
# we want to end up with user.club.title
def hash_to_eval_chain(hash)
  raise "Hash cannot contain multiple key-value pairs unless they are nested" if hash.size > 1
  hash.each_pair do |key, value|
    chain = key.to_s + "."
    if value.is_a? String or value.is_a? Symbol
      chain += value.to_s
    elsif value.is_a? Hash
      chain += hash_to_eval_chain(value)
    else
      raise "Only strings, symbols, and hashes are allowed as values in the hash."
    end
    # returning from inside the each_pair block only makes sense because we only ever accept hashes
    # with a single key-value pair
    return chain
  end
end

puts hash_to_eval_chain({ :club => :title }) # => club.title

puts hash_to_eval_chain({ :user => { :club => :title }}) # => user.club.title

puts hash_to_eval_chain({ :user => { :club => { :owners => :name }}}) # => user.club.owners.name

puts ({ :user => { :club => { :owners => :name }}}).to_s # => userclubownersname (close, but lacks the periods)
like image 435
adriandz Avatar asked Sep 14 '26 16:09

adriandz


2 Answers

<codegolfing mode=on>

def hash_to_arr(h)
    arr = []
    while h.kind_of?(Hash)
            # FIXME : check h.size ?
            k = h.keys[0]
            arr.push(k)
            h = h[k]
    end
    arr.push h
end

puts hash_to_arr(:club).join('.') #=> "club"
puts hash_to_arr(:club => :title).join('.') #=> "club.title"
puts hash_to_arr(:user => {:club => :title}).join('.') #=> "user.club.title"
puts hash_to_arr(:user => {:club => {:owners => :name}}).join('.') #=> "user.club.owners.name"
  • Call .join('.') to get the string.
  • No checks for other types than Hash, I expect them to repond nicely on #to_s when called by Array#join('.').
  • No recursive calls
  • Shorter code

The biggest change is to avoid iteration since we are interested in 1 element hashes. Btw, an array like [:club, :title, :owners] would maybe more straightforward for your usage.

Cheers,
   zimbatm

like image 171
zimbatm Avatar answered Sep 16 '26 08:09

zimbatm


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
like image 22
Greg Campbell Avatar answered Sep 16 '26 08:09

Greg Campbell