Recursively delete hashes with empty values - ruby

I'd like to delete empty hashes at different nested levels. And once that empty hash is deleted, I'd like to delete it's container hash as well. How would I do this?
Here is the hash I want to work on:
{
"query"=>{"filtered"=>{
"query"=>{"bool"=>{}},
"filter"=>{"query"=>{"query_string"=>{
"fields"=>[["standard_analyzed_name", "standard_analyzed_message"]],
"query"=>"Arnold AND Schwarz"
}}}
}},
"sort"=>[{"total_interactions"=>{"order"=>"desc"}}]
}
Below is the code that I have that does not remove the empty {"query"=>{"bool"=>{}} part:
def compactify_hash(main_hash)
main_hash.each do |key, value|
if(value.class == Hash && !value.empty?)
compactify_hash(value)
elsif(value.class == Hash && value.empty?)
main_hash.delete(key)
end
end
return main_hash
end

You have a few problems here:
You're modifying the hash in-place when you might not mean to, you'd probably want to name your method compactify_hash! if you really want to modify it in-place.
You have Arrays inside your Hash but you don't scan them for empty Hashes.
Most importantly, you never check your recursively compactified hashes to see if compactifying them has also emptied them. in here:
if(value.class == Hash && !value.empty?)
compactify_hash(value)
elsif(value.class == Hash && value.empty?)
main_hash.delete(key)
end
you need to check value.empty? after you compactify_hash(value).
You could do something like this instead:
def compactify_hash(main_hash)
main_hash.each_with_object({}) do |(k, v), h|
if(v.is_a?(Hash))
ch = compactify_hash(v)
h[k] = ch if(!ch.empty?)
elsif(v.is_a?(Array))
h[k] = v.map do |e|
e.is_a?(Hash) ? compactify_hash(e) : e
end
else
h[k] = v
end
end
end

I have assumed that for any hash h, you want to create another hash g which is the same as h except no nested hash in g will have a key-value pair [k,v] for which v.respond_to(:empty?) and v.empty? both return true. For example, if the following nested hash is present in h:
{ a: { b: '', c: {}, d: [] }, e: 3 }
then the corresponding nested hash in g will be:
{ e: 3 }
In effect, we "remove" the key-value pairs :b=> '', :c=> {} and :d=> [] because '', {} and [] all respond to :empty? and all are empty. That reduces the nested hash to:
{ a: {}, e: 3 }
Since the value of a is now empty, we remove that key-value pair, leaving the nested hash in g equal to:
{ e: 3 }
I believe this can be achieved as follows:
def remove(h)
loop do
h_new = remove_empties(h)
break h if h==h_new
h = h_new
end
end
def remove_empties(h)
h.each_with_object({}) do |(k,v),g|
case v
when Hash
g[k] = remove_empties(h[k]) unless v.empty?
else
g[k] = v unless v.respond_to?(:empty?) && v.empty?
end
end
end
For your hash, which I refer to as h:
remove(h)
#=> {:query=>
# {:filtered=>
# {:filter=>
# {:query=>
# {:query_string=>
# {:fields=>
[["standard_analyzed_name", "standard_analyzed_message"]],
# :query=>"Arnold AND Schwarz"
# }
# }
# }
# }
# },
# :sort=>[
# {:total_interactions=>
# {:order=>"desc"}
# }
# ]
# }
Note that the recursion is performed repeatedly until no further modifications of the hash are performed.

Related

Update a Hash Where the Values are Hashes

I have this hash where the keys are 0, 3, and 5 and the values are hashes.
{0=>{:occurrences=>1, :className=>"class"},
3=>{:occurrences=>3, :className=>"hello"},
5=>{:occurrences=>3, :className=>"nah"}}
How can I implement something like this:
h.map { |key|
if key[:occurrences] > 2
key[:occurrences] += 1
end
}
I know this syntax doesn't work. I want to increment the occurrence value when a condition is met and I am not sure how to access the key of a key but I would like the result to be:
{0=>{:occurrences=>1, :className=>"class"},
3=>{:occurrences=>4, :className=>"hello"},
5=>{:occurrences=>4, :className=>"nah"}}
To update the existing hash you can simply call each_value. It passes each value in your hash to the block and within the block you can update the value (based on a condition):
h = {
0=>{:occurrences=>1, :className=>"class"},
3=>{:occurrences=>3, :className=>"hello"},
5=>{:occurrences=>3, :className=>"nah"}
}
h.each_value { |v| v[:occurrences] += 1 if v[:occurrences] > 2 }
#=> {
# 0=>{:occurrences=>1, :className=>"class"},
# 3=>{:occurrences=>4, :className=>"hello"},
# 5=>{:occurrences=>4, :className=>"nah"}
# }
Just out of curiosity:
input.map do |k, v|
[k, v[:occurrences].to_i > 2 ? v.merge(occurrences: v[:occurrences] + 1) : v]
end.to_h
#⇒ {0=>{:occurrence=>1, :className=>"class"},
# 3=>{:occurrences=>4, :className=>"hello"},
# 5=>{:occurrences=>4, :className=>"nah"}}
h = { 0=>{ :occurrences=>1, :className=>"class" },
3=>{ :occurrences=>3, :className=>"hello" },
5=>{ :occurrences=>3, :className=>"nah" } }
f = h.dup
Non-destructive case
h.transform_values do |g|
g[:occurrences] > 2 ? g.merge(occurrences: g[:occurrences] + 1) : g
end
#=> { 0=>{ :occurrences=>1, :className=>"class" },
# 3=>{ :occurrences=>4, :className=>"hello" },
# 5=>{ :occurrences=>4, :className=>"nah" } }
h == f
#=> true
Destructive case
g = h.transform_values! do |g|
g[:occurrences] > 2 ? g.merge(occurrences: g[:occurrences] + 1) : g
end
#=> { 0=>{ :occurrences=>1, :className=>"class" },
# 3=>{ :occurrences=>4, :className=>"hello" },
# 5=>{ :occurrences=>4, :className=>"nah" } }
h == g
See Hash#transform_values and Hash#transform_values!, which made their debut in MRI v.2.4. Note that, in the destructive case, merge! is not needed.
For example I would want the entire hash returned but with updated
values: {0=>{:occurrence=>1, :className=>"class"},
3=>{:occurrences=>4, :className=>"hello"}, 5=>{:occurrences=>4,
:className=>"nah"}}. If the value of the occurrences key is greater
than two than I want to increment that value and still have the entire
hash.
Here you go:
h = {0=>{:occurrences=>1, :className=>"class"}, 3=>{:occurrences=>3, :className=>"hello"}, 5=>{:occurrences=>3, :className=>"nah"}}
new_h = h.map do |k, v|
if v[:occurrences] > 2
v[:occurrences] += 1
end
[k, v]
end.to_h

How to compare ruby hash with same key?

I have two hashes like this:
hash1 = Hash.new
hash1["part1"] = "test1"
hash1["part2"] = "test2"
hash1["part3"] = "test3"
hash2 = Hash.new
hash2["part1"] = "test1"
hash2["part2"] = "test2"
hash2["part3"] = "test4"
Expected output: part3
Basically, I want to iterate both of the hashes and print out "part3" because the value for "part3" is different in the hash. I can guarantee that the keys for both hashes will be the same, the values might be different. I want to print out the keys when their values are different?
I have tried iterating both hashes at once and comparing values but does not seem to give the right solution.
The cool thing about Ruby is that it is so high level that it is often basically English:
Print keys from the first hash if the values in the two hashes are different:
hash1.keys.each { |key| puts key if hash1[key] != hash2[key] }
Select the first hash keys that have different values in the two hashes and print each of them:
hash1.keys.select { |key| hash1[key] != hash2[key] }.each { |key| puts key }
Edit: I'll leave this should it be of interest, but #ndn's solution is certainly better.
p hash1.merge(hash2) { |_,v1,v2| v1==v2 }.reject { |_,v| v }.keys
# ["part3"]
hash1["part1"] = "test99"
p hash1.merge(hash2) { |_,v1,v2| v1==v2 }.reject { |_,v| v }.keys
# ["part1", "part3"]
This uses the form of Hash#merge that employs a block (here { |_,v1,v2| v1==v2 }) to determine the values of keys that are present in both hashes being merged. See the doc for an explanation of the three block variables, _, v1 and v2. The first block variable equals the common key. I've used the local variable _ for that, as is customary when the variable is not used in the block calculation.
The steps (for the original hash1):
g = hash1.merge(hash2) { |_,v1,v2| v1==v2 }
#=> {"part1"=>true, "part2"=>true, "part3"=>false}
h = g.reject { |_,v| v }
#=> {"part3"=>false}
h.keys
#=> ["part3"]
The obvious way is that of ndn, here a solution without blocks by converting to arrays, joining them and subtracting the elements that are the same, followed by converting back to hash and asking for the keys.
Next time it would be better to include what you tried so far.
((hash1.to_a + hash2.to_a) - (hash1.to_a & hash2.to_a)).to_h.keys
# ["part3"]

iterating over sql result to produce new array of hashes

I have a result of items from SQL query. I want to use these results to create a new array of hashes called products. The path I'm on just doesn't seem right. While iterating over each row, I need to conditional process some of the keys in the rows such as pseudo code below.
items = retrieve_items_from_db
products = items.map do |row|
{
row.each do | k, v|
if k == "Color"
#go do something and add updated key, value pair to products
else puts "SKEY:#{k} VALUE: #{v}"
# add k,v to products
k => v
end
}
end
Any pointers or suggestions? I've tried using inject({}) instead of map.
What you're trying to do here would be cool, but we can't put conditional statements in hashes.
I.e. you can't do:
{
if 2 == 2
math: "correct"
else
math: "incorrect"
end
}
You would need to instead do:
if 2 == 2
{ math: "correct" }
else
{ math: "incorrect" }
end
If you need to combine multiple hashes into one, the Hash#merge method is useful.
for example:
{ a: "a", b: "DEFAULT" }.merge({ b: "b", c: "c" })
# => { a: "a", b: "b", c: "c" }
You can see that the second hash has overridden values in the first one.
You could use inject here (or reduce, which does the same thing but is less scary-sounding), but since you're converting an array to an array, it'd be easier to use map.
products = items.map do |row|
if row.keys.include?(:price)
row = row.merge(price: "#{row[:price]} dollars")
else
row = row.merge(price: "not available for sale")
end
next row
end
Made it a nested mapping and then flattened:
products = items.map do |row|
row.map do |k, v|
if k == "Color"
# go do something
# add transformed k, v to products
else
puts "SKEY:#{k} VALUE: #{v}"
k => v # add k, v to products
end
end
end.flatten
I think this way reads the easiest:
items = retrieve_items_from_db
products = items.map do |row|
product = {}
row.each do | k, v|
if k == "Color"
#go do something and add updated key, value pair to products
product[transform_key(k)] = transform_value(v)
else
puts "SKEY:#{k} VALUE: #{v}"
# add k,v to products
product[k] = v
end
end
product
end
You can also use Hash::[] to convert an array of array-pairs into a Hash, but to me this is clunkier:
items = retrieve_items_from_db
products = items.map do |row|
Hash[row.map do | k, v|
if k == "Color"
#go do something and add updated key, value pair to products
[transform_key(k), transform_value(v)]
else
puts "SKEY:#{k} VALUE: #{v}"
# add k,v to products
[k, v]
end
end]
end

How do I merge two arrays of hashes based on same hash key value?

So I have two arrays of hashes:
a = [{"b"=>123,"c"=>456}, {"b"=>456,"c"=>555}]
b = [{"c"=>456,"d"=>789}, {"b"=>222,"c"=>444}]
How would I concatenate them with the condition that the value of the key c is equivalent in both a and b? Meaning I want to be able to concatenate with the condition of a['c'] == b['c']
This is the result I want to get:
final_array = [{"b"=>123,"c"=>456,"d"=>789}, {"b"=>456,"c"=>555}, {"b"=>222,"c"=>444}]
a = [{"b"=>123,"c"=>456}, {"b"=>456,"c"=>555}]
b = [{"c"=>456,"d"=>789}, {"b"=>222,"c"=>444}]
p a.zip(b).map{|h1,h2| h1["c"] == h2["c"] ? h1.merge(h2) : [h1 ,h2]}.flatten
# => [{"b"=>123, "c"=>456, "d"=>789}, {"b"=>456, "c"=>555}, {"b"=>222, "c"=>444}]
a = [{"b"=>123,"c"=>456}, {"b"=>456,"c"=>555}]
b = [{"c"=>456,"d"=>789}, {"b"=>222,"c"=>444}]
def merge_hashes_with_equal_values(array_of_hashes, key)
array_of_hashes.sort { |a,b| a[key] <=> b[key] }.
chunk { |h| h[key] }.
each_with_object([]) { |h, result| result << h.last.inject(&:merge) }
end
p merge_hashes_with_equal_values(a + b, 'c')
# => [{"b"=>222, "c"=>444}, {"c"=>456, "d"=>789, "b"=>123}, {"b"=>456, "c"=>555}]
Concatenate the arrays first, and pass it to the method with the hash key to combine on. Sorting that array then places the hashes to merge next to each other in another array, which makes merging a bit easier to program for. Here I chose #chunk to handle detection of continuous runs of hashes with equal keys to merge, and #each_with_object to compile the final array.
Since this method takes one array to work on, the length of the starting arrays does not need to be equal, and the ordering of those arrays does not matter. A downside is that the keys to operate on must contain a sortable value (no nils, for example).
Here is yet another approach to the problem, this one using a hash to build the result:
def merge_hashes_with_equal_values(array_of_hashes, key)
result = Hash.new { |h,k| h[k] = {} }
remainder = []
array_of_hashes.each_with_object(result) do |h, answer|
if h.has_key?(key)
answer[h.fetch(key)].merge!(h)
else
remainder << h
end
end.values + remainder
end
Enumerable#flat_map and Hash#update are the perfect methods for this purpose :
a = [{"b"=>123,"c"=>456}, {"b"=>456,"c"=>555}]
b = [{"c"=>456,"d"=>789}, {"b"=>222,"c"=>444}]
p a.zip(b).flat_map{|k,v| next k.update(v) if k["c"] == v["c"];[k,v]}
# >> [{"b"=>123, "c"=>456, "d"=>789}, {"b"=>456, "c"=>555}, {"b"=>222, "c"=>444}]

Create a hash map from an array that returns the largest value

I have an array of questions where each question has a category_id and a value.
I'd like to map these so that when the key (category_id) already exists in the hash, then the values are added together.
And, finally, I'd like to find the largest value in the hash:
h = Hash.new {|k, v| k[v] = 0}
#test_session.answered_questions.each do |q|
if h.key?(q.category_id)
#add q.value to the value stored in the hash
else
h = { q.category_id => q.value } #insert the "q.category_id" as key and with value "q.value"
end
end
key_with_max_value = h.max_by { |k, v| v }[0] #find the highest value
#result.category = key_with_max_value
#result.score = h[key_with_max_value].value
There is probably a lot better way to achieve this but I'm quite new to Ruby.
h = Hash.new(0)
#test_session.answered_questions.each {|q| h[q.category_id] += q.value}
#result.category, #result.score = h.max_by { |k, v| v }
Each value in the hash will be initialized to zero with Hash.new(0) and since h.max_by returns the key value pair, you can directly assign them to your #result variable.
You can simply do:
#test_session.answered_questions.each { |q| h[q.category_id] += q.value }
When a key is not present, it is assumed to have a value of 0 because of the way you initialized the hash, so it gets inserted with 0 + q.value.
See the documentation, or try it out.
Also, you can assign two variables separated by commas to h.max_by { |k, v| v }. This is called Multiple Assignment, and it works for arrays too:
a,b,c = [1,2,3]

Resources