Как мне извлечь из хеша под-хеш?

96

У меня есть хеш:

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}
сава
источник
4
примечание стороны: apidock.com/rails/Hash/slice%21
tokland
возможный дубликат Ruby Hash Filter
Джон Дворжак
1
@JanDvorak Этот вопрос касается не только возврата субхэша, но и изменения существующего. Очень похожие вещи, но ActiveSupport предлагает разные способы их решения.
skalee 05

Ответы:

59

Если вы специально хотите, чтобы метод возвращал извлеченные элементы, но 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) - у вас будет один цикл для выбора, другой цикл для включения, который будет называться h1.size times.
metakungfu
1
Хотя этот ответ подходит для чистого рубина, если вы используете рельсы, приведенный ниже ответ (с использованием встроенного sliceили except, в зависимости от ваших потребностей) намного чище
Krease
138

ActiveSupport, По крайней мере , так как 2.3.8, обеспечивает четыре удобные методы: #slice, #exceptи их разрушительные аналоги: #slice!и#except! . Они упоминались в других ответах, но суммировать их в одном месте:

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}

Обратите внимание на возвращаемые значения методов взрыва. Они не только адаптируют существующий хэш, но и возвращают удаленные (не сохраненные) записи. В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}

ActiveSupportне требует целых Rails, довольно легкий. Фактически, от него зависит множество не-рельсовых гемов, поэтому, скорее всего, он у вас уже есть в Gemfile.lock. Нет необходимости расширять класс Hash самостоятельно.

скали
источник
3
Результат x.except!(:c, :d)(на ура) должен быть # => {:a=>1, :b=>2}. Хорошо, если ты сможешь отредактировать свой ответ.
244an
28

Если вы используете рельсы , вам подойдет Hash # slice .

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

Если вы не используете рельсы , 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 - это, безусловно, путь. Намного чище и проясняет намерения.
Romário
1
Добавить, чтобы изменить код для адресации corectly core module, определить модуль и импортировать расширение Hash core ... модуль CoreExtensions модуль Hash def slice (* keys) :: Hash [[keys, self.values_at (* keys)]. Transpose] end end end Hash.include CoreExtensions :: Hash
Ронан Фауглас 08
5

Вы можете использовать slice! (* Ключи), который доступен в основных расширениях ActiveSupport.

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

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

initial_hash теперь будет

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

extract_slide теперь будет

{: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}
Энди,
Эквивалентное однострочное (и, возможно, более быстрое) решение: <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_ifи keep_ifявляются частью ядра Ruby. Здесь вы можете добиться желаемого, не исправляя 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)
Правин
источник