ハッシュからサブハッシュを抽出するにはどうすればよいですか?


95

私はハッシュを持っています:

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}



1
@JanDvorakこの質問は、サブハッシュを返すことだけでなく、既存のものを変更することについてもです。非常に似ていますが、ActiveSupportはそれらを処理するための異なる手段を持っています。
スケーリー2013

回答:


58

メソッドが抽出された要素を返すようにしたいがh1は同じままである場合:

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} 

そして、それをHashクラスにパッチしたい場合:

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

指定した要素をハッシュから削除するだけの場合は、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} 

2
これはO(n2)です。selectに1つのループがあり、h1.size回呼び出されるincludeに別のループがあります。
メタカンフー

1
この答えは純粋なルビーにはまともですが、レールを使用している場合は、以下の答え(必要に応じて組み込みsliceまたはを使用except)がより
明確

137

ActiveSupportは、少なくとも2.3.8以降、4つの便利なメソッドを提供しています:#slice#exceptおよびそれらの破壊的な対応物:#slice!および#except!。それらは他の回答で言及されましたが、それらを1か所にまとめると:

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}

bangメソッドの戻り値に注意してください。既存のハッシュを調整するだけでなく、削除された(保持されていない)エントリも返します。Hash#except!スーツ最高の質問に与えられた例:

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}

ActiveSupportRails全体を必要とせず、かなり軽量です。実際、非Rails gemの多くはそれに依存しているため、おそらくGemfile.lockにすでに存在しています。自分でHashクラスを拡張する必要はありません。


3
x.except!(:c, :d)(バングあり)の結果はになります# => {:a=>1, :b=>2}。回答を編集できる場合は問題ありません。
2015

28

railsを使用する場合はHash#slice適しています

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

railsを使用しない場合Hash#values_atは要求されたのと同じ順序で値を返すため、これを行うことができます。

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

例:

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

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

説明:

{:a => 1, :b => 2, :c => 3}私たちの外に{: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}

モンキーパッチを適用する方法だと思われる場合は、次の手順を実行してください。

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

2
Mokeyパッチは間違いなくIMOへの道です。はるかにクリーンで意図が明確になります。
ロマリオ2016年

1
コードを修正して、コアコアモジュールをアドレス指定し、モジュールを定義し、拡張Hashコアをインポートします...モジュールCoreExtensionsモジュールHash def slice(* keys):: Hash [[keys、self.values_at(* keys)]。transpose] end end Hash.include CoreExtensions :: Hashの終了
Ronan Fauglas


5

ActiveSupportのコア拡張で利用可能なスライスを使用できます(* keys)

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

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

initial_hashは

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

抽出されたスライドは

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

あなたは見ることができます slice.rb in ActiveSupport 3.1.3


あなたは抽出物を説明していると思います!。エキス!最初のハッシュからキーを削除し、削除されたキーを含む新しいハッシュを返します。スライス!反対のことを行います。すべて削除しますが(再び、削除キーを含む新しいハッシュを返す)初期ハッシュから指定されたキーを。だからスライス!「保持」操作に少し似ています。
ラスイーガン

1
ActiveSupportはRuby STIの一部ではありません
Volte

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

1
良くやった。彼が求めているものとはかなり違います。あなたのメソッドは次を返します:{:d =>:D、:b =>:B、:e => nil、:f => nil} {:c =>:C、:a =>:A、:d => :D、:b =>:B}
アンディ

同等の1行の(そしておそらくより高速な)ソリューション:<pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
ピークは

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

2

レールを使用する場合は、Hash.exceptを使用すると便利です。

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}

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)

1

ここに提案された方法の簡単なパフォーマンス比較が#selectあり、最速のようです

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)

改良は次のようになります。

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

そしてそれを使うには:

using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)

1

両方delete_ifkeep_ifRubyのコアの一部です。ここでは、Hash型にパッチを適用せずに、希望することを実現できます。

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}

詳細については、ドキュメントから以下のリンクを確認してください。


1

他の人が述べたように、Ruby 2.5はHash#sliceメソッドを追加しました。

Rails 5.2.0beta1には、独自のバージョンのHash#sliceも追加されており、以前のバージョンのRubyを使用しているフレームワークのユーザー向けに機能をシム化しています。 https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

何らかの理由で独自のものを実装することを検討している場合は、それも素晴らしいワンライナーです:

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

0

このコードは、求めている機能をHashクラスに挿入します。

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

あなたが提供した結果を生成します:

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}

注:このメソッドは、実際には抽出されたキー/値を返します。


0

Ruby 2.5で実行していない場合や、新しいメソッドを追加してHashクラスを汚染したくない場合に役立つ機能的なソリューションを次に示します。

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

次に、ネストされたハッシュにも適用できます。

my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]

0

スライスメソッドに追加するだけで、元のハッシュから分離したいサブハッシュキーが動的になる場合は、次のようにすることができます。

slice(*dynamic_keys) # dynamic_keys should be an array type 

0

抽出したいキーだけをループし、キーが存在することを確認するだけでそれを抽出することで、それを行うことができます。

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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.