ZetCode

Ruby Enumerable and Comparable

last modified October 18, 2026

Ruby's Enumerable and Comparable are mixin modules that give your own classes a large set of behaviors for free. By including Enumerable and defining each, you get methods such as map, select, reduce, and sort_by. By including Comparable and defining <=>, you get <, between?, clamp, and sorting support. This guide shows how to use both modules.

Enumerable in a Custom Class

To make a class enumerable, include the Enumerable module and define an each method that yields its elements. Every Enumerable method is then built on top of each. This example wraps an array in a custom collection and calls several Enumerable methods on it.

enumerable_custom.rb
class NumberCollection
  include Enumerable

  def initialize(*numbers)
    @numbers = numbers
  end

  def each
    return enum_for(:each) unless block_given?
    @numbers.each { |n| yield n }
    self
  end
end

nums = NumberCollection.new(5, 3, 8, 1, 9, 2)

p nums.map { |n| n * 2 }
p nums.select(&:even?)
p nums.reduce(:+)
p nums.sort_by { |n| -n }
p nums.min_by { |n| (n - 4).abs }
p nums.each_slice(2).to_a
p nums.include?(8)
p nums.sort
p nums.first(2)

The return enum_for(:each) unless block_given? line lets callers call each without a block and receive an Enumerator, which is how methods like each_slice without a block work. Note that min_by returns the element closest to 4, which is 5.

$ ./enumerable_custom.rb
[10, 6, 16, 2, 18, 4]
[8, 2]
28
[9, 8, 5, 3, 2, 1]
5
[[5, 3], [8, 1], [9, 2]]
true
[1, 2, 3, 5, 8, 9]
[5, 3]

Grouping and Aggregating

Enumerable also provides grouping and aggregation helpers such as group_by, partition, sum, and each_with_index. This example builds a small inventory class that includes Enumerable and uses these methods over its items.

enumerable_groups.rb
class Inventory
  include Enumerable

  Item = Struct.new(:name, :category, :price)

  def initialize
    @items = []
  end

  def add(name, category, price)
    @items << Item.new(name, category, price)
    self
  end

  def each(&block)
    @items.each(&block)
    self
  end
end

inv = Inventory.new
  .add("apple", :fruit, 0.5)
  .add("carrot", :vegetable, 0.3)
  .add("banana", :fruit, 0.25)
  .add("leek", :vegetable, 1.2)

p inv.group_by(&:category).transform_values { |items| items.map(&:name) }
p inv.sum(&:price).round(2)
p inv.partition { |i| i.price > 0.4 }.map { |g| g.map(&:name) }
p inv.each_with_index.map { |item, i| "#{i + 1}. #{item.name}" }
p inv.min_by(&:price).name
p inv.each_cons(2).count

Because each forwards its block to the internal array, every Enumerable method works on Inventory without further code. The partition call splits items into those costing more than 0.4 and the rest.

$ ./enumerable_groups.rb
{:fruit=>["apple", "banana"], :vegetable=>["carrot", "leek"]}
2.25
[["apple", "leek"], ["carrot", "banana"]]
["1. apple", "2. carrot", "3. banana", "4. leek"]
"banana"
3

Lazy Enumerators

Enumerable#lazy returns an Enumerator that defers work until a result is actually needed. Chained operations are evaluated element by element, so you can work with infinite sequences. Calling first(n) or to_a triggers evaluation.

enumerable_lazy.rb
# Infinite sequence of natural numbers
naturals = (1..Float::INFINITY).lazy

result = naturals
  .map { |n| n * n }
  .select { |n| n.even? }
  .first(5)

p result

# Lazy enumerator from a custom Enumerable
class Countdown
  include Enumerable

  def each
    n = 10
    while n > 0
      yield n
      n -= 1
    end
  end
end

p Countdown.new.lazy.map { |x| x * 10 }.reject(&:odd?).first(3)
p Countdown.new.lazy.take_while { |x| x > 7 }.to_a

Without lazy, the first example would try to map an infinite range and never finish. With it, only the squares needed to find five even values are computed. Any class that includes Enumerable gets lazy too, as the Countdown example shows.

$ ./enumerable_lazy.rb
[4, 16, 36, 64, 100]
[100, 90, 80]
[10, 9, 8]

Comparable in a Custom Class

Include Comparable and define <=> (the spaceship operator), which returns -1, 0, or 1, or nil when the objects cannot be compared. Comparable then supplies <, <=, >, >=, ==, between?, and clamp. The example below compares versions by major and minor number.

comparable_custom.rb
class Version
  include Comparable
  attr_reader :major, :minor

  def initialize(major, minor)
    @major = major
    @minor = minor
  end

  def <=>(other)
    return nil unless other.is_a?(Version)
    [major, minor] <=> [other.major, other.minor]
  end

  def to_s
    "#{major}.#{minor}"
  end
end

a = Version.new(1, 2)
b = Version.new(1, 10)
c = Version.new(2, 0)

puts a < b          # true
puts a <= a         # true
puts c > b          # true
puts a == Version.new(1, 2)  # true
puts b.between?(a, c)        # true
puts Version.new(3, 0).clamp(a, c)  # 2.0

versions = [c, a, b]
puts versions.sort.join(", ")
puts versions.min
puts versions.max

Comparing arrays with <=> is a convenient way to compare multi-part values: the major numbers are compared first, and the minor numbers break ties. Because Version now has a total order, sort, min, and max work on arrays of versions. Note that 1.10 is greater than 1.2, since the minor number is compared as an integer.

$ ./comparable_custom.rb
true
true
true
true
true
2.0
1.2, 1.10, 2.0
1.2
2.0

Comparable with Sorting and Clamping

Once <=> is defined, your objects can be sorted, reduced to a maximum, and bounded to a range. This example uses a temperature class. Note that == comes from Comparable, while eql? is a separate method and is not provided by it.

comparable_sort.rb
class Temperature
  include Comparable
  attr_reader :degrees

  def initialize(degrees)
    @degrees = degrees
  end

  def <=>(other)
    degrees <=> other.degrees
  end

  def inspect
    "#{degrees}C"
  end
end

temps = [Temperature.new(21), Temperature.new(-4), Temperature.new(35)]

p temps.sort
p temps.max
p Temperature.new(10).between?(Temperature.new(0), Temperature.new(20))
p Temperature.new(50).clamp(Temperature.new(0), Temperature.new(30))
p Temperature.new(5) == Temperature.new(5)
p Temperature.new(5).eql?(Temperature.new(5))

clamp returns the upper bound when the value is above the range, so 50 becomes 30. The final line shows that eql? still uses object identity by default, so two equal Temperature objects are not eql? unless you define it yourself.

$ ./comparable_sort.rb
[-4C, 21C, 35C]
35C
true
30C
true
false

Source References

Learn more from these resources: Ruby Enumerable Documentation, Comparable Documentation, and Enumerator::Lazy.

List all Ruby tutorials.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.