0

I have the following nested hash in a Ruby program. How can I write a for loop that ultimately prints out the element contained in each array?

alternatives = {
  "JAVA" => {
    "/usr/bin" => [
      "java", "keytool", "orbd", "pack200", "rmid",
      "rmiregistry", "servertool", "tnameserv", "unpack200"
    ],
    "/usr/lib" => [
      "jre", "jre_exports" 
    ],
    "/usr/share" => [
      "java.1", "keytool.1", "orbd.1", "pack200.1", "rmid.1",
      "rmiregistry.1", "servertool.1", "tnameserv.1", "unpack200.1"
    ]
  },
  "JDK" => {
    "/usr/bin" => [
      "javac", "appletviewer", "apt", 
      "extcheck", "jar", "jarsigner", "javadoc", "javah", "javap", 
      "jconsole", "jdb", "jhat", "jinfo", "jmap", "jps", 
      "jrunscript", "jstack", "jstat", "jstatd", "native2ascii",
      "policytool", "rmic", "schemagen", "servialver", "wsgen",
      "wsimport", "xjc" 
    ],
    "/usr/lib" => [
      "java_sdk", "java_sdk_exports"
    ],
    "/usr/share/man/man1" => [
      "javac.1", "appletviewer.1", "apt.1", 
      "extcheck.1", "jar.1", "jarsigner.1", "javadoc.1", "javah.1",
      "javap.1", "jconsole.1", "jdb.1", "jhat.1", "jinfo.1", "jmap.1",
      "jps.1", "jrunscript.1", "jstack.1", "jstat.1", "jstatd.1",
      "native2ascii.1", "policytool.1", "rmic.1", "schemagen.1",
      "servialver.1", "wsgen.1", "wsimport.1", "xjc" 
    ]
  }
}

Here's the pseudo code for what I'm trying to do

alternatives.each do |first_level_keys| # Gets the JAVA, JDK keys
  first_level_keys.each do |second_level_keys| # Gets the /usr/... keys
    second_level_keys.each do |array|
      array.each do |elem|
        puts "elem: " + elem
      end
    end
  end
end

I see "Accessing elements of nested hashes in ruby", but can't seem to make sense out of it.

Community
  • 1
  • 1
