简体   繁体   中英

How to search nested hash of arrays and arrays of hash and return multiple matching objects from the parent node?

Say I have the below ruby hash nested

hash_or_array = [{
  "book1" => "buyer1",
  "book2" => {
    "book21" => "buyer21", "book22" => ["buyer23", "buyer24", true]
  },
  "book3" => {
    "0" => "buyer31", "1" => "buyer32", "2" => "buyer33", 
    "3" => [{
      "4" => "buyer34",
      "5" => [10, 11],
      "6" => [{
        "7" => "buyer35"
      }]
    }]
  },
  "book4" => ["buyer41", "buyer42", "buyer43", "buyer35"],
  "book5" => {
    "book5,1" => "buyer5"
  }
}]

And I want to look for the string buyer35 . Upon match, it should return the following

[
    {
        "book3" => {
            "0" => "buyer31", "1" => "buyer32", "2" => "buyer33", 
            "3" => [{
                "4" => "buyer34",
                "5" => [10, 11],
                "6" => [{
                    "7" => "buyer35"
                }]
            }]
        }
    },
    {
        "book4" => ["buyer41", "buyer42", "buyer43", "buyer35"]
    }
]

The solution below (from another SO question, link below), returns the first match, but I am trying to figure out how to return multiple matches

def recurse(obj, target)
  case obj
  when Array
    obj.each do |e|
      case e
      when Array, Hash
        rv = recurse(e, target)
        return [rv] unless rv.nil?
      when target
        return e
      end
    end
  when Hash
    obj.each do |k,v|
      case v
      when Array, Hash
        rv = recurse(v, target)
        return {k=>rv} unless rv.nil?
      when target
        return {k=>v}
      end
    end
  end
  nil
end

This is the original question and answer: How to search nested hash of arrays and arrays of hash and only return matching object from the parent node?

UPDATE: The correct return format should be

[
    {
        "book3" => {
            "3" => [{
                "6" => [{
                    "7" => "buyer35"
                }]
            }]
        }
    },
    {
        "book4" => ["buyer41", "buyer42", "buyer43", "buyer35"]
    }
]

Here is a function that recursively searches for the target value in any nested arrays or hashes. The function is then used to select the entries in your top level hash_or_array for entries that contain the target and adds them to an array.

require 'pp'

def find_value(obj, target, found = false)
  found = true if obj == target
  return found unless obj.is_a?(Array) || obj.is_a?(Hash)
  case obj
  when Hash 
    obj.each { |k, v| found = find_value(v, target, found) }
  when Array
    obj.each { |v| found = find_value(v, target, found) }
  end
  found
end

found_entries = hash_or_array.inject([]) {|entries, obj| entries << obj.select { |k, v| find_value({ k => v }, "buyer35") }}

pp found_entries

=>

[{"book3"=>
   {"0"=>"buyer31",
    "1"=>"buyer32",
    "2"=>"buyer33",
    "3"=>[{"4"=>"buyer34", "5"=>[10, 11], "6"=>[{"7"=>"buyer35"}]}]},
  "book4"=>["buyer41", "buyer42", "buyer43", "buyer35"]}]

The code below appears to generate the desired output for this specific case:

hash_or_array.inject([]) do |result, x|
  x.keep_if { |k, v| v.to_s =~ /buyer35/ }
  result << x
end

Here is a recursive solution to your "real" question. Since it mutates the original object, I used a "trick" to make a deep copy first. The keep_entries_with produces an object with the original shape as your input, but since your output shape is different the second step just transforms the filtered result into the shape of your desired output.

deep_copy = Marshal.load(Marshal.dump(hash_or_array))

def keep_entries_with(target, obj)
  return unless obj.is_a?(Hash) || obj.is_a?(Array)
  case obj
  when Hash
    keep_entries_with(target, obj.values)
    obj.keep_if do |k, v|
      v == target ||
      v.is_a?(Hash) && v.values.any? { _1 == target || _1.is_a?(Hash) || _1.is_a?(Array) } ||
      v.is_a?(Array) && v.any? { _1 == target || _1.is_a?(Hash) || _1.is_a?(Array) }
    end
  when Array
    obj.each do |v|
      keep_entries_with(target, v)
    end
  end
end

filtered = keep_entries_with("buyer35", deep_copy)

final_result = filtered.first.map { |k, v| { k => v } }
pp final_result

which produces:

[{"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}},
 {"book4"=>["buyer41", "buyer42", "buyer43", "buyer35"]}]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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