Question

Filter Hash Keys with Regular Expressions in Ruby

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/
  • \A means the beginning of the string.
  • choice matches those exact letters.
  • \d+ matches one or more digits.
  • \z means 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.

Take Quiz

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:

  1. select reads the first pair: :irrelevant and "A String".
  2. :irrelevant.to_s becomes "irrelevant".
  3. "irrelevant" does not match /\Achoice\d+\z/, so that pair is excluded.
  4. :choice1.to_s becomes "choice1". It matches: it has choice followed by 1, so the pair is kept.
  5. :choice20 also matches because \d+ accepts multiple digits.
  6. :choice_name does not match because an underscore and letters are not digits.
  7. choices.values produces ["First", "Second"].
  8. 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, and choice3 while 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, and feature3 from a larger settings hash.
  • Delimited exports: Filter approved fields, then use join to 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.

Take Quiz

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

TaskRuby approachResult
Keep matching hash pairshash.select { ... }A new hash
Remove matching hash pairshash.reject { ... }A new hash
Transform every pairhash.transform_values { ... } or hash.map { ... }Changed values or an array
Get only valueshash.valuesAn array of values
Build delimited textarray.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.

Take Quiz

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")
ExpressionMeaning
Hash#selectReturns a new hash containing pairs that pass the block
key.to_sConverts a symbol or string key to a string
\A and \zBeginning and end of the complete string
\d+One or more digits
valuesReturns 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.

Take Quiz

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

  1. Accept a hash whose keys may be symbols or strings.
  2. Keep only keys named choice followed by one or more digits.
  3. Ignore unrelated keys and keys such as choice_name.
  4. Sort accepted keys by their numeric suffix.
  5. Return the selected values as one tab-delimited string.
Take Quiz

Keep learning

Related questions

View more