Custom Ranges in Ruby

0
56
Back to Basics: Boolean Expressions


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

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.

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

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.

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.

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.

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.

Let’s see it in action!

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.



Source link

Leave a reply

Please enter your comment!
Please enter your name here