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

时间:2009-12-31 19:33:07

标签: ruby recursion

我编写了一个方法,将值的哈希值(如有必要,嵌套)转换为一个可以与eval一起使用的链,从而动态地从对象返回值。

E.g。传递了像{:user =>这样的哈希值{:club => :title}},它将返回“user.club.title”,然后我可以进行评估。 (这点是为视图编写一个方法,允许我通过传入对象和我想要显示的属性列表来快速转储对象的内容,例如:item_row(@ user,:name, :电子邮件,{:club =>:title})

这就是我所拥有的。它有效,但我知道它可以改进。很想知道你是如何改进它的。

# 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)

8 个答案:

答案 0 :(得分:4)

< 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"
  • 调用.join('。')获取字符串。
  • 没有检查除Hash之外的其他类型,我希望它们在#to_s由Array#join('。')调用时很好地响应。
  • 没有递归通话
  • 更短的代码

最大的变化是避免迭代,因为我们对1个元素哈希感兴趣。顺便说一句,像[:club,:title,:owner]这样的阵列对你的使用来说可能会更直接。

干杯,
zimbatm

答案 1 :(得分:3)

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"

或者,如果你想要它超级高尔夫球,它是 69 64个字符:

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

答案 2 :(得分:3)

受到其他一些答案的启发,这是使用递归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

在问题中提到的情况下,extract(@user, {:club => :title})会产生@user.send(:club).send(:title)

编辑:正如zimbatm所提到的,像[:club,:title,:owner]这样的数组可能更清晰。如果您使用它(并且在支持Symbol#to_proc的环境中运行),您可以这样做:

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

答案 3 :(得分:1)

最近遇到了类似的问题 - 希望以递归方式遍历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

例如:

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

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

结果:

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

答案 4 :(得分:0)

我认为Ruby 1.9有Hash#flatten(level),它允许你递归地展平哈希(看起来你必须定义递归级别)。然后用“。”

加入生成的数组

编辑:只需要1.9来尝试这个 - 它只适用于数组嵌套在哈希值中,而不是更多哈希值。对不起,这对你不起作用。

答案 5 :(得分:0)

这很好但不是很好;请参阅下面的简化方法。

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"

我找不到一种优雅的方式将它全部集成到一个函数中,所以我希望两个就足够了。

基本上我意识到{:one => {:two => :three}}.to_a.flatten返回[:one, {:two => :three}]并说A-ha !,这是经典的汽车/ cdr式递归。

更好的方法:

你知道吗,我的做法比必要的更难。这更好(受Facets'Hash#recursively方法的启发):

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"

答案 6 :(得分:0)

myVal = data.try(:[],'user').try(:[],'club').try(:[],'title')

就这么简单;)

答案 7 :(得分:0)

为了记录,您还可以使用我接受的SO answer here

在包含哈希和数组并查找键并返回路径的哈希中进行深度搜索。