Question
Given the following Ruby hash, how can I keep only the entries whose keys are choice followed by one or more digits, such as :choice1 through :choice10?
params = {
irrelevant: "A String",
choice1: "Oh look, another one",
choice2: "Even more strings",
choice3: "But wait",
irrelevant2: "The last string"
}
The number of choice keys varies. How can I filter the hash using the key pattern choice plus one or more digits?
Also, how can I turn the selected hash values into a string separated by tab characters ("\t")?
Short Answer
You will learn how to use Ruby's Hash#select with a regular expression to retain matching key-value pairs. You will also see why symbol keys must be converted to strings before pattern matching and how to join the selected values with tab delimiters.
Concept
A Ruby Hash stores key-value pairs. To filter a hash, use select, which visits every pair and returns a new hash containing only pairs for which the block returns a truthy value.
In this case, the rule is based on the key's name:
- It must begin with
choice. - It must end with one or more digits.
- Nothing else may appear before or after that pattern.
A regular expression expresses this rule precisely:
/\Achoice\d+\z/
\Ameans the beginning of the string.choicematches those exact letters.\d+matches one or more digits.\zmeans the end of the string.
The hash keys in the example are symbols, such as :choice1. Regular expressions match strings, so convert each key with key.to_s first.
Using anchored patterns matters. Without anchors, a key such as :my_choice1_backup could match even though it is not a valid choice key.
Mental Model
Think of a hash as a basket of labelled cards. Each card has a label (the key) and some information (the value).
select acts like a checkpoint at the basket exit. It inspects each label and keeps only cards whose labels satisfy the rule. The regular expression is the label template:
The label must be exactly
choice, followed by a number.
After filtering, values.join("\t") takes the information from the kept cards and places a tab between each item, like filling columns in a row.
Syntax and Examples
Use Hash#select when you want a new hash containing matching pairs.
choices = params.select do |key, _value|
key.to_s.match?(/\Achoice\d+\z/)
end
A compact version is:
choices = params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
With the sample data, choices is:
{
choice1: "Oh look, another one",
choice2: "Even more strings",
choice3: "But wait"
}
To create a tab-delimited string from the selected values:
tab_delimited = choices.values.join("\t")
puts tab_delimited
# Oh look, another one Even more strings But wait
If you need both each key and its value in the output, build a string for each pair first:
rows = choices.map { |key, value| "#{key}=#{value}" }.join("\t")
# "choice1=Oh look, another one\tchoice2=Even more strings\tchoice3=But wait"
Step by Step Execution
Consider this code:
params = {
irrelevant: "A String",
choice1: "First",
choice20: "Second",
choice_name: "Not a numbered choice"
}
choices = params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
text = choices.values.join("\t")
Execution trace:
selectreads the first pair::irrelevantand"A String".:irrelevant.to_sbecomes"irrelevant"."irrelevant"does not match/\Achoice\d+\z/, so that pair is excluded.:choice1.to_sbecomes"choice1". It matches: it haschoicefollowed by1, so the pair is kept.:choice20also matches because\d+accepts multiple digits.:choice_namedoes not match because an underscore and letters are not digits.choices.valuesproduces["First", "Second"].join("\t")produces"First\tSecond".
The original params hash is unchanged because select returns a new hash.
Real World Use Cases
Filtering keys by a naming convention is useful when one hash contains both general metadata and a variable set of similarly named fields.
- Web form parameters: Keep fields such as
choice1,choice2, andchoice3while ignoring unrelated parameters. - Survey processing: Extract answer fields whose count changes by survey configuration.
- Import scripts: Read numbered columns or attributes from parsed data.
- Configuration handling: Gather keys such as
feature1,feature2, andfeature3from a larger settings hash. - Delimited exports: Filter approved fields, then use
jointo create TSV-style output.
For actual TSV files containing arbitrary text, prefer Ruby's CSV library with col_sep: "\t", because it correctly escapes tabs, quotes, and newlines inside values.
Real Codebase Usage
In production code, developers usually separate filtering from formatting when each step has a different responsibility:
CHOICE_KEY = /\Achoice\d+\z/
choices = params.select { |key, _| key.to_s.match?(CHOICE_KEY) }
output = choices.values.join("\t")
Naming the pattern makes the rule easier to reuse and test.
Guard clauses and validation
If matching choices are required, check for an empty result early:
choices = params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
raise ArgumentError, "At least one choice is required" if choices.empty?
Ordering numbered keys
Ruby hashes preserve insertion order, but insertion order is not necessarily numeric key order. For example, choice10 may appear before choice2 if it arrived first. Sort by the numeric suffix when the output needs numeric order:
ordered_choices = params
.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
.sort_by { |key, _| key.to_s.delete_prefix("choice").to_i }
.to_h
text = ordered_choices.values.join("\t")
Input types
Web frameworks may provide keys as strings rather than symbols. key.to_s supports both forms, so the same filter works for "choice1" and :choice1.
Common Mistakes
Forgetting that the keys are symbols
This is unsafe because the regular expression is being applied to a symbol:
# Broken
params.select { |key, _| key.match?(/\Achoice\d+\z/) }
Convert the key first:
params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
Using an unanchored pattern
This accepts text that merely contains the desired sequence:
# Too permissive
key.to_s.match?(/choice\d+/)
For example, it matches "old_choice1_copy". Require the entire key to match:
key.to_s.match?(/\Achoice\d+\z/)
Using \d* when at least one digit is required
* means zero or more, so it accepts choice with no number:
# Incorrect for this rule
/\Achoice\d*\z/
Use + for one or more digits:
/\Achoice\d+\z/
Expecting reject to keep matching keys
select keeps pairs when the condition is true. reject removes pairs when the condition is true.
# Keeps choice keys
params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
# Removes choice keys
params.reject { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
Joining a hash directly
join is an array method, not the usual way to format a hash. Decide whether you need keys, values, or formatted pairs:
choices.values.join("\t")
choices.keys.join("\t")
choices.map { |key, value| "#{key}=#{value}" }.join("\t")
Comparisons
| Task | Ruby approach | Result |
|---|---|---|
| Keep matching hash pairs | hash.select { ... } | A new hash |
| Remove matching hash pairs | hash.reject { ... } | A new hash |
| Transform every pair | hash.transform_values { ... } or hash.map { ... } | Changed values or an array |
| Get only values | hash.values | An array of values |
| Build delimited text | array.join("\t") | One string |
select versus filter
In Ruby, filter is an alias for select:
params.filter { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
Both return the same result. select is widely seen in Ruby code, while filter may read more clearly when the intent is explicitly filtering.
match? versus =~
key.to_s.match?(/\Achoice\d+\z/) # returns true or false
key.to_s =~ /\Achoice\d+\z/ # returns an index or nil
Use match? for a simple yes/no condition. It is clearer and does not create match-related global state. In older Ruby versions without match?, use =~ or !!(string =~ regex) in a boolean context.
Cheat Sheet
# Keep keys exactly like choice1, choice2, choice10, and so on.
pattern = /\Achoice\d+\z/
choices = params.select { |key, _| key.to_s.match?(pattern) }
# Create tab-delimited values.
text = choices.values.join("\t")
| Expression | Meaning |
|---|---|
Hash#select | Returns a new hash containing pairs that pass the block |
key.to_s | Converts a symbol or string key to a string |
\A and \z | Beginning and end of the complete string |
\d+ | One or more digits |
values | Returns an array of hash values |
join("\t") | Combines array items using tab characters |
Use /\Achoice\d+\z/ when choice with no number must be excluded. Use sorting by the numeric suffix if output must be choice1, choice2, choice10 regardless of insertion order.
FAQ
How do I select hash keys beginning with choice and ending in a number in Ruby?
Use select and an anchored regular expression:
params.select { |key, _| key.to_s.match?(/\Achoice\d+\z/) }
Why is key.to_s needed when filtering Ruby hash keys?
Keys may be symbols, such as :choice1. Regular expressions operate on strings, so to_s converts the symbol to "choice1". It also works if the key is already a string.
Does \d+ match choice10?
Yes. \d matches one digit, and + means one or more occurrences. Therefore it matches choice1, choice10, and choice999.
How can I exclude choice without a number?
Use \d+, not \d*:
/\Achoice\d+\z/
The + requires at least one digit.
How do I convert selected hash values to a tab-separated string?
Call values to get an array, then call join:
choices.values.join("\t")
Will the filtered hash keep its order?
Yes. Ruby hashes preserve insertion order, and select keeps the order of the retained pairs. Sort explicitly if you need numeric order based on the suffix.
Can I use reject instead of select?
Yes, but reverse the condition. reject removes pairs for which the block is true, while select keeps them. select is usually clearer when the goal is to retain matching choice keys.
Mini Project
Description
Build a small helper that extracts numbered survey choices from a mixed parameter hash and exports their values as a tab-delimited row. This mirrors a common task in form processing and lightweight data exports.
Goal
Create a method that keeps only choice keys with numeric suffixes, orders them numerically, and returns their values separated by tabs.
Requirements
- Accept a hash whose keys may be symbols or strings.
- Keep only keys named
choicefollowed by one or more digits. - Ignore unrelated keys and keys such as
choice_name. - Sort accepted keys by their numeric suffix.
- Return the selected values as one tab-delimited string.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.