Loading
All articles
Programming 18 min read

Ruby Arrays: A Visual, Hands-On Guide

Learn Ruby arrays visually: creating and reading them, adding and removing at both ends, stacks and queues, each, map, select and friends, sorting, combining, nested arrays, the gotchas, a real Array vs. Set benchmark and exercises with answers.

nbkhope
On this page

A playlist, a shopping cart, the high-score table, the players in a match, the lines of a file: programs are full of lists. In Ruby, a list is an Array, and Ruby's arrays are among the friendliest in any language. They grow and shrink on their own, hold anything you put in them, and come with a huge set of methods for searching, sorting and transforming.

In this guide you'll see how an array is laid out, add and remove elements at both ends, build a stack and a queue, chain iterators like map and select into tidy pipelines, sort and combine arrays, dodge two classic gotchas, and measure when an array is the wrong tool. Exercises with answers wrap it up.

All the outputs below come from real runs with Ruby 3.3. The examples use p, which shows arrays with their brackets and quotes.

One name, many boxes

Instead of five separate variables for five animals, an array keeps them all under one name, in order:

An array is a numbered shelf: one name, many boxes. The array animals holds "dog", "cat", "bear", "horse" and "eagle" at indexes 0 to 4, which are also -5 to -1 counting from the end. animals[1, 3] gives ["cat", "bear", "horse"]. animals.first is "dog", animals.last is "eagle", animals.size is 5, and animals[10] is nil. Use fetch(10) if you'd rather get an error

Creating arrays

1animals = ["dog", "cat", "bear", "horse", "eagle"]
2mixed = [12, "dog", 3.65, nil, true, [1, 2]]
3words = %w[apple banana cherry]
4zeros = Array.new(3, 0)
5squares = Array.new(5) { |index| index * index }
6digits = (1..5).to_a
7
8p animals
9p mixed
10p words
11p zeros
12p squares
13p digits
14p [].empty?
1["dog", "cat", "bear", "horse", "eagle"]
2[12, "dog", 3.65, nil, true, [1, 2]]
3["apple", "banana", "cherry"]
4[0, 0, 0]
5[0, 1, 4, 9, 16]
6[1, 2, 3, 4, 5]
7true
  • Square brackets with commas are the usual way.
  • A Ruby array can mix any kinds of values, even other arrays.
  • %w[...] is a shortcut for an array of single words.
  • Array.new(5) { |index| … } builds an array by running the block once per position.

Reading elements

1animals = ["dog", "cat", "bear", "horse", "eagle"]
2p animals[0]
3p animals[4]
4p animals[-1]
5p animals.first
6p animals.last(2)
7p animals[1, 3]
8p animals[1..3]
9p animals.size
10p animals[10]
11begin
12  animals.fetch(10)
13rescue IndexError => error
14  puts "fetch refused: #{error.message}"
15end
1"dog"
2"eagle"
3"eagle"
4"dog"
5["horse", "eagle"]
6["cat", "bear", "horse"]
7["cat", "bear", "horse"]
85
9nil
10fetch refused: index 10 outside of array bounds: -5...5

Counting starts at 0, so the last element is at size - 1, or simply at -1. Asking for a position that isn't there gives nil, quietly. Use fetch when a missing element should be an error.

Adding and removing elements

Adding and removing at both ends. With the array "dog", "cat", "bear": unshift("dinosaur") adds to the front, shift removes from the front, push("horse") adds to the back, and << works too, and pop removes from the back. pop and shift return the element they removed, so you can use it. insert(index, value) and delete_at(index) work anywhere in the middle
1animals = ["dog", "cat", "bear"]
2
3animals.push("horse")
4animals << "eagle"
5p animals
6
7removed = animals.pop
8puts "pop removed #{removed.inspect}"
9p animals
10
11animals.unshift("dinosaur")
12p animals
13
14first_one = animals.shift
15puts "shift removed #{first_one.inspect}"
16p animals
1["dog", "cat", "bear", "horse", "eagle"]
2pop removed "eagle"
3["dog", "cat", "bear", "horse"]
4["dinosaur", "dog", "cat", "bear", "horse"]
5shift removed "dinosaur"
6["dog", "cat", "bear", "horse"]

In the middle

1animals = ["dog", "cat", "bear", "horse", "eagle"]
2animals[1] = "turtle"
3p animals
4
5animals.insert(2, "owl")
6p animals
7
8animals.delete("bear")
9p animals
10
11removed = animals.delete_at(0)
12puts "delete_at(0) removed #{removed.inspect}"
13p animals
1["dog", "turtle", "bear", "horse", "eagle"]
2["dog", "turtle", "owl", "bear", "horse", "eagle"]
3["dog", "turtle", "owl", "horse", "eagle"]
4delete_at(0) removed "dog"
5["turtle", "owl", "horse", "eagle"]

