Related
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"],
"book5" => {
"book5,1" => "buyer5"
}
}]
And I want to search for a string that matches buyer35. On match, I want it to return the following result
"book3" => {
"3" => [{
"6" => [{
"7" => "buyer35"
}]
}]
}]
All, other non matching keys,values, arrays should be omitted. I have the following example, but it doesn't quite work
def search(hash)
hash.each_with_object({}) do |(key, value), obj|
if value.is_a?(Hash)
returned_hash = search(value)
obj[key] = returned_hash unless returned_hash.empty?
elsif value.is_a?(Array)
obj[key] = value if value.any? { |v| matches(v) }
elsif matches(key) || matches(value)
obj[key] = value
end
end
end
def matches(str)
match_criteria = /#{Regexp.escape("buyer35")}/i
(str.is_a?(String) || str == true || str == false) && str.to_s.match?(match_criteria)
end
....
=> search(hash_or_array)
Any help is appreciated. I realize, I need to use recursion, but can't quite figure how to build/keep track of the matched node from the parent node.
You can use the following recursive method.
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
recurse(hash_or_array, "buyer35")
#=> [{"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}}]
recurse(hash_or_array, "buyer24")
#=>[{"book2"=>{"book22"=>"buyer24"}}]
recurse(hash_or_array, "buyer33")
#=> [{"book3"=>{"2"=>"buyer33"}}]
recurse(hash_or_array, 11)
#=> [{"book3"=>{"3"=>[{"5"=>11}]}}]
recurse(hash_or_array, "buyer5")
#=>[{"book5"=>{"book5,1"=>"buyer5"}}]
If desired, one may write, for example,
recurse(hash_or_array, "buyer35").first
#=> {"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}}
Hash
data = {
:recordset => {
:row => {
:property => [
{:name => "Code", :value => "C0001"},
{:name => "Customer", :value => "ROSSI MARIO"}
]
}
},
:#xmlns => "http://localhost/test"
}
Code Used
result = data[:recordset][:row].each_with_object([]) do |hash, out|
out << hash[:property].each_with_object({}) do |h, o|
o[h[:name]] = h[:value]
end
end
I cannot get the following output:
[{"Code"=>"C0001", "Customer"=>"ROSSI MARIO", "Phone1"=>"1234567890"}
Error message:
TypeError no implicit conversion of Symbol into Integer
It works correctly in case of multi records
data = {
:recordset => {
:row => [{
:property => [
{:name => "Code", :value => "C0001"},
{:name => "Customer", :value => "ROSSI MARIO"},
{:name => "Phone1", :value => "1234567890"}
]
}, {
:property => [
{:name => "Code", :value => "C0002"},
{:name => "Customer", :value => "VERDE VINCENT"},
{:name => "Phone1", :value => "9876543210"},
{:name => "Phone2", :value => "2468101214"}
]
}]
},
:#xmlns => "http://localhost/test"
}
Code used
data.keys
#=> [:recordset, :#xmlns]
data[:recordset][:row].count
#=> 2 # There are 2 set of attribute-value pairs
result = data[:recordset][:row].each_with_object([]) do |hash, out|
out << hash[:property].each_with_object({}) do |h, o|
o[h[:name]] = h[:value]
end
end
#=> [
# {"Code"=>"C0001", "Customer"=>"ROSSI MARIO", "Phone1"=>"1234567890"},
# {"Code"=>"C0002", "Customer"=>"VERDE VINCENT", "Phone1"=>"9876543210", "Phone2"=>"2468101214"}
# ]
In the first case data[:recordset][:row] is not an Array, it's a Hash, so when you iterate it, the hash variable becomes the array:
[:property, [{:name=>"Code", :value=>"C0001"}, {:name=>"Customer", :value=>"ROSSI MARIO"}]]
In the second case, it's an Array, not a Hash, so when you iterate it, it becomes the hash:
{:property=>[{:name=>"Code", :value=>"C0001"}, {:name=>"Customer", :value=>"ROSSI MARIO"}, {:name=>"Phone1", :value=>"1234567890"}]}
You're always assuming it's the second format. You could force it into an array, and then flatten by 1 level to treat both instances the same:
result = [data[:recordset][:row]].flatten(1).each_with_object([]) do |hash, out|
out << hash[:property].each_with_object({}) do |h, o|
o[h[:name]] = h[:value]
end
end
# => [{"Code"=>"C0001", "Customer"=>"ROSSI MARIO"}] # result from example 1
# => [{"Code"=>"C0001", "Customer"=>"ROSSI MARIO", "Phone1"=>"1234567890"},
# {"Code"=>"C0002", "Customer"=>"VERDE VINCENT",
# "Phone1"=>"9876543210", "Phone2"=>"2468101214"}] # result from example 2
It's tempting to try and use Kernal#Array() instead of [].flatten(1), but you have to remember that Hash implements to_a to return a nested array of keys and values, so Kernal#Array() doesn't work like you'd want it to:
Array(data[:recordset][:row]) # using the first example data
# => [[:property, [{:name=>"Code", :value=>"C0001"}, {:name=>"Customer", :value=>"ROSSI MARIO"}]]]
You can create an array if it's not an array to normalize the input before processing it.
info = data[:recordset][:row]
info = [info] unless info.is_an? Array
result = info.each_with_object([]) do ....
I have a questions list, and I need to separate them. The relationship is
Question_set has_many questions
BookVolume has_many questions
Subject has_many book_volumes
Publisher has_many subjects
Section has_many :questions
Now I only put questions and their relative model id, name into hash inside an array.
data = []
question_set.questions.each do |q|
data << {publisher: {id: q.publisher.id, name: q.publisher.name}, subject: {id: q.book_volume.subject.id, name: q.book_volume.subject.name}, volume: {id: q.book_volume_id, name: q.book_volume.name}, chapter: [{id: q.section_id, name: q.section.name}]}
end
Therefore, the data basically will be
>>data
[
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16155,
:name => "C1"
}
]
},
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16158,
:name => "C2"
}
]
}
]
However, I want the chapter to be combined if they got the same publisher, subject and volume
So, in this case, it will be
>>data
[
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16155,
:name => "C2"
},
{
:id => 16158,
:name => "C2"
}
]
}
]
Code
def group_em(data)
data.group_by { |h| [h[:publisher], h[:subject], h[:volume]] }.
map do |k,v|
h = { publisher: k[0], subject: k[1], volume: k[2] }
h.update(chapters: v.each_with_object([]) { |f,a|
a << f[:chapter] }.flatten)
end
end
Example
Let data equal the array of hashes (the first array above).
group_em(data)
#=> [{:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapters=>[{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
# }
# ]
Here data contains only two hashes and those hashes have the same values for the keys :publisher, :subject and :volume. This code allows the array to have any number of hashes, and will group them by an array of the values of those three keys, producing one hash for each of those groups. Moreover, the values of the key :chapters are arrays containing a single hash, but this code permits that array to contain multiple hashes. (If that array will always have exactly one hash, consider making the value of :chapters the hash itself rather than an array containing that hash.)
Explanation
See Enumerable#group_by and Hash#update (aka Hash#merge!).
The steps are as follows.
h = data.group_by { |h| [h[:publisher], h[:subject], h[:volume]] }
#=> {
# [{:id=>96, :name=>"P1"},
# {:id=>233, :name=>"S1"},
# {:id=>1136, :name=>"V1"}
# ]=>[{:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapter=>[{:id=>16155, :name=>"C1"}]
# },
# {:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapter=>[{:id=>16158, :name=>"C2"}]
# }
# ]
# }
The first key-value pair is passed to map's block and the block variables are assigned.
k,v = h.first
#=> [[{:id=>96, :name=>"P1"}, {:id=>233, :name=>"S1"}, {:id=>1136, :name=>"V1"}],
# [{:publisher=>{:id=>96, :name=>"P1"}, :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"}, :chapter=>[{:id=>16155, :name=>"C1"}]},
# {:publisher=>{:id=>96, :name=>"P1"}, :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"}, :chapter=>[{:id=>16158, :name=>"C2"}]}]]
k #=> [{:id=>96, :name=>"P1"}, {:id=>233, :name=>"S1"}, {:id=>1136, :name=>"V1"}]
v #=> [{:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapter=>[{:id=>16155, :name=>"C1"}]},
# {:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapter=>[{:id=>16158, :name=>"C2"}]}]
and the block calculation is performed.
h = { publisher: k[0], subject: k[1], volume: k[2] }
#=> {:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"}
# }
a = v.each_with_object([]) { |f,a| a << f[:chapter] }
#=> [[{:id=>16155, :name=>"C1"}], [{:id=>16158, :name=>"C2"}]]
b = a.flatten
#=> [{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
h.update(chapters: b)
#=> {:publisher=>{:id=>96, :name=>"P1"},
# :subject=>{:id=>233, :name=>"S1"},
# :volume=>{:id=>1136, :name=>"V1"},
# :chapters=>[{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
# }
Hash#merge could be used in place of Hash#update.
How about:
data = {}
question_set.questions.each do |q|
key = "#{q.publisher.id}:#{q.book_volume.subject.id}:#{q.book_volume_id}"
if data[key].present?
data[key][:chapter] << {id: q.section_id, name: q.section.name}
else
data[key] = {publisher: {id: q.publisher.id, name: q.publisher.name}, subject: {id: q.book_volume.subject.id, name: q.book_volume.subject.name}, volume: {id: q.book_volume_id, name: q.book_volume.name}, chapter: [{id: q.section_id, name: q.section.name}]}
end
end
result = data.values
use the combination of publisher'id, subject'id and volume'id as a unique key to combine your data.
Given a Ruby hash of parameters that are infinitely nested, I want to write a function that returns true if a given key is in those parameters.
This is the function I have so far, but it's not quite right, and I'm at a loss as to why:
def has_key(hash, key)
hash.each do |k, v|
if k == key
return true
elsif v.class.to_s == "Array"
v.each do |inner_hash|
return has_key(inner_hash,key)
end
else
return false
end
end
end
The method should return the following results:
# all check for presence of "refund" key
has_key({
"refund" => "2"
}, "refund")
=> true
has_key({
"whatever" => "3"
}, "refund")
=> false
has_key({
"whatever" => "3",
"child_attributes" => [{
"refund" => "1"
}]
}, "refund")
=> true
has_key({
"whatever" => "3",
"child_attributes" => [{
"nope" => "4"
}]
}, "refund")
=> false
has_key({
"whatever" => "3",
"child_attributes" => [{
"a" => "1",
"refund" => "2"
}]
}, "refund")
=> true
has_key({
"whatever" => "3",
"child_attributes" => [
{"a" => "1", "b" => "2"},
{"aa" => "1", "refund" => "2"}
]
}, "refund")
=> true
has_key({
"whatever" => "3",
"child_attributes" => [
{"a" => "1", "b" => "2"},
{"grand_child_attributes" => [
{"test" => "3"}
]}
]
}, "refund")
=> false
has_key({
"whatever" => "3",
"child_attributes" => [
{"a" => "1", "b" => "2"},
{"grand_child_attributes" => [
{"test" => "3"}, {"refund" => "5"}
]}
]
}, "refund")
=> true
has_key({
"whatever" => "3",
"child_attributes" => [
{"a" => "1", "b" => "2"},
{"grand_child_attributes" => [
{"test" => "3", "refund" => "5"}
]}
]
}, "refund")
=> true
The problem with your code seems to be in here:
elsif v.class.to_s == "Array"
v.each do |inner_hash|
return has_key(inner_hash,key)
end
else
This would always return has_key(inner_array[0]) without checking subsequent values. The fix is to return only if it's true, else continue checking, like this:
elsif v.class.to_s == "Array"
v.each do |inner_hash|
if(has_key(inner_hash,key))
return true
end
end
else
return false
The following will work.
def has_key(hash, key)
hash.each do |k, v|
return true if k == key
if v.is_a? Array
v.each do |h|
rv = has_key(h, key)
return rv if rv
end
end
end
false
end
This passes all your tests. One more:
h = { "a" => 1,
"b" => [{ "c" => 2, "d" => 3 },
{"e"=> [{ "f" => "4" },
{ "g" => [{ "h" => 5 },
{ "i" => 6, "refund" => 7 }
]
}
]
}
]
}
has_key h, "refund"
#=> true
h["b"][1]["e"][1]["g"] = [{ "h"=>5 }]
h
#=> {"a"=>1, "b"=>[{"c"=>2, "d"=>3}, {"e"=>[{"f"=>"4"}, {"g"=>[{"h"=>5}]}]}]}
has_key h, "refund"
#=> false
Inspired by #Wand's answer, for
h = {"a"=>"3", "b"=>[{"c"=>"1", "d"=>"2"}, {"e"=>[{"test"=>"3", "refund"=>"5"}]}]}
you don't have to load JSON:
str = h.to_s
#=> "{\"a\"=>\"3\", \"b\"=>[{\"c\"=>\"1\", \"d\"=>\"2\"}, {\"e\"=>[{\"test\"=>\"3\", \"refund\"=>\"5\"}]}]}"
str =~ /\"refund\"=>/
#=> 60 (truthy)
I confess to being a little uncomfortable with any approach that converts the hash to a string, and then parsing the string, for fear that string formats may change in future.
I'd do something like:
class Hash
def key_exists?(key)
self.keys.include?(key) ||
self.values.any?{ |v|
Hash === v &&
v.key_exists?(key)
}
end
end
{'a' => 1}.key_exists?('a') # => true
{'b' => 1}.key_exists?('a') # => false
{'b' => {}}.key_exists?('a') # => false
{'b' => {'a' => {}}}.key_exists?('a') # => true
{'b' => {'a' => 1}}.key_exists?('a') # => true
{'b' => {'b' => {}}}.key_exists?('a') # => false
{'b' => {'b' => {'a' => 1}}}.key_exists?('a') # => true
Insert all the usual warnings about extending core classes and recommendations to use the alternative ways of doing it here.
Note: Similarly, "iterate over every key in nested hash" could be used to easily determine a true/false value and it demonstrates the safe way to extend a core class.
You could convert the hash to JSON and then check whether the JSON has "refund": present in it, as any key will be serialised to JSON in the form "key":.
require "json"
hash.to_json.include?('"refund":')
Give this a shot, I tried it, seems to work fine for me
def has_key(hash, key)
if hash.keys.include?(key)
true
else
# get the values of the current hash, flatten out to not include arrays
new_hash = hash.values.flatten
# then filter out any element that's not a hash
new_hash = new_hash.select {|b| b.is_a?(Hash)}
# merge all the hashes into single hash
new_hash = new_hash.inject {|first, second| first.merge(second)}
if new_hash
has_key(new_hash, key)
else
false
end
end
end
How can I merge two params together from my permissions hash that share the same "school_id" and "plan_type'. Then delete the permission that was merged from the hash, just leaving one. There can also be more than two that match.
[{"school_id"=>"1",
"plan_type"=>"All",
"view"=>"true",
"create"=>"true",
"approve"=>"true",
"grant"=>"true",
"region_id"=>nil},
{"school_id"=>"1", "plan_type"=>"All", "edit"=>"true", "region_id"=>nil},
{"school_id"=>"2",
"plan_type"=>"All",
"edit"=>"true",
"grant"=>"true",
"region_id"=>nil}]
def create_permissions(user, params)
permissions = params[:permissions].values.map { |perm|
if perm[:plan_type] == "" || perm[:plan_type] == "All Plans"
perm[:plan_type] = "All"
end
#perm_type = get_permission_type(perm)
case
when 'school' then perm.merge(region_id: nil)
when 'region' then perm.merge(school_id: nil)
end
}.tap { |permissions|
new_permissions = []
permissions.each do |perm|
set_permissions = permissions.find {|x| (x != perm && x[:school_id] == perm[:school_id] && x[:plan_type] == perm[:plan_type]) }
end
params[:user][:region_ids] = permissions.map { |perm| perm[:region_id] }.compact
params[:user][:school_ids] = permissions.map { |perm| perm[:school_id] }.compact
}
end
Output:
[{"school_id"=>"1",
"plan_type"=>"All",
"view"=>"true",
"create"=>"true",
"approve"=>"true",
"grant"=>"true",
"region_id"=>nil},
"edit"=>"true"
{"school_id"=>"2",
"plan_type"=>"All",
"edit"=>"true",
"grant"=>"true",
"region_id"=>nil}]
Group by school_id and then reduce by merging hashes:
input.group_by { |e| e['school_id'] }
.values
.map { |v| p v.reduce(&:merge) }
To group by many fields, one might use an array of desired fields, a concatenated string, whatever:
input.group_by { |e| [e['school_id'], e['plan_type']] }
.values
.map { |v| p v.reduce(&:merge) }
or, to keep nifty captions:
input.group_by { |e| "School: #{e['school_id']}, Plan: #{e['plan_type']}" }
.map { |k,v| [k, v.reduce(&:merge)] }
.to_h
#⇒ {
# "School: 1, Plan: All" => {
# "approve" => "true",
# "create" => "true",
# "edit" => "true",
# "grant" => "true",
# "plan_type" => "All",
# "region_id" => nil,
# "school_id" => "1",
# "view" => "true"
# },
# "School: 2, Plan: All" => {
# "edit" => "true",
# "grant" => "true",
# "plan_type" => "All",
# "region_id" => nil,
# "school_id" => "2"
# }
#}
arr1 = arr.group_by { |e| [e["school_id"],e["plan_type"]] }.values
=> {["1", "All"]=>[{"school_id"=>"1", "plan_type"=>"All", "view"=>"true", "create"=>"true", "approve"=>"true", "grant"=>"true", "region_id"=>nil}, {"school_id"=>"1", "plan_type"=>"All", "edit"=>"true", "region_id"=>nil}], ["2", "All"]=>[{"school_id"=>"2", "plan_type"=>"All", "edit"=>"true", "grant"=>"true", "region_id"=>nil}]}
arr1.map{ |i| i.inject({}) { |sum, e| sum.merge e}}
=> [{"school_id"=>"1", "plan_type"=>"All", "view"=>"true", "create"=>"true", "approve"=>"true", "grant"=>"true", "region_id"=>nil, "edit"=>"true"}, {"school_id"=>"2", "plan_type"=>"All", "edit"=>"true", "grant"=>"true", "region_id"=>nil}]