How to remove a key from Hash and get the remaining hash in Ruby/Rails?

526,272

Solution 1

Rails has an except/except! method that returns the hash with those keys removed. If you're already using Rails, there's no sense in creating your own version of this.

class Hash
  # Returns a hash that includes everything but the given keys.
  #   hash = { a: true, b: false, c: nil}
  #   hash.except(:c) # => { a: true, b: false}
  #   hash # => { a: true, b: false, c: nil}
  #
  # This is useful for limiting a set of parameters to everything but a few known toggles:
  #   @person.update(params[:person].except(:admin))
  def except(*keys)
    dup.except!(*keys)
  end

  # Replaces the hash without the given keys.
  #   hash = { a: true, b: false, c: nil}
  #   hash.except!(:c) # => { a: true, b: false}
  #   hash # => { a: true, b: false }
  def except!(*keys)
    keys.each { |key| delete(key) }
    self
  end
end

Solution 2

Oneliner plain ruby, it works only with ruby > 1.9.x:

1.9.3p0 :002 > h = {:a => 1, :b => 2}
 => {:a=>1, :b=>2} 
1.9.3p0 :003 > h.tap { |hs| hs.delete(:a) }
 => {:b=>2} 

Tap method always return the object on which is invoked...

Otherwise if you have required active_support/core_ext/hash (which is automatically required in every Rails application) you can use one of the following methods depending on your needs:

➜  ~  irb
1.9.3p125 :001 > require 'active_support/core_ext/hash' => true 
1.9.3p125 :002 > h = {:a => 1, :b => 2, :c => 3}
 => {:a=>1, :b=>2, :c=>3} 
1.9.3p125 :003 > h.except(:a)
 => {:b=>2, :c=>3} 
1.9.3p125 :004 > h.slice(:a)
 => {:a=>1} 

except uses a blacklist approach, so it removes all the keys listed as args, while slice uses a whitelist approach, so it removes all keys that aren't listed as arguments. There also exist the bang version of those method (except! and slice!) which modify the given hash but their return value is different both of them return an hash. It represents the removed keys for slice! and the keys that are kept for the except!:

1.9.3p125 :011 > {:a => 1, :b => 2, :c => 3}.except!(:a)
 => {:b=>2, :c=>3} 
1.9.3p125 :012 > {:a => 1, :b => 2, :c => 3}.slice!(:a)
 => {:b=>2, :c=>3} 

Solution 3

Why not just use:

hash.delete(key)

hash is now the "remaining hash" you're looking for.

Solution 4

There are many ways to remove a key from a hash and get the remaining hash in Ruby.

  1. .slice => It will return selected keys and not delete them from the original hash. Use slice! if you want to remove the keys permanently else use simple slice.

    2.2.2 :074 > hash = {"one"=>1, "two"=>2, "three"=>3}
     => {"one"=>1, "two"=>2, "three"=>3} 
    2.2.2 :075 > hash.slice("one","two")
     => {"one"=>1, "two"=>2} 
    2.2.2 :076 > hash
     => {"one"=>1, "two"=>2, "three"=>3} 
    
  2. .delete => It will delete the selected keys from the original hash(it can accept only one key and not more than one).

    2.2.2 :094 > hash = {"one"=>1, "two"=>2, "three"=>3}
     => {"one"=>1, "two"=>2, "three"=>3} 
    2.2.2 :095 > hash.delete("one")
     => 1 
    2.2.2 :096 > hash
     => {"two"=>2, "three"=>3} 
    
  3. .except => It will return the remaining keys but not delete anything from the original hash. Use except! if you want to remove the keys permanently else use simple except.

    2.2.2 :097 > hash = {"one"=>1, "two"=>2, "three"=>3}
     => {"one"=>1, "two"=>2, "three"=>3} 
    2.2.2 :098 > hash.except("one","two")
     => {"three"=>3} 
    2.2.2 :099 > hash
     => {"one"=>1, "two"=>2, "three"=>3}         
    
  4. .delete_if => In case you need to remove a key based on a value. It will obviously remove the matching keys from the original hash.

    2.2.2 :115 > hash = {"one"=>1, "two"=>2, "three"=>3, "one_again"=>1}
     => {"one"=>1, "two"=>2, "three"=>3, "one_again"=>1} 
    2.2.2 :116 > value = 1
     => 1 
    2.2.2 :117 > hash.delete_if { |k,v| v == value }
     => {"two"=>2, "three"=>3} 
    2.2.2 :118 > hash
     => {"two"=>2, "three"=>3} 
    
  5. .compact => It is used to remove all nil values from the hash. Use compact! if you want to remove the nil values permanently else use simple compact.

    2.2.2 :119 > hash = {"one"=>1, "two"=>2, "three"=>3, "nothing"=>nil, "no_value"=>nil}
     => {"one"=>1, "two"=>2, "three"=>3, "nothing"=>nil, "no_value"=>nil} 
    2.2.2 :120 > hash.compact
     => {"one"=>1, "two"=>2, "three"=>3}
    

Results based on Ruby 2.2.2.

Solution 5

If you want to use pure Ruby (no Rails), don't want to create extension methods (maybe you need this only in one or two places and don't want to pollute namespace with tons of methods) and don't want to edit hash in place (i.e., you're fan of functional programming like me), you can 'select':

>> x = {:a => 1, :b => 2, :c => 3}
=> {:a=>1, :b=>2, :c=>3}
>> x.select{|x| x != :a}
=> {:b=>2, :c=>3}
>> x.select{|x| ![:a, :b].include?(x)}
=> {:c=>3}
>> x
=> {:a=>1, :b=>2, :c=>3}
Share:
526,272
Misha Moroshko
Author by

