Level Up Your Ruby Skillz: Working With Arrays

molly

Molly Struve (she/her)

Posted on May 8, 2019

Level Up Your Ruby Skillz: Working With Arrays

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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!

πŸ’– πŸ’ͺ πŸ™… 🚩
molly
Molly Struve (she/her)

Posted on May 8, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related