5
\$\begingroup\$

I need to generate a Ruby hash that looks like this:

{ "children" => [
  { "children" => [
    { "children" => [
      { "children" => [] }
    ]}
  ]}
]}

... for an arbitrary level of nesting. So far the best I've come up with is:

def nested_hash(levels)
  return {} if levels < 1
  root = { 'children' => [] }
  children = root['children']
  (levels - 1).times { children = (children << { 'children' => [] }).first['children'] }
  root
end

This doesn't seem particularly terse or elegant. Can anyone offer suggestions on making this more terse or elegant?

\$\endgroup\$
1
  • \$\begingroup\$ why do you need arrays with a single element? \$\endgroup\$
    – tokland
    Oct 2, 2012 at 7:48

4 Answers 4

4
\$\begingroup\$

Recursion perhaps?

def nested_hash(levels)
  return if levels <= 0
  { "children" => [ nested_hash(levels - 1) ].compact }
end

Or it could be

def nested_hash(levels)
  return nil if levels <= 0
  array = nested_hash(levels - 1)
  { "children" => array.nil? ? [] : [array] }
end

if you prefer handling the nil upfront, instead of removing it with compact

In either case, nested_hash(3) will get you

{"children"=>[
  {"children"=>[
    {"children"=>[]}
  ]}
]}

Note that unlike yours, these ones will return nil when levels is zero or less. So you'll want to do the {} fallback elsewhere, e.g. hsh = nested_hash(x) || {}.

\$\endgroup\$
3
\$\begingroup\$
def nested_hash(levels)
  return {} if levels < 1
  array = Array.new(levels, "children")
  array.reduce(nil) { |memo, item| { item => [memo].compact } }
end

Array.new

Array.new(levels, "children") creates a levels-sized Array of "children" Strings.

Example:

Array.new(3, "foo")
#=> ["foo", "foo", "foo"]

reduce

reduce (which is an alias for inject) iterates over all elements of the Array, and accumulates the result in memo. We seed memowith nil.

Example:

The block passed to reduce looks like this in the first iteration:

{ "children" => [nil].compact }
#=> { "children" => [] }

memo now holds that result.

The block in the second iteration:

{ "children" => [{ "children" => [] }].compact }
#=> { "children" => [{ "children" => [] }] }
\$\endgroup\$
4
  • \$\begingroup\$ I just realized that instead of doing Array.new(levels, "children" you could also simply do array = ["children"] * levels \$\endgroup\$ Oct 2, 2012 at 1:06
  • \$\begingroup\$ Clever. In super-terse mode: def nested_hash(levels); levels < 1 ? nil : (["children"] * levels).reduce(nil) { |memo, item| { item => [memo].compact } }; end \$\endgroup\$
    – Flambino
    Oct 2, 2012 at 1:16
  • \$\begingroup\$ for me "terse" implies "readability" that's why I've expanded it a little bit \$\endgroup\$ Oct 2, 2012 at 13:56
  • \$\begingroup\$ Of course - by "super-terse" I meant "waaay to compacted to actually be useful"; wasn't being serious about writing code like that \$\endgroup\$
    – Flambino
    Oct 2, 2012 at 18:04
3
\$\begingroup\$
h = Hash.new { |hash, key| hash[key] = [Hash.new(&hash.default_proc)] }

Not sure if this fits your needs, but this creates a hash that if accessed with #[] and the key doesn't exist, automatically creates that key with the value being an array with 1 element: another hash which behaves identically.

So initially the hash is empty:

>> h = Hash.new { |hash, key| hash[key] = [Hash.new(&hash.default_proc)] }
=> {}

But will create the required structure when accessed.

>> h[:children][0][:children][0][:children]
=> [{}]
>> h
=> {:children=>[{:children=>[{:children=>[{}]}]}]}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks - that's a very neat implementation, but doesn't do what I want (when fed into a recursive parser, it would eventually cause a stack overflow). \$\endgroup\$ Oct 10, 2012 at 2:51
3
\$\begingroup\$

You can use the Ruby Gem xkeys which extends the hash to have nesting capabilities:

require 'xkeys'

h = {}.extend XKeys::Auto
h['children', 0, 'children', 0, 'children', 0, 'children'] = []

OR, maybe you have something like this...

path = ['children', 0, 'children', 0, 'children', 0, 'children']
h[*path] = []
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.