An array is an ordered list of elements that can be of any type. You can define an array by placing a list of elements between brackets like so:
irb :001 > [1, 'Bob', 4.33, 'another string']
You'll notice that the above array has strings, an integer, and a float. Arrays can have anything in them (even other arrays!). Now we need to save this array in a variable so we can play with it.
irb :002 > array = [1, 'Bob', 4.33, 'another string']
We'd like to find the first element of the array. We can just use the first
method.
irb :003 > array.first
=> 1
What about the last element?
irb :004 > array.last
=> "another string"
But what if we want to find the third element?
Arrays are what we call indexed lists. That means that each slot in an array is numbered. You can reference any element by its index number. The syntax to do this is typing the array name with the index in brackets []
directly following. Let's try it out.
irb :005 > array[3]
What would you expect the above code to return? Type it into irb and see what you get.
Are you surprised? You probably thought that you were going to get the number 4.33
back, but instead, you got "another string"
. That's because all array indices start with the number 0
. Try this in irb.
irb :006 > array[2]
=> 4.33
Now that's more like it. It's a little strange, but you'll get used to it.
Let's say you wanted to add or remove something from an array. There are a few methods that will help you perform these operations.
If you'd like to take the last item off of an array permanently, you can use the pop
method.
irb :007 > array.pop
=> "another string"
irb :008 > array
=> [1, "Bob", 4.33]
Note that when we called the pop
method, the original array
variable was modified (i.e., this is a method that mutates the caller), but the returned value is the popped element, as shown in the first line above. This is a critical distinction to understand. Make sure you distinguish between the value returned by an expression, and what the expression is actually doing.
Mutating the caller is similar to mutating arguments, but applies to the object used to call a method. For instance, in array.pop
, the array specified by array
is used to call the pop
method. The difference is that mutating an argument alters the value of an object passed to a method as an argument, while mutating the caller modifies the object used to call the method.
If you'd like to add that item back by mutating the array, you can use the push
method with the item to add passed as an argument.
irb :009 > array.push("another string")
=> [1, "Bob", 4.33, "another string"]
Another way to do the above would be with the shovel operator (<<
).
irb :010 > array.pop
=> "another string"
irb :011 > array << "another string"
=> [1, "Bob", 4.33, "another string"]
Both the push
and the <<
methods mutate the caller, so the original array is modified.
Often you'll have an array and you'll want to operate on many of the elements in the array the same way. Ruby has many methods that do these type of operations.
The map
method iterates over an array applying a block to each element of the array and returns a new array with those results. The irb session below shows how to use map
to get the square of all numbers in an array. The collect
method is an alias to map
- they do the same thing.
irb :001 > a = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb :002 > a.map { |num| num**2 }
=> [1, 4, 9, 16]
irb :003 > a.collect { |num| num**2 }
=> [1, 4, 9, 16]
irb :004 > a
=> [1, 2, 3, 4]
You'll notice that after performing these methods there is no change to the initial array. These methods are not destructive (i.e., they don't mutate the caller). How do you know which methods mutate the caller and which ones don't? You have to use the methods and pay attention to the output in irb; that is, you have to memorize or know through using it.
The delete_at
method can be helpful if you'd like to eliminate the value at a certain index from your array. You'll want to be careful with this one, because it modifies your array destructively. Once you call this method, you are changing your array permanently.
irb :005 > a.delete_at(1)
=> 2
irb :006 > a
=> [1, 3, 4]
As a side note, sometimes you will know the value that you want to delete, but not the index. In these situations you will want to use the delete
method. The delete
method permanently deletes all instances of the provided value from the array.
irb :007 > my_pets = ["cat", "dog", "bird", "cat", "snake"]
=> ["cat", "dog", "bird", "cat", "snake"]
irb :008 > my_pets.delete("cat")
=> "cat"
irb :009 > my_pets
=> ["dog", "bird", "snake"]
Another useful method is the uniq
method. This iterates through an array, deletes any duplicate values that exist, then returns the result as a new array.
irb :010 > b = [1, 1, 2, 2, 3, 3, 4, 4]
=> [1, 1, 2, 2, 3, 3, 4, 4]
irb :011 > b.uniq
=> [1, 2, 3, 4]
irb :012 > b
=> [1, 1, 2, 2, 3, 3, 4, 4]
Once again, notice that the uniq
method did not modify the original b
array; it returned a new array with the duplicates removed.
If you add the bang suffix (!
) to this method, you can perform the uniq
method destructively. Much like the way the delete
method works.
irb :013 > b = [1, 1, 2, 2, 3, 3, 4, 4]
=> [1, 1, 2, 2, 3, 3, 4, 4]
irb :014 > b.uniq!
=> [1, 2, 3, 4]
irb :015 > b
=> [1, 2, 3, 4]
uniq
and uniq!
are two different methods for Ruby Arrays. You cannot simply append a !
onto any method and achieve a destructive operation.
We talked in the loop section about using each
to iterate over an array. The Ruby standard library has many similar methods. Let's take a look at the select
method. This method iterates over an array and returns a new array that includes any items that return true
to the expression provided. That's a mouthful. As always, looking at code is more helpful than using a bunch of words.
irb :001 > numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb :002 > numbers.select { |number| number > 4 }
=> [5, 6, 7, 8, 9, 10]
irb :003 > numbers
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
The select
method selects all of the numbers that are greater than 4 and returns them in an array. It does not mutate the caller (the original numbers
array is unmodified).
The bang suffix (!
) at the end of the method name usually indicates that the method will change (or mutate) the caller permanently. Unfortunately this is not always the case. It is a good rule to be wary of any method that has the bang suffix and to make sure to check the Ruby documentation to see if it will behave destructively (the word "destructive" here just means mutating the caller).
Also, please note that there are methods like pop
and push
that are destructive, but do not have a !
at the end. It's a little confusing in the beginning, but as you write more programs in Ruby, you'll start to get a feel for which methods are destructive and which are not.
We talked about mutating the caller and mutating arguments earlier and showed examples to go along with it. This concept is so important that we wanted to cover it in even more depth. It's important to keep the concept in mind since it is possible that a method can unexpectedly change an object used as the caller or as an argument. This can be a major source of confusion which is why it's important to know what a method is doing to its arguments and calling object, and to know what that method returns.
We also wanted to revisit destructive methods within the context of using a method. Look at the two methods below and see if you can decipher why the first method mutates the argument, but the second one doesn't.
def mutate(arr)
arr.pop
end
def not_mutate(arr)
arr.select { |i| i > 3 }
end
a = [1, 2, 3, 4, 5, 6]
mutate(a)
not_mutate(a)
puts a
The last line will output 1
, 2
, 3
, 4
, and 5
. The mutate
method performed a destructive action (i.e., pop
) on its argument, thereby modifying the a
array, even though a
was initialized outside of the method. Therefore, the 6
element was popped out of the original array. The not_mutate
method performed a non-destructive action (i.e., select
), and therefore the original variable was unmodified.
It's worth noting that inside mutate
, arr.pop
mutates its caller. That, in turn, mutates the argument passed to mutate
. However, inside not_mutate
, arr.select
does not mutate its caller, so not_mutate
doesn't mutate its argument either.
We talked earlier about arrays being able to contain anything. You can also create arrays with arrays inside of them. Let's say you were having a sand volleyball tournament and wanted to keep track of all of the teams that were playing. You might create an array like this.
irb :001 > teams = [['Joe', 'Steve'], ['Frank', 'Molly'], ['Dan', 'Sara']]
=> [["Joe", "Steve"], ["Frank", "Molly"], ["Dan", "Sara"]]
Then you could find the teams by index.
irb :002 > teams[1]
=> ["Frank", "Molly"]
You could also have an array of hashes too! We won't get into too many crazy examples here, but play around with it in irb
.
You can compare arrays for equality using the ==
operator.
irb :001 > a = [1, 2, 3]
=> [1, 2, 3]
irb :002 > b = [2, 3, 4]
=> [2, 3, 4]
irb :003 > a == b
=> false
irb :004 > b.pop
=> 4
irb :005 > b.unshift(1)
=> [1, 2, 3]
irb :006 > a == b
=> true
You'll notice that we used the unshift
method in this example. unshift
is a lot like the push
method. However, instead of adding values to the end of the list, unshift
adds values to the start of the list.
The to_s
method is used to create a string representation of an array. Ruby does this behind the scenes when you use string interpolation to print an array to the screen.
irb :001 > a = [1, 2, 3]
=> [1, 2, 3]
irb :002 > "It's as easy as #{a}"
=> "It's as easy as [1, 2, 3]"
In order to get our array to print properly, Ruby is calling the to_s
method on our array and adding it into the string.
This section will introduce you to some common methods that Ruby has built-in to its Array class. You should bookmark that documentation page as it's probably something you'll want to refer to often.
The include? method checks to see if the argument given is included in the array. It has a question mark at the end of it which usually means that you should expect it to return a boolean value, true
or false
. (Such methods are called predicates.) Just like the methods that end in a "!", this is strictly by convention only and not a property of the language.
irb :001 > a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb :002 > a.include?(3)
=> true
irb :003 > a.include?(6)
=> false
The flatten method can be used to take an array that contains nested arrays and create a one-dimensional array.
irb: 001 > a = [1, 2, [3, 4, 5], [6, 7]]
=> [1, 2, [3, 4, 5], [6, 7]]
irb: 002 > a.flatten
=> [1, 2, 3, 4, 5, 6, 7]
Is the flatten method destructive? Find out for yourself in either irb, or by consulting the Array documentation.
The each_index method iterates through the array much like the each method, however the variable represents the index number as opposed to the value at each index. It passes the index of the element into the block and you may do as you please with it. The original array is returned.
irb: 001 > a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb: 002 > a.each_index { |i| puts "This is index #{i}" }
This is index 0
This is index 1
This is index 2
This is index 3
This is index 4
=> [1, 2, 3, 4, 5]
Another useful method that works in a similar way to each_index
is each_with_index.
irb: 001 > a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb: 002 > a.each_with_index { |val, idx| puts "#{idx+1}. #{val}" }
1. 1
2. 2
3. 3
4. 4
5. 5
=> [1, 2, 3, 4, 5]
each_with_index
gives us the ability to manipulate both the value and the index by passing in two parameters to the block of code. The first is the value and the second is the index. You can then use them in the block.
The sort method is a handy way to order an array. It returns a sorted array.
irb :001 > a = [5, 3, 8, 2, 4, 1]
=> [5, 3, 8, 2, 4, 1]
irb :002 > a.sort
=> [1, 2, 3, 4, 5, 8]
Once again, test in irb to see if the sort
method is destructive. (It's not, but test it out for yourself.) We won't remind you to test this in the future, but when you see methods like this in the future, ask yourself "is this method returning new data, or is the original data being modified?".
The product method can be used to combine two arrays in an interesting way. It returns an array that is a combination of all elements from all arrays.
irb :001 > [1, 2, 3].product([4, 5])
=> [[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]]
There are too many interesting methods to cover, but we wanted to give you a taste of the power of Ruby arrays and the many handy methods that come built-in with Ruby. If you ever think "I want my array to...", there is probably a method that already does this. First, check the documentation.
each
provides a simple way of iterating over a collection in Ruby and is preferred to using the for
loop. The each
method works on objects that allow for iteration and is commonly used along with a block. If given a block, each
runs the code in the block once for each element in the collection and returns the collection it was invoked on. If no block is given, it returns an Enumerator. Let's look at some simple examples:
irb :001 > a = [1, 2, 3]
irb :002 > a.each { |e| puts e }
1
2
3
=> [1, 2, 3]
The above shows the most common way of using each
. We're iterating over each element on the array a
and printing it out. Finally it returns [1, 2, 3]
.
We can also modify the elements in a
and print them out:
irb :003 > a = [1, 2, 3]
irb :004 > a.each { |e| puts e + 2 }
3
4
5
=> [1, 2, 3]
Again, we print out the modified values and return the original collection a
.
In this chapter, so far, we've been using curly brace blocks exclusively. Don't forget that you can also use do...end
as well:
irb :005 > a = [1, 2, 3]
irb :006 > a.each do |e|
irb :007 * puts e + 2
irb :008 * end
3
4
5
=> [1, 2, 3]
Here is a final example with no block; an Enumerator is returned:
irb :009 > a = [1, 2, 3]
irb :010 > a.each
=> #<Enumerator: ...>
map
also works on objects that allow for iteration. Like each
, when given a block it invokes the given block once for each element in the collection. Where it really differs from each
is the returned value. map
creates and returns a new array containing the values returned by the block. Let's see it in action:
irb :011 > a = [1, 2, 3]
irb :012 > a.map { |x| x**2 }
=> [1, 4, 9]
We square each element in the block and create a new array containing the returned values by the block. Finally the new array is returned.
To really examine that map
creates a new array consisting of the returned value of the block, let's see an example with map
and puts
:
irb :013 > a = [1, 2, 3]
irb :014 > a.map { |x| puts x**2 }
1
4
9
=> [nil, nil, nil]
Because puts
returns nil
every time the block is invoked nil
is returned which makes up the values in the newly created returned array.
Finally, if no block is given, map
returns an Enumerator:
irb :015 > a = [1, 2, 3]
irb :016 > a.map
=> #<Enumerator: ...>
each
and map
are important methods to know but can be quite confusing in the beginning. Another way to remember these methods: use each
for iteration and map
for transformation.
Arrays are an extremely valuable data set. They can be used to store many different kinds of data and you'll see them very often in the wild. Ruby's array class has lots of built-in methods that can be used to perform many of the daily functions that programmers use. Let's practice working with arrays for a bit with some exercises.
Below we have given you an array and a number. Write a program that checks to see if the number appears in the array.
arr = [1, 3, 5, 7, 9, 11]
number = 3
arr.each do |num|
if num == number
puts "#{number} is in the array."
end
end
# ... or...
if arr.include?(number)
puts "#{number} is indeed in the array."
end
Video Walkthrough
What will the following programs return? What is the value of arr
after each?
1. arr = ["b", "a"]
arr = arr.product(Array(1..3))
arr.first.delete(arr.first.last)
2. arr = ["b", "a"]
arr = arr.product([Array(1..3)])
arr.first.delete(arr.first.last)
1. returns 1
arr = [["b"], ["b", 2], ["b", 3], ["a", 1], ["a", 2], ["a", 3]]
2. returns [1, 2, 3]
arr = [["b"], ["a", [1, 2, 3]]]
Video Walkthrough
How do you return the word "example" from the following array?
arr = [["test", "hello", "world"],["example", "mem"]]
Either of the following solutions will work:
arr.last.first
arr[1][0]
as well as several other variations. The video walkthrough talks about the first solution.
Video Walkthrough
What does each method return in the following example?
arr = [15, 7, 18, 5, 12, 8, 5, 1]
1. arr.index(5)
2. arr.index[5]
3. arr[5]
1. 3
2. NoMethodError: undefined method `[]' for #<Enumerator: [15, 7, 18, 5, 12, 8, 5, 1]:index>
from (irb):81
from /usr/local/rvm/rubies/ruby-2.5.3/bin/irb:16:in `<main>'
3. 8
Video Walkthrough
What is the value of a
, b
, and c
in the following program?
string = "Welcome to America!"
a = string[6]
b = string[11]
c = string[19]
a = "e"
b = "A"
c = nil
Notice that when you reference an index of a string that is beyond the length of the string, Ruby returns nil
and doesn't throw an error.
Video Walkthrough
You run the following code...
names = ['bob', 'joe', 'susan', 'margaret']
names['margaret'] = 'jody'
...and get the following error message:
TypeError: no implicit conversion of String into Integer
from (irb):2:in `[]='
from (irb):2
from /Users/username/.rvm/rubies/ruby-2.5.3/bin/irb:12:in `<main>'
What is the problem and how can it be fixed?
You are attempting to set the value of an item in an array using a string as the key. Arrays are indexed with integers, not strings. You would modify the array by doing the following:
names[3] = 'jody' # => ["bob", "joe", "susan", "jody"]
Video Walkthrough
Use the each_with_index
method to iterate through an array of your creation that prints each index and value of the array.
top_five_games = ["mario brothers",
"excite bike",
"ring king",
"castlevania",
"double dragon"]
top_five_games.each_with_index do | game, index |
puts "#{index + 1}. #{game}"
end
Video Walkthrough
Write a program that iterates over an array and builds a new array that is the result of incrementing each value in the original array by a value of 2. You should have two arrays at the end of this program, The original array and the new array you've created. Print both arrays to the screen using the p
method instead of puts
.
arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |n|
new_arr << n + 2
end
p arr
p new_arr
Or:
arr = [1, 2, 3, 4, 5]
new_arr = arr.map do |n|
n + 2
end
p arr
p new_arr
Video Walkthrough