Ruby: самый простой способ отфильтровать хэш-ключи?

225

У меня есть хеш, который выглядит примерно так:

params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

И я хочу простой способ отклонить все ключи, которые не являются выбором + int. Это может быть выбор1 или выбор1 - выбор10. Различается.

Как выделить ключи, выбрав только слово и цифру или цифры после них?

Бонус:

Превратите хеш в строку с символом табуляции (\ t) в качестве разделителя. Я сделал это, но потребовалось несколько строк кода. Обычно мастера-рубики могут делать это в одну или несколько строк.

Дерек
источник
Что касается бонусного вопроса, не могли бы вы уточнить, как бы вы хотели, чтобы строка выглядела?
mikej
Конечно, приведенный выше пример хеша даст: «О, смотри, еще одна строка \ tEven more \ tBut wait» (без \ t в конце строки, только между ними)
Дерек
Ваш пример строки обрезан в комментарии. Вы можете использовать ссылку редактирования, чтобы отредактировать свой вопрос и добавить туда пример. Вы также можете опубликовать рубин, который вы придумали, в качестве примера того, чего вы хотите достичь.
mikej
2
возможный дубликат Ruby Hash Filter
jbochi

Ответы:

281

Изменить на первоначальный ответ : Даже если этот ответ (на момент написания этого комментария) является выбранным ответом, оригинальная версия этого ответа устарела.

Я добавляю обновление здесь, чтобы помочь другим не отвлекаться на этот ответ, как я.

Как упоминается в другом ответе, Ruby> = 2.5 добавил Hash#sliceметод, который ранее был доступен только в Rails.

Пример:

> { one: 1, two: 2, three: 3 }.slice(:one, :two)
=> {:one=>1, :two=>2}

Конец редактирования. Далее следует оригинальный ответ, который, я думаю, будет полезен, если вы используете Ruby <2.5 без Rails, хотя я полагаю, что на данном этапе это довольно необычный случай.


Если вы используете Ruby, вы можете использовать selectметод. Вам нужно будет преобразовать ключ из символа в строку, чтобы выполнить регулярное выражение. Это даст вам новый хэш с выбором.

choices = params.select { |key, value| key.to_s.match(/^choice\d+/) }

или вы можете использовать delete_ifи изменять существующий хэш, например

params.delete_if { |key, value| !key.to_s.match(/choice\d+/) }

или, если вам нужны только ключи, а не значения, вы можете сделать:

params.keys.select { |key| key.to_s.match(/^choice\d+/) }

и это даст просто массив ключей, например, [:choice1, :choice2, :choice3]

MikeJ
источник
1
Превосходно! Как только я это вижу, это так просто! Большое спасибо, и спасибо всем, кто нашел время, чтобы ответить.
Дерек
2
Символы могут быть проанализированы с регулярными выражениями в настоящее время IIRC.
Эндрю Гримм
@ Андрей, я думаю ты прав. Я был на 1.8.7, когда тестировал ответ на этот вопрос.
mikej
1
Аналогом .selectявляется .reject, если это делает ваш код более идиоматических.
Джо Ацбергер
У #sliceменя способ работает на 2.5.3 даже без активной поддержки.
серый волк
305

В Ruby выбор Hash # является правильным вариантом. Если вы работаете с Rails, вы можете использовать Hash # slice и Hash # slice !. например (рельсы 3.2.13)

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

h1.slice(:a, :b)         # return {:a=>1, :b=>2}, but h1 is not changed

h2 = h1.slice!(:a, :b)   # h1 = {:a=>1, :b=>2}, h2 = {:c => 3, :d => 4}
lfx_cool
источник
12
следите за строковыми ключами..h1 = {'a' => 1,: b => 2} вернет только {: b => 2} с h1.slice (: a
,:
Отличное решение. Я использовал это при возврате результата в rspec, где я хотел проанализировать значение хеша внутри хеша. `` `test = {: a => 1,: b => 2, c => {: A => 1,: B => 2}} решение ==> test.c.slice (: B)` ` `
PriyankaK
Замените Rails на ActiveSupport, и это прекрасно;)
Geoffroy
5
это не отвечает на вопрос, так как он хочет получить способ извлечения значений для ключей, которые являются «choice + int». Ваше решение может извлечь только заранее известные ключи.
metakungfu
46

Самый простой способ - включить gem 'activesupport'(или gem 'active_support').

Тогда в вашем классе вам нужно только

require 'active_support/core_ext/hash/slice'

и позвонить

params.slice(:choice1, :choice2, :choice3) # => {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}

Я считаю, что не стоит декларировать другие функции, которые могут иметь ошибки, и лучше использовать метод, который был изменен в течение последних нескольких лет.

Нуно Коста
источник
Activerecord для этого не нужен хотя бы на 2.5.
серый волк
13

Самый простой способ - включить гем 'activesupport' (или гем 'active_support').

params.slice(:choice1, :choice2, :choice3)

