DEV Community

loading...
Cover image for Understanding Ruby - Enumerable - Searching and Filtering

Understanding Ruby - Enumerable - Searching and Filtering

Brandon Weaver
Staff Eng / Ruby Lead / Global Neurodiversity Chair at @Square. Autistic / ADHD, He / Him. I'm the Lemur guy.
・5 min read

Introduction

Enumerable. Debatably one of, if not the, most powerful features in Ruby. As a majority of your time in programming is dealing with collections of items it's no surprise how frequently you'll see it used.

Difficulty

Foundational

Some knowledge required of functions in Ruby. This post focuses on foundational and fundamental knowledge for Ruby programmers.

Prerequisite Reading:

Enumerable

Enumerable is an interface module that contains several methods for working with collections. Many Ruby classes implement the Enumerable interface that look like collections. Chances are if it has an each method it supports Enumerable, and because of that it's quite ubiquitous in Ruby.

Note: This idea was partially inspired by Lamar Burdette's recent work on Ruby documentation, but takes its own direction.

Searching and Filtering

Sometimes you need to find a specific value in a collection. Ruby has methods for that. Sometimes you need to find more than one, and perhaps only ones that match a certain condition, or maybe even ones that don't match the condition at all.

#find / #detect

find is how you find one element in a collection:

[1, 2, 3].find { |v| v == 2 }
# => 2

[1, 2, 3].find { |v| v == 5 }
# => nil
Enter fullscreen mode Exit fullscreen mode

Oddly it takes a single argument, something that responds to call, as a default:

[1, 2, 3].find(-> { 1 }) { |v| v == 5 }
# => 1
Enter fullscreen mode Exit fullscreen mode

I honestly do not understand this myself as you cannot give it a value like this:

[1, 2, 3].find(1) { |v| v == 5 }
# NoMethodError (undefined method `call' for 1:Integer)
Enter fullscreen mode Exit fullscreen mode

There is currently a bug tracker issue open against this, but it hasn't seen updates in a fair amount of time not including my recent question on it.

find is useful for finding a single value in a collection and returning it as soon as it finds it, rather than using something like select.first which would iterate all elements.

#find_index

find_index is very similar to find except that it finds the index of the item rather than returning the actual item:

[1, 2, 3].find_index { |v| v == 2 }
# => 1
Enter fullscreen mode Exit fullscreen mode

Interestingly it takes an argument rather than a Block Function for a value to search for:

[1, 2, 3].find_index(3)
# => 2
Enter fullscreen mode Exit fullscreen mode

...which makes a bit more sense than a default argument like in the case of find, but to change those would break all types of potential code.

I have not found a direct use for find_index at this point, and cases where I would use it I tend to reach for slicing and partitioning methods instead.

#select / #find_all / #filter

select is a method with a lot of aliases in find_all and filter. If you come from Javascript filter might be more comfortable, and with the introduction of filter_map it may see more popularity. select is more common in general usage.

select is used to get all elements in a collection that match a condition:

[1, 2, 3, 4, 5].select(&:even?)
# => [2, 4]
Enter fullscreen mode Exit fullscreen mode

Currently it uses a Block Function to check each element.

select is typically used and great for filtering lists by a positive condition.

#reject

reject, however, is great for negative conditions like everything except Numeric entries:

[1, 'a', 2, :b, 3, []].reject { |v| v.is_a?(Numeric) }
# => ["a", :b, []]
Enter fullscreen mode Exit fullscreen mode

Though in this particular case I would likely use grep_v instead which we'll cover in a moment. grep right below will have some additional insights on this distinction.

Often times Ruby methods will have a dual that does the opposite. select and reject, all? and none?, the list goes on. Chances are there's an opposite method out there.

reject has many of the same uses as select except that it inverts the condition and instead rejects elements which match a condition.

#grep

grep is interesting in that it's based on Unix's grep command, but in Ruby it takes something that responds to === as an argument:

[1, :a, 2, :b].grep(Symbol)
# => [:a, :b]
Enter fullscreen mode Exit fullscreen mode

It behaves similarly to select, and there are tickets out to consider adding the === behavior to select, similarly with reject and grep_v.

Where it differs is that its block does something different:

[1, :a, 2, :b].grep(Numeric) { |v| v + 1 }
# => [2, 3]
Enter fullscreen mode Exit fullscreen mode

It acts very much like map for any elements which matched the condition.

grep behaves mildly similarly to filter_map except that every element in the block has already been filtered via ===. When you need more power for conditional checking if an element belongs in the new list use filter_map, otherwise grep makes a lot of sense.

#grep_v

grep_v is the dual of grep, similar to select and reject. grep_v behaves similarly to reject except it uses grep's style:

[1, :a, 2, :b].grep_v(Symbol)
# => [1, 2]

[1, :a, 2, :b].grep_v(Symbol) { |v| v + 1 }
# => [2, 3]
Enter fullscreen mode Exit fullscreen mode

Just as with reject it makes sense in cases where you want the opposite data from grep but still want the same condition.

#uniq

uniq will get all unique items in a collection:

[1, 2, 3, 1, 1, 2].uniq
# => [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

It also takes a block to let you decide exactly what criteria you want the new collection to be unique by:

(1..10).uniq { |v| v % 5 }
# => [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

Which can be very useful for unique sizes, names, or other criteria. In the above example we're doing something a bit unique in searching for remainders from modulo which can be very useful in certain algorithmic problems.

uniq is great when you want to get a unique collection of elements, but if you find yourself using uniq a lot you may want to consider using a Set instead, which we'll cover in a later article.

Wrapping Up

The next few articles will be getting into the various parts of Enumerable, grouped by functionality:

  1. Transforming
  2. Predicate Conditions
  3. Searching and Filtering
  4. Sorting and Comparing
  5. Counting
  6. Grouping
  7. Combining
  8. Iterating and Taking
  9. Coercion

While lazy is part of Enumerable that deserves a post all its own, and we'll be getting to that one soon too.

Want to keep up to date on what I'm writing and working on? Take a look at my new newsletter: The Lapidary Lemur

Discussion (0)