Reputation: 25378
I have the following flat Ruby hash:
{
"builder_rule_0_filter"=>"artist_name",
"builder_rule_0_operator"=>"contains",
"builder_rule_0_value_0"=>"New Found Glory",
"builder_rule_1_filter"=>"bpm",
"builder_rule_1_operator"=>"less",
"builder_rule_1_value_0"=>"150",
"builder_rule_2_filter"=>"days_ago",
"builder_rule_2_operator"=>"less",
"builder_rule_2_value_0"=>"40",
"builder_rule_3_filter"=>"release_date_start",
"builder_rule_3_operator"=>"between",
"builder_rule_3_value_0"=>"2019-01-01",
"builder_rule_3_value_1"=>"2019-12-31",
}
I'd like to convert it to a nested hash that's a bit more organized/useable:
{
"0"=>
{
"filter"=>"artist_name",
"operator"=>"contains",
"values"=>
{
"0"=>"New Found Glory"
}
},
"1"=>
{
"filter"=>"bpm",
"operator"=>"less",
"values"=>
{
"0"=>"150"
}
}
"2"=>
{
"filter"=>"days_ago",
"operator"=>"less",
"values"=>
{
"0"=>"40"
}
}
"3"=>
{
"filter"=>"release_date_start",
"operator"=>"between",
"values"=>
{
"0"=>"2019-01-01"
"1"=>"2019-12-31"
}
}
}
So, how can convert a flat hash (which I'm getting from a form) and convert it to be nested based on on those key names?
Upvotes: 1
Views: 331
Reputation: 26788
First you can split up the hash into multiple hashes according to their prefix number (feel free to run this to see the return val)
groups = input.
group_by { |k,v| k.match(/builder_rule_(\d+)/)[1] }.
transform_values(&:to_h)
at this point, creating the inner objects is easier if you just use some hard-coding to build the key-valls:
result = groups.each_with_object({}) do |(prefix, hash), memo|
memo[prefix] = {
"filter" => hash["builder_rule_#{prefix}_filter"],
"operator" => hash["builder_rule_#{prefix}_operator"],
"values" => hash.select do |key, val|
key =~ /builder_rule_#{prefix}_value/
end.sort_by { |key, val| key }.map { |(key, val)| val }
}
end
It might possibly be confusing what .sort_by { |key, val| key }.map { |(key, val)| val }
means. I can spell it out:
hash.select { |key, val| key =~ /builder_rule_#{prefix}_value/ }
gets the key-vals which are going to be used for the "values" array. It returns a hash..sort_by { |key, val| key }
turns the hash into an array of [key, val]
tuples , sorted by the key. This is so that the values appear in the correct order..map { |(key, val)| val }
turns the nested array into a single-level array, discarding the keysYou could also use sort_by(&:first).map(&:second)
, though you need active support to use Array#second
Upvotes: 5
Reputation: 106147
The accepted answer is good, but does a bit more work than is strictly necessary. I would do all of the work with each_with_object
:
input.each_with_object({}}) do |(key, val), hsh|
_, num, name = *key.match(/^builder_rule_(\d+)_(.*)/)
hsh[num] = {} unless hsh.key?(num)
if name.start_with?(/value_\d/)
hsh[num]["values"] = [] unless hsh[num].key?("values")
hsh[num]["values"] << val
else
hsh[num][name] = val
end
end
# => {
# "0" => {
# "filter" => "artist_name",
# "operator" => "contains",
# "values" => ["New Found Glory"]
# },
# "1" => {
# "filter" => "bpm",
# "operator" => "less",
# "values" => ["150"]
# },
# "2" => {
# "filter" => "days_ago",
# "operator" => "less",
# "values" => ["40"]
# },
# "3" => {
# "filter" => "release_date_start",
# "operator" => "between",
# "values" => ["2019-01-01", "2019-12-31"]
# }
# }
See it in action on repl.it: https://repl.it/@jrunning/GiddyEnragedLinks
Upvotes: 1
Reputation: 29613
Assuming the depth is fixed at 3 and the rules are consistent (as shown in the example), you can make this a single pass transformation as follows:
h.each_with_object(Hash.new {|h,k| h[k]= {}}) do |(k,v),obj|
new_k, k2, k3 = k[/(?<=builder_rule_).*/].split('_')
k2 == 'value' ? (obj[new_k]["values"] ||= {}).merge!({k3 => v}) : obj[new_k][k2] = v
end
Here we are constructing a new Hash
where first level keys default to an empty Hash
(h.each_with_object(Hash.new {|h,k| h[k]= {}}) do |(k,v),obj|
).
Then we split the key based on the underscore after 'builder_rule_' new_k, k2, k3 = k[/(?<=builder_rule_).*/].split('_')
Then if the second level key (k2) equals 'value' we need to make it values and merge the third level key (k3) and the value into a new hash otherwise we assign k2 to the value. k2 == 'value' ? (obj[new_k]["values"] ||= {}).merge!({k3 => v}) : obj[new_k][k2] = v
This will actually return the requested requirement where values
is a Hash
with key indexes
{
"0"=>{
"filter"=>"artist_name",
"operator"=>"contains",
"values"=>
{
"0"=>"New Found Glory"
}
},
"1"=>{
"filter"=>"bpm",
"operator"=>"less",
"values"=>
{
"0"=>"150"
}
},
"2"=>{
"filter"=>"days_ago",
"operator"=>"less",
"values"=>
{
"0"=>"40"
}
},
"3"=>{
"filter"=>"release_date_start",
"operator"=>"between",
"values"=>
{
"0"=>"2019-01-01",
"1"=>"2019-12-31"
}
}
}
Upvotes: 1