翟英昌
источник
4
Пожалуйста, добавьте больше деталей к вашему ответу.
Мохит Джайн
13

Если вы работаете с рельсами и у вас есть ключи в отдельном списке, вы можете использовать *обозначение:

keys = [:foo, :bar]
hash1 = {foo: 1, bar:2, baz: 3}
hash2 = hash1.slice(*keys)
=> {foo: 1, bar:2}

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

Роберт
источник
9

Это одна строка, чтобы решить полный оригинальный вопрос:

params.select { |k,_| k[/choice/]}.values.join('\t')

Но большинство решений, приведенных выше, решают проблему, в которой вам нужно знать ключи заранее, используя sliceили просто регулярное выражение.

Вот еще один подход, который работает для простых и более сложных сценариев использования, который можно менять во время выполнения

data = {}
matcher = ->(key,value) { COMPLEX LOGIC HERE }
data.select(&matcher)

Теперь это не только учитывает более сложную логику сопоставления ключей или значений, но также облегчает тестирование, и вы можете поменять логику сопоставления во время выполнения.

Ex для решения оригинальной проблемы:

def some_method(hash, matcher) 
  hash.select(&matcher).values.join('\t')
end

params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

some_method(params, ->(k,_) { k[/choice/]}) # => "Oh look, another one\\tEven more strings\\tBut wait"
some_method(params, ->(_,v) { v[/string/]}) # => "Even more strings\\tThe last string"
metakungfu
источник
1
Мне очень нравится матч
Калин
6

Если вам нужен оставшийся хеш:

params.delete_if {|k, v| ! k.match(/choice[0-9]+/)}

или если вы просто хотите ключи:

params.keys.delete_if {|k| ! k.match(/choice[0-9]+/)}
ayckoster
источник
В зависимости от того, сколько у вас значений choiceX и irrelevantX, выберите OR или delete_if. Если у вас есть больше выбора X, лучше удалить delete_if.
Айкостер
6

Поместите это в инициализатор

class Hash
  def filter(*args)
    return nil if args.try(:empty?)
    if args.size == 1
      args[0] = args[0].to_s if args[0].is_a?(Symbol)
      self.select {|key| key.to_s.match(args.first) }
    else
      self.select {|key| args.include?(key)}
    end
  end
end

Тогда вы можете сделать

{a: "1", b: "b", c: "c", d: "d"}.filter(:a, :b) # =>  {a: "1", b: "b"}

или

{a: "1", b: "b", c: "c", d: "d"}.filter(/^a/)  # =>  {a: "1"}
montrealmike
источник
5
params.select{ |k,v| k =~ /choice\d/ }.map{ |k,v| v}.join("\t")
Puhlze
источник
4

Что касается бонусного вопроса:

  1. Если у вас есть вывод из #selectметода, подобного этому (список 2-элементных массивов):

    [[:choice1, "Oh look, another one"], [:choice2, "Even more strings"], [:choice3, "But wait"]]

    затем просто возьмите этот результат и выполните:

    filtered_params.join("\t")
    # or if you want only values instead of pairs key-value
    filtered_params.map(&:last).join("\t")
  2. Если у вас есть вывод из #delete_ifметода, подобного этому (хэш):

    {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}

    затем:

    filtered_params.to_a.join("\t")
    # or
    filtered_params.values.join("\t")
MBO
источник
Великолепные решения. Один вопрос: является ли Filter_params.map (&: last) .join ("\ t") сокращенным от Filter_params.map (| i | i.last) .join ("\ t")? Если так, что является «последним»? Могу ли я использовать &: значение, чтобы получить значение хеша?
Дерек
mapпринимает блок в качестве аргумента. Вы можете создать блок на лету, с &:methodконструкцией, которая создаст блок {|v| v.method }. В моем случае &:lastбыл вызван массив (2-элементный массив) аргумент. Если вы хотите поиграть с ним, сначала проверьте, какой аргумент вы получаете в блок (получите 1 аргумент, не разбивайте его на несколько аргументов!) И попробуйте найти 1 метод (вы не можете связать методы с &:method), который вернул бы вам то, что вы хотеть. Если это возможно, то вы можете использовать ярлык, если нет, то вам нужен полный блок
MBO
2
params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

choices = params.select { |key, value| key.to_s[/^choice\d+/] }
#=> {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}
Аруп Ракшит
источник
0

У меня была похожая проблема, в моем случае решение было одним вкладышем, который работает, даже если ключи не являются символами, но вы должны иметь ключи критериев в массиве

criteria_array = [:choice1, :choice2]

params.select { |k,v| criteria_array.include?(k) } #=> { :choice1 => "Oh look another one",
                                                         :choice2 => "Even more strings" }

Другой пример

criteria_array = [1, 2, 3]

params = { 1 => "A String",
           17 => "Oh look, another one",
           25 => "Even more strings",
           49 => "But wait",
           105 => "The last string" }

params.select { |k,v| criteria_array.include?(k) } #=> { 1 => "A String"}
Хосе Паес
источник