Reputation: 1975
I have a nested hash and I would like to rearrange the key/val pairs. The example below shows a hash of styles that points to hash of languages, which then points to a hash of the type of language it is. I want to reformat it to look like the new_hash
example. I understand to structure it by iterating through the hash through different levels and creating the hash like that, however, the part I'm concerned/confused about is creating the array that :style
points to and then pushing the correct style to it.
I assumed the code snippet would work as I expect it to. My new_hash
will have a key of :language
which points to another hash. This hash has a key of :style
that points to an array in which I will store all the styles associated with each respective language. The :javascript
hash should have two styles in its array since it exists twice in the original hash
, however, when running this code snippet, the array is not adding both styles. It seems that during one iteration when assigning the hash, :javascript
is assigned the style of :oo
but in another iteration, it gets replaced with :functional
. I'm not sure of the syntax to initialize the array and add multiple items to it while iterating through the hash.
hash = {
:oo => {
:ruby => {:type => "Interpreted"},
:javascript => {:type => "Interpreted"},
},
:functional => {
:scala => {:type => "Compiled"},
:javascript => {:type => "Interpreted"}
}
}
new_hash = {
:ruby => {
:type => "Interpreted", :style => [:oo]
},
:javascript => {
:type => "Interpreted", :style => [:oo, :functional]
},
:scala => {
:type => "Compiled", :style => [:functional]
}
}
hash.each do |style, programming_language|
programming_language.each do |language, type|
type.each do |key, value|
new_hash[language] = {:style => [style]}
end
end
end
Upvotes: 2
Views: 986
Reputation: 110665
You could use the forms of Hash#update (aka merge!
) and Hash#merge that employ a hash to determine the values of keys that are present in both hashes being merged. See the docs for details.
hash.each_with_object({}) do |(style,language_to_type_hash),h|
language_to_type_hash.each do |language,type_hash|
h.update(language=> { type: type_hash[:type], style: [style] }) do |_,o,_|
o.merge(style: [style]) { |_,ostyle_arr,nstyle_arr| ostyle_arr + nstyle_arr }
end
end
end
#=> {:ruby =>{:type=>"Interpreted", :style=>[:oo]},
# :javascript=>{:type=>"Interpreted", :style=>[:oo, :functional]},
# :scala =>{:type=>"Compiled", :style=>[:functional]}}
Upvotes: 2
Reputation: 29308
Hash::new
allows you to specify a default value for a non existent key so in your case the default value would be {type: nil, style: []}
This functionality will allow you to loop only once and implement as follows
programming_languages = {
:oo => {
:ruby => {:type => "Interpreted"},
:javascript => {:type => "Interpreted"},
},
:functional => {
:scala => {:type => "Compiled"},
:javascript => {:type => "Interpreted"}
}
}
programming_languages.each_with_object(Hash.new {|h,k| h[k] = {type: nil, style: []}}) do |(style,languages),obj|
languages.each do |language,type_hash|
obj[language][:style] << style
obj[language][:type] = type_hash[:type]
end
end
Output:
#=> {:ruby=>{:type=>"Interpreted", :style=>[:oo]},
:javascript=>{:type=>"Interpreted", :style=>[:oo, :functional]},
:scala=>{:type=>"Compiled", :style=>[:functional]}}
Upvotes: 1
Reputation: 164679
Once we give the hashes better names, it becomes a bit easier to work it out. I've also made use of sets so we don't have to worry about duplicates.
require 'set'
# Our new hash of language info. _new to differentiate between
# the hash of languages under the hash of styles.
languages_new = {}
# For each style...
styles.each do |style, languages|
# For each language in that style...
languages.each do |language, info|
# Add a new hash for that language if there isn't one already
languages_new[language] ||= {}
# For each bit of info about that language...
info.each do |key, val|
# Add a new set for that info if there isn't one already
# The `var = hash[key] ||= new_var` pattern allows
# conditional initialization while also using either the
# new or existing set.
set = languages_new[language][key] ||= Set.new
# Add the info to it
set.add(val)
end
# Handle the special case of style.
set = languages_new[language][:style] ||= Set.new
set.add(style)
end
end
Note that rather than hard coding the initialization of hashes and sub-hashes, I've done it in each level of looping. This means I don't have to list out all the keys, and it will handle new and unexpected keys.
By using sets for the values I make no assumptions about how many values a bit of language information can have.
Upvotes: 0
Reputation: 1975
Realized that this can be solved iterating the hash twice. Once to initialize the array, and the second time to then add the necessary items to it. Though not sure if this can done only iterating the hash once.
new = {}
languages.each do |style, programming_language|
programming_language.each do |language, type|
type.each do |key, value|
new[language] = {:type => nil , :style => []}
end
end
end
languages.each do |style, programming_language|
programming_language.each do |language, type|
type.each do |key, value|
new[language][:type] = value
new[language][:style] << style
end
end
end
new
Upvotes: 0