delete("bear") removes by value (every copy of it); delete_at(0) removes by position.

Stacks and queues

Those four end methods make an array a ready-made stack or queue, two of the most useful patterns in programming:

Two classic ways to use an array. A stack is last in, first out, using push and pop: like an undo history, where the latest change is undone first. A queue is first in, first out, using push and shift: like a print queue, where the first file sent prints first
1# A stack: the last thing in is the first thing out (like undo)
2history = []
3history.push("type 'Hello'")
4history.push("make it bold")
5history.push("change the color")
6puts "Undo: #{history.pop}"
7puts "Undo: #{history.pop}"
8p history
9
10# A queue: the first thing in is the first thing out (like a line at a shop)
11print_queue = []
12print_queue.push("report.pdf")
13print_queue.push("photo.jpg")
14print_queue.push("ticket.png")
15puts "Printing: #{print_queue.shift}"
16puts "Printing: #{print_queue.shift}"
17p print_queue
1Undo: change the color
2Undo: make it bold
3["type 'Hello'"]
4Printing: report.pdf
5Printing: photo.jpg
6["ticket.png"]

Going through an array

1animals = ["dog", "turtle", "bear"]
2
3animals.each { |animal| puts animal }
4
5animals.each_with_index do |animal, index|
6  puts "#{index + 1}. #{animal}"
7end
8
9index = 0
10while index < animals.length
11  puts animals[index].upcase
12  index += 1
13end
1dog
2turtle
3bear
41. dog
52. turtle
63. bear
7DOG
8TURTLE
9BEAR

each hands every element, in order, to the block between the braces (or between do and end). each_with_index adds its position. The while loop works too, but in Ruby you'll almost always see each: there's no index to manage, and no way to go off the end.

The iterator toolbox: map, select and friends

Beyond each, arrays have iterators that build something new from the elements. These are the ones you'll use daily:

map, select, reject and find at a glance, applied to prices 12.5, 3.0, 45.0, 7.25, 19.99 and 2.5. map { price * 1.1 } gives an array of the same size with each one changed: 13.75, 3.3, 49.5, 7.98, 21.99, 2.75. select { price > 10 } keeps the matches: 12.5, 45.0, 19.99. reject { price > 10 } drops the matches: 3.0, 7.25, 2.5. find { price > 10 } gives just the first match: 12.5
1prices = [12.5, 3.0, 45.0, 7.25, 19.99, 2.5]
2
3p prices.map { |price| (price * 1.1).round(2) }
4p prices.select { |price| price > 10 }
5p prices.reject { |price| price > 10 }
6p prices.find { |price| price > 10 }
7p prices.count { |price| price < 5 }
8p prices.sum
9p prices.min
10p prices.max
11p prices.sum / prices.size
12p prices.reduce { |total, price| total + price }
1[13.75, 3.3, 49.5, 7.98, 21.99, 2.75]
2[12.5, 45.0, 19.99]
3[3.0, 7.25, 2.5]
412.5
52
690.24
72.5
845.0
915.04
1090.24
MethodGives youThink of it as
mapa new array, same size"turn each one into…"
selectthe elements that match"keep the ones where…"
rejectthe elements that don't"throw out the ones where…"
findthe first match (or nil)"find me one where…"
counthow many match"how many are…"
sum, min, maxone number"add up / smallest / biggest"
reduceone value built step by step"combine them all into…"

Chaining them into a pipeline

Because each of these returns a new array, you can chain them, one step per line:

Chaining iterators: filter, transform, combine. Four orders, coffee 3.50, cake 4.25 not paid, sandwich 7.00 and juice 4.00, go through .select { paid }, which keeps coffee, sandwich and juice; then .map { price }, which gives 3.50, 7.00 and 4.00; then .sum, which gives 14.5. Each step hands a new array to the next one, and the original orders array is never changed
1orders = [
2  { item: "coffee", price: 3.5, paid: true },
3  { item: "cake", price: 4.25, paid: false },
4  { item: "sandwich", price: 7.0, paid: true },
5  { item: "juice", price: 4.0, paid: true },
6]
7
8total_paid = orders
9  .select { |order| order[:paid] }
10  .map { |order| order[:price] }
11  .sum
12
13puts "Paid so far: #{total_paid}"
Paid so far: 14.5

