How do I extract a sub-hash from a hash?
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}
Related videos on Youtube
![sawa](https://i.stack.imgur.com/zL1re.jpg?s=256&g=1)
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, 2022Comments
-
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 over 12 yearsside note: apidock.com/rails/Hash/slice%21
-
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 over 12 yearsNice 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 almost 12 yearsI 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 about 10 yearsAn equivalent one-line (and perhaps faster) solution:<pre>
def subhash(*keys) select {|k,v| keys.include?(k)} end
-
metakungfu about 9 yearsThis is O(n2) - you'll have one loop on the select, another loop on the include that will be called h1.size times.
-
244an over 8 yearsThe result of
x.except!(:c, :d)
(with bang) should be# => {:a=>1, :b=>2}
. Good if you can edit your answer. -
Krease over 8 yearsWhile this answer is decent for pure ruby, if you're using rails, the below answer (using built-in
slice
orexcept
, depending on your needs) is much cleaner -
Romário about 8 yearsMokey patching is definitely the way to go IMO. Much cleaner and makes the intent clearer.
-
Ronan Fauglas almost 8 yearsAdd 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 over 7 yearsActiveSupport is not part of the Ruby STI
-
obenda over 3 years.slice & .except are the right answer, see bellow
-
Cary Swoveland over 2 yearsConsidering 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.