rubyguides

Enumerable#select

What does select do?

select returns an array containing all elements for which the block evaluates to truthy. It filters a collection based on a condition — keeping elements that match, discarding the rest.

Unlike find, which returns only the first match, select collects all matching elements into a new array. If no elements match, it returns an empty array [].

Basic Usage

With a block

[1, 2, 3, 4, 5].select { |n| n > 2 }   # => [3, 4, 5]
[1, 2, 3, 4, 5].select { |n| n > 10 }  # => []

With a predicate argument

You can pass a pattern object instead of a block. The method calls === on the pattern for each element:

Passing a class like Integer or String as a predicate to select uses Ruby’s case-equality operator === under the hood. This shortcut is compact and reads naturally: [1, 2, 3, 4].select(Integer) means “select all the integers from this list.” The regex form /^a/ works the same way, matching each element against the pattern.

[1, 2, 3, 4].select(Integer)   # => [1, 2, 3, 4]
[1, 2, 3, 4].select(String)     # => []

words = ["apple", "banana", "cherry", "apricot"]
words.select(/^a/)              # => ["apple", "apricot"]

This is equivalent to:

Ruby’s pattern-matching === operator is what makes predicate arguments work: Integer === 1 is true, while Integer === "hello" is false. The regex form /^a/ === "apple" returns true when the pattern matches. When you pass a class or regex to select, Ruby calls === on it for each element behind the scenes.

[1, 2, 3, 4].select { |n| Integer === n }  # => [1, 2, 3, 4]

Without a block

When called without a block or argument, select returns an Enumerator:

When select is called without a block, it returns an Enumerator instead of an array. This is Ruby’s convention for all Enumerable methods: omit the block to get a lazy enumerator that can be chained with other methods or iterated later.

[1, 2, 3].select  # => #<Enumerator: [1, 2, 3]:select>

This allows chaining with other methods:

The enumerator returned by a blockless select can be passed around, stored, or combined with other enumerator methods before evaluation. This lazy-by-default pattern is useful when the filtering logic is determined in one part of the program but the iteration happens elsewhere.

[1, 2, 3, 4, 5].select.with_index { |n, i| i > 1 }
# => [3, 4, 5]

Filtering Arrays

By numeric condition

When select is called without a block, it returns an Enumerator. Chaining with_index on that enumerator gives you access to both the element and its zero-based position. Here, the block filters by index rather than by element value, keeping only elements at positions greater than 1.

temperatures = [12, 15, 8, 22, 19, -3, 25]
warm_days = temperatures.select { |t| t > 18 }
warm_days  # => [22, 19, 25]

Using symbolic method references

Filtering an array of temperatures by threshold is the simplest form of select: a single numeric comparison inside the block. The block’s return value is the last expression evaluated, so t > 18 returns true or false directly without an explicit return statement.

numbers = [1, 2, 3, 4, 5, 6]
even = numbers.select(&:even?)
even  # => [2, 4, 6]

words = ["cat", "dog", "elephant", "bee"]
long_words = words.select { |w| w.length > 3 }
long_words  # => ["elephant"]

Chaining with other Enumerable methods

The symbol-to-proc shorthand &:even? calls the named method on each element — it is equivalent to { |n| n.even? }. This shortcut works for any method that takes no arguments and is especially clean with predicate methods that return a boolean.

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  .select { |n| n.even? }
  .map { |n| n * 2 }
# => [4, 8, 12, 16, 20]

Filtering Hashes

When called on a hash, select passes each key-value pair to the block:

Chaining select with map filters the collection first and then transforms only the survivors. Order matters: filtering before mapping means the block inside map runs on fewer elements, which is the more efficient arrangement when some elements will be discarded.

ages = { alice: 30, bob: 17, charlie: 25, diana: 16 }

adults = ages.select { |name, age| age >= 18 }
adults  # => { alice: 30, charlie: 25 }

# Keeping only entries where the key matches a pattern
config = { host: "localhost", port: 5432, ssl: false, debug: true }
ssl_options = config.select { |k, v| k.to_s.include?("ssl") || k == :debug }
ssl_options  # => { ssl: false, debug: true }

Using hash value as filter criterion

When select is called on a Hash, the block receives both key and value as two arguments. This is different from arrays where the block receives a single element — the two-argument destructuring gives you direct access to both the key and its associated value without nested indexing.

products = {
  widget: { price: 100, in_stock: true },
  gadget: { price: 200, in_stock: false },
  doodad: { price: 50, in_stock: true }
}

available = products.select { |_, info| info[:in_stock] }
available.keys  # => [:widget, :doodad]

expensive = products.select { |_, info| info[:price] > 75 }
expensive.keys  # => [:widget, :gadget]

find_all: The Alias

select and find_all are identical — find_all is simply an alias:

When the block destructures |_, info|, the underscore signals that the key is unused — only the value hash matters. This is a common Ruby idiom for filtering hashes by nested attributes, and it scales to deeply nested structures by chaining key access inside the block.

[1, 2, 3, 4].select      { |n| n > 2 }  # => [3, 4]
[1, 2, 3, 4].find_all    { |n| n > 2 }  # => [3, 4]

Both methods share the same implementation, return type, and behaviour. Use whichever reads more naturally in your context. select is more common in Ruby code; find_all may read better when you want to emphasise “finding all” matches rather than “selecting by” a condition.

# These are functionally identical:
results.select { |item| item.active? }
results.find_all { |item| item.active? }

select vs reject

select and reject are opposites:

MethodReturns elements where block is…Equivalent to…
selecttruthy (matching)keeping matches
rejectfalsy (non-matching)removing matches

