rubyguides

Working Arrays in Ruby: A Practical Guide

Arrays are one of the most common data structures in Ruby, and working arrays well means knowing which method to reach for in each situation. This guide covers creating, filtering, transforming, sorting, and iterating over arrays with practical examples you can use right away.

TL;DR

Use arrays when you want ordered collections, fast indexed access, and simple iteration. Build them with literals or Array.new, then choose the method that matches the job: map for transformation, select for filtering, sort_by for ordering, and each_with_index when the position matters. The rest of this guide shows how those pieces fit together in small, practical steps.

How to create and initialize working arrays

From scratch

Create an array with literal syntax:

numbers = [1, 2, 3, 4, 5]
words = ["apple", "banana", "cherry"]
mixed = [1, "two", 3.0, nil]

With Array.new

The literal syntax is concise but Array.new gives you more control when you need a specific size or pre-filled values. This is useful when you know the array’s shape before you have the data to put in it, like pre-allocating space for computed results or building a grid of default values for a game board.

# Create array with 5 nil elements
empty = Array.new(5)
# => [nil, nil, nil, nil, nil]

# Create array with 5 elements set to 0
zeros = Array.new(5, 0)
# => [0, 0, 0, 0, 0]

# Create array with block (each element computed)
squares = Array.new(5) { |i| i ** 2 }
# => [0, 1, 4, 9, 16]

The block form is preferred when each element needs a unique value, because the single-value form can cause issues with mutable objects.

That distinction matters more than it first looks. If every slot points at the same mutable object, one change can show up everywhere. The block form avoids that surprise and gives each slot its own value, which is usually what you want for defaults and computed entries.

From other objects

Convert ranges, strings, and other objects to arrays:

# Range to array
(1..10).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# String to array of characters
"hello".chars
# => ["h", "e", "l", "l", "o"]

# String to array of words
"hello world".split
# => ["hello", "world"]

# CSV or delimited string
"a,b,c".split(",")
# => ["a", "b", "c"]

Conversion is often the fastest way to get from raw input to a usable array. Once the data is in array form, the rest of Ruby’s array and enumerable methods can do the heavy lifting for you without extra parsing code.

Adding and removing elements

Adding elements

# Append to end
array = [1, 2, 3]
array << 4
array.push(5, 6)
# => [1, 2, 3, 4, 5, 6]

# Prepend to beginning
array.unshift(0)
# => [0, 1, 2, 3, 4, 5, 6]

# Insert at specific index
array.insert(2, "two")
# => [0, 1, "two", 2, 3, 4, 5, 6]

Removing elements

These methods modify the collection in place, which is useful when you are modeling a queue, a list of tasks, or any ordered set of items meant to evolve over time. The push/shift combination works like a FIFO queue while push/pop gives you a stack. Each mutating method avoids extra allocations when the array content changes frequently.

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

# Remove and return last element
last = array.pop
# last = 5, array = [1, 2, 3, 4]

# Remove and return first element
first = array.shift
# first = 1, array = [2, 3, 4]

# Remove element at specific index
array.delete_at(1)
# array = [2, 4]

# Remove all elements that match
array = [1, 2, 3, 2, 4]
array.delete(2)
# array = [1, 3, 4]

Picking the right removal method is mostly about what you know at the call site. If you know the value, remove by value. If you know the position, remove by index. That little choice keeps the code readable and avoids extra searching.

Searching and filtering

Finding elements

fruits = ["apple", "banana", "cherry", "date"]

# Find first match
fruits.find { |f| f.length > 5 }
# => "banana"

# Find index of first match
fruits.index { |f| f.start_with?("c") }
# => 2

# Check if any element matches
fruits.any? { |f| f.start_with?("b") }
# => true

# Check if all elements match
fruits.all? { |f| f.length < 10 }
# => true

Filtering arrays

Whereas find returns a single element and any? returns a boolean, select and reject return new arrays. This distinction matters because the return type changes how you compose the result with the rest of your code. A boolean feeds an if statement while a filtered array feeds the next method in a chain.

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

# Select elements matching condition
evens = numbers.select { |n| n.even? }
# => [2, 4, 6, 8, 10]

# Reject elements matching condition
odds = numbers.reject { |n| n.even? }
# => [1, 3, 5, 7, 9]

# Partition into matching/non-matching
evens, odds = numbers.partition { |n| n.even? }
# evens = [2, 4, 6, 8, 10], odds = [1, 3, 5, 7, 9]

Transforming arrays

