如何从散列中提取子散列?

我有个大麻:

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

提取这样的子散列的最佳方法是什么?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
74236 次浏览

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}

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.

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}
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)

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

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

h = {a:1, b:2}
h1 = h.except(:a) # {b: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.

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
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}

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)

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}

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"}]

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:

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)

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

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)

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