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}
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}
Jawaban:
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}
slice
except
ActiveSupport
, Setidaknya sejak 2.3.8, menyediakan empat metode yang mudah: #slice
, #except
dan 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}
ActiveSupport
tidak 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.
x.except!(:c, :d)
(dengan bang) seharusnya # => {:a=>1, :b=>2}
. Bagus jika Anda bisa mengedit jawaban Anda.
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
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}
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
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}
def subhash(*keys) select {|k,v| keys.include?(k)} end
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}
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)
Berikut adalah perbandingan kinerja cepat dari metode yang disarankan, #select
sepertinya 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)
Keduanya delete_if
dan keep_if
merupakan bagian dari inti Ruby. Di sini Anda dapat mencapai apa yang Anda inginkan tanpa menambal Hash
jenisnya.
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:
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)
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.
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"}]
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
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)