Question

I have the following method to delete the specified values from a Ruby hash in my Rails application:

def remove_hash_values(hash, target = nil)
    hash.delete_if {|key, value| value == target}
    hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)}
end

Something like this works:

remove_hash_values(some_hash, :some_symbol)

However, this does not work:

remove_hash_values(some_hash, {})

It works in irb, which is what confuses me. I'm positive the correct hash is being passed (checked with many puts statements). My Ruby version is ruby-2.0.0-p247, and I'm using Rails 3. Any help would be greatly appreciated.

EDIT: This is not working, either:

def remove_hash_values(hash, target = nil)
    hash.each do |key, value|
        hash.delete(key) if value == target
    end
    hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)}
end

What the hell am I doing wrong?!

EDIT 2: Just realised that I'm actually using a HashWithIndifferentAccess, and not a Hash. That could be causing some trickery, so I'll try converting it to a Hash first and reporting back.

Was it helpful?

Solution

I've tried something similar, e.g.

2.0.0-p195 :092 > class Hash
2.0.0-p195 :093?>   def delete_values! target
2.0.0-p195 :094?>     delete_if { |k, v| v == target }
2.0.0-p195 :095?>     each_value { |v| v.delete_values!(target) if v.is_a?(Hash) }
2.0.0-p195 :096?>   end
2.0.0-p195 :097?> end

Works ok:

2.0.0-p195 :098 > { a: 1, b: 2, c: 3 }.delete_values! 2
 => {:a=>1, :c=>3} 

Also works:

2.0.0-p195 :101 > { a: 1, b: 2, c: { d: {}, e: 5 } }.delete_values!({})
 => {:a=>1, :b=>2, :c=>{:e=>5}} 

Note that this approach won't delete an empty hash if you should make an empty hash further down in the structure--if you had a hash containing only empty hashes--you need to switch the order of the recursion and deletion, e.g.

2.0.0-p195 :092 > class Hash
2.0.0-p195 :093?>   def delete_values! target
2.0.0-p195 :094?>     each_value { |v| v.delete_values!(target) if v.is_a?(Hash) }
2.0.0-p195 :095?>     delete_if { |k, v| v == target }
2.0.0-p195 :096?>   end
2.0.0-p195 :097?> end

OTHER TIPS

(Shown as answer rather than comment to enable display of code.)

Please share your failing test. The following works when run from the Rails 4.0 console with Ruby 2.0p247:

def remove_hash_values(hash, target = nil)
  hash.delete_if {|key, value| value == target}
  hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)}
end

some_hash = HashWithIndifferentAccess.new
some_hash[:foo] = :some_symbol
some_hash[:bar] = {}

remove_hash_values(some_hash, {})

puts some_hash.inspect   # => {"foo"=>:some_symbol}

Isaac, I like your idea of using recursion, because it deals with hashes nested to any level. I think I understand why your code doesn't work, and how you can fix it. Here's your code:

def remove_hash_values(hash, target = nil)
  hash.delete_if {|key, value| value == target}
  hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)}
end

Suppose

hash = {a: 10, b: {c: 10}}

and we invoke:

remove_hash_values(hash, 10)

I expect you want remove_hash_values(hash, 10) to return {}, but I believe it returns {b: {}}. Let's go through the calculations:

remove_hash_values(hash, 10)
hash.delete_if {|key, value| value == target}       # hash => {b: {c: 10}} 
hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)} calls (next line)

  remove_hash_values({c: 10})
  hash.delete_if {|key, value| value == target} # hash => {} 
  hash.each_value {|obj| remove_hash_values(obj, target) if obj.is_a?(Hash)} # hash => {}

hash => {b: {}}

Here's how I think you could do it, though I've haven't checked my code. First suppose target is not a hashor another structure, just a simple value. Try this:

def remove_hash_values(hash, target = nil)
  hash.keys.each do |key|
    value = hash[key]
    case value
    when Hash
      val = remove_hash_values(hash[key], target)  
      if val == {}
        hash.delete(key)
      else
        hash[key] = val
      end
    else
      hash.delete(key) if val == target
    end
  end
  hash
end

If you want to generalize this, so that target could be a (possibly nested) hash, I think this may work:

def remove_hash_values(hash, target = nil)
  hash.keys.each do |key|
    value = hash[key]
    if value.is_a? Hash
      if target.is_a? Hash && hash[key] == target
        hash.delete(key)
        next
      end
    else
      # value is not a hash
      hash.delete(key) if !(target.is_a? Hash) && if hash[key] == target)
      next
    end
    # Value is a hash, target may or may not be a hash, value != target
    val = remove_hash_values(hash[key], target)  
    if val == target
      hash.delete(key)
    else    
      hash(key)= val
    end
  end
  hash
end 
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top