Dealing with durations

Whenever business logic is about times, it quite often is not only about simple time comparisons. Especially statistics and filters tend to need durations.
Durations consist of a beginning and an ending.
Ruby offers the Range class for dealing with durations.

A naive duration implementation

Creating a Task model:

rails g model Task name:string && rake db:migrate

By default the attribute created_at is migrated.. The timestamp is used for staving the objects creation time.
Besides there are two more methods implemented in the model:

class Task < ApplicationRecord
  scope :created_between, ->(begin_at, end_at) {
    where("created_at BETWEEN :begin AND :end", { begin: begin_at, end: end_at })
  }

  def created_between?(begin_at, end_at)
    begin_at < created_at && created_at > end_at
  end
end

The named scope finds all tasks, being created in certain period.
Additionally it should be possible the assert, if the task was created in a certain period.
Both cases demand to assign the timestamps for begin and end directly:

task = Task.new name: 'Breakfast'
task.created_between? 3.hours.ago, 1.hour.ago # => true
Task.created_between 3.hours.ago, 1.hour.ago

The SQL, generated by the named scope:

SELECT "tasks".* FROM "tasks" WHERE (created_at BETWEEN '2016-08-21 09:20:47.188535' AND '2016-08-21 11:20:47.188803')

Though the implementation works, it is quite naive one.
A better solution is to use range objects whenever it is about durations. On the one hand it easier to assign just a single parameter instead and on the other hand The Range class provides some convenience methods:

duration = 1.hour.ago..Time.current
duration.begin # => Sun, 28 Aug 2016 09:37:51 UTC +00:00
duration.end    # => Sun, 28 Aug 2016 10:37:51 UTC +00:00
duration.cover? 30.minutes.ago # => true

By means of the Range object, it is way easier to implement the same functionality:

class Task < ApplicationRecord
  scope :created_between, ->(duration) { where(created_at: duration) }

  def created_between?(duration)
    duration.cover? created_at
  end
end

Since the ActiveRecord uses Range objects for generating “BETWEEN” SQL, there is no need for writing plain SQL anymore. And by letting the Range object care about the decision, if a timestamp is within a duration, the complexity decreases.
Assigning a duration is easy:

task = Task.new name: 'Breakfast'
task.created_between? 3.hours.ago..1.hour.ago # => true
Task.created_between 3.hours.ago..1.hour.ago

The SQL generated by the named scope even cares about using the correct table names, which can be necessary in combination with JOINs:

SELECT "tasks".* FROM "tasks" WHERE ("tasks".created_at BETWEEN '2016-08-21 09:45:23.188535' AND '2016-08-21 11:45:23.188803')