I have an issue trying to update a nested Hash using a key.
The nested hash I have is like this:
main_hash = {
"Energy"=>
{"name"=>"Energy", "uri"=>"energy",
"children"=>
开发者_运维问答 {"Renewable Energy"=>{"name"=>"Renewable Energy", "uri"=>"energy/renewable_energy"}}}
,
"Farming"=>
{"name"=>"Farming", "uri"=>"farming",
"children"=>
{"Organic Gas"=>
{"name"=>"Organic Gas", "uri"=>"farming/organic_gas"
"children" =>
{"Gas Oil"=>{"name"=>"Gas Oil", "uri"=>"farming/organic_gas/gas_oil"}}
}}}}
What I would like to do is to update an item from the hash (e.g., I want to add another child to "Organic Gas"). I know I can do this:
main_hash["Farming"]["children"]["Organic Gas"]["children"].merge!(another_hash)
The problem is I need to get that dynamically as it can get quite deeply nested.
So to get to the desired level, I would do this (which does work as above).
main_hash.send(:fetch, "Farming").send(:fetch, "children").send(:fetch, "Organic Gas").send(:fetch, "children")
It would really be great if I could call "send" method dynamically like below (obviously it won't work).
main_hash.send(:try, 'send(:fetch, "Farming").send(:fetch, "children").send(:fetch, "Organic Gas").send(:fetch, "children")')
I hope it makes it clear what I want to achieve. I have gone through all Ruby Hash's built in function and I can't get the one suited for my need.
Any help would be greatly appreciated.
Cheers.
I'm not sure a Hash is really the best data structure here. You're trying to use it to represent a tree, which is fine and all, but it might be a bit clearer if you just explicitly made it a tree:
class Tree
attr_reader :name, :uri, :children, :parent
def initialize(name, uri, *children)
@children = children
@name, @uri = name, uri
end
def <<(child)
@children << child
end
def find(name)
each_branch.detect {|branch| branch.name == name }
end
def leaf?
@children.empty?
end
# The parameter `i` just decides whether or not to include self.
def each_branch( i=true, &blk )
enum = Enumerator.new do |y|
y.yield self if i
@children.each do |c|
next unless c.is_a? Tree
y.yield c
c.each_branch( false ).each {|b| y.yield b }
end
end
block_given? ? enum.each( &blk ) : enum
end
# This yields each leaf and its parent.
def each_leaf( parent=self, &blk )
enum = Enumerator.new do |y|
@children.each do |c|
if !c.leaf?
c.each_leaf( c ).each do |l,p|
y.yield l, p
end
else y.yield c, parent
end
end
end
block_given? ? enum.each( &blk ) : enum
end
end
(I just borrowed those enumerators from a tree structure I'd made before - the each_leaf
method might be helpful too, and you can check for the class not being Tree
instead of leaf?
returning true
if you had a tree structure that could contain other objects, like strings).
Then you could do:
root_tree = Tree.new "Farming", "farming"
root_tree << Tree.new( "Organic Gas", "organic_gas" )
gas = root_tree.find "Organic gas"
gas << Tree.new(...)
I think this is just a case of finding the right data structure for the job. Even if the tree method were a bit less efficient, it's clearer what's going on, and will probably lead to fewer bugs in your dynamic code down the track.
If the problem is that you don't want the original tree to be modified, only copied, then just redefine:
class Tree
attr_accessor :children
def <<(child)
new_tree = self.dup
new_tree.children = @children + [child]
new_tree
end
end
That way, you retain the original tree but return a new tree with the extra child added.
To answer the original question, you can use the XKeys gem to traverse a dynamic path as follows:
require 'xkeys' # on rubygems.org
main_hash.extend XKeys::Hash
path = ['Farming', 'children', 'Organic Gas', 'children']
main_hash[*path].merge!(another_hash)
I would also recommend using :children instead of 'children' to avoid having huge numbers of duplicate strings.
精彩评论