I have a class, with one method:
class ScreenerProbe
def initialize
#body = {}
end
def import_simple_matrix(questions, answers)
#body['aliases'] = answers.map { |pair|
return { val: pair["text"], text: pair["value"] }
}
# this code doesn't seem to run
questions.each.with_index do |text, index|
#body["questions"][index + 1] = {
"title" => text,
"type" => 'default',
}
end
end
The problem is that some of the code doesn't seem to run, even though I expect it to.
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-darwin20]
When calling return from inside a loop, it won't only "exit" the loop, you will return from the outer method, in this case import_simple_matrix.
This is probably not what you want, and the reason why some assignments, that you're expecting, won't occur.
Related
I have a complex multi nested array of hashes like below:
{
"Food":[
{
"id": "01",
"name":"ABC",
"branch":"London",
"platter_cost":"£40.00",
"Kebab":[
{
"name":"chicken",
"value":"£8.12"
},
{
"name":"lamb",
"value":"£9.67"
}
],
"sides":[
{
"type":"drinks",
"options":[
{
"id":1,
"name":"Coke",
"price":"£4.70"
},
{
"id":2,
"name":"Pepsi",
"price":"£2.90"
},
{
"id":3,
"name":"Tango",
"price":"£4.00"
}
]
},
{
"type":"chips",
"options":[
{
"id":4,
"name":"Peri-Peri",
"price":"£4.00"
}
]
}
]
},
{
"id": "02",
"name":"XYZ",
"branch":"Manchester",
"platter_cost":"£30.00",
"Kebab":[
{
"name":"chicken",
"value":"£5.22"
},
{
"name":"lamb",
"value":"£6.35"
}
],
"sides":[
{
"type":"drinks",
"options":[
{
"id":77,
"name":"coke",
"price":"£3.70"
},
{
"id":51,
"name":"Orange",
"price":"£4.00"
},
{
"id":33,
"name":"Apple",
"price":"£2.00"
}
]
},
{
"type":"chips",
"options":[
{
"id":20,
"name":"peri-peri",
"price":"£4.00"
},
{
"id":18,
"name":"cheesy",
"price":"£3.50"
}
]
}
]
}
]
}
I have a method to return a cost value based on the arguments. Example:
def total_cost(id: "01", options: [1, 4], kebab: 'chicken')
platter_cost + (sum of)options + cost of chicken kebab
end
Arguments explanation:
First argument: id is the main id(company_id),
Second argument: options: [1, 4]. 1 and 4 are the id's inside the Side options, The ids are unique so it doesn't matter the options are chips or drinks.
Third argument: is the cost of the chicken kebab.
So the output for the id: "01" is £16.82. coke_cost + tango_cost + chicken_kebab_cost
what is the clean and efficient way to get the results?
So far I tried the below but am a bit lost on which way to choose. Thanks in advance.
def dishes
file = File.read('food.json')
obj = JSON.parse(file)
obj['food']
end
def self_hash # Trying to create a single hash like an active record object
h = {}
dishes.each do |dish|
h["id"] = dish["id"]
h["platter_cost"] = dish["platter_cost"]
h["kebab"] = dish["kebab"].each{ |k| {"chicken: #{k["chicken"]}", "lamb: #{k["lamb"]}"} } # Not working
end
end
This is an awkward data structure to work with. It's unfortunate it can't be changed, but we can do things to make it easier to work with.
First, turn it into a class so we have something to hang behavior off of.
class Dishes
attr_reader :dishes
def initialize(dishes)
#dishes = dishes
end
Now we need to get the right pieces of dishes. Unfortunately dishes is poorly designed. We can't just do dishes[id] we need to search through Arrays for matches. With a class we can write methods to abstract away working with this awkward data structure.
Let's abstract away having to dig into the Food key every time.
def menus
#dishes.fetch(:Food)
end
Note that it's the Symbol :Food, not the string "Food". "Food":[...] produces a Symbol.
Note that I'm using fetch because unlike [] it will throw a KeyError if Food is not found. This makes error handling much easier. I'll be using fetch consistently through the code.
Also note that the method is called menus because this appears to be a better description of what dishes["Food"] is: a list of menus for various locations.
Now we can search menus for a matching id using Enumerable#find. Again, we abstract this away in a method.
def menu(id)
menu = menus.find { |m| m.fetch(:id) == id }
raise "Can't find menu id #{id}" if !menu
return Menu.new(menu)
end
Not only is finding a menu abstracted away, but we also have proper error handling if we can't find it.
Now that we've found the menu we want, we can ignore the rest of the data structure. We have a Menu class just for working with the menu.
class Menu
attr_reader :menu
def initialize(menu)
#menu = menu
end
We can now fetch the kebabs. Searching an Array is awkward. Let's turn it into a more useful Hash keyed on the name of the kebab.
# Turn the list of kebabs into a hash keyed on
# the name. Cache the result.
def kebabs
#kebabs ||= menu.fetch(:Kebab).each_with_object({}) { |k,h|
h[ k[:name] ] = k
}
end
Now we can search the Hash of kebabs for matching names using Hash#fetch_values. Note it's names because someone might want to order more than one delicious kebab.
def find_kebabs(names = [])
kebabs.fetch_values(*names)
end
An advantage of this approach is we'll get a KeyError if a kebab does not exist.
Like with the kebabs, we want to turn all the sides into one hash keyed on the ID. Getting all the sides is a bit tricky. They're broken up into several different Arrays. We can use flat_map to flatten the sides into one Array.
def sides
# Flatten out the list of sides into one Array.
# Then turn it into a Hash keyed on the ID
#sides ||= menu.fetch(:sides).flat_map { |types|
types.fetch(:options)
}.each_with_object({}) { |s,h|
h[ s[:id] ] = s
}
end
Now that it's flattened we can search the Hash just like we did with kebabs.
def find_sides(ids = [])
sides.fetch_values(*ids)
end
Now that we have these methods we can find the sides and kebabs. Again, the data structure is working against us. The price is in a string with a £. If we want to total up the prices we need to turn "£4.00" into 4.00
def price_to_f(price)
price.gsub(/^\D*/, '').to_f
end
And where the price is stored is inconsistent. For kebabs it's value and for sides its price. More methods to smooth this over.
def side_price(side)
price_to_f(side.fetch(:price))
end
def kebab_price(kebab)
price_to_f(kebab.fetch(:value))
end
(Note: Kebab and Side could be their own classes with their own price methods)
Finally we can put it all together. Find the items and sum their prices.
def price(kebabs:[], sides:[])
price = find_kebabs(kebabs).sum { |k| kebab_price(k) }
price += find_sides(sides).sum { |s| side_price(s) }
return price
end
It would look like so.
dishes = Dishes.new(data)
menu = dishes.menu("01")
p menu.price(kebabs: ["chicken"], sides: [1,3])
If any kebabs or sides are not found you get a KeyError.
menu.price(kebabs: ["chicken"], sides: [1,398,3])
test.rb:149:in `fetch_values': key not found: 398 (KeyError)
We can make the error handling a bit more robust by writing up some custom KeyError exceptions.
class Menu
class SideNotFoundError < KeyError
def message
#message ||= "Side not found: #{key}"
end
end
class KebabNotFoundError < KeyError
def message
#message ||= "Kebab not found: #{key}"
end
end
end
Then we can modify our finder methods to throw these exceptions instead of a generic KeyError.
def find_sides(ids = [])
sides.fetch_values(*ids)
rescue KeyError => e
raise SideNotFoundError, key: e.key
end
def find_kebabs(names = [])
kebabs.fetch_values(*names)
rescue KeyError => e
raise KebabNotFoundError, key: e.key
end
These more specific errors allow for more robust error handling while maintaining the Menu black box.
begin
price = menu.price(kebabs: ["chicken"], sides: [1,398,3])
# more code that depends on having a price
rescue Menu::KebabNotFoundError => e
# do something when a kabab is not found
rescue Menu::SideNotFoundError => e
# do something when a side is not found
end
This might seem like overkill, I'm sure someone can come up with some clever compressed code. It's worth it. I work with awkward and inconsistent data structures all the time; a class makes working with them much easier in the long run.
It breaks the problem down into small pieces. These pieces can then be unit tested, documented, given robust error handling, and used to build more functionality.
Here it is all spelled out.
class Dishes
attr_reader :dishes
def initialize(dishes)
#dishes = dishes
end
def menus
dishes.fetch(:Food)
end
def menu(id)
menu = menus.find { |m| m[:id] == id }
raise "Can't find menu id #{id}" if !menu
return Menu.new(menu)
end
end
class Menu
attr_reader :menu
def initialize(menu)
#menu = menu
end
def sides
# Flatten out the list of sides and turn it into
# a Hash keyed on the ID.
#sides ||= menu.fetch(:sides).flat_map { |types|
types.fetch(:options)
}.each_with_object({}) { |s,h|
h[ s[:id] ] = s
}
end
# Turn the list of kebabs into a hash keyed on
# the name.
def kebabs
#kebabs ||= menu.fetch(:Kebab).each_with_object({}) { |k,h|
h[ k[:name] ] = k
}
end
def find_sides(ids = [])
sides.fetch_values(*ids)
rescue KeyError => e
raise SideNotFoundError, key: e.key
end
def find_kebabs(names = [])
kebabs.fetch_values(*names)
rescue KeyError => e
raise KebabNotFoundError, key: e.key
end
def price_to_f(price)
price.gsub(/^\D*/, '').to_f
end
def side_price(side)
price_to_f(side.fetch(:price))
end
def kebab_price(kebab)
price_to_f(kebab.fetch(:value))
end
def price(kebabs:[], sides:[])
price = find_kebabs(kebabs).sum { |k| kebab_price(k) }
price += find_sides(sides).sum { |s| side_price(s) }
return price
end
class SideNotFoundError < KeyError
def message
#message ||= "Side not found: #{key}"
end
end
class KebabNotFoundError < KeyError
def message
#message ||= "Kebab not found: #{key}"
end
end
end
def total_cost(h, id:, options:, kebab:)
g = h[:Food].find { |g| g[:id] == id }
g[:Kebab].find { |f| f[:name] == kebab }[:value][1..-1].to_f +
g[:sides].sum do |f|
f[:options].sum { |f| options.include?(f[:id]) ? f[:price][1..-1].to_f : 0 }
end
end
total_cost(h, id: "01", options: [1, 3], kebab: 'chicken')
#=> 16.82
total_cost(h, id: "01", options: [1, 3, 4], kebab: 'chicken')
#=> 20.82
The first step results in
g #=> {:id=>"01", :name=>"ABC", :branch=>"London", :platter_cost=>"£40.00",
# :Kebab=>[{:name=>"chicken", :value=>"£8.12"},
# {:terms=>"lamb", :value=>"£9.67"}],
# :sides=>[{:type=>"drinks",
# :options=>[
# {:id=>1, :name=>"Coke", :price=>"£4.70"},
# {:id=>2, :name=>"Pepsi", :price=>"£2.90"},
# {:id=>3, :name=>"Tango", :price=>"£4.00"}
# ]
# },
# {:type=>"chips",
# :options=>[
# {:id=>4, :name=>"Peri-Peri", :price=>"£4.00"}
# ]
# }
# ]
# }
Note: [].sum #=> 0.
I'd like to calculate the difference for various values inside 2 hashes with the same structure, as concisely as possible. Here's a simplified example of the data I'd like to compare:
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
I have a very simple method to get the value I want to compare. In reality, the hash can be nested a lot deeper than these examples, so this is mostly to keep the code readable:
def get_y(data)
data["x"]["y"]
end
I want to create a method that will calculate the difference between the 2 values, and can take a method like get_y as an argument, allowing me to re-use the code for any value in the hash. I'd like to be able to call something like this, and I'm not sure how to write the method get_delta:
get_delta(hash1, hash2, get_y) # => 8
The "Ruby way" would be to pass a block:
def get_delta_by(obj1, obj2)
yield(obj1) - yield(obj2)
end
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
get_delta_by(hash1, hash2) { |h| h["x"]["y"] }
#=> 8
A method could be passed (indirectly) via:
def get_y(data)
data["x"]["y"]
end
get_delta_by(hash1, hash2, &method(:get_y))
#=> 8
Building on Stefan's response, if you want a more flexible get method you can actually return a lambda from the function and pass arguments for what you want to get. This will let you do error handling nicely:
Starting with the basics from above...
def get_delta_by(obj1, obj2)
yield(obj1) - yield(obj2)
end
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
get_delta_by(hash1, hash2) { |h| h["x"]["y"] }
Then we can define a get_something function which takes a list of arguments for the path of the element to get:
def get_something(*args)
lambda do |data|
args.each do |arg|
begin
data = data.fetch(arg)
rescue KeyError
raise RuntimeError, "KeyError for #{arg} on path #{args.join(',')}"
end
end
return data
end
end
Finally we call the function using the ampersand to pass the lambda as a block:
lambda_getter = get_something("x","y")
get_delta_by(hash1, hash2, &lambda_getter)
That last bit can be a one liner... but wrote it as two for clarity here.
In Ruby 2.3, you can use Hash#dig method, if it meets your needs.
hash1.dig("x", "y") - hash2.dig("x", "y")
#=> 8
I have a simple function which takes a JSON and 'does something' with it. The main part works good BUT the function returns not only what I want but additionally the result of .each loop!
The code:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
# do stuff - we have only 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# this is what I want to return to init.pp
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
Variables in the init.pp are:
$default_volume_groups = {
'sys' => {
'physical_volumes' => [
'/dev/sda2',
],
'logical_volumes' => {
'root' => {'size' => '4G'},
'swap' => {'size' => '256M'},
'var' => {'size' => '8G'},
'docker' => {'size' => '16G'},
},
},
}
and the second argument from a hieradata:
modified_volume_groups:
logical_volumes:
cloud_log:
size: '16G'
In the init.pp I have something like this to test it:
notice(mlh($default_volume_groups, $modified_volume_groups))
which gives me a result:
syslogical_volumesvarsize8Gdockersize16Gcloud_logsize16Gswapsize256Mrootsize4Gphysical_volumes/dev/sda2
Notice: Scope(Class[Ops_lvm]): sys
The "long" part before the Notice is the proper result from the puts but the Notice: Scope(): sys is this what I do not want to!
I know that this is the result of this each loop over the default_volumes_groups:
lvm_default_hash.keys.each do |key|
# some stuff
end
How to block of this unwanted result? It blows my puppet's logic because my init.pp sees this sys and not what I want.
Does someone knows how to handle such problem?
Thank you!
I found how to handle this problem but maybe someone could explain me why it works in this way :)
This does not work (short version):
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
but this works:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
# empty Hash
hash_to_return = {}
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# assigned value in the 'each' loop we want to return to puppet
hash_to_return = Hash[key => pv_array.merge(merged_lv_hash)]
end
# returned Hash - instead of previous 'puts'
return hash_to_return
end
end
end
end
Now I have what I need!
Notice: Scope(Class[Ops_lvm]): sysphysical_volumes/de
You've got it -- the first one doesn't work because in Ruby, the return value of a block or function is the last evaluated statement. In the case of the one that didn't work, the last evaluated statement was the .each. As it turns out, each evaluates to the enumerable that it was looping through.
A simple example:
def foo
[1, 2, 3].each do |n|
puts n
end
end
If I were to run this, the return value of the function would be the array:
> foo
1
2
3
=> [1, 2, 3]
So what you have works, because the last thing evaluated is return hash_to_return. You could even just go hash_to_return and it'd work.
If you wanted to get rid of the return and clean that up a little bit (and if you're using Ruby 1.9 or above), you could replace your each line with:
lvm_default_hash.keys.each_with_object({}) do |key, hash_to_return|
This is because each_with_object evaluates to the "object" (in this case the empty hash passed into the method, and referred to as hash_to_return in the block params). If you do this you can remove the return as well as the initialization hash_to_return = {}.
Hope this helps!
Your custom function has rvalue type which means it needs to return value. If you don't specify return <something> by default, your last statement is implicitly your return.
In the example above, first one that does not work correctly, has last statement inside each block:
puts Hash[key => pv_array.merge(merged_lv_hash)]
Your second example is correct simply because you set value for hash_to_return in each block and then "return" it outside of each block. Not sure if this is the behavior you want since last assigned hash value (in last loop inside each block) will be the one that will be returned from this function.
I have created a wildcard matcher for RR that matches JSON strings by parsing them into hashes. This is because JSON (de)serialization doesn't preserve order; if we have:
{ 'foo': 42, 'bar': 123 }
... then after (de)serialization, we might find that our update method is called with:
{ 'bar': 123, 'foo': 42 }
The wildcard matcher looks like this:
class RR::WildcardMatchers::MatchesJsonString
attr_reader :expected_json_hash
def initialize(expected_json_string)
#expected_json_hash = JSON.parse(expected_json_string)
end
def ==(other)
other.respond_to?(:expected_json_hash) && other.expected_json_hash == self.expected_json_hash
end
def wildcard_matches?(actual_json_string)
actual_json_hash = JSON.parse(actual_json_string)
#expected_json_hash == actual_json_hash
end
end
module RR::Adapters::RRMethods
def matches_json(expected_json_string)
RR::WildcardMatchers::MatchesJsonString.new(expected_json_string)
end
end
... and we're using it like:
describe 'saving manifests' do
before do
#manifests = [
{ :sections => [], 'title' => 'manifest1' },
{ :sections => [], 'title' => 'manifest2' }
]
mock(manifest).create_or_update!(matches_json(#manifests[0].to_json)) { raise 'uh oh' }
mock(manifest).create_or_update!(matches_json(#manifests[1].to_json))
parser = ContentPack::ContentPackParser.new({
'manifests' => #manifests
})
#errors = parser.save
end
it 'updates manifests' do
manifest.should have_received.create_or_update!(anything).twice
end
end
This in accordance with the RR documentation. However, instead of mock() expecting an argument that matches JSON, it expects the argument to be a MatchesJsonString object:
1) ContentPack::ContentPackParser saving manifests updates manifests
Failure/Error: mock(Manifest).create_or_update!(matches_json(#manifests[0].to_json)) { raise 'uh oh' }
RR::Errors::TimesCalledError:
create_or_update!(#<RR::WildcardMatchers::MatchesJsonString:0x13540def0 #expected_json_hash={"title"=>"manifest1", "sections"=>[]}>)
Called 0 times.
Expected 1 times.
# ./spec/models/content_pack/content_pack_parser_spec.rb:196
The answer is that there's a typo in the documentation to which I linked. This (my emphasis):
#wildcard_matches?(other)
wildcard_matches? is the method that actually checks the argument against the expectation. It should return true if other is considered to match, false otherwise. In the case of DivisibleBy, wildcard_matches? reads:
... should actually read:
#wildcard_match?(other)
...
One of my colleagues suggested that we compare our code with one of the matchers defined in the rr gem, and then the difference stood out.
I have the following class called Tree that builds a simple tree
class Tree
attr_accessor :children, :node_name
def initialize(name, children=[])
#children = children
#node_name = name
end
def visit_all(&block)
visit &block
children.each {|c| c.visit_all &block}
end
def visit(&block)
block.call self
end
end
ruby_tree = Tree.new("grandpa",
[Tree.new("dad", [Tree.new("child1"), Tree.new("child2")]),
Tree.new("uncle", [Tree.new("child3"), Tree.new("child4")])])
puts "Visiting a node"
ruby_tree.visit {|node| puts node.node_name}
puts
puts "visiting entire tree"
ruby_tree.visit_all {|node| puts node.node_name}
Now what I am trying to do is to be able to create a tree as nested hashes instead. For example, for this one this would be:
{'grandpa'=>{'dad'=>{'child 1'=>{},'child 2'=>{}}, 'uncle'=>{'child 3'=>{}, 'child 4'=>{}}}}
Any ideas that could help?
It was melting my brain so I wrote a spec for it:
# encoding: UTF-8
require 'rspec' # testing/behaviour description framework
require_relative "../tree.rb" # pull in the actual code
# Everything in the `describe` block is rspec "tests"
describe :to_h do
# contexts are useful for describing what happens under certain conditions, in the first case, when there is only the top of the tree passed to to_h
context "One level deep" do
# a let is a way of declaring a variable in rspec (that keeps it useful)
let(:ruby_tree) { Tree.new "grandpa" }
let(:expected) { {"grandpa" => {} } }
subject { ruby_tree.to_h } # this the behaviour you're testing
it { should == expected } # it should equal what's in expected above
end
# The next two contexts are just testing deeper trees. I thought that each depth up to 3 should be tested, as past 3 levels it would be the same as 3.
context "Two levels deep" do
let(:ruby_tree) {
Tree.new( "grandpa",
[Tree.new("dad"), Tree.new("uncle") ]
)
}
let(:expected) do
{"grandpa" => {
"dad" => {}, "uncle" => {}
}
}
end
subject { ruby_tree.to_h }
it { should == expected }
end
context "grandchildren" do
let(:ruby_tree){
ruby_tree = Tree.new("grandpa",
[Tree.new("dad", [Tree.new("child1"), Tree.new("child2")]),
Tree.new("uncle", [Tree.new("child3"), Tree.new("child4")])])
}
let(:expected) {
{'grandpa'=>{'dad'=>{'child1'=>{},'child2'=>{}}, 'uncle'=>{'child3'=>{}, 'child4'=>{}}}}
}
subject { ruby_tree.to_h }
it { should == expected }
end
end
class Tree
def to_h
hash ={} # create a hash
# `reduce` is a synonym for `inject`, see the other answer for a link to the docs,
# but it's a type of fold
# http://en.wikipedia.org/wiki/Fold_(higher-order_function),
# which will take a list of several objects and
# fold them into one (or fewer, but generally one) through application of a function.
# It reduces the list through injecting a function, hence the synonyms.
# Here, the current node's list of children is folded into one hash by
# applying Hash#merge to each child (once the child has been been made
# into a one key hash, possibly with children too), and then assigned as
# the current node's hash value, with the node_name as the key.
hash[#node_name] = children.reduce({}){|mem,c| mem.merge c.to_h}
hash # return the hash
end
end
I'm certain this could be done better, but it works at least.
Btw, the hash you provided has some extra spaces in it that I don't think should be there? e.g. "child 1" when it should be "child1", unless you really want that added in?
class Tree
def to_hash
{ #node_name => #children.inject({}) { |acum, child| acum.merge(child.to_hash) } }
end
end
p ruby_tree.to_hash
See documentation for inject here
Break it into simpler subproblems and use recursion:
def make_node(name,subhash)
Tree.new(name,subhash.keys.collect{|k|make_node(k,subhash[k])})
end
def make_root(hash)
make_node(hash.keys[0],hash[hash.keys[0]])
end
Then to prove it works:
tree_like_this = make_root({'grandpa' => { 'dad' => {'child 1' => {}, 'child 2' => {} },
'uncle' => {'child 3' => {}, 'child 4' => {} } } })
puts 'tree like this'
tree_like_this.visit_all{|n|puts n.node_name}
This was an exercise from Seven Languages In Seven Weeks. The original exercise said to put it all in initialize.