Each order here is a hash. Arrays of hashes are how Ruby programs usually hold a list of records.

Sorting, counting and grouping

1scores = [72, 95, 88, 61, 95, 90]
2names = ["eve", "Bob", "alice", "Dave"]
3
4p scores.sort
5p scores.sort.reverse
6p scores.sort { |first, second| second <=> first }
7p scores.uniq
8p scores.max(3)
9p names.sort
10p names.sort_by(&:downcase)
11p names.max_by(&:length)
12p scores.tally
13p scores.group_by { |score| score >= 90 ? "A" : "not A" }
1[61, 72, 88, 90, 95, 95]
2[95, 95, 90, 88, 72, 61]
3[95, 95, 90, 88, 72, 61]
4[72, 95, 88, 61, 90]
5[95, 95, 90]
6["Bob", "Dave", "alice", "eve"]
7["alice", "Bob", "Dave", "eve"]
8"alice"
9{72=>1, 95=>2, 88=>1, 61=>1, 90=>1}
10{"not A"=>[72, 88, 61], "A"=>[95, 95, 90]}
  • Plain sort puts uppercase letters before lowercase ("Bob" before "alice"). sort_by(&:downcase) sorts the way people expect.
  • <=> is the "spaceship" operator, which compares two values. Swapping the two sides sorts in reverse.
  • &:downcase is shorthand for { |name| name.downcase }.
  • tally counts each value; group_by sorts elements into buckets.

Combining arrays

Combining arrays: +, -, & and |. With mine holding apple, banana, cherry and yours holding banana, cherry, date: mine + yours gives everything, duplicates included; mine - yours gives only what's in mine, apple; mine & yours gives what's in both, banana and cherry; and mine | yours gives what's in either, with no duplicates: apple, banana, cherry, date
1mine = ["apple", "banana", "cherry"]
2yours = ["banana", "cherry", "date"]
3
4p mine + yours
5p mine - yours
6p mine & yours
7p mine | yours
8p [[1, 2], [3, [4, 5]]].flatten
9p ["Ana", "Bruno", "Carla"].zip([90, 85, 77])
10p (1..10).each_slice(3).to_a
11p [1, 2, 3, 4].each_cons(2).to_a
1["apple", "banana", "cherry", "banana", "cherry", "date"]
2["apple"]
3["banana", "cherry"]
4["apple", "banana", "cherry", "date"]
5[1, 2, 3, 4, 5]
6[["Ana", 90], ["Bruno", 85], ["Carla", 77]]
7[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
8[[1, 2], [2, 3], [3, 4]]

zip pairs up two arrays like the teeth of a zipper; each_slice(3) cuts an array into groups of 3; each_cons(2) gives every pair of neighbors.

Arrays inside arrays

An array of arrays makes a grid: rows of columns.

1board = [
2  ["X", "O", "X"],
3  [" ", "X", "O"],
4  ["O", " ", "X"],
5]
6
7board.each { |row| puts row.join(" | ") }
8p board[1][2]
9p board.map { |row| row[0] }
10p board.transpose[0]
11diagonal = (0..2).map { |index| board[index][index] }
12puts "Diagonal: #{diagonal.join}"
13puts "X wins the diagonal!" if diagonal.all?("X")
1X | O | X
2  | X | O
3O |   | X
4"O"
5["X", " ", "O"]
6["X", " ", "O"]
7Diagonal: XXX
8X wins the diagonal!

board[1][2] is row 1, column 2. transpose swaps rows and columns, so board.transpose[0] is the first column.

Two gotchas

Gotcha 1: Array.new(3, []) shares one inner array

Array.new(3, []) shares one inner array. With Array.new(3, []), all three slots point to the same array, so grid[0] << "oops" shows up in every row. With Array.new(3) { [] }, the block runs once per slot, giving three separate arrays
1grid = Array.new(3, [])
2grid[0] << "oops"
3p grid
4
5safe_grid = Array.new(3) { [] }
6safe_grid[0] << "fine"
7p safe_grid
[["oops"], ["oops"], ["oops"]]
[["fine"], [], []]

Array.new(3, []) creates one empty array and puts it in all three slots. Changing it through one slot changes "all of them", because they're the same array. The block form runs once per slot and creates a fresh array each time. (Numbers, as in Array.new(3, 0), are safe because you can't change a number in place.)

Gotcha 2: methods with ! change the original

