ChatGPT解决这个技术问题 Extra ChatGPT

Ruby: Easiest Way to Filter Hash Keys?

I have a hash that looks something like this:

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

And I want a simple way to reject all the keys that aren't choice+int. It could be choice1, or choice1 through choice10. It varies.

How do I single out the keys with just the word choice and a digit or digits after them?

Bonus:

Turn the hash into a string with tab (\t) as a delimiter. I did this, but it took several lines of code. Usually master Rubicians can do it in one or so lines.

In terms of the bonus question, can you clarify what you'd like the string to look like.
Sure, the above example hash would yield: "Oh look, another one\tEven more strings\tBut wait" (with not \t at the end of the string, only between them)
Your example string has been cut off in the comment. You can use the edit link to edit your question and add the example in there. You can also post the ruby you've come up with so far as an example of what you want to achieve.
possible duplicate of Ruby Hash Filter

J
Jason Swett

Edit to original answer: Even though this is answer (as of the time of this comment) is the selected answer, the original version of this answer is outdated.

I'm adding an update here to help others avoid getting sidetracked by this answer like I did.

As the other answer mentions, Ruby >= 2.5 added the Hash#slice method which was previously only available in Rails.

Example:

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

End of edit. What follows is the original answer which I guess will be useful if you're on Ruby < 2.5 without Rails, although I imagine that case is pretty uncommon at this point.

If you're using Ruby, you can use the select method. You'll need to convert the key from a Symbol to a String to do the regexp match. This will give you a new Hash with just the choices in it.

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

or you can use delete_if and modify the existing Hash e.g.

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

or if it is just the keys and not the values you want then you can do:

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

and this will give the just an Array of the keys e.g. [:choice1, :choice2, :choice3]


Excellent! Once I see it, this is so simple! Thanks very much, and thank you to the others who took the time to answer also.
Symbols can be parsed with regular expressions nowadays IIRC.
@Andrew, I think you're right. I was on 1.8.7 when I was testing the answer to this question.
The counterpart to .select is .reject, if that makes your code more idiomatic.
The #slice way works for me on 2.5.3 even without activesupport.
l
lfx_cool

In Ruby, the Hash#select is a right option. If you work with Rails, you can use Hash#slice and Hash#slice!. e.g. (rails 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}

watch out for stringified keys..h1 = {'a'=>1,:b=>2} will only return {:b=>2} with h1.slice(:a,:b)
Awesome solution. I used this in returning result in rspec, where I wanted to parse a value of a hash within a hash. ``` test = {:a=>1, :b=>2, c=>{:A=>1, :B=>2}} solution==> test.c.slice(:B) ```
Replace Rails by ActiveSupport and this is perfect ;)
this does not answer the question though, as he want a way to extract the values for keys that are "choice+int". Your solution can only extract keys known ahead of time.
N
Nuno Costa

The easiest way is to include the gem 'activesupport' (or gem 'active_support').

Then, in your class you only need to

require 'active_support/core_ext/hash/slice'

and to call

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

I believe it's not worth it to be declaring other functions that may have bugs, and it's better to use a method that has been tweaked during last few years.


Activerecord is not needed for this at least on 2.5.
R
Robert

If you work with rails and you have the keys in a separate list, you can use the * notation:

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

As other answers stated, you can also use slice! to modify the hash in place (and return the erased key/values).


翟英昌

The easiest way is to include the gem 'activesupport' (or gem 'active_support').

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


Please add more details to your answer.
m
metakungfu

This is a one line to solve the complete original question:

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

But most the solutions above are solving a case where you need to know the keys ahead of time, using slice or simple regexp.

Here is another approach that works for simple and more complex use cases, that is swappable at runtime

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

Now not only this allows for more complex logic on matching the keys or the values, but it is also easier to test, and you can swap the matching logic at runtime.

Ex to solve the original issue:

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"

I really like the matcher
w
webgoesviral

With Hash Slice

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :b=>2}

# If you have an array of keys you want to limit to, you should splat them:
valid_keys = [:mass, :velocity, :time]
search(options.slice(*valid_keys))


A
Arnaud Le Blanc

With Hash::select:

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

a
ayckoster

If you want the remaining hash:

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

or if you just want the keys:

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

Depending on how many choiceX and irrelevantX you have select OR delete_if might be better. If you have more choiceX delete_if is better.
m
montrealmike

Put this in an initializer

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

Then you can do

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

or

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

P
Puhlze
params.select{ |k,v| k =~ /choice\d/ }.map{ |k,v| v}.join("\t")

M
MBO

As for bonus question:

If you have output from #select method like this (list of 2-element arrays): [[:choice1, "Oh look, another one"], [:choice2, "Even more strings"], [:choice3, "But wait"]] then simply take this result and execute: filtered_params.join("\t") # or if you want only values instead of pairs key-value filtered_params.map(&:last).join("\t") If you have output from #delete_if method like this (hash): {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"} then: filtered_params.to_a.join("\t") # or filtered_params.values.join("\t")


Great solutions. One question: Is filtered_params.map(&:last).join("\t") is short for filtered_params.map(|i| i.last).join("\t")? If so, what is 'last'? could I use &:value to get the value of the hash?
map takes block as it's argument. You can create block on the fly, with &:method construction, which would create block {|v| v.method }. In my case &:last was called on array (2-element array) argument. If you want to play with it, first check what argument are you receiving into block (get 1 argument, do not deconstruct it into multiple arguments!) and try to find 1 method (you cannot chain methods with &:method) which would return you what you want. If it's possible, then you can use shortcut, if not, then you need full block
A
Arup Rakshit
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"}

J
Jose Paez

I had a similar problem, in my case the solution was a one liner which works even if the keys aren't symbols, but you need to have the criteria keys in an array

criteria_array = [:choice1, :choice2]

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

Another example

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