Найти пары ключ / значение глубоко внутри хеша, содержащего произвольное количество вложенных хешей и массивов - PullRequest
34 голосов
/ 28 ноября 2011

Веб-служба возвращает хеш, который содержит неизвестное количество вложенных хешей, некоторые из которых содержат массив, который, в свою очередь, содержит неизвестное количество вложенных хешей.

Некоторые ключи не являются уникальными - то есть присутствуют в более чем одном из вложенных хэшей.

Однако все ключи, которые меня действительно волнуют, уникальны.

Можно ли как-нибудь дать ключ хешу верхнего уровня и вернуть его значение, даже если пара ключ-значение похоронена глубоко в этом болоте?

(веб-сервис Amazon API Рекламы продуктов, который незначительно изменяет структуру результатов, которые он дает, в зависимости от количества результатов и типов поиска, разрешенных в каждой категории продуктов.)

Ответы [ 9 ]

30 голосов
/ 29 ноября 2011

Вот простое рекурсивное решение:

def nested_hash_value(obj,key)
  if obj.respond_to?(:key?) && obj.key?(key)
    obj[key]
  elsif obj.respond_to?(:each)
    r = nil
    obj.find{ |*a| r=nested_hash_value(a.last,key) }
    r
  end
end

h = { foo:[1,2,[3,4],{a:{bar:42}}] }
p nested_hash_value(h,:bar)
#=> 42
25 голосов
/ 11 ноября 2015

Нет необходимости в исправлении обезьян, просто используйте гем Hashie: https://github.com/intridea/hashie#deepfind

user = {
  name: { first: 'Bob', last: 'Boberts' },
  groups: [
    { name: 'Rubyists' },
    { name: 'Open source enthusiasts' }
  ]
}

user.extend Hashie::Extensions::DeepFind

user.deep_find(:name)   #=> { first: 'Bob', last: 'Boberts' }

Для произвольных перечисляемых объектов доступно другое расширение, DeepLocate: https://github.com/intridea/hashie#deeplocate

24 голосов
/ 22 мая 2013

Объединение нескольких ответов и комментариев выше:

class Hash
  def deep_find(key, object=self, found=nil)
    if object.respond_to?(:key?) && object.key?(key)
      return object[key]
    elsif object.is_a? Enumerable
      object.find { |*a| found = deep_find(key, a.last) }
      return found
    end
  end
end
9 голосов
/ 29 ноября 2011

Несмотря на то, что это кажется распространенной проблемой, я просто потратил некоторое время, пытаясь найти / придумать именно то, что мне нужно, что, я думаю, соответствует вашим требованиям. Ни одна из ссылок в первом ответе не является точечной.

class Hash
  def deep_find(key)
    key?(key) ? self[key] : self.values.inject(nil) {|memo, v| memo ||= v.deep_find(key) if v.respond_to?(:deep_find) }
  end
end

Так дано:

hash = {:get_transaction_list_response => { :get_transaction_list_return => { :transaction => [ { ... 

Следующее:

hash.deep_find(:transaction)

найдет массив, связанный с: ключом транзакции.

Это неоптимально, поскольку ввод будет продолжать повторяться, даже если заполнено memo .

7 голосов
/ 22 ноября 2016

Вариант едва известного решения: он найдет все значения ключа в хэше, а не в первом совпадении.

class Hash
  def deep_find(key, object=self, found=[])
    if object.respond_to?(:key?) && object.key?(key)
      found << object[key]
    end
    if object.is_a? Enumerable
      found << object.collect { |*a| deep_find(key, a.last) }
    end
    found.flatten.compact
  end
end

{a: [{b: 1}, {b: 2}]}.deep_find(:b) вернет [1, 2]

5 голосов
/ 19 октября 2018

Ruby 2.3 представляет Hash # dig , что позволяет вам:

h = { foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)           #=> 1
h.dig(:foo, :zot)                 #=> nil
0 голосов
/ 30 апреля 2017

Поскольку Rails 5 ActionController :: Parameters больше не наследуется от Hash, мне пришлось изменить метод и сделать его специфичным для параметров.

module ActionController
  class Parameters
    def deep_find(key, object=self, found=nil)
      if object.respond_to?(:key?) && object.key?(key)
        return object[key]
      elsif object.respond_to?(:each)
        object = object.to_unsafe_h if object.is_a?(ActionController::Parameters)
        object.find { |*a| found = deep_find(key, a.last) }
        return found
      end
    end
  end
end

Если ключ найден, он возвращает значение этого ключа, но не возвращает объект ActionController :: Parameter, поэтому сильные параметры не сохраняются.

0 голосов
/ 27 июля 2015

Я закончил тем, что использовал это для небольшого поиска, я написал:

def trie_search(str, obj=self)
  if str.length <= 1
    obj[str]
  else
    str_array = str.chars
    next_trie = obj[str_array.shift]
    next_trie ? trie_search(str_array.join, next_trie) : nil
  end
end

Примечание: в данный момент это только для вложенных хэшей. В настоящее время нет поддержки массивов.

0 голосов
/ 29 мая 2015

Я использую следующий код

def search_hash(hash, key)
  return hash[key] if hash.assoc(key)
  hash.delete_if{|key, value| value.class != Hash}
  new_hash = Hash.new
  hash.each_value {|values| new_hash.merge!(values)}
  unless new_hash.empty?
    search_hash(new_hash, key)
  end
end
...