jamesvphan
jamesvphan

Reputation: 1975

Iterate through nested hash to create an array while adding items to it

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

Answers (4)

Cary Swoveland
Cary Swoveland

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

engineersmnky
engineersmnky

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

Schwern
Schwern

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

jamesvphan
jamesvphan

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

Related Questions