---
title: Custom Ranges in Ruby
teaser: How can we create ranges out of custom objects?
tags: ruby
author: Joël Quenneville
published_on: 2022-04-15
---

Ruby allows you to create [ranges for custom objects] by implementing just a
couple standard methods: `<=>` and `succ`.

[ranges for custom objects]: https://ruby-doc.org/core-3.1.1/Range.html#class-Range-label-Ranges+and+Other+Classes

## Problem

You need to generate an array of months between two arbitrary dates. Normally
this sort of thing can be solved with a **range** but date ranges give you an
entry for every _day_, not every month.

You could generate all the days and then filter them such that you only keep one
per month but that forces you to create a lot more data than you actually
need. It also leaves you with an array of `Date` objects which don't really
represent the concept you are trying to work with.

```ruby
(date1..date2).select { |date| date.day == 1 }
```

## Domain Object

Instead of working with `Date` objects that represent the concept of a _day in
time_, we might want to implement our own [value object] that represents the
concept of a _month in time_.

```ruby
class Month
  def initialize(months)
    # months since Jan, 1BCE (there is no year zero)
    # https://en.wikipedia.org/wiki/Year_zero
    @months = months
  end
end
```

The standard way to model time in software is to store a single counter since a
set point in time (e.g. 24,267 months since 1BCE) rather than multiple counter
like in English (e.g. 2022 years and 4 months since 1BCE). It makes implementing
both math and domain operations much easier.

To make this object a bit nicer to work with, we might add some convenience
methods like:

* A `.from_parts` class method as an [alternate constructor]
* `#month` and `#day` accessors to get human values
* a custom `#inspect` to make it easier to read output from the console and
  tests

You can see a full implementation in [this gist].

[value object]: https://thoughtbot.com/upcase/videos/value-objects
[alternate constructor]: https://thoughtbot.com/blog/meditations-on-a-class-method
[this gist]: https://gist.github.com/JoelQ/70e2810af465fb679da888389ba4c481

## Becoming rangeable

Ruby can construct a range out of any object that implements the `<=>`
comparison operator. If you want that range to be iterable, you also need to
implement `succ` to generate the next value.

```ruby
class Month
  # other methods...

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

  def succ
    self.class.new(months.succ)
  end
end
```

Note that since we are using a single value internally, we get to just delegate
the methods to the internal number.

## Generating our array

Let's see it in action!

```ruby
Month.from_parts(2021, 10)..Month.from_parts(2022, 3)
=> [October 2021, November 2021, December 2021, January 2022, February 2022, March 2022]
```

By implementing just a couple methods, we are now able to generate a series of
months. Yay interfaces and polymorphism! As a bonus, we also get a nice value
object that will likely have more relevant methods for our domain than a regular
`Date` would.
