DEV Community

Cover image for Level Up Your Ruby Skillz: Working With Arrays
Molly Struve (she/her)
Molly Struve (she/her)

Posted on • Updated on

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!

Top comments (22)

Collapse
 
kdraypole profile image
Kobe Raypole

Awesome article! I use the with_index method quite often. Another option is each_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 doing

result = ['a', 'b', 'c'].map.with_index(1) do |letter, index|
  "#{letter}:#{index}"
end
# result = ["a:1", "b:2", "c:3"]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
molly profile image
Molly Struve (she/her)

WHAT?!!!!!! I had no clue, thank you for sharing!!!

Collapse
 
danazar96 profile image
Aidan Walters-Williams • Edited

Fantastic article Molly! I have been using

result = [1, 2, 3].map do |number|
  ['a', 'b', 'c'].map do |letter|
    "#{number}:#{letter}"
  end
end
result.flatten

for ages without knowing there was a better alternative!

Collapse
 
ludamillion profile image
Luke Inglis

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.

Collapse
 
tadman profile image
Scott Tadman • Edited

They keep adding new toys to the language. One of my more recent discoveries is transform_keys and transform_values for Hash which they shipped without fanfare in Ruby 2.4. That's proven really valuable for "symbolizing" keys and cleaning up values.

Collapse
 
lennythedev profile image
Lenmor Ld

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 😳

Collapse
 
grsahil20 profile image
Sahil

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 nesting

a =  [[1, 2, 3], [4, [5, [6, 7]]]]
b = a.flatten
# b = [1, 2, 3, 4, 5, 6, 7]

Also, you can do a.flatten!, which will overwrite a itself and assign the new value.

a =  [[1, 2, 3], [4, [5, [6, 7]]]]
a.flatten!
# a = [1, 2, 3, 4, 5, 6, 7]
Collapse
 
lukewduncan profile image
Luke Duncan • Edited

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...

Collapse
 
molly profile image
Molly Struve (she/her)

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.

Collapse
 
lukewduncan profile image
Luke Duncan

Of course! After all, it is your work!!!

Collapse
 
botanical profile image
Jennifer Tran

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!

Collapse
 
aritdeveloper profile image
Arit Developer

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!

Collapse
 
molly profile image
Molly Struve (she/her)

We all gotta start somewhere πŸ˜ƒ

Collapse
 
swiknaba profile image
Lud

For single methods without argument, you can use shorthand syntax (aka. "passing a proc"):

[1, 2, 3, 4].count(&:even?)
# => 2
Collapse
 
codeandclay profile image
Oliver

You can also define your own methods and do something similar using &method.

def dog?(animal)
  ["spaniel", "schnauzer"].include? animal
end  
["perch", "spaniel", "haddock", "schnauzer", "cod"].count(&method(:dog?))
#=> 2
Collapse
 
tadman profile image
Scott Tadman

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.

Collapse
 
molly profile image
Molly Struve (she/her)

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.

Thread Thread
 
tadman profile image
Scott Tadman • Edited

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.