简体   繁体   English

改进Ruby中的哈希遍历的递归函数

[英]Improve this recursive function for hash traversal in Ruby

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. 我已经编写了一个方法来将值的哈希值(如果需要嵌套)转换为可以与eval一起使用以动态返回对象值的链。

Eg passed a hash like { :user => { :club => :title }}, it will return "user.club.title", which I can then eval. 例如,传递了像{:user => {:club =>:title}}这样的哈希值,它将返回“user.club.title”,然后我可以进行评估。 (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, eg: item_row(@user, :name, :email, { :club => :title }) (这点是为视图编写一个方法,允许我通过传入对象和我想要显示的属性列表来快速转储对象的内容,例如: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)

<codegolfing mode=on> <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. 调用.join('。')来获取字符串。
  • No checks for other types than Hash, I expect them to repond nicely on #to_s when called by Array#join('.'). 没有检查除Hash之外的其他类型,我希望它们在#to_s由Array#join('。')调用时很好地响应。
  • No recursive calls 没有递归调用
  • Shorter code 更短的代码

The biggest change is to avoid iteration since we are interested in 1 element hashes. 最大的变化是避免迭代,因为我们对1个元素哈希感兴趣。 Btw, an array like [:club, :title, :owners] would maybe more straightforward for your usage. 顺便说一句,像[:club,:title,:owner]这样的阵列对你的使用来说可能更直接。

Cheers, 干杯,
zimbatm zimbatm

zimbatm's code-golfy answer so inspired me that I decided to improve on it. zimbatm的代码 - 高尔夫答案让我很有启发,我决定改进它。

def hash_to_arr(hash)
  arr = []
  arr[arr.size], hash = hash.to_a[0] while hash.kind_of?(Hash)
  arr << hash
end

# > h = { :one => { :two=> { :three => { :four=> :five } } } }
# > hash_to_arr(h).join "."
# => "one.two.three.four.five"

Or, if you want it super-golfy, it's 或者,如果你想要它超级高尔夫球,它就是 69 69 64 chars: 64个字符:

def f(h)a=[];a[a.size],h=h.to_a[0]while h.kind_of? Hash;a<<h end

Inspired by some of the other answers, here's a way to do it using recursive send() rather than eval(): 受其他一些答案的启发,这是使用递归send()而不是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) . 在问题中提到的情况下, extract(@user, {:club => :title})将导致@user.send(:club).send(:title)

EDIT : as mentioned by zimbatm, an array like [:club, :title, :owner] might be cleaner. 编辑 :正如zimbatm所提到的,像[:club,:title,:owner]这样的数组可能更清晰。 If you used that instead (and are running in an environment that supports Symbol#to_proc), you could just do: 如果您使用它(并且在支持Symbol#to_proc的环境中运行),您可以这样做:

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

Ran into a similar problem recently- wanted to recursively traverse a Hash as though it were a tree while keeping track of the path taken: 最近遇到了类似的问题 - 希望以递归的方式遍历Hash,就好像它是一棵树,同时跟踪所采用的路径:

def dfs(node, path = [], &block)
  case node
  when Hash
    node.each do |key, value|
      path.push(key)
      dfs(value, path, &block)
      path.pop
    end
  when Array
    node.each do |elem|
      yield elem, path if block_given?
    end
  else
    yield node, path if block_given?
  end
end

For example: 例如:

tree = {a:{b:1,c:{d:[2,3],e:{f:4}}}}

dfs(tree) do |node, path|
  puts "%s = %s" % [path.join('.'), node]
end

results in: 结果是:

a.b = 1
a.c.d = 2
a.c.d = 3
a.c.e.f = 4

For the record, you could also use my accepted SO answer here . 为了记录,你也可以在这里使用我接受的SO答案

That does a deep seek in a hash consisting of hashes and arrays and looking for a key and returning a path. 这样可以深入搜索由哈希和数组组成的哈希,并查找密钥并返回路径。

I think Ruby 1.9 has Hash#flatten(level), that allows you to flatten a hash recursively (it looks like you have to define the level of recursion). 我认为Ruby 1.9有Hash#flatten(level),它允许你递归地展平哈希(看起来你必须定义递归级别)。 Then just join the resulting array with "." 然后用“。”加入生成的数组。

EDIT: Just got to 1.9 to try this -- it only works when an array is nested in a hash value, not more hashes. 编辑:只需要1.9来尝试这个 - 它只适用于数组嵌套在哈希值,而不是更多的哈希值。 Sorry, this won't work for you. 对不起,这对你不起作用。

This is good but not great; 这很好但不是很好; see below for a much-simplified method. 请参阅下面的简化方法。

def hash_to_eval_chain(hsh)
  make_eval_chain(hsh).join "."
end

private
def make_eval_chain(obj)
  if obj.is_a? Hash
    raise "Hash cannot contain multiple key-value pairs unless they are nested" if obj.size > 1
    return make_eval_chain(obj.to_a.flatten)
  elsif obj.is_a? Array
    return [ obj.first, *make_eval_chain(obj.last) ] if obj.last.is_a? Hash
    return obj if [String, Symbol, Array].include? obj.class
    raise "Only strings, symbols, and hashes are allowed as values in the hash."
  else
    raise "Expected Hash, received #{obj.class}"
  end
end

# Usage:
# irb> hash_to_eval_chain { :one => { :two=> { :three => { :four=> :five } } } }
# => "one.two.three.four.five"

I couldn't find an elegant way to roll it all into one function, so I hope two will suffice. 我找不到一种优雅的方式将它全部集成到一个函数中,所以我希望两个就足够了。

Basically I realized that {:one => {:two => :three}}.to_a.flatten returns [:one, {:two => :three}] and said A-ha!, this is classic car/cdr-style recursion. 基本上我意识到{:one => {:two => :three}}.to_a.flatten返回[:one, {:two => :three}]并说A-ha !,这是经典车/ cdr-风格递归。

BETTER METHOD: 更好的方法:

You know what, I was making that WAY harder than necessary. 你知道吗,我正在做那个比必要更难的方法。 This is better (inspired by Facets' Hash#recursively method): 这是更好的(受Facets的哈希#递归方法的启发):

def hash_to_eval_chain(hsh)
  make_eval_chain(hsh).flatten.join "."
end

private
def make_eval_chain(obj)
  if obj.is_a? Hash
    return obj.map {|key, val| [key, *make_eval_chain(val)] } if obj.is_a?(Hash) && obj.size <= 1
    raise "Hash cannot contain multiple key-value pairs unless they are nested"
  else
    return obj if [String, Symbol, Array].any? { |klass| obj.is_a? klass }
    raise "Only strings, symbols, and hashes are allowed as values in the Hash."
  end
end

# Usage:
# irb> hash_to_eval_chain { :one => { :two=> { :three => { :four=> :five } } } }
# => "one.two.three.four.five"
myVal = data.try(:[],'user').try(:[],'club').try(:[],'title')

就如此容易 ;)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM