Reputation: 579
I have a rails API where I'm able to query vehicles and count and group by different attributes. I would like to fill the response with zero values when a group by is used.
Here is a simple example:
data = {
AUDI: {
Petrol: 379,
Diesel: 326,
Electric: 447
},
TESLA: {
Electric: 779
}
}
Since Tesla doesn't have any petrol or diesel cars, then that key isn't even included in the response. I believe this is a consequence of the group_by in postgres, which doesn't include zero counts in the results.
I'm trying to create a function which can "fill" the hash with these missing zero values eg.
fill_hash(data)
should return
data = {
AUDI: {
Petrol: 379,
Diesel: 326,
Electric: 447
},
TESLA: {
Petrol: 0,
Diesel: 0,
Electric: 779
}
}
This simple case I got working with 3 methods:
def collect_keys(hash, keys = [])
hash.each do |_, value|
if value.is_a?(Hash)
keys.concat(value.keys)
collect_keys(value, keys)
end
end
keys.uniq
end
def fill_missing_keys(hash, all_keys, default_value = 0)
hash.each do |_, value|
if value.is_a?(Hash)
all_keys.each do |k|
value[k] = default_value unless value.key?(k)
end
fill_missing_keys(value, all_keys, default_value)
end
end
hash
end
def fill_hash(hash, default_value = 0)
all_keys = collect_keys(hash)
fill_missing_keys(hash, all_keys, default_value)
end
# Example usage:
hash = { a: { x: 1 }, b: { y: 1 } }
filled_hash = complete_hash(hash)
puts filled_hash
# Output should be: {:a=>{:x=>1, :y=>0}, :b=>{:y=>1, :x=>0}}
My problem is that the hashes can get more complicated. The simple case only grouped by 2 attributes, but here is an example where we group by 3 attributes.
{
AUDI: {
Deregistered: {
Diesel: 56
},
Registered: {
Petrol: 379,
Diesel: 270,
Electric: 447
}
},
TESLA: {
Registered: {
Electric: 779
}
}
}
My desired output is:
{
AUDI: {
Deregistered: {
Petrol: 0,
Diesel: 56,
Electric: 0
},
Registered: {
Petrol: 379,
Diesel: 270,
Electric: 447
}
},
TESLA: {
Deregistered: {
Petrol: 0,
Diesel: 0,
Electric: 0
},
Registered: {
Petrol: 0,
Diesel: 0,
Electric: 779
}
}
}
Eg. there's keys missing in both last and second to last layer.
Upvotes: 3
Views: 124
Reputation: 29588
Here's what I came up with, which seems to satisfy the request, including identical order of keys.
# This will build a "Default" Hash
# for Example:
# {:AUDI=>{:Petrol=>0, :Diesel=>0, :Electric=>0},
# :TESLA=>{:Petrol=>0, :Diesel=>0, :Electric=>0}}
def build_default_hash(obj, default_value: 0)
return obj.transform_values {default_value.dup} unless obj.values.first.is_a?(Hash)
m = obj.values.reduce(&:deep_merge)
obj.keys.product([build_default_hash(m, default_value:)]).to_h
end
# deep_merge the default hash with the existing Hash
def deep_normalize_hash(h, default_value:0)
build_default_hash(h, default_value:).deep_merge(h)
end
Usage:(Working Example)
h = {
AUDI: {
Deregistered: {
Diesel: 56
},
Registered: {
Petrol: 379,
Diesel: 270,
Electric: 447
}
},
TESLA: {
Registered: {
Electric: 779
}
}
}
deep_normalize_hash(h)
#=> {:AUDI=>
# {:Deregistered=>{:Diesel=>56, :Petrol=>0, :Electric=>0},
# :Registered=>{:Diesel=>270, :Petrol=>379, :Electric=>447}},
# :TESLA=>
# {:Deregistered=>{:Diesel=>0, :Petrol=>0, :Electric=>0},
# :Registered=>{:Diesel=>0, :Petrol=>0, :Electric=>779}}}
You could easily blend this into the Hash
class, such that usage could be h.deep_normalize
, and while I would generally discourage doing so, given that this is in a rails context no one would even notice a little more core class manipulation.
Upvotes: 2
Reputation: 579
I've managed to get something working here. The idea is to first create a set of all the unique keys at all levels of the original hash. Then I create a new hash and iterate over the original hash and for each key I make sure it exists in the new hash. I need to do some more testing, but for now this seems to work with any n layers.
module HashHelper
def fill_hash(hash, default_value = 0)
keys = collect_keys_at_all_levels(hash)
create_complete_hash(hash, keys, 0, default_value)
end
def collect_keys_at_all_levels(hash, keys = {}, depth = 0)
keys[depth] ||= Set.new
hash.each do |key, value|
keys[depth].add(key)
if value.is_a?(Hash)
collect_keys_at_all_levels(value, keys, depth + 1)
end
end
keys
end
def create_complete_hash(hash, keys, depth = 0, default_value = 0)
new_hash = {}
keys[depth].each do |key|
if hash.key?(key)
if hash[key].is_a?(Hash)
new_hash[key] = create_complete_hash(hash[key], keys, depth + 1, default_value)
else
new_hash[key] = hash[key]
end
else
if keys[depth + 1]
new_hash[key] = create_complete_hash({}, keys, depth + 1, default_value)
else
new_hash[key] = default_value
end
end
end
new_hash
end
end
Upvotes: 0
Reputation: 370
How about creating a class VehicleCount from which all manufacturers are an instance of, and in the parent set all possible counts to 0?
Class VehicleCount
attr_accessor :Petrol, :Diesel, :Electric
def initialize
@:Petrol = @Diesel = @Electric = 0
end
end
Class Vehicle
attr_accessor :Registered, :Deregistered
def initialize
@Registered = VehicleCount.new
@Deregistered = VehicleCount.new
end
end
Audi = Vehicle.new
Audi.Deregistered.Diesel = 56
Audi.Registered.Petrol = 379
Audi.Registered.Diesel = 270
Audi.Registered.Electric = 447
Something like that.
Upvotes: 0