DEV Community

Cover image for The Last Enumerable
Iona Brabender
Iona Brabender

Posted on • Edited on

The Last Enumerable

photo by @pinamessina

Enumerables in Ruby can be extremely powerful, and can turn a messy loop into a few lines of clean and efficient code. However, given the vast number of methods available, it can be difficult to determine which one will work best for you. In this post, we'll examine ten useful enumerable methods with which you should be able to overcome many of your coding challenges.

Setting Up Your Enumerables Using do .. end or { ... }

When coding in Ruby, you have the option to set up your enumerables using either do ... end, or using curly brackets. Using the do ... end method is considered a little easier to read, especially for beginner coders, while the curly brackets option will give you a cleaner look. As we'll be using the latter option in our examples below, we'll quickly remind ourselves of how one method translates to the other.

Option 1: do ... end Method

    Character.all.find_all do |character|
        character.name == "Katara"
    end
Enter fullscreen mode Exit fullscreen mode

Option 2: Curly Brackets Method

    Character.all.find_all {|character| character.name == "Katara" } 
Enter fullscreen mode Exit fullscreen mode

The first option is very similar to the second, with the key differences being that the opening bracket is replaced with do, the closing bracket is replaced with end, and the code is spread out over 3 lines rather than 1. Either method is completely acceptable, so use whichever you feel most comfortable with.

10 Essential Enumerables

We can now take a look at the ten enumerable method every coder should have in their back pocket. To demonstrate how each method can be used, we'll be working with data based on characters from the tv series, Avatar: The Last Airbender. For our examples, we've created a Character class, where each character has several attributes, including a name, age, home nation, special skill, and role.

1. map

When coding in Ruby, map will probably become one of your most commonly used enumerable methods. It iterates over each element, and returns a new array containing the results of that iteration. It is especially advantageous because it can be used it in conjunction with other enumerable methods, as we'll see in some of the examples below.

Our Avatar characters have a variety of different special skills. In this example, we're interested in retrieving a list of those skills using map.

    def self.all_skills
        skills = all.map { |character| character.special_skill }
    end

    Character.all_skills

=> ["Airbending", "Firebending", "Earthbending", "Waterbending", "Firebending", "Hook Swords", "Waterbending", "Knives", "Firebending", "Boomerang", "Fans", "Earthbending", "Chi Blocking", "Waterbending", "Firebending"]
Enter fullscreen mode Exit fullscreen mode

We now have an array containing all of the special skills mastered by our characters. However, several of our characters share the same skills, and some of the elements are therefore repeated. We can easily solve this by adding a .uniq, which will ensure that each skill only appears once in our array.

    def self.all_skills
        skills = all.map { |character| character.special_skill }.uniq
    end

    Character.all_skills

    => ["Airbending", "Firebending", "Earthbending", "Waterbending", "Hook Swords", "Knives", "Boomerang", "Fans", "Chi Blocking"]
Enter fullscreen mode Exit fullscreen mode

Great! We now have an array of unique elements.

2. find_all

find_all is another useful method to have on hand. It returns an array containing those elements which meet the conditions specified in our enumerable code.

In this example, we're going to search for every character that belongs to the Water Nation.

    def self.home_nations(nation)
        all.find_all { |character| character.home_nation == nation }
    end

    Character.home_nations("Water Nation")

