loading...
Cover image for Level Up Your Ruby Skillz: Working With Arrays

Level Up Your Ruby Skillz: Working With Arrays

molly_struve profile image Molly Struve (she/her) Updated on ・9 min read

Level Up Your Ruby Skillz (3 Part Series)

1) Level Up Your Ruby Skillz: Working With Arrays 2) Level Up Your Ruby Skillz: Working With Hashes 3) Level Up Your Ruby Skillz: Writing Compact Code

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!

Level Up Your Ruby Skillz (3 Part Series)

1) Level Up Your Ruby Skillz: Working With Arrays 2) Level Up Your Ruby Skillz: Working With Hashes 3) Level Up Your Ruby Skillz: Writing Compact Code

Posted on Sep 18 '19 by:

molly_struve profile

Molly Struve (she/her)

@molly_struve

International Speaker πŸ—£ Runner πŸƒβ€β™€οΈ Always Ambitious. Never Satisfied. I ride πŸ¦„'s IRL

Discussion

markdown guide
 

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"]
 

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

 

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!

 

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 😳

 

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

 

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

 

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]
 

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"):

[1, 2, 3, 4].count(&:even?)
# => 2
 

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
 

This is such a useful reference! Thank you for this, it's always great to be reminded how awesome Ruby is! πŸ’™

 

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.

 

Ruby's plethora of built-in array methods of one of the best things about the language!

Thanks for this round up ❀️

 

Thanks MOLLY for sharing it was very useful to me :)