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
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 long list of values in the inner_value field from which I want only some values
I have array in this format:
hash_array = [
{
"array_value" => 1,
"inner_value" => [
{"iwantthis" => "forFirst"},
{"iwantthis2" => "forFirst2"},
{"Idontwantthis" => "some value"},
{"iwantthis3" => "forFirst3"},
{"Idontwantthis2" => "some value"},
{"Idontwantthis3" => "some value"},
{"Idontwantthis4" => "some value"},
{"Idontwantthis5" => "some value"},
{"Idontwantthis6" => "some value"},
]
},
{
"array_value" => 2,
"inner_value" => [
{"iwantthis" => "forSecond"},
{"Idontwantthis" => "some value"},
{"iwantthis3" => "forSecond3"},
{"iwantthis2" => "forSecond2"},
{"Idontwantthis2" => "some value"},
{"Idontwantthis3" => "some value"},
{"Idontwantthis4" => "some value"},
{"Idontwantthis5" => "some value"},
{"Idontwantthis6" => "some value"},
]
},
]
Desired Output:
[
{
"array_value" => 1,
"inner_value" => [
{"iwantthis" => "forFirst"},
{"iwantthis2" => "forFirst2"},
{"iwantthis3" => "forFirst3"}
]
},
{
"array_value" => 2,
"inner_value" => [
{"iwantthis" => "forSecond"},
{"iwantthis2" => "forSecond2"},
{"iwantthis3" => "forSecond3"}
]
},
]
I have tried using running loop in this but its too much costly.
So I tried something like this:
hash_array.select { |x| x["inner_value"].select {|y| !y["iwantthis"].nil? } }
but this ain't working either..
Note:Order/Sort does not matter
Your aim is not to select, you have to modify the input:
hash_array.map { |hash| hash['inner_value'] = hash['inner_value'].first }
#=> [
# {
# "array_value"=>1,
# "inner_value"=> {
# "iwantthis"=>"forFirst"
# }
# },
# {
# "array_value"=>2,
# "inner_value"=> {
# "iwantthis"=>"forSecond"
# }
# }
# ]
Here you'd basically change the value of whole hash['inner_value'] to what you want.
To do this with known key:
hash_array.map do |hash|
hash['inner_value'] = hash['inner_value'].find { |hash| hash['iwantthis'] }
end # `iwantthis` is the key, that can change
For multiple keys:
keys = %w(iwantthis Idontwantthis)
hash_array.map do |hash|
hash['inner_value'] = keys.flat_map do |key|
hash['inner_value'].select {|hash| hash if hash[key] }
end
end
#=> [{"array_value"=>1, "inner_value"=>[{"iwantthis"=>"forFirst"}, {"Idontwantthis"=>"some value"}]}, {"array_value"=>2, "inner_value"=>[{"iwantthis"=>"forSecond"}, {"Idontwantthis"=>"some value"}]}]
you can use map
hash_array.map{|k| {"array_value" => k['array_value'], 'inner_value' => k['inner_value'][0]} }
#=> [{"array_value"=>1, "inner_value"=>{"iwantthis"=>"forFirst"}}, {"array_value"=>2, "inner_value"=>{"iwantthis"=>"forSecond"}}]
I am using Ruby on Rails 4.1 and I would like to permit the following incoming parameters by using the StrongParameters gem:
# Parameters:
{
"my_key" => {
"one" => {
"0" => { "a" => "a_value", "b" => "b_value"},
"1" => { "a" => "a_value", "b" => "b_value"},
"2" => { "a" => "a_value", "b" => "b_value"}
},
"two" => {
"0" => { "c" => "c_value", "d" => "d_value"},
"1" => { "c" => "c_value", "d" => "d_value"},
"2" => { "c" => "c_value", "d" => "d_value"}
}
}
}
In controller I tried
params
.require(:my_key)
.permit(
[
:one => [
"0" => [:a, :b],
"1" => [:a, :b],
"2" => [:a, :b]
],
:two => [
"0" => [:c, :d],
"1" => [:c, :d],
"2" => [:c, :d]
]
]
)
and
params
.require(:my_key)
.permit(
{
:one => {
"0" => [:a, :b],
"1" => [:a, :b],
"2" => [:a, :b]
},
:two => {
"0" => [:c, :d],
"1" => [:c, :d],
"2" => [:c, :d]
}
}
)
But I get the error
ActionController::UnpermittedParameters (found unpermitted parameters: a, b)
How above parameters should be permitted?
Here is what you need to do:
Remove Strong Parameters gem from your Gemfile.
Use this in the controller.
params.require(:my_key).permit({:one=>[:a, :b],:two=>[:c, :d]})
I think this has something to do with how nested attributes work. The ids "0", "1", "2" etc are implicit.
You can test in the console like this:
$ bin/rails c
Loading development environment (Rails 4.1.2)
2.1.0 :001 > params = ActionController::Parameters.new "my_key"=>{"one"=>{"0"=>{"a"=>"a_value","b"=>"b_value"},"1"=>{"a"=>"a_value","b"=>"b_value"},"2"=>{"a"=>"a_value","b"=>"b_value"}},"two"=>{"0"=>{"c"=>"c_value","d"=>"d_value"},"1"=>{"c"=>"c_value","d"=>"d_value"},"2"=>{"c"=>"c_value","d"=>"d_value"}}}
=> {"my_key"=>{"one"=>{"0"=>{"a"=>"a_value", "b"=>"b_value"}, "1"=>{"a"=>"a_value", "b"=>"b_value"}, "2"=>{"a"=>"a_value", "b"=>"b_value"}}, "two"=>{"0"=>{"c"=>"c_value", "d"=>"d_value"}, "1"=>{"c"=>"c_value", "d"=>"d_value"}, "2"=>{"c"=>"c_value", "d"=>"d_value"}}}}
2.1.0 :002 > p = params.require(:my_key).permit({:one=>[:a, :b],:two=>[:c, :d]})
=> {"one"=>{"0"=>{"a"=>"a_value", "b"=>"b_value"}, "1"=>{"a"=>"a_value", "b"=>"b_value"}, "2"=>{"a"=>"a_value", "b"=>"b_value"}}, "two"=>{"0"=>{"c"=>"c_value", "d"=>"d_value"}, "1"=>{"c"=>"c_value", "d"=>"d_value"}, "2"=>{"c"=>"c_value", "d"=>"d_value"}}}
2.1.0 :003 >
I would like to transform this
def some_process(k,v)
return "#{v}_#{k}"
end
a_hash = {
"i_love_hashes" => {
"thing" => 20,
"other_thing" => "0",
"yet_another_thing" => "i disagree",
"_peculiar_thing" => [
{"count" => 30,
"name" => "freddie"},
{"count" => 15,
"name" => "johhno"},
{"count" => 12,
"name" => "mohammed"},
]
},
"as_do_i" => {
"thing" => 10,
"other_thing" => "2",
"yet_another_thing" => "i strongly agree",
"_peculiar_thing" => [
{"count" => 10,
"name" => "frodo"},
{"count" => 4,
"name" => "bilbo"},
{"count" => 2,
"name" => "elizabeth"},
]
}
}
into this
{
"i_love_hashes"=>{
"thing"=>20,
"other_thing"=>"0",
"yet_another_thing"=>"i disagree",
"_peculiar_thing"=> [
{"count"=>30, "name"=>"freddie", :sinister_name=>"freddie_i_love_hashes"},
{"count"=>15, "name"=>"johhno", :sinister_name=>"johhno_i_love_hashes"},
{"count"=>12, "name"=>"mohammed", :sinister_name=>"mohammed_i_love_hashes"}
]},
"as_do_i"=>{
"thing"=>10,
"other_thing"=>"2",
"yet_another_thing"=>"i strongly agree",
"_peculiar_thing"=>[
{"count"=>10, "name"=>"frodo", :sinister_name=>"frodo_as_do_i"},
{"count"=>4, "name"=>"bilbo", :sinister_name=>"bilbo_as_do_i"},
{"count"=>2, "name"=>"elizabeth", :sinister_name=>"elizabeth_as_do_i"}
]
}
}
this is the code I am currently using to achieve this
a_hash.each_with_object({}) do |(k,v),o|
o.merge!({k =>
v.each_with_object({}) do |(a,b),g|
g.merge!({ a =>
(b.is_a?(Array) ? b.collect {|x| x.merge({sinister_name: (some_process k, x["name"])})} : b)
})
end
})
end
Ignoring the specific details of what is being returned by "some_process" (what is important is that it depends on the outer most key and the inner name values, in this example), are there any alternatives that would be considered more elegant?
Why not do a recursive function?
def add_siniter(hash)
hash[:siniter_name] = "#{hash['name']}_i_love_hashes"
hash
end
def format_hash(item)
case item
when Hash then item.keys.each{|key| format_hash(item[key])}
when Array then item.map!{|h| add_siniter(h)}
end
end
format_hash(a_hash)