109

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}
sawa
  • 165,429
  • 45
  • 277
  • 381
  • 5
    side note: http://apidock.com/rails/Hash/slice%21 – tokland Jan 26 '12 at 21:59
  • 1
    @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. – skalee Dec 05 '13 at 12:52

17 Answers17

156

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.

John
  • 9,249
  • 5
  • 44
  • 76
skalee
  • 12,331
  • 6
  • 55
  • 57
  • 3
    The result of `x.except!(:c, :d)` (with bang) should be `# => {:a=>1, :b=>2}`. Good if you can edit your answer. – 244an Dec 13 '15 at 16:11
59

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} 
Gazler
  • 83,029
  • 18
  • 279
  • 245
  • 2
    This is O(n2) - you'll have one loop on the select, another loop on the include that will be called h1.size times. – magicgregz May 30 '15 at 22:27
  • 3
    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 – Krease Mar 31 '16 at 21:25
  • .slice & .except are the right answer, see bellow – Oz Ben-David Feb 06 '21 at 18:35
41

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}
dhulihan
  • 11,053
  • 9
  • 40
  • 45
30

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
magicgregz
  • 7,471
  • 3
  • 35
  • 27
  • 2
    Mokey patching is definitely the way to go IMO. Much cleaner and makes the intent clearer. – Romário May 09 '16 at 23:42
  • 1
    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 – Ronan Fauglas Sep 08 '16 at 10:33
6

You can use slice!(*keys) which is available in the core extensions of ActiveSupport

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

extracted_slice = initial_hash.slice!(:a, :c)

initial_hash would now be

{:b => 2, :d =>4}

extracted_slide would now be

{:a => 1, :c =>3}

You can look at slice.rb in ActiveSupport 3.1.3

Ajay Barot
  • 1,681
  • 1
  • 21
  • 37
Vijay
  • 314
  • 1
  • 4
  • 8
  • 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. – Russ Egan Sep 27 '12 at 17:48
  • 1
    ActiveSupport is not part of the Ruby STI – Volte Dec 15 '16 at 04:54
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}
Ryan LeCompte
  • 4,281
  • 1
  • 14
  • 14
  • 1
    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} – Andy Jan 26 '12 at 21:26
  • An equivalent one-line (and perhaps faster) solution:
      `def subhash(*keys) 
        select {|k,v| keys.include?(k)}
      end`
    – peak May 28 '14 at 04:06
4
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
keys = [:b, :d, :e, :f]

h2 = (h1.keys & keys).each_with_object({}) { |k,h| h.update(k=>h1.delete(k)) }
  #=> {:b => :B, :d => :D}
h1
  #=> {:a => :A, :c => :C}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
3

Both delete_if and keep_if are part of Ruby core. Here you can achieve what you would like to without patching the Hash type.

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

For futher info, check the links below from the documentation:

Mark
  • 5,994
  • 5
  • 42
  • 55
3

if you want to extract from data base record also it is better to use slice

hash = { a: 1, b: 2, c: 3, d: 4 }
hash.slice!(:a, :b)  # => {:c=>3, :d=>4}
hash                 # => {:a=>1, :b=>2}

https://api.rubyonrails.org/classes/Hash.html#method-i-slice-21

milad rahmani
  • 101
  • 1
  • 2
2

if you use rails, it may be convenient to use Hash.except

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}
gayavat
  • 18,910
  • 11
  • 45
  • 55
2

As others have mentioned, Ruby 2.5 added the Hash#slice method.

Rails 5.2.0beta1 also added it's own version of Hash#slice to shim the functionality for users of the framework that are using an earlier version of Ruby. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

If looking to implement your own for whatever reason, it's a nice one liner as well:

 def slice(*keys)
   keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
 end unless method_defined?(:slice)
josh
  • 332
  • 1
  • 7
1

Here is a quick performance comparison of the suggested methods, #select seems to be the fastest

k = 1_000_000
Benchmark.bmbm do |x|
  x.report('select') { k.times { {a: 1, b: 2, c: 3}.select { |k, _v| [:a, :b].include?(k) } } }
  x.report('hash transpose') { k.times { Hash[ [[:a, :b], {a: 1, b: 2, c: 3}.fetch_values(:a, :b)].transpose ] } }
  x.report('slice') { k.times { {a: 1, b: 2, c: 3}.slice(:a, :b) } }
end

Rehearsal --------------------------------------------------
select           1.640000   0.010000   1.650000 (  1.651426)
hash transpose   1.720000   0.010000   1.730000 (  1.729950)
slice            1.740000   0.010000   1.750000 (  1.748204)
----------------------------------------- total: 5.130000sec

                     user     system      total        real
select           1.670000   0.010000   1.680000 (  1.683415)
hash transpose   1.680000   0.010000   1.690000 (  1.688110)
slice            1.800000   0.010000   1.810000 (  1.816215)

The refinement will look like this:

module CoreExtensions
  module Extractable
    refine Hash do
      def extract(*keys)
        select { |k, _v| keys.include?(k) }
      end
    end
  end
end

And to use it:

using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)
Vadym Tyemirov
  • 8,288
  • 4
  • 42
  • 38
  • 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. – Cary Swoveland Mar 22 '22 at 19:11
1
class Hash
  def extract(*keys)
    key_index = Hash[keys.map{ |k| [k, true] }] # depends on the size of keys
    partition{ |k, v| key_index.has_key?(k) }.map{ |group| Hash[group] }  
  end
end

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2, h1 = h1.extract(:b, :d, :e, :f)
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
0

Here's a functional solution that can be useful if you're not running on Ruby 2.5 and in the case that you don't wan't to pollute your Hash class by adding a new method:

slice_hash = -> keys, hash { hash.select { |k, _v| keys.include?(k) } }.curry

Then you can apply it even on nested hashes:

my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]
Martinos
  • 2,116
  • 1
  • 21
  • 28
0

Just an addition to slice method, if the subhash keys which you want to separate from original hash is going to be dynamic you can do like,

slice(*dynamic_keys) # dynamic_keys should be an array type 
YasirAzgar
  • 1,385
  • 16
  • 15
0

We can do it by looping on keys only we want to extract and just checking the key is exist and then extract it.

class Hash
  def extract(*keys)
    extracted_hash = {}
    keys.each{|key| extracted_hash[key] = self.delete(key) if self.has_key?(key)}
    extracted_hash
  end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.extract(:b, :d, :e, :f)
Praveen
  • 240
  • 1
  • 5
0

This code injects the functionality you're asking for into the Hash class:

class Hash
    def extract_subhash! *keys
      to_keep = self.keys.to_a - keys
      to_delete = Hash[self.select{|k,v| !to_keep.include? k}]
      self.delete_if {|k,v| !to_keep.include? k}
      to_delete
    end
end

and produces the results you provided:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
p h1.extract_subhash!(:b, :d, :e, :f) # => {b => :B, :d => :D}
p h1 #=> {:a => :A, :c => :C}

Note: this method actually returns the extracted keys/values.

Andy
  • 11,215
  • 5
  • 31
  • 33