Change tree structure in ruby to use nested hashes - ruby

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.

Related

Accessing nested hashes with accessors in ruby

So I have a hash:
a = {
foo: {
bar: 1
}
}
Now I can access value 1 with a[:foo][:bar].
How would I go on about generating methods from this automatically so I could access the value with a.foo.bar?
Is this even possible? If it is how could I generate this for a predetermined hash?
This is doable with https://ruby-doc.org/stdlib-3.0.0/libdoc/ostruct/rdoc/OpenStruct.html from the standard library.
To make this work recursively we can use:
require 'ostruct'
def to_os(obj)
case obj
when Hash
OpenStruct.new(obj.transform_values { |h| to_os(h) })
when Array
obj.map { |o| to_os(o) }
else
obj
end
end
a = { foo: { bar: 1 } }
b = to_os(a)
puts b[:foo]
puts b.foo
puts b[:foo][:bar]
But as noted in the comments this becomes an OpenStruct, which means that the output of the upper code is:
#<OpenStruct bar=1>
#<OpenStruct bar=1>
1
So what we deduce from this that the nested hash gets lost.

get values from a multi array nested hashes ruby

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.

Extending a ruby class (hash) with new function (recursive_merge)

If I want to recursively merge 2 hashes, I can do so with the following function:
def recursive_merge(a,b)
a.merge(b) {|key,a_item,b_item| recursive_merge(a_item,b_item) }
end
This works great, in that I can now do:
aHash = recursive_merge(aHash,newHash)
But I'd like to add this as a self-updating style method similar to merge!. I can add in the returning function:
class Hash
def recursive_merge(newHash)
self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
end
end
But am not sure how to re-create the bang function that updates the original object without association.
class Hash
def recursive_merge!(newHash)
self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
# How do I set "self" to this new hash?
end
end
edit example as per comments.
h={:a=>{:b => "1"}
h.recursive_merge!({:a=>{:c=>"2"})
=> {:a=>{:b=>"1", :c="2"}}
The regular merge results in :b=>"1" being overwritten by :c="2"
Use merge! rather than attempt to update self. I don't believe it makes sense to use merge! anywhere but at the top level, so I wouldn't call the bang version recursively. Instead, use merge! at the top level, and call the non-bang method recursively.
It may also be wise to check both values being merged are indeed hashes, otherwise you may get an exception if you attempt to recursive_merge on a non-hash object.
#!/usr/bin/env ruby
class Hash
def recursive_merge(other)
self.merge(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
end
def recursive_merge!(other)
self.merge!(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
end
end
h1 = { a: { b:1, c:2 }, d:1 }
h2 = { a: { b:2, d:4 }, d:2 }
h3 = { d: { b:1, c:2 } }
p h1.recursive_merge(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1.recursive_merge(h3) # => {:a=>{:b=>1, :c=>2}, :d=>{:b=>1, :c=>2}}
p h1.recursive_merge!(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1 # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
If you have a specific reason to fully merge in place, possibly for speed, you can experiment with making the second function call itself recursively, rather than delegate the recursion to the first function. Be aware that may produce unintended side effects if the hashes store shared objects.
Example:
h1 = { a:1, b:2 }
h2 = { a:5, c:9 }
h3 = { a:h1, b:h2 }
h4 = { a:h2, c:h1 }
p h3.recursive_merge!(h4)
# Making recursive calls to recursive_merge
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>1, :b=>2}}
# Making recursive calls to recursive_merge!
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>5, :b=>2, :c=>9}}
As you can see, the second (shared) copy of h1 stored under the key :c is updated to reflect the merge of h1 and h2 under the key :a. This may be surprising and unwanted. Hence why I recommend using recursive_merge for the recursion, and not recursive_merge!.

How to do recursion in ruby

I have a hash of hashes to display as tree, something like routes. Below, I added an example of an expected result and the result I got.
Example hash:
hash = {
'movies' => {
'action' => {
'2007' => ['video1.avi', 'x.wmv'],
'2008' => ['']
},
'comedy' => {
'2007' => [],
'2008' => ['y.avi']
}
},
'audio' => {
'rock' => {
'2003' => [],
'2004' => ['group', 'group1']
}
}
}
I expected this result:
movies
movies\action
movies\action\2007
movies\action\2007\video1.avi
movies\action\2007\x.wmv
movies\action\2008
movies\comedy\2007
movies\comedy\2008
movies\comedy\2008\y.avi
audio
audio\rock\2003
audio\rock\2004
audio\rock\2004\group
audio\rock\2004\group1
Here are some code I made:
def meth(key, val)
val.each do |key1, val1|
puts "#{key}/#{key1}"
meth(key1, val1) if val1
end
end
hash.each do |key, val|
puts key
meth(key,val)
end
It returns this result:
movies
movies/action
action/2007
2007/video1.avi
2007/x.wmv
action/2008
2008/
movies/comedy
comedy/2007
comedy/2008
2008/y.avi
audio
audio/rock
rock/2003
rock/2004
2004/group
2004/group1
Can anybody explain how to do this?
UPDATE
Thanks for answers. In this case I figured out using this code. The hint was to set key1 to the previous result.
def meth key, val
val.each do |key1, val1|
puts "#{key}/#{key1}"
key1 = "#{key}/#{key1}"
meth(key1, val1) if val1
end
end
you could change the code to:
def meth(key, val)
val.each do |key1, val1|
puts "#{key}/"
if (val1 && val1.is_a?(Hash))
meth(key1, val1)
else
puts "#{val1}"
end
end
end
you are expecting the method to work differently dependant on where it's called but that's not the case. The method does the same regardless of where it's called (e.g. if it's called by it self).
Recursion is the act of deviding one problem into smaller subproblems. There'll always be at least two. In your case the two sub problems is
- print two values
- print the key and iterate a hash
At least one of your subproblems need to end the recursion otherwise it will run forever. In the above case the first subproblem ends the recursion.
You have to keep track of the path as an array:
def meth key, val
val.each do |key1, val1|
puts key.join("/")+"/"+key1
meth(key + [key1], val1) if val1
end
end
meth [], root_of_hash
When I have a nested structure that can contain different types of classes I like to create a case statement so it is easy to define what will happen in different scenarios.
def print_tree(input, path=[])
case input
when Hash then input.flat_map{|x,y| print_tree(y, path+[x])}
when Array then input.empty? ? [path] : input.map{|x| path+[x]}
end
end
puts print_tree(my_hash).map{|z|z.join('/')}

Traversing a Hash Recursively in Ruby

I'm having a problem with this function that traverses a Hash. The Hash may contain an Array of Hashes. I want the method to search for an id and then return just the nested hash it finds.
It seems to work for the traversal, but it returns the original value passed in.
require 'rubygems'
require 'ruby-debug'
def find_by_id(node, find_this="")
if node.is_a?(Hash)
node.each do |k,v|
if v.is_a?(Array)
v.each do |elm|
if elm["_id"] == find_this && !find_this.empty?
return elm # THIS IS WHAT I WANT!
else
find_by_id(elm, find_this)
end
end
end
end
end
end
x = {"name" => "first", "_id"=>'4c96a9a56f831b0eb9000005', "items"=>["name" => "second", "_id"=>'4c96a9af6f831b0eb9000009', "others"=>[{"name" => "third", "_id"=>'4c96a9af6f831b0eb9000007'}, {"name" => "fourth", "_id"=>'4c96a9af6f831b0eb9000008'}] ] }
find_by_id(x, '4c96a9af6f831b0eb9000008')
When you invoke find_by_id recursively, you're not doing anything with the return value. You need to check whether it found something and if so return that, i.e.:
result = find_by_id(elm, find_this)
return result if result
You also need to return nil at the end of the method (after the each loop), so it returns nil if nothing was found. If you don't, it'll return the return value of each which is the hash that you iterated over.
Edit:
Here's the full code with the changes I outlined:
def find_by_id(node, find_this="")
if node.is_a?(Hash)
node.each do |k,v|
if v.is_a?(Array)
v.each do |elm|
if elm["_id"] == find_this && !find_this.empty?
return elm # THIS IS WHAT I WANT!
else
result = find_by_id(elm, find_this)
return result if result
end
end
end
end
end
# Return nil if no match was found
nil
end
Edit2:
An alternative approach, that I find cleaner, is to separate the logic for iterating the structure from the logic for finding the element with the right id:
def dfs(hsh, &blk)
return enum_for(:dfs, hsh) unless blk
yield hsh
hsh.each do |k,v|
if v.is_a? Array
v.each do |elm|
dfs(elm, &blk)
end
end
end
end
def find_by_id(hsh, search_for)
dfs(hsh).find {|node| node["_id"] == search_for }
end
By making dfs return an Enumerable we can use the Enumerable#find method, which makes the code a bit simpler.
This also enables code reuse if you ever need to write another method that needs to iterate through the hash recursively, as you can just reuse the dfs method.

Resources