rubyguides

Array#select

What does select do?

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

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

Array#select is defined directly on the Array class but behaves identically to Enumerable#select — Array includes Enumerable, and Array overrides select with an optimised implementation.

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 }  # => []

Both conditions return a new array each time. The first call keeps 3, 4, and 5 because each element satisfies the n > 2 test. The second call returns an empty array because no element is greater than 10 — select never raises an error when nothing matches; it hands back a clean [] so downstream code can iterate safely without a nil check.

With a pattern argument

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

[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"]

The pattern argument form is a compact alternative to a block when you already have a class or regex that defines what “match” means. Passing Integer keeps every element for which Integer === element is true — in other words, actual Integer instances. Passing a regex like /^a/ does the same with Regexp#===, which checks whether the pattern matches the string. This is useful when the filter condition is a reusable matcher that already understands the decision logic.

This is equivalent to:

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

The block and the pattern argument produce the same result because Ruby dispatches === for each case. The block version is more explicit about what is happening under the hood, which can help when someone reading the code does not yet know that Integer === n is the real comparison. The pattern version is terser and often reads closer to natural language once the team is comfortable with the idiom.

Without a block

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

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

Calling select without a block returns an Enumerator object. An enumerator is a lazy wrapper that remembers what you want to do but does not execute until you ask for values. This is what makes chaining possible — the enumerator carries the pending select operation forward and allows methods like with_index to attach additional context before the actual iteration begins. The enumerator itself is a real Ruby object with its own methods, so you can inspect it, pass it around, or feed it to another method that expects an Enumerable.

This allows chaining with other methods:

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

The with_index modifier attaches a zero-based index to each iteration, which means the block now receives two parameters: the element and its position. In this example, elements at index 0 and 1 (1 and 2) are rejected because i > 1 is false for those positions. Elements starting at index 2 (3, 4, 5) pass through. This pattern is useful when you need to filter by position rather than by value, such as skipping a header row or keeping only the first N results.

Filtering Numbers

By numeric condition

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

The block receives each temperature value in turn and evaluates whether it exceeds the threshold. Because the return value is a new array, the original temperatures list remains intact — you can run multiple filter passes against the same dataset without mutating anything. The negative value -3 is silently dropped alongside the values 12, 15, and 8 because none of them satisfy t > 18.

Using symbolic method references

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

The &:even? shorthand calls Symbol#to_proc, which converts the symbol into a block that sends even? to each element. This pattern works with any method that takes no arguments, so select(&:positive?) or select(&:zero?) are equally valid. The key constraint is that the method must return a truthy or falsy value — select only cares about the boolean result, not about the element itself.

Chaining with map

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]

Chaining select with map is a classic Ruby pipeline: filter first, then transform what remains. The select pass narrows the dataset to even numbers, and map squares each survivor. The order matters — if you swapped the two calls, map would square every number first and select(&:even?) would then check whether each square is even (which all squares of integers are), producing a very different (and possibly unintended) result.

Finding by attribute (arrays of hashes)

When an array contains hashes, use the hash key to filter:

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

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

Each hash in the array is passed to the block as u, where you can reach into the hash with symbol keys. The first select call applies two conditions — role must be "admin" and active must be truthy — so Bob (who is an editor) and Dave (who is a viewer) are both excluded. The second call is broader: it keeps every user whose :active flag is true, regardless of role.

Filtering with multiple criteria

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

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

Multi-criteria filtering with hashes is a common pattern when working with data from APIs, CSV files, or database result sets. The do...end block syntax is preferred here because the condition spans two lines, making each criterion easier to read independently. Order #3 (total 75) is excluded because it falls below the 80 threshold, even though its status is "pending". Only order #1 satisfies both constraints.

find_all as an alias

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

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

Both share the same implementation, return type, and behaviour. select is more common in Ruby code; find_all reads more naturally when emphasising “finding all” matches. Internally, Ruby’s C implementation dispatches both method names to the same function, so there is zero performance difference between them. Use whichever name makes the intent clearest in the surrounding context — consistency within a codebase matters more than which spelling you pick.

select vs reject

select and reject are opposites:

MethodReturns elements where block is…Equivalent to…
selecttruthy (matching)keeping matches
rejectfalsy (non-matching)removing matches
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

The condition is the same in both calls — n > 3 — but the interpretation flips depending on the method. select treats true as “keep this element,” while reject treats true as “discard this element.” When you read reject aloud as “reject elements where,” the behaviour maps directly to natural language, which is one reason the pair is so commonly used together.

Together, they partition a collection:

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]

