t56k
t56k

Reputation: 6981

Delete all similar keys from nested hash

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

Answers (2)

Cary Swoveland
Cary Swoveland

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

Schwern
Schwern

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

Related Questions