Chapter 3: Arrays | Learn Enough Ruby to Be Dangerous | Learn Enough to Be Dangerous
You have to make a choice. Choose...wisely.

Get occasional notifications about things like product discounts, blog posts, and new or updated tutorials. Unsubscribe at any time.

Quick Checkout
or Pay by Credit Card
Error processing your payment
  • You didn't choose whether or not to be added to the mailing list
Confirm
$0.00

Payments and credit card details are securely managed and protected by Learn Enough's payment processor, Stripe. More information on their site:

CART
Total
$0.00

Your Cart is Empty

$30
$300
$300
$XY
$XY
1234
Get Single Tutorial
MORE INFO

Learn Enough Ruby to Be Dangerous is available as an ebook, an offline video series, and as a structured, self-paced online course. The course includes full online access to the book content, streaming videos, progress tracking, exercises, and community exercise answers.

All Access Subscription
MORE INFO

The Learn Enough All Access Subscription includes the entire Learn Enough introductory sequence and the full Ruby on Rails Tutorial. More than 2500 pages of book content and 53 hours of video that teach you to code from total beginner up to professional-grade web development.

Sign up for the course and get access to the full tutorial and streaming screencasts!

Chapter 3 Arrays

In Chapter 2, we saw that strings can be thought of as sequences of characters in a particular order. In this chapter, we’ll learn about the array data type, which is the general Ruby container for a list of arbitrary elements in a particular order. We’ll start by explicitly connecting strings and arrays via the String#split method (Section 3.1), and then learn about various other array methods throughout the rest of the chapter.

3.1 Splitting

So far we’ve spent a lot of time understanding strings, and there’s a natural way to get from strings to arrays via the split method:

>> "ant bat cat".split(" ")     # Split a string into a three-element array.
=> ["ant", "bat", "cat"]

We see from this result that split returns a list of the strings that are separated from each other by a space in the original string.

Splitting on space is one of the most common operations, but we can split on nearly anything else as well:

>> "ant,bat,cat".split(",")
=> ["ant", "bat", "cat"]
>> "ant, bat, cat".split(", ")
=> ["ant", "bat", "cat"]
>> "antheybatheycat".split("hey")
=> ["ant", "bat", "cat"]

We can even split a string into its component characters by splitting on the empty string:

>> "badger".split("")
=> ['b', 'a', 'd', 'g', 'e', 'r']

We’ll put this basic technique to good use in Section 5.3.

Perhaps the most common use of split is with no arguments; in this case, the default behavior is to split on whitespace (such as spaces, tabs, or newlines):

>> "ant bat cat".split
=> ["ant", "bat", "cat"]
>> "ant     bat\t\tcat\n    duck".split
=> ["ant", "bat", "cat", "duck"]

We’ll investigate this case more closely when discussing regular expressions in Section 4.3.

3.1.1 Exercises

  1. Assign a to the result of splitting the string “A man, a plan, a canal, Panama” on comma-space. How many elements does the resulting array have?
  2. Can you guess the method to reverse a in place? (Google around if necessary.)

3.2 Array access

Having connected strings with arrays via the split method, we’ll now discover a second close connection as well. Let’s start by assigning a variable to an array of characters created using split:

>> a = "badger".split("")
=> ["b", "a", "d", "g", "e", "r"]

We can access particular elements of a using the same bracket notation we first encountered in Section 2.6, as seen in Listing 3.1.

Listing 3.1: Array access with the bracket notation.
>> a[0]
=> "b"
>> a[1]
=> "a"
>> a[2]
=> "d"

We see from Listing 3.1 that, as with strings, arrays are zero-offset, meaning that the “first” element has index 0, the second has index 1, and so on. This convention can be confusing, and in fact it’s common to refer to the initial element for zero-offset arrays as the “zeroth” element as a reminder that the indexing starts at 0. This convention can also be confusing when using multiple languages (some of which start array indexing at 1), as illustrated in the xkcd comic strip “Donald Knuth”.1

So far we’ve dealt exclusively with arrays of characters, but Ruby arrays can contain all kinds of elements:

>> soliloquy = "To be, or not to be, that is the question:"
>> a = ["badger", 42, soliloquy.include?("To be")]
=> ["badger", 42, true]
>> a[2]
=> true
>> a[3]
=> nil