Map and friends

These filtering methods separate the “what” from the “how”. You say what should stay or what should go, and Ruby handles the iteration in a readable way. When you need transformation instead of filtering, map and its cousins take over. They change every element in the array and return a new one of the same length but with different values.

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

# Transform each element
squared = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]

# Transform and flatten (for nested results)
nested = numbers.flat_map { |n| [n, n * 2] }
# => [1, 2, 2, 4, 3, 6, 4, 8, 5, 10]

# Collect is alias for map
doubled = numbers.collect { |n| n * 2 }
# => [2, 4, 6, 8, 10]

Chaining transformations

Where a single map transforms one array into another, chaining combines several operations into a single pipeline. This is especially powerful for data processing: you filter out unwanted records, transform the ones that remain, and then limit the result to what you need. The intermediate arrays are created and discarded automatically, so you avoid naming temporary variables for each step.

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

# Chain filter, transform, and limit
result = numbers
  .select { |n| n > 3 }
  .map { |n| n ** 2 }
  .take(3)
# => [16, 25, 36]

Chaining works well when each step has a clear responsibility: filter first, then transform, then limit. Each method call describes one operation and the data flows through without intermediate variables. The trade-off is that debugging a long chain can be harder than inspecting a series of named locals.

Sorting arrays

Basic sorting

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

# Sort ascending
numbers.sort
# => [1, 1, 2, 3, 4, 5, 6, 9]

# Sort descending
numbers.sort.reverse
# => [9, 6, 5, 4, 3, 2, 1, 1]

# Sort with block
numbers.sort { |a, b| b <=> a }
# => [9, 6, 5, 4, 3, 2, 1, 1]

Custom object sorting

Sorting with sort_by is often clearer than building a custom comparison block because the key is obvious. The reader can see exactly what drives the ordering, which makes the sort rule easy to change later. When sorting hashes or structs, sort_by with a symbol-to-proc shortcut keeps the line short and the intent explicit.

users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 }
]

# Sort by age
users.sort_by { |u| u[:age] }
# => [{:name=>"Bob", :age=>25}, {:name=>"Alice", :age=>30}, {:name=>"Charlie", :age=>35}]

# Sort by name length
users.sort_by { |u| u[:name].length }
# => [{:name=>"Bob", :age=>25}, {:name=>"Alice", :age=>30}, {:name=>"Charlie", :age=>35}]

Sorting is one kind of transformation, but sometimes you do not need a new array at all. You just need to walk through each element and do something with it. Ruby’s iteration methods handle that case cleanly, with variants that track the index and others that accumulate values as they go.

Iterating efficiently

Each variations

fruits = ["apple", "banana", "cherry"]

# Each with index
fruits.each_with_index { |fruit, i| puts "#{i}: #{fruit}" }

# Each with object
fruits.each_with_object("") { |fruit, str| str << fruit[0] }
# => "abc"

# Cycle (repeat infinitely)
fruits.cycle(2) { |f| puts f }
# prints: apple, banana, cherry, apple, banana, cherry

Reducing to a single value

When you need both the element and its position, each_with_index keeps the loop compact. When you want to derive a single accumulated value instead of a new array, reduce can replace a longer manual loop and make the result easier to understand. Reducing is common for totals, joins, and building hashes from arrays.

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

# Sum all elements
numbers.sum
# => 15

# With initial value
numbers.sum(10)
# => 25

# Custom reduction
product = numbers.reduce(1) { |acc, n| acc * n }
# => 120

# Inject is alias for reduce
sum = numbers.inject(:+)
# => 15

Reduction is one of the best ways to turn a collection into a single answer. It works for totals, joins, hashes, and many small summary tasks, which is why it shows up so often in Ruby code that wants to stay compact without losing clarity.

Array comparisons

Checking equality

a = [1, 2, 3]
b = [1, 2, 3]
c = [1, 2, 4]

a == b  # => true
a == c  # => false

# Compare arrays element-by-element
a.zip(c).each { |x, y| puts "#{x} vs #{y}" }

Set operations

Equality checking is straightforward in Ruby because == compares arrays element by element. Set operations take that idea further by letting you ask broader questions about two collections: what do they share, how do they combine, and what is unique to each. These operators are especially handy for diffing lists or merging results from multiple data sources.

a = [1, 2, 3, 4]
b = [3, 4, 5, 6]

# Intersection (common elements)
a & b
# => [3, 4]

# Union (all unique elements)
a | b
# => [1, 2, 3, 4, 5, 6]