Chris F
  • 14,337
  • 30
  • 94
  • 192
  • Instead of saying something like "this for example", put something more definitive in as the anchor text to give people an idea what they're going to. See "[6.1 Link text](http://www.w3.org/TR/WCAG10-HTML-TECHS/#link-text)" and "[Don't use 'click here' as link text](http://www.w3.org/QA/Tips/noClickHere)". – the Tin Man Aug 26 '14 at 23:34

5 Answers5

4

I know that you asked for a loop-based solution, but if you are willing to use the Hash and Array methods in the standard library, there is no need.

Here is how I would do it:

alternatives.values.collect(&:values).flatten

Explanation:

  • .values removes the keys, giving you an array of hashes: [{'usr/bin' => [...], 'usr/lib' => [...]}, ...]
  • .collect(&:values) takes that array of hashes and removes the keys again, giving you an array of arrays of arrays of strings, like this: [[["java",...],[jre, ...]], ...]
  • .flatten transforms the array into a 1-dimensional array of strings.

I would add uniq.sort! for removing any duplicates and sorting the result alphabetically, unless you want the duplicates (then remove the uniq)

alternatives.values.collect(&:values).flatten.uniq.sort!
kikito
  • 51,734
  • 32
  • 149
  • 189
2

I think this is all you need:

alternatives.each_value {|h| h.each_value {|a| a.each {|e| puts "elem: #{e}"}}}
elem: java
elem: keytool
elem: orbd
elem: pack200
...
elem: wsgen.1
elem: wsimport.1
elem: xjc

If the object has nested arrays and hashes, with the levels of nesting varied, you could print all the values that are not themselves arrays or hashes as follows. For key-value pairs in hashes, this will print value if it is not an array or hash.

def print_innermost(o)
  case o
  when Hash then  o.each { |_,v| print_innermost(v) }
  when Array then o.each { |e|   print_innermost(e) }
  else puts "elem: #{o}"
  end
end

print_innermost(alternatives)
elem: java
elem: keytool
elem: orbd
elem: pack200
...
elem: wsgen.1
elem: wsimport.1
elem: xjc

alternatives = [{a: {b: {c: [1,2], d: {e: [3,4], f: 5}}}}, [{g: [6,7]}], 8]

print_innermost(alternatives)
elem: 1
elem: 2
elem: 3
elem: 4
elem: 5
elem: 6
elem: 7
elem: 8
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

"Accessing elements of nested hashes in ruby" solves a similar problem but with a big difference: in that example the OP knows what keys is he looking for. In your case, it seems to me that you are only trying to access all the values in the lists, so the answers submitted in the other questions do not apply entirely.

If what you are trying to do is just accessing the elements in the arrays, you can approach it in different ways. My particular approach is not in a for loop (for loops are not even used in Ruby, see "for vs each in Ruby" for more info), but using a recursive method. You create a function that accepts an argument. If it is an array, it will print the elements, else we assume it is a Hash and the function is called over every value (we simply ignore the keys)

The code would be:

def get_elements(obj)
  if obj.is_a?(Array)
    obj.each {|element| puts "elem:" + element }
  else
    obj.each {|key, value| get_elements(value) }
  end
end

get_elements(alternatives)

Or, if you understand blocks you could yield the elements so you can do whatever with them in another part of the code

def get_elements(obj)
  if obj.is_a?(Array)
    obj.each {|element| yield element }
  else
    obj.each {|key, value| get_elements(value) }
  end
end

get_elements(alternatives) {|elem| puts "elem:" + elem }
Community
  • 1
  • 1
Chubas
  • 17,823
  • 4
  • 48
  • 48
  • Instead of saying something like "link" or "this post", put something more definitive in as the anchor text to give people an idea what they're going to. See "[6.1 Link text](http://www.w3.org/TR/WCAG10-HTML-TECHS/#link-text)" and "[Don't use 'click here' as link text](http://www.w3.org/QA/Tips/noClickHere)". – the Tin Man Aug 26 '14 at 23:35
0

You don't need to get the keys for the first level hash, and then use the keys to get the values--because you can go straight for the values:

alternatives.values.each do |hash| # Gets the values for JAVA, JDK keys
  hash.values.each do |arr| # Gets the values for the /usr/... keys
    arr.each do |elmt|
      puts "elem: " + elmt
    end
  end
end

If your hash has various levels of nesting, then recursion is the answer, although recursion is pretty hard to learn.

Warning! Recursion ahead!

def get_arrays(hash)
  results = []

  hash.values.each do |value|
    case value
      when Hash
        get_arrays(value).each do |elmt|
          results << elmt
        end
      when Array
        results << value
    end

  end

  results
end


h = {
  a: [1, 2],
  b: 100,
  c: {
       d: [3, 4, 5],
       e: { f: [6, 7] }
     },
  j: { 
       k: [8, 9], 
       l: [10, 11, 12]
     },
}



results = get_arrays(h)
p results

results.each do |arr|
  arr.each do |elmt|
    print "#{elmt} "
  end
  puts 
end


--output:--
[[1, 2], [3, 4, 5], [6, 7], [8, 9], [10, 11, 12]]
1 2 
3 4 5 
6 7 
8 9 
10 11 12 
7stud
  • 46,922
  • 14
  • 101
  • 127
0

I used a modified 7stud's answer.

Since I also need the values of the keys (e.g., JAVA, JDK, /usr/...., I ended doing this:

alternatives.keys.each do |alt_keys| # Gets the values for JAVA, JDK keys
  alternatives[alt_keys].each do |arr| # Gets the values for the /usr/... keys
    puts "array key: " + arr.to_s
    alternatives[alt_keys][arr].each do |elmt|
      puts "elem: " + elmt
    end
  end
end

Thanks to all, I will try the other answers too later.

Chris F
  • 14,337
  • 30
  • 94
  • 192