One of the things that sets Ruby apart from other programming languages is its impressive set of built-in features. One of my favourite method is Enumerable#detect. It returns the first element for which the block yields a truthy value and stops searching upon the first hit.

require "prime"
arr = (1..10).to_a
arr.detect { |n| Prime.prime?(n) } #=> 2

Okay, let’s spice it up! How do we find last matching element? A possible solution could be to filter the whole array using Enumerable#filter, and use the last match.

require "prime"
arr = (1..10).to_a
arr.filter { |n| Prime.prime?(n) }.last #=> 7

However, this approach means traversing the entire array just to get that final match and discarding rest of the matches.

Ruby to Rescue

Ruby provides another awesome method Enumerable#reverse_each, it allows to gracefully traverse an array in reverse order without mutating the original array.

Combining it with Array#detect, we now have an optimized solution where we can find the last matching element without traversing the full array, making it doubly awesome.

require "prime"
arr = (1..10).to_a
arr.reverse_each.detect { |n| Prime.prime?(n) }

Wait! Ruby is actually more awesome!!

Ruby provides a method that exactly matches our use case: Array#rindex. This method returns the index of the last element for which the block returns a truthy value.

While similar to Enumerable#detect, there is a slight difference between the two: Enumerable#detect returns the matched element itself, whereas Array#rindex returns the index of that element.

require "prime"
arr = (1..10).to_a
index = arr.rindex { |n| Prime.prime?(n) }
arr[index] #=> 7

Benchmark It!!

# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
  gem "prime"
end

require "benchmark/ips"
require "prime"

ARR = (1..100_000).to_a

Benchmark.ips do |x|
  x.report("reverse_each.detect") { ARR.reverse_each.detect { |n| Prime.prime?(n) } }
  x.report("filter.last") { ARR.filter { |n| Prime.prime?(n) }.last }
  x.report("rindex") { ARR[ARR.rindex { |n| Prime.prime?(n) }] }

  x.compare!
end

# ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin22]
# Warming up --------------------------------------
#  reverse_each.detect     7.306k i/100ms
#          filter.last     1.000 i/100ms
#               rindex     7.623k i/100ms
# Calculating -------------------------------------
#  reverse_each.detect     73.400k (± 1.2%) i/s -    372.606k in   5.077127s
#          filter.last      8.764 (± 0.0%) i/s -     44.000 in   5.025497s
#               rindex     76.693k (± 1.0%) i/s -    388.773k in   5.069692s

# Comparison:
#               rindex:    76693.4 i/s
#  reverse_each.detect:    73400.3 i/s - 1.04x  slower
#          filter.last:        8.8 i/s - 8750.82x  slower

The results are in, and it looks like Array#rindex is the clear winner! While reverse_each.detect puts up a valiant effort, it falls just short of the mark. And as for filter.last, well, let’s just say it’s taking the scenic route… at a snail’s pace.