We see here that the square bracket access notation works as usual for an array of mixed types, which shouldn’t come as a surprise. We also see that trying to access an array index outside the defined range returns nil (a value which we saw before in the context of puts (Listing 1.4)). This might be a surprise if you have previous programming experience, since many languages raise an error if you try to access an element that’s out of range, but Ruby is more tolerant in this regard.

Another convenient feature of Ruby bracket notation is supporting negative indices, which count from the end of the array:

>> a[-2]
=> 42

Among other things, negative indices give us a compact way to select the last element in an array. Because arrays, like strings, respond to a length method, we could do it directly:

>> a[a.length - 1]
=> true

But it’s even easier like this:

>> a[-1]
=> true

This is such a common operation that Ruby supplies a special last method just for doing it:

>> a.last
=> true

A final common case is where we want to access the final element and remove it at the same time. We’ll cover the method for doing this in Section 3.4.2.

3.2.1 Exercises

  1. Write a for loop to print out the characters obtained from splitting “honey badger” on the empty string.
  2. See if you can guess the value of a[100] in a boolean context. Use !! to confirm.

3.3 Array slicing

In addition to supporting the bracket notation described in Section 3.2, Ruby supports a technique known as array slicing for accessing multiple elements at a time. In anticipation of learning to sort in Section 3.4, let’s redefine our array a to have purely numerical elements:

>> a = [42, 8, 17, 99]
=> [42, 8, 17, 99]

One way to slice an array is to provide two arguments, an index and a number of elements, which returns the given number of elements starting at the given index. For example, for an array with four elements, slice(2, 2) returns the “second” and “third” ones (recall that the “first” or zeroth element has index 0):

>> a.slice(2, 2)
=> [17, 99]

Another technique (and my favored one) is to use the Range data type we met briefly in Listing 2.18, which returns the elements with index between the beginning and end of the range:

>> a.slice(1..3)
=> [8, 17, 99]

Unlike most languages, Ruby lets us perform array slicing directly with the bracket notation:

>> a[2, 2]
=> [17, 99]
>> a[1..3]
=> [8, 17, 99]

The second example above, using a range, is probably the most common way to slice arrays in real-world Ruby code.

Finally, it’s worth noting that ranges can easily be converted to arrays using the “to array” method, to_a:

>> (1..10).to_a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>> ('a'..'z').to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
    "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

We see in the second example above that ranges work on letters as well as numbers.

3.3.1 Exercises

  1. Define an array with the numbers 1 through 10. Use slicing and length to select the third element through the third-to-last. Accomplish the same task using a negative index.
  2. Show that strings also support the slice method by selecting just bat from the string "ant bat cat". (You might have to experiment a little to get the indices just right.)
  3. By combining a range, to_a, and array slicing, create an array containing the first 13 letters in the alphabet.

3.4 More array methods

In addition to last, length, and slice, arrays respond to a wealth of other methods. As usual, the documentation is a good place to go for details.

As with strings, arrays respond to an include? method to test for element inclusion:

>> a = [42, 8, 17, 99]
=> [42, 8, 17, 99]
>> a.include?(42)       # Test for element inclusion.
=> true
>> a.include?("foo")
=> false

3.4.1 Sorting and reversing

You can also sort an array in place—an excellent trick that in ye olden days of C often required a custom implementation.2 In Ruby, we just call sort:

>> a.sort
=> [8, 17, 42, 99]
>> a                     # `a` hasn't changed as the result of `sort`.
=> [42, 8, 17, 99]

As you might expect for an array of integers, a.sort sorts the array numerically (unlike, e.g., JavaScript, which confusingly sorts them “alphabetically”, so that 17 comes before 8). We also see that (again unlike JavaScript) sorting an array doesn’t change the array itself.

Ruby includes a second sort method that does change the array, sorting it in place:

>> a.sort!
=> [8, 17, 42, 99]
>> a                     # `a` has changed as the result of `sort!`.
=> [8, 17, 42, 99]

Here the sort! (read “sort-bang”) method makes use of Ruby’s ability to include punctuation in method names, as we saw in Section 2.5 with boolean methods like include?. In this case, the exclamation point indicates that the method mutates (changes) the underlying object. Any time you see a Ruby method ending in !, it’s more than likely that some sort of mutation is occurring.3

Another useful method—one we’ll put to good use in developing our palindrome theme starting in Section 5.3—is the reverse method:

>> a.reverse
=> [99, 42, 17, 8]
>> a                     # Like `sort`, `reverse` doesn't mutate the array.
=> [8, 17, 42, 99]