# Difference
a - b
# => [1, 2]

b - a
# => [5, 6]

Array comparisons let you understand whether two collections contain the same values or when you need to combine sets of results. The set-style operators keep those ideas in one place and make the intent much clearer than a hand-written loop.

Common pitfalls

Mutating during iteration

Never modify the same array you are iterating over:

# WRONG - unpredictable behavior
numbers = [1, 2, 3]
numbers.each { |n| numbers.delete(n) if n.odd? }
# => [2]

# CORRECT - work on a copy
numbers = [1, 2, 3]
numbers.dup.each { |n| numbers.delete(n) if n.odd? }
# => [2]

Reference vs copy

This is one of the easiest mistakes to make because the code looks harmless at first glance. The safer pattern is to iterate over a copy or build a new array, then swap the result in once the new version is ready. The same careful habit applies to assignment, where Ruby copies references rather than objects. Using dup or clone creates a shallow copy so your mutations stay contained to the new array.

# WRONG - both point to same array
original = [1, 2, 3]
copy = original
copy << 4
original  # => [1, 2, 3, 4] - original modified!

# CORRECT - create a new array
original = [1, 2, 3]
copy = original.dup
# or: copy = original.clone
copy << 4
original  # => [1, 2, 3]

Performance with large arrays

Mutation bugs and reference surprises are the most visible pitfalls, but performance can quietly become a problem too. When arrays grow large, creating intermediate copies for every select and map adds up. Ruby’s lazy enumerator and shorthand block syntax can help you avoid that overhead without changing the shape of the code.

For large datasets, consider these tips:

# Use lazy for chainable enumerators
result = (1..1_000_000)
  .lazy
  .select { |n| n.even? }
  .map { |n| n ** 2 }
  .first(10)

# Avoid creating intermediate arrays with blocks
# Prefer:
array.select(&:positive?)
# Over:
array.select { |n| n.positive? }

For larger arrays, the biggest win usually comes from reducing temporary objects and avoiding extra passes. Lazy enumeration, selective filtering, and a little restraint around intermediate arrays can make the code both faster and easier to reason about.

The core theme of this guide is simple: working arrays are flexible because Ruby gives you many small methods that compose well. If you choose the method that matches the job, the code stays compact and the shape of the data stays obvious all the way through.

Practical takeaways

The main thing to remember about working arrays is that the method you choose usually says as much about the design as the result does. map says you are changing shape, select says you are filtering, sort_by says you are ordering by a rule, and each_with_index says the position matters. That vocabulary makes the code easier to scan because the method name carries part of the meaning for you.

When a task starts to feel awkward, it is often because the array is carrying too much responsibility. Break the work into smaller steps: normalize the input, transform it, filter it, and only then reduce or compare it. That keeps the data flow simple and helps each step stay focused on one job.

The examples in this guide also show a broader Ruby habit: a lot of array work becomes easier once you move from a manual loop with state to a small enumerable chain with clear intent. That shift usually makes the code shorter, but the real win is that it becomes easier to change safely later.

If you keep those ideas in mind, working arrays stops feeling like a pile of special cases and starts behaving like a toolkit for working arrays. You can build, reshape, sort, and summarize them without changing the shape of the surrounding code very much.

Here is a quick reference to the most common patterns for working arrays:

# Every pattern in one place
data = [5, 3, 8, 1, 9, 2, 7]

# Filter → transform → sort
data.select { |n| n > 2 }       # [5, 3, 8, 9, 7]
    .map { |n| n * 10 }          # [50, 30, 80, 90, 70]
    .sort                         # [30, 50, 70, 80, 90]

# Reduce to a single value
data.sum                         # 35
data.select(&:even?).sum          # 10

# Partition by condition
data.partition { |n| n.even? }   # [[8, 2], [5, 3, 1, 9, 7]]

Frequently asked questions

When should I use an array vs a hash?

Use an array when the order of items matters and you need indexed access. Reach for a hash when you need to look things up by name or key. Arrays are better for sequences and lists; hashes are better for dictionaries and mappings.

How do I pick between map, each, and collect?

map and collect are aliases that return a new array of transformed values. each iterates but returns the original array, so use it for side effects like printing or logging. Choose map when you want the transformed result.

What is the fastest way to check if an array includes a value?

For small arrays, include? is fine. For large arrays where you check membership repeatedly, convert to a Set first with require 'set' and use Set#include?, which is backed by a hash table and much faster for repeated lookups.

See Also