Misha Moroshko

I build products that make humans happier. Previously Front End engineer at Facebook. Now, reimagining live experiences at https://muso.live

Updated on July 08, 2022

Comments

  • Misha Moroshko
    Misha Moroshko almost 2 years

    To add a new pair to Hash I do:

    {:a => 1, :b => 2}.merge!({:c => 3})   #=> {:a => 1, :b => 2, :c => 3}
    

    Is there a similar way to delete a key from Hash ?

    This works:

    {:a => 1, :b => 2}.reject! { |k| k == :a }   #=> {:b => 2}
    

    but I would expect to have something like:

    {:a => 1, :b => 2}.delete!(:a)   #=> {:b => 2}
    

    It is important that the returning value will be the remaining hash, so I could do things like:

    foo(my_hash.reject! { |k| k == my_key })
    

    in one line.

  • Misha Moroshko
    Misha Moroshko about 13 years
    I would like to do foo(h.reject!{ |k| k == :a }). With your suggestion, I will have to do it in two lines.
  • Misha Moroshko
    Misha Moroshko about 13 years
    @dbryson: I agree that sometimes it doesn't worth it. I just wonder why there are merge, merge!, delete, but no detele!...
  • Bert Goethals
    Bert Goethals about 13 years
    if you realy need it as a one liner do: foo(hash.delete(key) || hash)
  • David J.
    David J. almost 12 years
    It would be more consistent with Ruby conventions if delete did not modify its parameter and if delete! existed and did modify its parameter.
  • Mulan
    Mulan almost 11 years
    +1 It's worth mentioning that this method is destructive on h. Hash#except will not modify the original hash.
  • Fryie
    Fryie almost 11 years
    You don't have to use the full Rails stack. You can include include ActiveSupport in any Ruby application.
  • Michael Kohl
    Michael Kohl over 10 years
    Hash#except and Hash#except! have been mentioned enough already. The Proc.new version is not very readable as you mention and also more complicated than use_remaining_hash_for_something(begin hash.delete(:key); hash end). Maybe just delete this answer.
  • frediy
    frediy over 10 years
    Shortened my answer and removed what had already been said. Keeping my answer along with your comment because they answer the question and make good recommendations for use.
  • obaqueiro
    obaqueiro about 10 years
    h = {:a => 1, :b => 2, :c => 3}; h[:a]=nil; h.each{|k,v| puts k} Is not the same as: h = {:a => 1, :b => 2, :c => 3}; h.delete(:a); h.each{|k,v| puts k}
  • MhdSyrwan
    MhdSyrwan almost 9 years
    This doesn't return the remaining hash as mentioned in the question, it will return the value associated with the deleted key.
  • eggmatters
    eggmatters almost 9 years
    delete returns the key but it does also alter the hash. As to why there is no delete!, my guess is that it semantically doesn't make sense to call delete on something and not actually delete it. calling hash.delete() as opposed to hash.delete!() would be a no-op.
  • Vignesh Jayavel
    Vignesh Jayavel over 8 years
    keys_to_delete.each {|k| hash.delete(k)} is much faster for large datasets. correct me if wrong.
  • trans
    trans over 8 years
    Just require 'facets/hash/except' and their are no "issues" (not sure what issues they would be anyway other than not 100% AS API). If you are doing a Rails project using AS makes sense, if not Facets has a much smaller footprint.
  • rewritten
    rewritten over 8 years
    @trans ActiveSupport nowadays has a quite small footprint too, and you can require only parts of it. Just like facets, but with many more eyes on it (so I suppose it gets better reviews).
  • Jezen Thomas
    Jezen Thomas over 8 years
    Readability win, and predictability fail. I'd advise people to not take this approach. I upvoted the answer anyway because it was helpful.
  • GMA
    GMA about 8 years
    To add to Fryie's answer, you don't even need to load all of ActiveSupport; you can just include them then require "active_support/core_ext/hash/except"
  • GMA
    GMA about 8 years
    too late to edit: I meant "include the gem" not "include them"
  • Madis Nõmme
    Madis Nõmme over 7 years
    slice and except are added by using ActiveSupport::CoreExtensions::Hash. They are not part of Ruby core. They can be used by require 'active_support/core_ext/hash'
  • Jackson
    Jackson almost 7 years
    @DavidJ. contrary to popular belief, an exclamation point does not indicate mutation, but rather "unusual behavior." I imagine there isn't a delete! because a key-deleting method that mutated seemed like the only "expected" behavior.
  • A moskal escaping from Russia
    A moskal escaping from Russia over 5 years
    To remove a key from a hash isn't the same as removing the value of a key from a hash. As this might lead people to confuse, it'd be better to remove this answer.
  • Jimbali
    Jimbali over 5 years
    Use h.dup.tap { |hs| hs.delete(:a) } to avoid modifying the original hash.
  • Madis Nõmme
    Madis Nõmme over 4 years
    Since Ruby 2.5 Hash#slice is in the standard library. ruby-doc.org/core-2.5.0/Hash.html#method-i-slice Yay!
  • Nakilon
    Nakilon over 4 years
    @VigneshJayavel, you are right but OP wanted the hash to be returned. each would return the array.
  • iconoclast
    iconoclast about 4 years
    @GMA: when your five-minutes-of-editing are up, you can always copy, delete, modify, and repost a comment.
  • Pablo
    Pablo over 3 years
    Thanks for you very comprehensive answer.