1numbers = [3, 1, 2]
2sorted = numbers.sort
3p numbers
4p sorted
5
6numbers.sort!
7p numbers
8
9doubled = numbers.map { |number| number * 2 }
10p doubled
11numbers.map! { |number| number * 10 }
12p numbers
1[3, 1, 2]
2[1, 2, 3]
3[1, 2, 3]
4[2, 4, 6]
5[10, 20, 30]

sort, map, select, reverse and uniq return a new array; their ! twins (sort!, map!, select!…) change the array in place. Methods like push, pop, << and delete change the array even without a !.

When an array is the wrong tool: searching

include? answers "is this in the list?":

1require "set"
2
3guest_list = ["Ana", "Bruno", "Carla"]
4p guest_list.include?("Bruno")
5p guest_list.include?("Zeca")
6p guest_list.index("Carla")
7
8guests = Set.new(guest_list)
9p guests.include?("Bruno")
1true
2false
32
4true

But to answer that, an array has to look at its elements one by one. A Set is built for exactly this question: it jumps straight to the answer. We measured 10,000 checks against lists of growing size:

Is it in the list? Array vs. Set. 10,000 include? checks, half found and half not, against N names, measured for this article with Ruby 3.3.2 on Windows, median of 5 runs, then the median of 3 program runs, on a logarithmic scale. 100 names: Array 5.9 milliseconds, Set 0.8 milliseconds. 1,000 names: Array 57.5 milliseconds, Set 1.0 millisecond. 10,000 names: Array 563.0 milliseconds, Set 1.7 milliseconds. 100,000 names: Array 11.5 seconds, Set 3.2 milliseconds. An array checks its elements one by one, so 10 times more names means about 10 times more work. A Set jumps straight to the answer, almost the same speed for 100 names or 100,000

With 100,000 names, the array needed 11.5 seconds; the Set, 3.2 milliseconds: about 3,600 times faster. Every time the list grew 10×, the array's time grew about 10× too, while the Set barely noticed.

A map of everyday array methods

A map of everyday array methods. Create: [1, 2, 3], %w[a b c], Array.new(3) with a block, (1..5).to_a. Read: [0] and [-1], first and last, [1, 3] and [1..3], size and empty?. Add and remove: push and <<, pop, unshift and shift, insert and delete. Loop: each, each_with_index, each_slice, while. Transform: map, select and reject, find, reduce. Measure: sum, min and max, count, tally. Order: sort and sort_by, reverse, uniq, max_by. Combine: + and -, & and |, zip, flatten

Practice time

Try these before peeking at the answers.

Exercise 1: Predict the output (warm-up). What does this print?

1line = [1, 2, 3]
2line.push(4)
3line.shift
4line.unshift(0)
5line.pop
6p line

Exercise 2: Shopping cart. Given a cart of items with a price and a quantity, print each line with its subtotal, then the total.

Exercise 3: Duplicates. A sign-up list has repeats: ["ana", "bruno", "ana", "carla", "bruno", "ana", "diego"]. Print the list without duplicates (keeping the original order), how many duplicates were removed, and which names signed up more than once.

