How do I extract a sub-hash from a hash?

70,223

Solution 1

If you specifically want the method to return the extracted elements but h1 to remain the same:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.select {|key, value| [:b, :d, :e, :f].include?(key) } # => {:b=>:B, :d=>:D} 
h1 = Hash[h1.to_a - h2.to_a] # => {:a=>:A, :c=>:C} 

And if you want to patch that into the Hash class:

class Hash
  def extract_subhash(*extract)
    h2 = self.select{|key, value| extract.include?(key) }
    self.delete_if {|key, value| extract.include?(key) }
    h2
  end
end

If you just want to remove the specified elements from the hash, that is much easier using delete_if.

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h1.delete_if {|key, value| [:b, :d, :e, :f].include?(key) } # => {:a=>:A, :c=>:C} 
h1  # => {:a=>:A, :c=>:C} 

Solution 2

ActiveSupport, at least since 2.3.8, provides four convenient methods: #slice, #except and their destructive counterparts: #slice! and #except!. They were mentioned in other answers, but to sum them in one place:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.slice(:a, :b)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except(:a, :b)
# => {:c=>3, :d=>4}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

Note the return values of the bang methods. They will not only tailor existing hash but also return removed (not kept) entries. The Hash#except! suits best the example given in the question:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except!(:c, :d)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2}

ActiveSupport does not require whole Rails, is pretty lightweight. In fact, a lot of non-rails gems depend on it, so most probably you already have it in Gemfile.lock. No need to extend Hash class on your own.

Solution 3

Ruby 2.5 added Hash#slice:

h = { a: 100, b: 200, c: 300 }
h.slice(:a)           #=> {:a=>100}
h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}

Solution 4

If you use rails, Hash#slice is the way to go.

{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# =>  {:a => :A, :c => :C}

If you don't use rails, Hash#values_at will return the values in the same order as you asked them so you can do this:

def slice(hash, *keys)
  Hash[ [keys, hash.values_at(*keys)].transpose]
end

def except(hash, *keys)
  desired_keys = hash.keys - keys
  Hash[ [desired_keys, hash.values_at(*desired_keys)].transpose]
end

ex:

slice({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {'bar' => 'foo', 2 => 'two'}

except({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {:foo => 'bar'}

Explanation:

Out of {:a => 1, :b => 2, :c => 3} we want {:a => 1, :b => 2}

hash = {:a => 1, :b => 2, :c => 3}
keys = [:a, :b]
values = hash.values_at(*keys) #=> [1, 2]
transposed_matrix =[keys, values].transpose #=> [[:a, 1], [:b, 2]]
Hash[transposed_matrix] #=> {:a => 1, :b => 2}

If you feels like monkey patching is the way to go, following is what you want:

module MyExtension
  module Hash 
    def slice(*keys)
      ::Hash[[keys, self.values_at(*keys)].transpose]
    end
    def except(*keys)
      desired_keys = self.keys - keys
      ::Hash[[desired_keys, self.values_at(*desired_keys)].transpose]
    end
  end
end
Hash.include MyExtension::Hash

Solution 5

module HashExtensions
  def subhash(*keys)
    keys = keys.select { |k| key?(k) }
    Hash[keys.zip(values_at(*keys))]
  end
end

Hash.send(:include, HashExtensions)

{:a => :A, :b => :B, :c => :C, :d => :D}.subhash(:a) # => {:a => :A}
Share:
70,223

Related videos on Youtube

sawa
Author by

sawa

I have published the following two Ruby gems: dom: You can describe HTML/XML DOM structures in Ruby language seamlessly with other parts of Ruby code. Node embedding is described as method chaining, which avoids unnecessary nesting, and confirms to the Rubyistic coding style. manager: Manager generates a user's manual and a developer's chart simultaneously from a single spec file that contains both kinds of information. More precisely, it is a document generator, source code annotation extracter, source code analyzer, class diagram generator, unit test framework, benchmark measurer for alternative implementations of a feature, all in one. Comments and contributions are welcome. I am preparing a web service that is coming out soon.

Updated on March 23, 2022

Comments

  • sawa
    sawa over 2 years

    I have a hash:

    h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
    

    What is the best way to extract a sub-hash like this?

    h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
    h1 #=> {:a => :A, :c => :C}
    
    • tokland
      tokland over 12 years
    • skalee
      skalee over 10 years
      @JanDvorak This question is not only about returning subhash but also about modifying existing one. Very similar things but ActiveSupport has different means to deal with them.
  • Andy
    Andy over 12 years
    Nice job. Not quite what he's asking for. Your method returns: {:d=>:D, :b=>:B, :e=>nil, :f=>nil} {:c=>:C, :a=>:A, :d=>:D, :b=>:B}
  • Russ Egan
    Russ Egan almost 12 years
    I think you are describing extract!. extract! removes the keys from the initial hash, returning a new hash containing the removed keys. slice! does the opposite: remove all but the specified keys from the initial hash (again, returning a new hash containing the removed keys). So slice! is a bit more like a "retain" operation.
  • peak
    peak about 10 years
    An equivalent one-line (and perhaps faster) solution:<pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
  • metakungfu
    metakungfu about 9 years
    This is O(n2) - you'll have one loop on the select, another loop on the include that will be called h1.size times.
  • 244an
    244an over 8 years
    The result of x.except!(:c, :d) (with bang) should be # => {:a=>1, :b=>2}. Good if you can edit your answer.
  • Krease
    Krease over 8 years
    While this answer is decent for pure ruby, if you're using rails, the below answer (using built-in slice or except, depending on your needs) is much cleaner
  • Romário
    Romário about 8 years
    Mokey patching is definitely the way to go IMO. Much cleaner and makes the intent clearer.
  • Ronan Fauglas
    Ronan Fauglas almost 8 years
    Add to modify code to address corectly core module, define module and import extend Hash core... module CoreExtensions module Hash def slice(*keys) ::Hash[[keys, self.values_at(*keys)].transpose] end end end Hash.include CoreExtensions::Hash
  • Volte
    Volte over 7 years
    ActiveSupport is not part of the Ruby STI
  • obenda
    obenda over 3 years
    .slice & .except are the right answer, see bellow
  • Cary Swoveland
    Cary Swoveland over 2 years
    Considering the fact that the benchmark was done for just the one data set and that the results were all quite close I question whether there is a statistical basis for your conclusion "#select seems to be the fastest". As an aside, I re-ran your benchmark (pure Ruby, in March, 2022) and slice was nearly three times as fast as the other two.