As with sort, reverse returns a reversed array, but doesn’t change the array itself. Can you guess the method for mutating an array by reversing it in place?

3.4.2 Pushing and popping

One useful pair of array methods is push and pop; push lets us append an element to the end of an array, while pop removes it:

>> a.push(6)                   # Pushing onto an array
=> [8, 17, 42, 99, 6]
>> a.push("foo")
=> [8, 17, 42, 99, 6, "foo"]
>> a.pop                      # `pop` returns the value itself
=> "foo"
>> a.pop
=> 6
>> a.pop
=> 99
>> a
=> [8, 17, 42]

As noted in the comments, pop returns the value of the final element (while removing it as a side effect), while push simply returns the resulting array.

We are now in a position to appreciate the comment made in Section 3.2 about obtaining the last element of the array, as long as we don’t mind mutating it:

>> the_answer_to_life_the_universe_and_everything = a.pop
=> 42

Finally, Ruby supports a second (and very commonly used) notation for pushing onto arrays, the so-called “shovel operator” <<:

>> a << "badger"
=> [8, 17, "badger"]
>> a << "ant" << "bat" << "cat"
=> [8, 17, "badger", "ant", "bat", "cat"]

As seen in the second example, the shovel operator can be chained, pushing a sequence of elements onto the end of the array.

3.4.3 Undoing a split

A final example of an array method, one that brings us full circle from Section 3.1, is join. Just as split splits a string into array elements, join joins array elements into a string (Listing 3.2).

Listing 3.2: Different ways to join.
>> a = ["ant", "bat", "cat", 42]
=> ["ant", "bat", "cat", 42]
>> a.join                         # Join on default (empty space).
=> "antbatcat42"
>> a.join(", ")                   # Join on comma-space.
=> "ant, bat, cat, 42"
>> a.join(" -- ")                 # Join on double dashes.
=> "ant -- bat -- cat -- 42"

Note that 42, which is an integer, is automatically converted to a string in the join.

3.4.4 Exercises

  1. Confirm your guess about the version of reverse that mutates an array by reversing it in place.
  2. The split and join methods are almost inverse operations, but not quite. In particular, confirm using == that a.join(" ").split(" ") in Listing 3.2 is not the same as a. Why not?
  3. Using the array documentation, figure out how to push onto or pop off the front of an array. Hint: The names aren’t intuitive at all, so you might have to work a bit.

3.5 Array iteration

One of the most common tasks with arrays is iterating through their elements and performing an operation with each one. This might sound familiar, since we solved the exact same problem with strings in Section 2.6, and indeed the solution is virtually the same. All we need to do is adapt the for loop from Listing 2.20 to arrays, i.e., replace soliloquy with a, as shown in Listing 3.3.

Listing 3.3: Combining array access and a for loop.
>> for i in 0..(a.length - 1)
?>   puts a[i]
>> end
ant
bat
cat
42

That’s convenient, but it’s not the best way to iterate through arrays, and Mike Vanier still wouldn’t be happy (Figure 3.1).

images/figures/mike_vanier
Figure 3.1: Mike Vanier is still annoyed by typing out for loops.

Luckily, looping the Right Way™ is easier than it is in most other languages, so we can actually cover it here (unlike in, e.g., Learn Enough JavaScript to Be Dangerous, when we had to wait until Chapter 5). The trick is to use a special each method that is particularly characteristic of Ruby, as shown in Listing 3.4.

Listing 3.4: Using each to iterate over an array the Right Way™.
>> a.each do |element|
?>   puts element
>> end
ant
bat
cat
42

The array a responds to the each method by yielding each element in turn, which in this case we simply print to the screen. (The syntax in Listing 3.4 uses a Ruby block, but don’t worry about it for now; we’ll cover blocks in more detail in Section 5.4.) Using the each method, we can iterate directly through the elements in an array, thereby avoiding having to type out Mike Vanier’s bête noire, “for (i = 0; i < N; i++)”. The result is cleaner code and a happier programmer (Figure 3.2).

images/figures/mike_vanier_happier
Figure 3.2: Using each has made Mike Vanier a little happier.

3.5.1 Exercises

  1. Combine reverse and each to print out an array’s elements in reverse order. Hint: You can call each method in sequence, as in a.reverse.each, a technique known as method chaining (covered in Section 5.3).

Join the Mailing List

Get occasional notifications about things like product discounts, blog posts, and new or updated tutorials. Unsubscribe at any time.