How do I extract a sub-hash from a hash?
Asked Answered
S

18

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}
Sidnee answered 26/1, 2012 at 21:1 Comment(2)
side note: apidock.com/rails/Hash/slice%21Untouched
@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.Cocaine
W
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} 
Whitewash answered 26/1, 2012 at 21:11 Comment(3)
This is O(n2) - you'll have one loop on the select, another loop on the include that will be called h1.size times.Streeter
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 cleanerNeri
.slice & .except are the right answer, see bellowHibernia
C
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.

Cocaine answered 17/10, 2012 at 19:20 Comment(1)
The result of x.except!(:c, :d) (with bang) should be # => {:a=>1, :b=>2}. Good if you can edit your answer.Handhold
W
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} 
Whitewash answered 26/1, 2012 at 21:11 Comment(3)
This is O(n2) - you'll have one loop on the select, another loop on the include that will be called h1.size times.Streeter
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 cleanerNeri
.slice & .except are the right answer, see bellowHibernia
S
42

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}
Sirocco answered 20/4, 2018 at 17:2 Comment(0)
S
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
Streeter answered 30/5, 2015 at 22:20 Comment(2)
Mokey patching is definitely the way to go IMO. Much cleaner and makes the intent clearer.Naquin
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::HashHamann
P
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

Pettitoes answered 11/4, 2012 at 16:39 Comment(2)
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.Flapper
ActiveSupport is not part of the Ruby STIHadj
E
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}
Emplacement answered 26/1, 2012 at 21:24 Comment(2)
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}Peekaboo
An equivalent one-line (and perhaps faster) solution:<pre> def subhash(*keys) select {|k,v| keys.include?(k)} endThayne
C
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}
Casmey answered 28/7, 2015 at 4:43 Comment(0)
H
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:

Hardhack answered 3/10, 2018 at 13:47 Comment(0)
L
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

Lilytrotter answered 19/7, 2021 at 8:9 Comment(0)
D
2

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

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}
Dynamics answered 18/6, 2012 at 14:4 Comment(0)
M
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)
Moderator answered 23/5, 2019 at 18:3 Comment(0)
M
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)
Malayalam answered 26/1, 2012 at 21:38 Comment(0)
F
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)
Freethinker answered 16/7, 2017 at 17:34 Comment(1)
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.Casmey
P
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.

Peekaboo answered 26/1, 2012 at 21:23 Comment(0)
H
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"}]
Hindquarter answered 2/5, 2018 at 21:47 Comment(0)
B
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 
Barefoot answered 5/7, 2019 at 10:10 Comment(0)
E
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)
Exchangeable answered 5/7, 2019 at 11:7 Comment(0)
O
0

I arrived here in search of subtly different behavior which is available in ActiveSupport since 3.0.0 as Hash#extract!:

a = {a: 1, b: 2, c: 3}

b = a.extract!(:b)
# => {b: 2}

a
# => {a: 1, c: 3}
Oni answered 1/5 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.