=> [#<Character:0x00007fb3bea8ebb0 @age=35, @home_nation="Water Nation", @name="Hakoda", @role="Chief of the Southern Water Tribe", @special_skill="Waterbending">,
 #<Character:0x00007fb3bea8e958 @age=14, @home_nation="Water Nation", @name="Katara", @role="Member of Team Avatar", @special_skill="Waterbending">,
 #<Character:0x00007fb3bea8e700 @age=15, @home_nation="Water Nation", @name="Sokka", @role="Member of Team Avatar", @special_skill="Boomerang">,
 #<Character:0x00007fb3bea8e318 @age=16, @home_nation="Water Nation", @name="Yue", @role="Princess of the Northern Water Tribe", @special_skill="Waterbending">]
Enter fullscreen mode Exit fullscreen mode

We get exactly what we asked for: an array of all the characters who belong to the Water Nation. We're now able to use or manipulate that data in whichever way we choose.

As we mentioned earlier, map is really handy, as we can utilise it in combination with other methods. Let's take a look at how we can apply map to our home_nations method, to extract more specific data.

    def self.home_nations(nation)
        characters = all.find_all { |character| character.home_nation == nation }
        characters.map { |character| character.name }
    end

    Character.home_nations("Water Nation")

    => ["Hakoda", "Katara", "Sokka", "Yue"]
Enter fullscreen mode Exit fullscreen mode

Our return value is now simply an array of the names of those characters who belong to the Water Nation, rather than the complete character instances. As before, we use find_all to first generate a list of all the relevant characters. We then use map to iterate over each character in our new Water Tribe characters array, and return only the names. Both the first and second results are useful in their own way - it completely depends on what return value you're looking for.

3. any?

Moving away from arrays as return values, we can take a look at any?, which allows us to confirm if any elements meet the conditions we've set out, and returns true or false based on the result.

For this example, we can again take a look at our characters' special skills. While we know there are plenty of earthbenders, firebenders, and waterbenders left in the four nations, it looks as though all of the airbenders have been wiped out. If only there were a way to check if there were any left... It turns out there is!

    def self.special_skill_finder(skill)
        all.any? { |character| character.special_skill == skill }
    end

    Character.special_skill_finder("Airbending")

    => true
Enter fullscreen mode Exit fullscreen mode

Phew! Our return value of "true" tells us that there is at least one airbender left in the world. Luckily for Aang though, our code doesn't reveal any other information to those pesky firebenders.

4. count

count is a useful enumerable method for when we're interested in how many there are of something. We can set up our code to count the occurrences of a certain attribute, and have that number returned to us.

In this example, let's count how many Fire Lords there are.

    def self.how_many(role)
        all.count { |character| character.role == role }
    end

    Character.how_many("Fire Lord")

    => 1
Enter fullscreen mode Exit fullscreen mode

As promised, count iterates over our characters, and returns the number of Fire Lords we have in our list. It looks as though there's still one. Aang better get a move on!

5. find

While find_all is more generally applicable, find is another helpful method. Rather than returning all of the elements which satisfy the conditions we specify, it only returns the first element to do so.

In this example, we're interested in Team Avatar, and we want to return the first character on our list who is part of that team.

    def self.character_role(role_type)
        all.find { |character| character.role == role_type }
    end

    Character.character_role("Member of Team Avatar")

    => #<Character:0x00007f85d3b02380 @age=14, @home_nation="Water Nation", @name="Katara", @role="Member of Team Avatar", @special_skill="Waterbending">
Enter fullscreen mode Exit fullscreen mode

While there may be other characters than just Katara who belong to Team Avatar, find only returns to us the first result which matches.

6. reject

reject can be very useful when we want to know what doesn't meet a specific criterium. reject will iterate over the elements in a given array, and return those which don't match that condition.

In this example, we want to help Team Avatar work out who they can trust. Therefore, we want a list of all the characters who don't belong to the Fire Nation. We just need a list of names, so we can use map again to make sure those are the only details included in our new array.

    def self.not_from(nation)
        characters = all.reject { |character| character.home_nation == nation }
        characters.map { |character| character.name }
    end

    Character.not_from("Fire Nation")

    => ["Aang", "Bumi", "Hakoda", "Jet", "Katara", "Sokka", "Suki", "Toph", "Yue"]
Enter fullscreen mode Exit fullscreen mode

Perfect! We've rejected the characters who belong to the Fire Nation, and Aang has a comprehensive list of trustworthy people.

7. reduce

reduce is another method we can use when we're working with numbers. It takes an array of numbers, reduces it to a single number, and returns that value.

In this example, we want to know the combined age of all of our Avatar: The Last Airbender characters. As reduce works on numbers, we'll need to extract the ages of our characters using map before we're able to reduce the data to a single value.

    def self.combined_age
        ages = all.map { |character| character.age }
        ages.reduce { |total, age| total += age }
    end

    Character.combined_age

    => 526
Enter fullscreen mode Exit fullscreen mode

Here, we mapped all of our characters' ages to a new array, and then, using +=, specified that we wanted to add each value to the total, which, unless otherwise specified, initialises at 0.

8. min_by

min_by is handy when we want to know which element is the lowest, based on a certain condition. It will then provide us with that element as the return value.

In this example, we want to know who our youngest character is.

    def self.youngest_character # Min_by
        all.min_by { |character| character.age }
    end

    Character.youngest_character

    => #<Character:0x00007ff6ce19e228 @age=12, @home_nation="Earth Nation", @name="Toph", @role="Member of Team Avatar", @special_skill="Earthbending">
Enter fullscreen mode Exit fullscreen mode

It looks like Toph is the baby of the group!

9. max_by

max_by works in much the same way as min_by, but instead returns the element with the maximum value.

We're also going to add an extra dimension here, which can similarly be used with min_by. Rather than looking for the single oldest character, we want to know who the three oldest characters are.

    def self.oldest_character(num) # Max_by
        all.max_by(num) { |character| character.age }
    end

    Character.oldest_character(3)

    => [#<Character:0x00007f8b7cb3ac68 @age=112, @home_nation="Air Nation", @name="Aang", @role="Avatar", @special_skill="Airbending">,
 #<Character:0x00007f8b7cb3aad8 @age=112, @home_nation="Earth Nation", @name="Bumi", @role="King of Omashu", @special_skill="Earthbending">,
 #<Character:0x00007f8b7cb3a920 @age=50, @home_nation="Fire Nation", @name="Iroh", @role="Fire Nation General", @special_skill="Firebending">]
Enter fullscreen mode Exit fullscreen mode

We specified that we wanted three characters, by including 3 between parentheses as our argument. As expected, we get back our three oldest characters: Bumi, Iroh, and (despite appearances) Avatar Aang.

10. sort_by

The final enumerable we'll look at is sort_by. We can use this method when we want to put our instances in order based upon a certain condition, and want to** receive the sorted array as the return value**.

This time, we want to sort our characters by name. Again, we don't need too much information, so let's combine sort_by with map to get a more succinct return value.

    def self.sort_characters_by_name # Sort_By
        characters = all.sort_by { |character| character.name }
        characters.map { |character| character.name }
    end

    Character.sort_characters_by_name

    => ["Aang", "Azula", "Bumi", "Hakoda", "Iroh", "Jet", "Katara", "Mai", "Ozai", "Sokka", "Suki", "Toph", "Ty Lee", "Yue", "Zhao"]
Enter fullscreen mode Exit fullscreen mode

Great! We now have our whole beloved cast of characters organised in alphabetical order.

Is that it?

Certainly not! This list is simply a collection of some of the most commonly used enumerable methods. With these at hand, you should be able to solve a variety of coding problems.

In addition to the methods highlighted here, there are plenty more available for exploration by any budding coder. A comprehensive list, along with explanations and examples can be found on here.

Sources

  1. "Enumerable", ruby-doc, Accessed July 5 2020

Top comments (0)