2 min read

Utiliser les Ranges Ruby pour manipuler des dates

Un nouvel exemple de la puissance de la librairie standard de Ruby

Il est assez fréquent d’avoir besoin de définir des périodes temporelles dans une application, le plus souvent elle est modélisée par une date de début et une date de fin, et de comparer ces dates à la date courante. Problème : cela donne souvent lieu à des méthodes dont la lisibilité laisse à désirer.

class User
  def paying_member?
    subscription_started_at <= Date.current && 
      (subscription_ended_at.nil? || subscription_ended_at > Date.current)
  end
end

Et puis j'ai vu ce tweet de Matt Swanson qui m'a rappelé qu'il était possible de créer des Range Ruby avec des dates pour simplifier certaines de ces opérations

Comme on peut le voir, mon exemple précédent se trouve fortement simplifié et plus lisible grâce à l'utilisation de la classe Range par rapport à la version initiale.

class User
  def paying_member?
    subscription_period.cover?(Date.current)
  end
  
  def subscription_period
    subscription_started_at..subscription_ended_at
  end
end
Au cas où l’utilisateur n’a pas mis fin à son abonnement le Range deviendra infini et notre predicat restera valide de la même façonc

Et maintenant que nous avons un objet sous la main pour modéliser la période d'abonnement d'un utilisateur il devient plus facile de modifier le comportement de notre application ou d'ajouter des fonctionnalités.

Par exemple, si il est acté que la date de fin d'un abonnement correspond au premier jour où l'utilisateur n'est plus abonné, on peut modifier notre période pour exclure la date de fin du Range

class User
  # ...
  
  def subscription_period
    subscription_started_at...subscription_ended_at
  end
end

Dans une application Rails on peut aussi profiter des méthodes Range#includes?  ou Range#overlaps? ajoutées par ActiveSupport. On peut les utiliser pour par exemple partitionner les utilisateurs selon le moment où ils ont souscrit, ou par exemple savoir si 2 abonnés ont été abonné au service sur la même période de temps.

class User
  # ...

  def subscriber_kind
    case subscription_period
    when innovator_period then 'Innovator'
    when early_adopter_period then 'Early adopter'
    when early_majority_period then 'Early majority'
    when late_majority_period then 'Late majority'
    when laggards_period then 'Laggard'
  end

  def share_subscription_with(user)
    subscription_period.overlaps?(user.subscription_period)
  end
end
La méthode Range#includes? est un alias de Range#=== ce qui permet son usage implicite avec un case / when

Et pour finir, voici quelques lien pour aller plus loin sur le sujet

Range | Ruby API (v3.1)
Active Support Core Extensions — Ruby on Rails Guides
Active Support Core ExtensionsActive Support is the Ruby on Rails component responsible for providing Ruby language extensions and utilities.It offers a richer bottom-line at the language level, targeted both at the development of Rails applications, and at the development of Ruby on Rails itself.Af…