Ruby: Get all keys in a hash (including sub keys)

16,797

Solution 1

This will give you an array of all the keys for any level of nesting.

def get_em(h)
  h.each_with_object([]) do |(k,v),keys|      
    keys << k
    keys.concat(get_em(v)) if v.is_a? Hash
  end
end

hash = {"a" => 1, "b" => {"c" => {"d" => 3}}}
get_em(hash) #  => ["a", "b", "c", "d"]

Solution 2

I find grep useful here:

def get_keys(hash)
  ( hash.keys + hash.values.grep(Hash){|sub_hash| get_keys(sub_hash) } ).flatten
end

p get_keys my_nested_hash #=> ["a", "b", "c"]

I like the solution as it is short, yet it reads very nicely.

Solution 3

Version that keeps the hierarchy of the keys

  • Works with arrays
  • Works with nested hashes

keys_only.rb

# one-liner
def keys_only(h); h.map { |k, v| v = v.first if v.is_a?(Array); v.is_a?(Hash) ? [k, keys_only(v)] : k }; end

# nicer
def keys_only(h)
  h.map do |k, v|
    v = v.first if v.is_a?(Array);

    if v.is_a?(Hash)
      [k, keys_only(v)]
    else
      k
    end
  end
end

hash = { a: 1, b: { c: { d: 3 } }, e: [{ f: 3 }, { f: 5 }] }
keys_only(hash)
# => [:a, [:b, [[:c, [:d]]]], [:e, [:f]]]

P.S.: Yes, it looks like a lexer :D

Bonus: Print the keys in a nice nested list

# one-liner
def print_keys(a, n = 0); a.each { |el| el.is_a?(Array) ? el[1] && el[1].class == Array ? print_keys(el, n) : print_keys(el, n + 1) : (puts "  " * n + "- #{el}") }; nil; end

# nicer
def print_keys(a, n = 0)
  a.each do |el|
    if el.is_a?(Array)
       if el[1] && el[1].class == Array
         print_keys(el, n)
       else
         print_keys(el, n + 1)
       end
    else
      puts "  " * n + "- #{el}"
    end
  end

  nil
end


> print_keys(keys_only(hash))
- a
  - b
      - c
        - d
  - e
    - f

Solution 4

def get_all_keys(hash)
  hash.map do |k, v|
    Hash === v ? [k, get_all_keys(v)] : [k]
  end.flatten
end

Solution 5

Please take a look of following code:

hash = {"a" => 1, "b" => {"c" => 3}}
keys = hash.keys + hash.select{|_,value|value.is_a?(Hash)}
       .map{|_,value| value.keys}.flatten
p keys

result:

["a", "b", "c"]

New solution, considering @Bala's comments.

class Hash
  def recursive_keys
    if any?{|_,value| value.is_a?(Hash)}
       keys + select{|_,value|value.is_a?(Hash)}
                    .map{|_,value| value.recursive_keys}.flatten
    else
       keys
    end
  end
end

hash =  {"a" => 1, "b" => {"c" => {"d" => 3}}, "e" => {"f" => 3}}
p hash.recursive_keys

result:

["a", "b", "e", "c", "d", "f"]
Share:
16,797

Related videos on Youtube

user2211703
Author by

user2211703

Updated on June 18, 2022

Comments

  • user2211703
    user2211703 about 2 years

    let's have this hash:

    hash = {"a" => 1, "b" => {"c" => 3}}
    hash.get_all_keys 
    => ["a", "b", "c"]
    

    how can i get all keys since hash.keys returns just ["a", "b"]

    • Cary Swoveland
      Cary Swoveland over 10 years
      Thanks, but in future consider holding off a bit before selecting an answer, as it may discourage other, possibly better ones being offered.
  • Bala
    Bala over 10 years
    This may fail if the hash is hash = {"a" => 1, "b" => {"c" => {"c" => 3}}, "c" => {"b" => 3}}
  • uncutstone
    uncutstone over 10 years
    You are right. If hash is nested multi-level, I should give a recursive solution.
  • uncutstone
    uncutstone over 10 years
    @Bala, I give a new solution.
  • uncutstone
    uncutstone over 10 years
    === is asymmetric, that means v === Hash gives different result from Hash === v.
  • hirolau
    hirolau over 10 years
    I think === is hard to read and understand, it should really only be used in case statements.
  • Cary Swoveland
    Cary Swoveland over 10 years
    Interesting use of tap. You might want to use __method__ in place of :get_all_keys. If the method were renamed and that not changed, it might take awhile to find the bug. Aside: do you know why respond to? does not check to see if its argument is an existing method?
  • spickermann
    spickermann over 10 years
    @CarySwoveland: Thank you, I like the idea with __method__. I do not understand your question about the behavior of respond_to?, please explain. It seams like I missed something important.
  • Cary Swoveland
    Cary Swoveland over 10 years
    v.respond_to?(sym) => false for any Symbol sym that is not a method. Ruby could check at runtime to see if sym is a method and raise an exception if not, but it evidently does not do so. I was wondering why. It just occurred to me that for method_missing to be useful, Ruby probably never checks to see if a Symbol is an existing method.
  • Cary Swoveland
    Cary Swoveland over 10 years
    Arup, I think each_with_object is greatly underused. Here, for example, one would often see reduce([]) (aka inject) instead, but that requires the array holding the keys to be returned to the block, which may require that ugly ; keys) at the end. I considered each_with_object here but decided to instead have keys = [] before and keys after. One is often faced with this choice. I think the simpler way is more readable, but it does add two lines. Do you have an opinion about this?
  • Arup Rakshit
    Arup Rakshit over 10 years
    @CarySwoveland In this kind of situation, I always used each_with_object. For me it looks more expressive in Ruby way. As I often found Ruby experts like short and concise, but expressive code. I tired to do the same. :)
  • hirolau
    hirolau over 10 years
    True, but that is, just like the case statement, a function build for the case equality operator.
  • spickermann
    spickermann over 10 years
    Ruby seams to check only if the method name is in the methods array and therefore will not recognize if method_missing might respond. And to analyze all possible behavior of a method_missing is almost impossible without actually running that method, what might have side effects. Interesting read about the new respond_to_missing? in Ruby 1.9.2: blog.marc-andre.ca/2010/11/15/methodmissing-politely
  • A moskal escaping from Russia
    A moskal escaping from Russia over 6 years
    But it doesn't return nested keys { a: { b: 1 } }.keys # [:a] :(.