When I first started out, there was a senior engineer that I worked with who was a wizard when it came to working with arrays. He taught me all the best tricks to writing succinct and clean code when it came to dealing with arrays. Here are the methods that I find the most useful and I think are good to have in your Ruby toolbox right from the start.
If you want to jump straight to the code without the explanations checkout the cheatsheet at the bottom!
each
Before we dive into some of the fancier methods above, we first need to start with the most basic, each
. each
will call a block once for every given element in an array. When it is done, it will return the original array. That last part is key and is easy to forget. Even those of us who have been working with Ruby for a while sometimes forget it. Here is an example.
result = [1, 2, 3].each do |number|
puts 'hi'
end
That code will produce the following result when run in a console. NOTE: In the example below and those that follow, irb simply means I am in a Ruby console.
irb:> result = [1, 2, 3].each do |number|
> puts "hi #{number}"
> end
hi 1
hi 2
hi 3
=> [1, 2, 3]
For each number in our array we printed "hi" plus that number. Then, after we have finished traversing the entire array, our original array is returned.
Keep in mind, I am using the do/end
block notation above, but you can also use the bracket syntax for your block which is shown below.
irb:> result = [1, 2, 3].each{|number| puts "hi #{number}"}
hi 1
hi 2
hi 3
=> [1, 2, 3]
As you can see, regardless of syntax, the result is the same. I am going to continue to use the do/end
syntax throughout this guide because I think it makes the code and logic easier to understand. With that said, all of these methods will work with the bracket syntax as well.
map
In the early days, when I was new to Ruby, every time I wanted to build an array I did something like this:
result = []
[1, 2, 3, 4].each do |number|
result << number + 2
end
# result = [3, 4, 5, 6]
I quickly learned there was a better way, and that is by using map
. map
returns a new array with the results of executing the block once for every element in your original array. Here is an example:
result = [1, 2, 3, 4].map do |number|
number + 2
end
# result = [3, 4, 5, 6]
We add 2 to every number in our original array and then group those results up into a new array, which is returned. Now our code is a bit cleaner and more compact.
flat_map
map
is great for collecting a set of results, but what happens when you want to map over nested arrays? That is when flat_map
comes in handy. If you find yourself with a set of nested arrays then you might want to checkout flat_map
. For example, say you have code like this with a couple of nested arrays.
result = []
[1, 2, 3].each do |number|
['a', 'b', 'c'].each do |letter|
result << "#{number}:#{letter}"
end
end
# result = ["1:a", "1:b", "1:c", "2:a", "2:b", "2:c", "3:a", "3:b", "3:c"]
We get a single level array, which is what we wanted, but how could we tighten this up? Let's try using map
.
result = [1, 2, 3].map do |number|
['a', 'b', 'c'].map do |letter|
"#{number}:#{letter}"
end
end
# result = [["1:a", "1:b", "1:c"], ["2:a", "2:b", "2:c"], ["3:a", "3:b", "3:c"]]
Hmmm, that is not quite what we want. We want a flat, single level array and map
is creating a nested one. In order to flatten that nested array we can use flat_map
.
result = [1,2,3].flat_map do |number|
['a', 'b', 'c'].map do |letter|
"#{number}:#{letter}"
end
end
# result = ["1:a", "1:b", "1:c", "2:a", "2:b", "2:c", "3:a", "3:b", "3:c"]
flat_map
works similar to map
in that it collects the results from your block into an array, but as a bonus, it will flatten it. Under the hood, flat_map
is concatenating all of the inner arrays into a single one. Using flat_map
returns that single level array we wanted.
select
Similar to the map
example, when I was starting out, if I wanted to conditionally select elements from an array, for example, choose all the even numbers, I would do something like this:
result = []
[1, 2, 3, 4].each do |number|
result << number if number.even?
end
# result = [2, 4]
It works, but there is an even more succinct way, and that is by using select
. select
returns an array containing all elements for which the given block returns a true value. This means we can rewrite our above block of code like this!
result = [1, 2, 3, 4].select do |number|
number.even?
end
# result = [2,4]
detect
Now we are going to kick it up a notch. What if instead of wanting all the even numbers back from an array, you only want the first even number that you find? For that you can use detect
. detect
will return the first entry for which your block evaluates to true. So if we run a similar block of code as above, but replace select
with detect
, you can see we get back only the first even number.
result = [1, 2, 3, 4].detect do |number|
number.even?
end
# result = 2
One important thing to note here is that we are now returning a number(our entry) and NOT an array.
But what happens if our block never evaluates to true? What if there are no even numbers in our array? In that case, detect
will return nil.
result = [1, 3, 5, 7].detect do |number|
number.even?
end
# result = nil
To summarize, detect
will return the first entry your block evaluates to true for OR it will return nil if no entry evaluates to true for your block.
reject
Now let's look at the inverse of select
, which is reject
. reject
will return all entries for which your block evaluates FALSE. So instead of doing this:
result = []
[1, 2, 3, 4].each do |number|
result << number if !number.even?
end
# result = [1, 3]
We can simplify the above code and do something like this instead:
result = [1, 2, 3, 4].reject do |number|
number.even?
end
# result = [1, 3]
This time we will return each number which is not even, so those where number.even?
returns false.
partition
We have just seen two ways we can filter through arrays in Ruby using select
and reject
. But what if you want to straight up separate your single array into two arrays, one for even numbers and one for odd numbers? One way to accomplish this is by doing:
even = [1, 2, 3, 4].select do |number|
number.even?
end
# even = [2, 4]
odd = [1, 2, 3, 4].reject do |number|
number.even?
end
# odd = [1, 3]
But, there is an even better way, you can use partition
! Hold on to your seats for this one. partition
will return TWO arrays, the first containing the elements of the original array for which the block evaluated true and the second containing the rest. This means we can take what we wrote above and simplify it to:
result = [1, 2, 3, 4].partition do |number|
number.even?
end
# result = [[2, 4], [1, 3]]
As you can see, partition
will return two arrays, one with even numbers, and one for odd numbers. If we want to assign our even
and odd
variables all we have to do is
even = result.first
odd = result.last
However, as you can probably guess, there is an even better way! We can eliminate that single result
variable altogether and write something like this
even, odd = [1, 2, 3, 4].partition do |number|
number.even?
end
# even = [2, 4] and odd = [1, 3]
This syntax is going to automatically assign the first array to even
and the second array to odd
. You can use this array assignment syntax anytime you are dealing with nested arrays. Here is an example of how you can breakup 3 arrays.
irb:> a, b, c = [[1], [2], [3]]
=> [[1], [2], [3]]
irb:> a
=> [1]
irb:> b
=> [2]
irb:> c
=> [3]
count
count
for the most part is pretty self explanatory, by default, it will count the number of elements in your array.
irb:> [1, 1, 2, 2, 3, 3].count
=> 6
But, did you know it can do so much more? For starters, can pass count
an argument. If you pass count
an argument, it will count the number of times that argument occurs in your array.
irb:> [1, 1, 2, 2, 3, 3].count(1)
=> 2
irb:> ['a', 'a', 'b', 'c'].count('c')
=> 1
You can also pass count
a block!π²When passed a block, count
will return the count for the number of entries that block evaluates to true for.
irb:> [1, 1, 2, 2, 3, 3].count do |number|
number.odd?
end
=> 4
Every number that is odd in our array was counted and the result returned was 4.
with_index
Last but not least, I want to talk about traversing an array with an index. Often when we want to keep track of where we are in an array of elements we will do something like this.
irb:> index = 0
irb:> ['a', 'b', 'c'].each do |letter|
puts index
index += 1
end
0
1
2
However, there is a better way! You can use with_index
with each
, or any of the methods I listed above, to help you keep track of where you are in an array. Here are some examples of how you can use it. (REMEMBER: Array indexes start at 0 π)
irb:> ['a', 'b', 'c'].each.with_index do |letter, index|
puts index
end
0
1
2
In this example we are simply iterating over our array and printing out the index for each element.
result = ['a', 'b', 'c'].map.with_index do |letter, index|
"#{letter}:#{index}"
end
# result = ["a:0", "b:1", "c:2"]
In this example, we are combining the index with the letter in our array to form a new array using the map
method.
result = ['a', 'b', 'c'].select.with_index do |letter, index|
index == 2
end
# result = ["c"]
This example is a little trickier. Here we are using our index to help us select the element in our array that is at index equal to 2. In this case, that element is "c".
chaining
The last tidbit of knowledge I want to leave you with is that any of the methods above that return an array(all except count
and detect
), you can chain together. For these examples I am going to use bracket notation because I think it's easier to read chaining methods from left to right rather than up and down.
For example, you can do this:
result = [1, 2, 3, 4].map{|n| n + 2}.select{|n| n.even?}.reject{|n| n == 6}.map{|n| "hi #{n}"}
# result = ["hi 4"]
Let's break down what is happening here given what we learned above.
1) map
is going to add 2 to each of our array elements and return [3, 4, 5, 6]
2) select
will select only the even numbers from that array and return [4, 6]
3) reject
will remove any number equal to 6 which leaves us with [4]
4) Our final map
will prepend "hi" to that 4 and return ["hi 4"]
You Made it!!!!
Congrats, you made it all the way to the end! Hopefully, you find these array methods useful as you are writing your Ruby code. If anything is unclear, PLEASE let me know in the comments. This is my first time writing a tutorial so I welcome any and all feedback π€
If you would like all of these code examples without the lengthy explanations checkout this cheatsheet that @lukewduncan graciously put together!
Top comments (22)
Awesome article! I use the
with_index
method quite often. Another option iseach_with_index
.At first, there appears to be no difference until you take into account that
with_index
accepts an optional "start index". Therefore you can start your array index at 1 by doingWHAT?!!!!!! I had no clue, thank you for sharing!!!
Fantastic article Molly! I have been using
for ages without knowing there was a better alternative!
Guess it pays to keep a beginner's mind. I've been using Ruby professionally for 7+ years and some of these were new to me.
They keep adding new toys to the language. One of my more recent discoveries is
transform_keys
andtransform_values
forHash
which they shipped without fanfare in Ruby 2.4. That's proven really valuable for "symbolizing" keys and cleaning up values.Nice! As a beginner in Ruby, this is really helpful.
The thing that always trips me up is forgetting the
|
to surround the current element and using(
instead of{
I keep thinking like ES6 in Ruby π³
Additional Bonus for you
flatten
. Flatten, as name suggests flatten out an array no matter how much nesting level is involved for an array. Below is the example for 3 level nestingAlso, you can do
a.flatten!
, which will overwritea
itself and assign the new value.Thanks for the awesome article Molly! I found it very helpful, the examples are clear and concise!
I've created a short cheatsheet on a Paper doc so people can keep it handy: paper.dropbox.com/doc/Ruby-Array-M...
Wow, this is really fantastic! Thank you for putting that together. I will keep this in mind for future posts.
Do you mind if I add a link to your cheatsheet at the bottom of the post and share it on Twitter? I think people will find it very useful and I will definitely be sure to give you credit for it.
Of course! After all, it is your work!!!
I've used some of these methods before but it was great to learn new ones such as detect and reject! I also never realized that some of these methods return the original array. Thank you for pointing that out and thank you for such a great tutorial!
What I LOVE about this tutorial, Molly, is how you include examples of how you used to code, before becoming proficient at these different methods. I laughed at each "newbie example" you gave, cos that's exactly how I've been doing things too! LOL. Thank you for this awesome resource!
We all gotta start somewhere π
For single methods without argument, you can use shorthand syntax (aka. "passing a proc"):
You can also define your own methods and do something similar using
&method
.A lot of these convenience methods work on any
Enumerator
, and a lot of things emit those. You can even write your own with a few lines of code.YES! One of my favorite ruby methods currently is to_enum so you can use all these convenience methods with your own.
I originally had this post titled "Level Up Your Ruby Skillz: Working with Enumerators" but wanted to make it very approachable to new devs learning Ruby and figured starting with straight Arrays would be better.
I didn't know about the buddy method enum_for which also looks super handy.
Everything in Ruby is an object, but maybe we should also say that everything, with the right attitude (or method call!), can be an Enumerator, too.