Running select and reject side by side with the same condition splits the array into two complementary groups. Because both methods iterate from left to right, the relative order of elements within each group matches the original array. This means passing contains 3, 4, 5 in the same sequence as they appeared, and failing preserves the order of 1, 2. If you need this split often, Ruby provides a dedicated method that does it in one pass.

This is equivalent to using partition:

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

Using partition is cleaner when you need both groups because it iterates once instead of twice. With select and reject written separately, Ruby walks the array two times — once for each method. In practice, for small arrays the difference is negligible, but with large datasets the single-pass behaviour of partition can cut the work in half. Choose partition when you genuinely need both halves; stick with select or reject alone when you only need one side of the split.

Mutating with select! (bang variant)

The bang variant select! mutates the original array in place instead of returning a new one. If no changes are made, it returns nil.

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

result = numbers.select! { |n| n > 2 }

numbers  # => [3, 4, 5]
result   # => [3, 4, 5]
result.equal?(numbers)  # => true  (same object)

The exclamation mark convention in Ruby signals mutation — select! modifies the receiver directly rather than allocating a fresh array. The equal? check confirms that result and numbers point to the exact same object in memory. This matters when you have other variables referencing the original array; they will also see the changed content, which can be either desirable (when you want to propagate the update) or surprising (when you expected a copy).

When select! returns nil

If the block condition matches the current array (no change needed), select! returns nil:

numbers = [3, 4, 5]

result = numbers.select! { |n| n > 2 }
result  # => nil  (no changes made, numbers stays [3, 4, 5])

This nil return is a deliberate Ruby convention: when a bang method makes no changes, it returns nil to signal that nothing happened. This lets you write conditional logic like if numbers.select! { |n| n > 10 } — the if branch only runs when elements were actually removed. The trap is that chaining off a bang method that returns nil will raise a NoMethodError, so check the result before calling further methods on it.

Comparing select vs select!

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

# select returns a new array, original unchanged
filtered = original.select { |n| n > 2 }
original   # => [1, 2, 3, 4, 5]
filtered   # => [3, 4, 5]

# select! modifies the original
mutated = original.select! { |n| n > 2 }
original   # => [3, 4, 5]
mutated    # => [3, 4, 5]
mutated.equal?(original)  # => true

Use select! when you want to reuse the same variable rather than allocating a new array. The non-bang select is safer in functional-style code where immutability is valued — it never surprises you by changing a collection you are still referencing elsewhere. The bang variant is a better fit inside tight loops or memory-constrained paths where the allocation overhead of a new array adds up across thousands of iterations. For most everyday Ruby code, the non-bang form is the default choice because clarity and safety outweigh the memory savings.

Performance Considerations

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

# 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)

This full-scan behaviour is the key performance characteristic of select. Unlike find, which can stop after the first match, select must examine every element to guarantee it collects all matches. If your collection has a million elements and only the first three match, Ruby will still inspect the remaining 999,997. For early-exit filtering, find or any? may be the better tool, depending on whether you need the matched value or just a boolean.

Memory usage

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

# Eager: builds full array first
result = (1..1_000_000).to_a.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)

Lazy enumeration defers the work so that select only runs the block for elements you actually consume. When you call result.first(10), Ruby finds the first ten primes and stops — it never checks the remaining 999,990 numbers. This pattern shines when you are searching a large or potentially infinite range and you only need a handful of results. The tradeoff is that lazy enumerators are slower per-element than eager ones, so for small collections the overhead outweighs the benefit.

Bang variant performance

select! avoids allocating a new array, which can matter for large arrays. However, it still iterates through all elements — the performance difference is solely in memory allocation, not iteration cost.

Practical Examples

Filtering transactions

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].abs }

income    # => 3500
expenses  # => 1150

This example illustrates a real-world pattern: using select and reject with the same condition to split a dataset, then calling sum on each subset. Because select and reject both return arrays, you can chain any Array or Enumerable method directly after them. The abs call on the expenses side converts negative amounts to positive values so the sum reflects actual outflows rather than negative numbers, which keeps the reporting numbers intuitive.

Combining with group_by

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]

Using select and reject with the same condition guarantees that the two result arrays are disjoint — every element from the original appears in exactly one of them. This is a simpler alternative to group_by when you only have two categories and you want separate variables rather than a hash of groups. The tradeoff is that it iterates twice, so for very large datasets partition may still be the better choice.

With map for transformation

products = [
  { name: "Widget", price: 100, in_stock: true },
  { name: "Gadget", price: 200, in_stock: false },
  { name: "Doodad", price: 50, in_stock: true }
]

available_prices = products
  .select { |p| p[:in_stock] }
  .map { |p| p[:price] }

available_prices  # => [100, 50]

See Also