Note: As of Ruby 3.0.0, Hash#except
is now part of the language. However, Ruby does not implement Hash#except!
.
Sometimes I want to remove a specific key-value pair from a Ruby hash and get the resulting hash back. If you're using Rails or ActiveSupport you can accomplish this using Hash#except
:
hash = { a: 1, b: 2, c: 3 }
hash.except(:a) # => { b: 2, c: 3 }
# note, the original hash is not modified
hash # => { a: 1, b: 2, c: 3 }
# it also works with multiple key-value pairs
hash.except(:a, :c) # => { b: 2 }
# and returns the original hash if the key doesn't exist
hash.except(:q) # => { a: 1, b: 2, c: 3 }
# there's also Hash#except!
hash.except!(:b) # => { a: 1, c: 3 }
# this method does modify the original hash
hash # => { a: 1, c: 3 }
# and also returns the original hash if the key doesn't exist
hash.except!(:b) # => { a: 1, c: 3 }
Great. But what if we want to do this with plain old Ruby?
hash = { a: 1, b: 2, c: 3, d: 4 }
# the destructive case with removing exactly one key-value pair is easy
hash.tap { |h| h.delete(:a) } # => { b: 2, c: 3, d: 4 }
hash # => { b: 2, c: 3, d: 4 }
# the non-destructive case with one key-value pair isn't much harder
hash.dup.tap { |h| h.delete(:c) } # => { b: 2, d: 4 }
hash # => { b: 2, c: 3, d: 4 }
# but when we want to remove several key-value pairs, it gets a little messy
hash.dup.tap { |h| [:b, :c].map { |k| h.delete(k) } } # => { d: 4 }
hash # => { b: 2, c: 3, d: 4 }
# using reject makes it more palatable
hash.reject { |k, _| [:b, :c].include? k } # => { d: 4 }
hash # => { b: 2, c: 3, d: 4 }
# and, of course, we can also use reject!
hash.reject! { |k, _| [:b, :c].include? k } # => { d: 4 }
hash # => { d: 4 }
# not so fast...reject! doesn't behave the way we want when the keys aren't there :-(
hash.reject! { |k, _| [:b, :c].include? k } # => nil
hash # => { d: 4 }
# so maybe tap-map-delete is the best we can do here
hash.tap { |h| [:b, :c].map { |k| h.delete(k) } } # => { d: 4 }
hash # => { d: 4 }
How would you choose to do it?