Arbeiten mit Zeiträumen

Immer wenn es um Zeiten geht, bleibt es oft nicht nur bei einfachen Zeitvergleichen. Besonders bei Auswertungen oder Filtern sind Zeiträume von Interesse.
Zeiträume sind durch einen Anfang und ein Ende gekennzeichnet.
Ruby bietet dafür passenderweise die Range Klasse.

Naive Implementierung eines Zeitraumes

Ein Model Task wird erstellt:

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

Standardmäßig wird dabei auch das Attribut created_at migriert. Der Zeitstempel wird für jedes neu erstellte Objekt gespeichert.
Außerdem werden in dem Model zwei weitere Methoden implementiert:

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

Der benannte Scope findet alle Aufgaben, die in einem Zeitraum erstellt wurden.
Zusätzlich soll die Aufgabe geprüft werden können, ob sie in einem bestimmten Zeitraum erstellt wurde.
In beiden Fällen muss der Start- und Endezeitstempel des Zeitraumes direkt übergeben werden:

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

Das SQL des benannten scopes:

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

Diese Implementierung funktioniert zwar, ist allerdings naiv.
Es sollte im Falle von Zeiträumen eher mit Ruby Range Objekten gearbeitet werden. Das hat zum Einen den Vorteil, dass dann lediglich nur ein Parameter übergeben werden muss und zum Anderen bietet die Range Klasse einige komfortable Methoden:

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

Mit Hilfe des Range Objektes läßt sich die gleiche Funktionalität sehr viel einfacher implementieren:

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

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

Da ActiveRecord anhand des Range Objektes weiß, dass ein BETWEEN SQL bauen muss, braucht kein SQL selber geschrieben werden. Und indem sich das Range Objekt darum kümmert, zu entscheiden, ob ein Zeitstempel innerhalb eines Zeiraumes liegt, wird die Komplexität verringert.
Einen Zeitraum zu übergeben, bedeutet keinen Aufwand:

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

Das SQL des benannten Scopes kümmert sich sogar darum, dass der richtige Tabellenname gesetzt wird, was bei JOINs mit anderen Tabellen wichtig ist:

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