Exercise 4: Teams. Split 10 players into teams of 3 (the last team gets whoever's left) and print each team with its number.

Exercise 5: Leaderboard. Given pairs of [name, points], print the top 3, best first, with their positions.

Exercise 6: Rotate a picture (challenge). A tiny picture is stored as rows of characters. Turn it 90° to the right. (Hint: think about what transpose does, and then what's still backwards.)

Quick quiz

  1. What's the index of the last element of a 5-element array? Give two ways to write it.
  2. Which pair of methods makes a stack, and which makes a queue?
  3. What's the difference between map and each?
  4. Why does Array.new(3, []) surprise people?
  5. When should you reach for a Set instead of an array?

Answers

Exercise 1: Step by step: [1, 2, 3, 4], then shift removes the 1, unshift(0) adds 0 at the front, and pop removes the 4:

1line = [1, 2, 3]
2line.push(4)
3line.shift
4line.unshift(0)
5line.pop
6p line
[0, 2, 3]

Exercise 2: each for the lines, and sum with a block for the total:

1cart = [
2  { item: "notebook", price: 4.5, quantity: 3 },
3  { item: "pen", price: 1.25, quantity: 4 },
4  { item: "backpack", price: 32.0, quantity: 1 },
5]
6
7cart.each do |line|
8  subtotal = line[:price] * line[:quantity]
9  puts "#{line[:item].ljust(10)} #{line[:quantity]} x #{format('%5.2f', line[:price])} = #{format('%6.2f', subtotal)}"
10end
11total = cart.sum { |line| line[:price] * line[:quantity] }
12puts "Total: #{format('%.2f', total)}"
1notebook   3 x  4.50 =  13.50
2pen        4 x  1.25 =   5.00
3backpack   1 x 32.00 =  32.00
4Total: 50.50

Exercise 3: uniq keeps the first copy of each name, in order; tally finds the repeaters:

1sign_ups = ["ana", "bruno", "ana", "carla", "bruno", "ana", "diego"]
2unique = sign_ups.uniq
3puts "Unique: #{unique.inspect}"
4puts "Removed #{sign_ups.size - unique.size} duplicates"
5p sign_ups.tally.select { |name, count| count > 1 }.keys
Unique: ["ana", "bruno", "carla", "diego"]
Removed 3 duplicates
["ana", "bruno"]

Exercise 4: each_slice(3) makes the groups, and with_index(1) numbers them from 1:

1players = %w[Ana Bruno Carla Diego Eva Fabio Gabi Hugo Iris Joao]
2players.each_slice(3).with_index(1) do |team, number|
3  puts "Team #{number}: #{team.join(', ')}"
4end
1Team 1: Ana, Bruno, Carla
2Team 2: Diego, Eva, Fabio
3Team 3: Gabi, Hugo, Iris
4Team 4: Joao

Exercise 5: Sort by negative points to get the highest first, take 3, and unpack each pair in the block with (name, points):

1scores = [["Ana", 1200], ["Bruno", 950], ["Carla", 1430], ["Diego", 800], ["Eva", 1310]]
2
3top_three = scores.sort_by { |name, points| -points }.first(3)
4top_three.each_with_index do |(name, points), index|
5  puts "#{index + 1}. #{name.ljust(6)} #{points}"
6end
1. Carla  1430
2. Eva    1310
3. Ana    1200

Exercise 6: transpose turns columns into rows; reversing each of those rows finishes the turn to the right:

1picture = [
2  ["#", ".", "."],
3  ["#", "#", "."],
4  ["#", "#", "#"],
5]
6
7rotated = picture.transpose.map(&:reverse)
8
9puts "Before:"
10picture.each { |row| puts row.join }
11puts "After turning right:"
12rotated.each { |row| puts row.join }
1Before:
2#..
3##.
4###
5After turning right:
6###
7##.
8#..

Quiz: (1) Index 4, which you can write as array[4], array[-1] or array.last. (2) A stack uses push + pop (last in, first out); a queue uses push + shift (first in, first out). (3) each just visits the elements and returns the original array; map builds and returns a new array from what the block returns. (4) It puts the same array in every slot, so changing one changes them all; use Array.new(3) { [] }. (5) When the main question is "is this in here?", especially for big lists: in our test, a Set answered about 3,600 times faster with 100,000 names.

Cheat sheet

I want to…Write
Make an array[1, 2, 3], %w[a b c], Array.new(3) { … }, (1..5).to_a
Read an elementlist[0], list[-1], list.first, list.last
Read a slicelist[1, 3], list[1..3]
Count elementslist.size, list.count { … }, list.empty?
Add to the end / frontlist.push(x) or list << x / list.unshift(x)
Remove from the end / frontlist.pop / list.shift
Insert / delete in the middlelist.insert(2, x) / list.delete(x), list.delete_at(2)
Visit every elementlist.each { |item| … }, each_with_index
Transform / filtermap { }, select { }, reject { }, find { }
Add up / extremessum, min, max, max_by { }
Sortsort, sort_by { }, reverse
Remove duplicates / count valuesuniq / tally
Combine+, -, &, |, zip, flatten
Cut into groupseach_slice(3)
Check membership fastrequire "set", then Set.new(list).include?(x)

Wrapping up

Arrays are Ruby's everyday list, and they can do a lot:

  • Indexes start at 0, negative indexes count from the end, and a missing index gives nil (or an error with fetch).
  • push/pop and unshift/shift work at the two ends, which makes stacks and queues trivial.
  • each visits; map, select, reject, find, sum and friends compute, and chain into readable pipelines.
  • sort, uniq, tally, group_by, zip and the set operators answer most questions about a list in one line.
  • Watch for shared defaults (Array.new(3) { [] }, not Array.new(3, [])) and for methods that change the array in place.
  • For "is it in here?" on big lists, use a Set: about 3,600 times faster than an array with 100,000 names in our test.

Want more Ruby? Read What Is the Ruby Programming Language?, then Ruby String Methods, Ruby Hashes and Basic File I/O in Ruby.

Looking for a reference text for Ruby? Get the Well-Grounded Rubyist.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 4 min read

Ruby Blocks and Iterators: Write Your Own

Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

nbkhope
Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.