The choice between select and find_all is purely stylistic — they share the same C implementation. Use select when the operation reads as “pick the ones that match,” and find_all when you want to emphasise that every matching element is being collected.

numbers = [1, 2, 3, 4, 5]

numbers.select { |n| n > 3 }    # => [4, 5] - keep > 3
numbers.reject { |n| n > 3 }    # => [1, 2, 3] - remove > 3

Together, they partition a collection:

When you need both the passing and failing subsets, running select and reject separately iterates the collection twice. The partition method shown next handles both groups in a single pass, halving the iteration cost for this common split operation.

numbers = [1, 2, 3, 4, 5]

passing = numbers.select    { |n| n >= 3 }
failing = numbers.reject     { |n| n >= 3 }

passing  # => [3, 4, 5]
failing  # => [1, 2]

This is equivalent to using partition:

Using select and reject separately gives you two independent arrays from a single pass over the collection. The partition method achieves the same split in one call, returning a two-element array of [matches, non_matches] that you can destructure into separate variables.

passing, failing = numbers.partition { |n| n >= 3 }
passing  # => [3, 4, 5]
failing  # => [1, 2]

Inverting a filter

You can also use reject to achieve the inverse of select:

Calling select { |n| n.odd? } and reject { |n| n.even? } produce the same result, but the two expressions communicate different intent. Choose select when you are thinking in terms of what to keep, and reject when you are thinking in terms of what to discard.

numbers = [1, 2, 3, 4, 5]

numbers.select  { |n| n.odd? }   # => [1, 3, 5]
numbers.reject  { |n| n.even? }   # => [1, 3, 5] - same result

Performance Considerations

select always iterates through the entire collection — it cannot short-circuit like find or any?. The time complexity is O(n) regardless of when matches are found.

Inverting a filter by swapping select for reject keeps the condition itself unchanged — only the treatment of matching elements flips. This is cleaner than negating the condition inside the block, which can double-negate logic and make the intention harder to parse.

# Always checks all elements, even if first one matches
[1, 2, 3, 4, 5].select { |n| n > 1 }
# Checks: 1, 2, 3, 4, 5 (all five iterations)

Memory usage

select creates a new array containing all matching elements. For very large collections with few matches, consider lazy iteration:

Because select always walks the entire collection, it visits every element even when the matching subset is small. This is O(n) in both time and space: the result array holds every match. For large collections with sparse matches, lazy.select avoids building the full intermediate array.

# Eager: builds full array first
result = (1..1_000_000).select { |n| n.prime? }

# Lazy: yields matches one at a time
result = (1..1_000_000).lazy.select { |n| n.prime? }
# then take what you need: result.first(10)

Practical Examples

Filtering user records

The lazy enumerator defers computation until values are requested, so first(10) only finds the first ten primes rather than computing all of them. For large ranges or infinite sequences, lazy evaluation is the difference between a responsive program and one that exhausts memory.

users = [
  { name: "Alice", role: "admin", active: true },
  { name: "Bob", role: "editor", active: false },
  { name: "Carol", role: "admin", active: true },
  { name: "Dave", role: "viewer", active: true }
]

admins = users.select { |u| u[:role] == "admin" && u[:active] }
admins.map { |u| u[:name] }  # => ["Alice", "Carol"]

Finding matching file paths

Selecting admin users from an array of hashes is a common pattern in Ruby applications that work with structured data. The block receives each hash and checks multiple keys at once — role and active — returning only the entries where both conditions hold.

require "pathname"

files = Pathname.new(".")
matching = files.children.select { |p| p.file? && p.extname == ".rb" }
matching  # => [#<Pathname:app.rb>, #<Pathname:config.rb>, ...]

Grouping by a condition

Using Pathname#children with select filters a directory listing to only the entries that satisfy both file? and an extension check. This same pattern works for any filesystem filtering task: list all directories, find files modified in the last week, or locate symlinks.

scores = [45, 78, 92, 55, 88, 33, 67]

passed = scores.select { |s| s >= 60 }
failed = scores.reject { |s| s >= 60 }

passed  # => [78, 92, 88, 67]
failed  # => [45, 55, 33]

Filtering with multiple criteria

Partitioning a collection into passed and failed groups with select and reject is more explicit than using partition when the two groups need different downstream processing. Each method returns its own array, so you can handle passes and fails in separate pipelines without destructuring.

orders = [
  { id: 1, status: "pending", total: 100 },
  { id: 2, status: "shipped", total: 250 },
  { id: 3, status: "pending", total: 75 },
  { id: 4, status: "delivered", total: 300 }
]

# Pending orders over $80
high_pending = orders.select do |o|
  o[:status] == "pending" && o[:total] > 80
end
high_pending  # => [{ id: 1, status: "pending", total: 100 }]

Transforming after filtering

Multiple conditions inside a single select block create AND logic naturally: both status and total must match. For OR logic between conditions, combine two select calls with | on the resulting arrays, or use a single block with || between the predicates.

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

squares_of_evens = numbers.select(&:even?).map { |n| n**2 }
squares_of_evens  # => [4, 16, 36, 64, 100]

With sum or count

Squaring even numbers after selecting them is a two-step transformation: filter, then map. When the transformation is deterministic and you want both the original and the result, chaining select and map gives you two independent collections to work with downstream.

transactions = [
  { description: "Salary", amount: 3000 },
  { description: "Rent", amount: -1000 },
  { description: "Groceries", amount: -150 },
  { description: "Bonus", amount: 500 }
]

income = transactions.select { |t| t[:amount] > 0 }.sum { |t| t[:amount] }
expenses = transactions.reject { |t| t[:amount] > 0 }.sum { |t| t[:amount] }

income    # => 3500
expenses  # => -1150

See Also