Reputation: 6981
Given a hash like this:
{
id: 1,
name: "test",
children: [
{ id: 1, name: "kid 1" },
{ id: 2, name: "kid 2" }
]
}
How can I remove all id
keys recursively?
Upvotes: 2
Views: 116
Reputation: 110645
This of course calls for a recursive solution. The following method does not mutate the original hash.
Code
def recurse(obj, key_to_delete)
case obj
when Array
obj.map { |e| recurse(e, key_to_delete) }
else # hash
obj.reject { |k,_| k == key_to_delete }.transform_values do |v|
case v
when Hash, Array
recurse(v, key_to_delete)
else
v
end
end
end
end
Example
h = { id: 1, name: "test", children: [
{ id: 1, name: "kid 1" }, { id: 2, name: "kid 2", grandkids: [
{ id: 3, name: "gkid1" }] }
]
}
recurse(h, :id)
#=> { :name=>"test", :children=>[
# {:name=>"kid 1"}, {:name=>"kid 2", :grandkids=>[{:name=>"gkid1"}]}
# ]
# }
Explanation
The operations performed by recursive methods are always difficult to explain. In my experience the best way is to salt the code with puts statements. However, that in itself is not enough because when viewing output it is difficult to keep track of the level of recursion at which particular results are obtained and either passed to itself or returned to a version of itself. The solution to that is to indent and un-indent results, which is what I've done below. Note the way I've structured the code and the few helper methods I use are fairly general-purpose, so this approach can be adapted to examine the operations performed by other recursive methods.
INDENT = 8
@col = -INDENT
def indent; @col += INDENT; end
def undent; @col -= INDENT; end
def pu(s); print " "*@col; puts s; end
def puhline; pu('-'*(70-@col)); end
def recurse(obj, key_to_delete)
begin # rem
indent # rem
puhline # rem
pu "passed obj = #{obj}, key_to_delete = #{key_to_delete}" # rem
case obj
when Array
pu "processing Array..." # rem
obj.map do |e|
pu " calling recurse(#{e}, #{key_to_delete})..." # rem
recurse(e, key_to_delete)
end
else # hash
obj.reject { |k,_| k == :id }.
tap { |h| pu " obj with :id removed=#{h}" }. # rem
transform_values do |v|
pu " in tranform_values, v=#{v}" # rem
case v
when Hash, Array
pu " v is Hash or Arrary" # rem
pu " calling recurse(#{v}, #{key_to_delete})..." # rem
recurse(v, key_to_delete)
else
pu " keeping the literal v" # rem
v
end
end
end.tap { |obj| pu "returning #{obj}" }
ensure
puhline # rem
undent
end
end
recurse(h, :id)
----------------------------------------------------------------------
passed obj = {:id=>1, :name=>"test", :children=>[{:id=>1, :name=>"kid 1"},
{:id=>2, :name=>"kid 2", :grandkids=>[{:id=>3, :name=>"gkid1"}]}]},
key_to_delete = id
obj with :id removed={:name=>"test", :children=>[{:id=>1, :name=>"kid 1"},
{:id=>2, :name=>"kid 2", :grandkids=>[{:id=>3, :name=>"gkid1"}]}]}
in tranform_values, v=test
keeping the literal v
in tranform_values, v=[{:id=>1, :name=>"kid 1"},
{:id=>2, :name=>"kid 2", :grandkids=>[{:id=>3, :name=>"gkid1"}]}]
v is Hash or Arrary
calling recurse([{:id=>1, :name=>"kid 1"},
{:id=>2, :name=>"kid 2", :grandkids=>[{:id=>3, :name=>"gkid1"}]}], id)...
--------------------------------------------------------------
passed obj = [{:id=>1, :name=>"kid 1"},
{:id=>2, :name=>"kid 2", :grandkids=>[{:id=>3, :name=>"gkid1"}]}],
key_to_delete = id
processing Array...
calling recurse({:id=>1, :name=>"kid 1"}, id)...
------------------------------------------------------
passed obj = {:id=>1, :name=>"kid 1"}, key_to_delete = id
obj with :id removed={:name=>"kid 1"}
in tranform_values, v=kid 1
keeping the literal v
returning {:name=>"kid 1"}
------------------------------------------------------
calling recurse({:id=>2, :name=>"kid 2",
:grandkids=>[{:id=>3, :name=>"gkid1"}]}, id)...
------------------------------------------------------
passed obj = {:id=>2, :name=>"kid 2", :grandkids=>
[{:id=>3, :name=>"gkid1"}]},
key_to_delete = id
obj with :id removed={:name=>"kid 2", :grandkids=>
[{:id=>3, :name=>"gkid1"}]}
in tranform_values, v=kid 2
keeping the literal v
in tranform_values, v=[{:id=>3, :name=>"gkid1"}]
v is Hash or Arrary
calling recurse([{:id=>3, :name=>"gkid1"}], id)...
----------------------------------------------
passed obj = [{:id=>3, :name=>"gkid1"}],
key_to_delete = id
processing Array...
calling recurse({:id=>3, :name=>"gkid1"}, id)...
--------------------------------------
passed obj = {:id=>3, :name=>"gkid1"},
key_to_delete = id
obj with :id removed={:name=>"gkid1"}
in tranform_values, v=gkid1
keeping the literal v
returning {:name=>"gkid1"}
--------------------------------------
returning [{:name=>"gkid1"}]
----------------------------------------------
returning {:name=>"kid 2", :grandkids=>[{:name=>"gkid1"}]}
------------------------------------------------------
returning [{:name=>"kid 1"},
{:name=>"kid 2", :grandkids=>[{:name=>"gkid1"}]}]
--------------------------------------------------------------
returning {:name=>"test", :children=>[{:name=>"kid 1"},
{:name=>"kid 2", :grandkids=>[{:name=>"gkid1"}]}]}
----------------------------------------------------------------------
#=> {:name=>"test", :children=>[{:name=>"kid 1"},
{:name=>"kid 2", :grandkids=>[{:name=>"gkid1"}]}]}
Upvotes: 0
Reputation: 164639
You can write a function to recursively traverse Hashes and Arrays.
def delete_recursively(thing, key_to_delete)
case thing
when Hash
# Delete the key
thing.delete(key_to_delete)
# Recurse into each remaining hash value.
thing.each_value do |value|
delete_recursively(value, key_to_delete)
end
when Array
# Recurse into each value of the array.
thing.each do |value|
delete_recursively(value, key_to_delete)
end
end
end
This can be expanded to include other data types as necessary.
Upvotes: 5