Bagaimana cara mengekstrak sub-hash dari hash?

96

Saya memiliki hash:

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

Apa cara terbaik untuk mengekstrak sub-hash seperti ini?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
sawa
sumber
4
catatan samping: apidock.com/rails/Hash/slice%21
tokland
kemungkinan duplikat dari Ruby Hash Filter
John Dvorak
1
@JanDvorak Pertanyaan ini tidak hanya tentang mengembalikan subhash tetapi juga tentang mengubah yang sudah ada. Hal yang sangat mirip tetapi ActiveSupport memiliki cara berbeda untuk mengatasinya.
skalee

Jawaban:

59

Jika Anda secara khusus ingin metode untuk mengembalikan elemen yang diekstrak tetapi h1 tetap sama:

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} 

Dan jika Anda ingin menambalnya ke dalam kelas 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

Jika Anda hanya ingin menghapus elemen tertentu dari hash, itu jauh lebih mudah menggunakan 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
sumber
2
Ini adalah O (n2) - Anda akan memiliki satu loop pada seleksi, loop lain pada include yang akan dipanggil h1.size times.
metakungfu
1
Meskipun jawaban ini layak untuk ruby ​​murni, jika Anda menggunakan rel, jawaban di bawah ini (menggunakan sliceexcept
bawaan
138

ActiveSupport, Setidaknya sejak 2.3.8, menyediakan empat metode yang mudah: #slice, #exceptdan rekan-rekan mereka yang merusak: #slice!dan #except!. Mereka disebutkan dalam jawaban lain, tetapi untuk menjumlahkannya di satu tempat:

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}

Perhatikan nilai kembalian dari metode bang. Mereka tidak hanya akan menyesuaikan hash yang ada tetapi juga mengembalikan entri yang dihapus (tidak disimpan). Yang Hash#except!paling cocok dengan contoh yang diberikan dalam pertanyaan:

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}

ActiveSupporttidak membutuhkan seluruh Rails, cukup ringan. Faktanya, banyak permata non-rel bergantung padanya, jadi kemungkinan besar Anda sudah memilikinya di Gemfile.lock. Tidak perlu memperluas kelas Hash Anda sendiri.

skalee
sumber
3
Hasil dari x.except!(:c, :d)(dengan bang) seharusnya # => {:a=>1, :b=>2}. Bagus jika Anda bisa mengedit jawaban Anda.
244an
28

Jika Anda menggunakan rel , hash # slice adalah cara yang tepat.

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

Jika Anda tidak menggunakan rails , Hash # values_at akan mengembalikan nilai dalam urutan yang sama seperti yang Anda minta sehingga Anda dapat melakukan ini:

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

Penjelasan:

Dari yang {:a => 1, :b => 2, :c => 3}kita inginkan{: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}

Jika Anda merasa seperti menambal monyet adalah cara yang harus dilakukan, berikut ini yang Anda inginkan:

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
metakungfu
sumber
2
Penambalan mokey jelas merupakan cara untuk masuk IMO. Jauh lebih bersih dan membuat maksudnya lebih jelas.
Romário
1
Tambahkan untuk mengubah kode ke alamat modul inti yang benar, tentukan modul dan impor perluasan Hash core ... modul CoreExtensions modul Hash def slice (* keys) :: Hash [[keys, self.values_at (* keys)]. Transpose] end end akhiri Hash.include CoreExtensions :: Hash
Ronan Fauglas
27

Ruby 2.5 menambahkan Hash # slice :

h = { a: 100, b: 200, c: 300 }
h.slice(:a)           #=> {:a=>100}
h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}
dhulihan
sumber
5

Anda dapat menggunakan slice! (* Kunci) yang tersedia di ekstensi inti ActiveSupport

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

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

initial_hash sekarang akan menjadi

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

extracted_slide sekarang akan menjadi

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

Anda bisa melihat slice.rb in ActiveSupport 3.1.3

Vijay
sumber
Saya pikir Anda sedang mendeskripsikan ekstrak !. ekstrak! menghapus kunci dari hash awal, mengembalikan hash baru yang berisi kunci yang dihapus. mengiris! melakukan hal sebaliknya: hapus semua kecuali kunci yang ditentukan dari hash awal (sekali lagi, mengembalikan hash baru yang berisi kunci yang dihapus). Sangat bagus! sedikit lebih seperti operasi "pertahankan".
Russ Egan
1
ActiveSupport bukan bagian dari 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}
Ryan LeCompte
sumber
1
Pekerjaan yang baik. Tidak seperti yang dia minta. Metode Anda mengembalikan: {: d =>: D,: b =>: B,: e => nil,: f => nil} {: c =>: C,: a =>: A,: d => : D,: b =>: B}
Andy
Solusi satu baris yang setara (dan mungkin lebih cepat): <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
puncaknya
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}
Cary Swoveland
sumber
2

jika Anda menggunakan rel, mungkin lebih mudah menggunakan Hash.except

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

Berikut adalah perbandingan kinerja cepat dari metode yang disarankan, #selectsepertinya yang tercepat

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)

Perbaikan akan terlihat seperti ini:

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

Dan untuk menggunakannya:

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

Keduanya delete_ifdan keep_ifmerupakan bagian dari inti Ruby. Di sini Anda dapat mencapai apa yang Anda inginkan tanpa menambal Hashjenisnya.

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}

Untuk info lebih lanjut, periksa tautan di bawah dari dokumentasi:

Menandai
sumber
1

Seperti yang disebutkan orang lain, Ruby 2.5 menambahkan metode Hash # slice.

Rails 5.2.0beta1 juga menambahkan versi Hash # slice miliknya sendiri untuk memberikan fungsionalitas bagi pengguna framework yang menggunakan versi Ruby sebelumnya. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

Jika ingin menerapkan milik Anda sendiri karena alasan apa pun, itu juga bagus:

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

Kode ini memasukkan fungsionalitas yang Anda minta ke dalam kelas 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

dan memberikan hasil yang Anda berikan:

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}

Catatan: metode ini sebenarnya mengembalikan kunci / nilai yang diekstrak.

Andy
sumber
0

Berikut adalah solusi fungsional yang dapat berguna jika Anda tidak menjalankan Ruby 2.5 dan jika Anda tidak ingin mencemari kelas Hash Anda dengan menambahkan metode baru:

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

Kemudian Anda dapat menerapkannya bahkan pada hash bersarang:

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

Hanya tambahan untuk metode slice, jika kunci subhash yang ingin Anda pisahkan dari hash asli akan menjadi dinamis, Anda dapat melakukannya,

slice(*dynamic_keys) # dynamic_keys should be an array type 
YasirAzgar
sumber
0

Kita dapat melakukannya dengan mengulang pada kunci yang ingin kita ekstrak dan hanya memeriksa apakah kunci tersebut ada dan kemudian mengekstraknya.

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
sumber