🔙

Unknowns of Ruby's Enumerable

with examples

Mads Ohm Larsen
October 2024

The Enumerable module in Ruby is a collection of methods that can be used on any object that responds to the #each method.

[1, 2, 3].each { |i| puts i }

This will print 1, 2 and 3 on separate lines.

The Enumerable module also includes methods like #map, #select, #reduce and #sort.

[1, 2, 3].map { |i| i * 2 }

This will return [2, 4, 6].

As long as you respond to #each, you can use any of these methods on any object.

There is, however, a bunch of “unknown” or “forgotten”, seldom used, methods that are also included in the Enumerable module.

Tally

Lets start with #tally. Like #count returns the number of elements in the collection, #tally will return a hash with the number of occurrences of each element in the collection.

[1, 2, 3, 1, 2, 1].tally

This will return { 1 => 3, 2 => 2, 3 => 1 }.

This is useful if you, for example, have a list of users each in a separate country and you want to know how many users are from each country.

User = Data.define(:name, :country)

users = [
  User["John", "Denmark"],
  User["Jane", "Denmark"],
  User["Alice", "Sweden"],
  User["Bob", "Sweden"],
  User["Charlie", "Denmark"],
  User["Mark", "Germany"],
]

users.map(&:country).tally

This will return { "Denmark" => 3, "Sweden" => 2, "Germany" => 1 }.

Minmax

Mostly just forgotten, but if you need both the minimum and maximum value in a collection, you can use #minmax.

[1, 2, 3, 4, 5].minmax

This will return [1, 5].

There’s also a #minmax_by method that returns the minimum and maximum value in the collection as determined by the given block.

Partition

I think most are familiar with the #group_by method.

User = Data.define(:name, :country)

users = [
  User["John", "Denmark"],
  User["Jane", "Denmark"],
  User["Alice", "Sweden"],
  User["Bob", "Sweden"],
  User["Charlie", "Denmark"],
  User["Mark", "Germany"],
]

users.group_by(&:country)

This will return a hash with the countries as keys and the users in that country as values.

#partition, however, takes a block and splits the collection into two groups based on the result of the block.

[1, 2, 3, 4, 5].partition(&:even?)

This will return [[2, 4], [1, 3, 5]].

Useful, when you know there’s two different groups you want your collection split into.

Slicing

#slice_when along with #slice_before and #slice_after can be seen as specialized versions of #partition. Where #partition only splits in two, #slice_* will slice into as many groups as necessary.

For example:

(1..10).slice_after { |i| i % 3 == 0 }

will return an enumerator with the following content:

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

#slice_before works in a similar fashion, but will include the element that caused the slice.

(1..10).slice_before { |i| i % 3 == 0 }
# => [[1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]

#slice_when yields two elements at a time to the block and will slice when the block returns true.

In the following example we slice when the difference between the two elements is greater than 1:

[1, 2, 4, 9, 10, 11, 15, 16].slice_when { |i, j| i + 1 != j }
# => [[1, 2], [4], [9, 10, 11], [15, 16]]

Chunk

#chunk is a bit more complex. It returns an enumarator where the values are tuples, the first containing the value yielded from the block and the second containing an array of all the elements that yielded that value.

(0..11).chunk { |i| i / 3 } # integer division
# => [[0, [0, 1, 2]], [1, [3, 4, 5]], [2, [6, 7, 8]], [3, [9, 10, 11]]]

#chunk_while is similar to #slice_when, but slices when the block returns false instead of true.

[1, 2, 4, 9, 10, 11, 15, 16].chunk_while { |i, j| i + 1 != j }
# => [[1], [2, 4, 9], [10], [11, 15], [16]]

Mapping

Some map methods are seldom used on Enumerable objects.

#filter_map does exactly what it says, it filters and maps. If the block returns a truthy value, it will be included in the result.

[1, 2, 3, 4, 5].filter_map { |i| i * 2 if i.even? }

This will return [4, 8].

#flat_map (aliased as #collect_concat) is a combination of #map and #flatten.

Take, for example:

[[0, 1], [2, 3]].flat_map { |e| e.map { |i| i + 100 } }

which is identical

[[0, 1], [2, 3]].map { |e| e.map { |i| i + 100 } }.flatten

This will return [100, 101, 102, 103].

Grep

Last two methods are #grep and #grep_v. These come directly from the Unix command line tool grep.

#grep will return an array with all the elements that match the given pattern.

["apple", "banana", "orange"].grep(/e/)
# => ["apple", "orange"]

#grep is a shorthand for #select with a regex pattern:

["apple", "banana", "orange"].select { |e| /e/ === e }
# => ["apple", "orange"]

#grep_v is named after the -v flag in grep and will return all the elements that do not match the given pattern.

["apple", "banana", "orange"].grep_v(/e/)
# => ["banana"]

#grep_v is thus a shorthand for #reject with a regex pattern:

["apple", "banana", "orange"].reject { |e| /e/ === e }
# => ["banana"]

These were just an exploration of some of the lesser-known methods in the Enumerable module and their uses.