Criando eventos recorrentes com Recurrence

18 de Dezembro de 2009

Recurrence é uma biblioteca criada para gerar eventos recorrentes de maneira simples. Eu criei essa gem há tempos, mas nunca escrevi nada sobre ela. Eis que muitas pessoas começaram a me mandar e-mails perguntando como utilizá-la e chegou a hora de fazer um artigo mostrando seu uso na prática.

A primeira coisa que você precisa fazer é instalar a gem.

sudo gem install recurrence

Depois, basta adicionar à biblioteca.

require "rubygems"
require "recurrence"

A maneira mais simples de usar é instanciar um objeto através dos métodos de classe daily, weekly, monthly e yearly, que permitem criar eventos diários, semanais, mensais e anuais, respectivamente.

r = Recurrence.daily

Para retornar a próxima data, você deve utilizar o método Recurrence#next.

r.next
#=> Fri, 18 Dec 2009
 
r.next
#=> Fri, 18 Dec 2009

Você também pode utilizar o método Recurrence#next!, que altera a variável interna de data, como mostra o exemplo abaixo.

r.next!
#=> Fri, 18 Dec 2009
 
r.next!
#=> Sat, 19 Dec 2009

Se você quiser, pode especificar o intervalo de dias que o evento pode se repetir; basta utilizar a opção :starts e :ends.

# define the starting date to 2009-12-24
r = Recurrence.daily(:starts => Date.new(2009, 12, 24))
 
# define the ending date to 2009-12-31
r = Recurrence.daily(:ends => Date.new(2009, 12, 31))

Você também pode passar essas datas como strings, desde que elas sejam interpretadas pelo método Date#parse.

r = Recurrence.daily(:starts => "2009-12-24")

Os atalhos são uma excelente maneira de tornar o código mais semântico, mas em alguns casos é melhor seguir com o bom e velho método new.

r = Recurrence.new(:every => :week, :on => :saturday, :interval => 2)

Como você pode ver no exemplo acima, estamos criando um evento semanal, que acontece aos sábados, a cada duas semanas. Ao utilizar o método Recurrence#next! você terá algo como isso:

r.next!
#=> Sat, 19 Dec 2009
 
r.next!
#=> Sat, 02 Jan 2010

Você também pode especificar uma lista com os dias da semana que o evento deve ocorrer.

r = Recurrence.weekly(:on => [:saturday, :sunday])
 
r.next!
#=> Sat, 19 Dec 2009
 
r.next!
#=> Sun, 20 Dec 2009
 
r.next!
#=> Sat, 26 Dec 2009

Para acessar uma lista com todos os eventos disponíveis, basta utilizar o método Recurrence#events.

r.events
#=> [Sun, 20 Dec 2009, Sun, 27 Dec 2009]

Esse método fará irá guardar a lista de datas geradas, aumentando a performance se ele for acessado mais de uma vez. Se você quiser regerar essas datas, pode utilizar o método Recurrence#events!.

Também é possível verificar se uma data está no intervalo especificado no objeto Recurrence.

r = Recurrence.weekly(:on => :saturday)
 
r.include? "2009-12-19"
#=> true

Como usar na prática

Como você pode ver, o Recurrence é totalmente parametrizado. Essa abordagem permite que você armazene os itens necessários para montar sua recorrência em um banco de dados, por exemplo. Digamos que você tenha um modelo Event, com o seguinte schema:

class CreateEvents < ActiveRecord::Migration
  def self.up
    create_table :events do |t|
      t.string :title,        :null => false
      t.binary :meta,         :null => false
      t.date   :scheduled_to, :null => false
    end
  end
end

Os atributos que serão utilizados no Recurrence são :meta e :scheduled_to. O primeiro irá armazenar o tipo de recorrência e as informações como dia, intervalo, etc. Já o segundo, irá armazenar a próxima data do evento, para diminuir a geração de datas. O modelo Event pode armazenar os atributos como este exemplo:

class Event < ActiveRecord::Base
  serialize :meta, Hash
 
  validates_presence_of :meta, :title, :scheduled_to
end

Veja um exemplo de como cadastrar um evento “Pagamento”, que acontece todo mês no dia 5.

options = {:every => :month, :on => 5}
r = Recurrence.new(options)
 
event = Event.new(:title => "Pagamento", :meta => options, :scheduled_to => r.next)
event.save!

Na hora de montar o objeto de recorrência, você pode utilizar algo como isto:

event = Event.first
 
r = Recurrence.new(event.meta.merge(:starts => event.scheduled_to))
r.next
#=> Tue, 05 Jan 2010

Se você precisar exibir um evento mais de uma vez, utilize este mesmo processo e exiba os eventos com o método events.

Finalizando

Criar eventos recorrentes nem sempre é uma tarefa simples, principalmente no que diz respeito à performance. O José Valim fez uma série de contribuições que ajudaram a melhor esse ponto, além de outras funcionalidades como os atalhos.

Para conhecer mais sobre as opções disponíveis do Recurrence, acesse o README do projeto no Github.

Comentários #

#1 Gedean disse:
19 Dez 09, 03:11PM

E quanto à outros intervalos de tempo: bimestre, trimestre, semestre, etc?

#2 Nando Vieira disse:
20 Dez 09, 03:55AM

Gedean, basta usar um evento mensal com intervalo de 3 ou 6 meses: Recurrence.monthly(:interval => 3)

#3 Renato Aquino disse:
21 Dez 09, 11:31AM

No seu exemplo de utilização, quando vai fazer a recuperação do evento não deveria ser efetuada uma busca por ordem no campo scheduled_to?

O que ocorre quando um evento no passado não foi executado mas continua o registro?
O recurrence vai responder com a data no passado ou calculará apenas a nova data de acordo com as regras informadas?

#4 Nando Vieira disse:
21 Dez 09, 05:00PM

Renato, como é um exemplo simples, peguei o primeiro registro direto. Em uma aplicação real, você deve fazer exatamente o que disse. Quanto às perguntas:

1) O cálculo de datas é feito à partir do valor informado na opção :starts e, se não for especificado nada, será a data atual por padrão. Desse modo, toda a lógica em volta do que deve ser ou não exibido fica por conta do desenvolvedor, já que a ideia é ser generalista.

2) Depende. Os dois casos são possíveis. Se você especificar algo como r.events(:starts => Time.now).each {|date| puts date.to_s }, apenas os eventos que ocorrer à partir de hoje são retornados.

#5 Cyrille disse:
13 Jan 10, 02:21PM

Hello, your plugin seems very helpful !
I've a quick question and a improvement suggestion :

In your events exemple will it be possible to use the same AR model for both recurring and non recurring events? If yes what would be the the option hash for non recurring events?

Maybe a nice feature will be a method for retrieving the recurring hash in plain human language (a string) such as {:every => :month, :on => 5} => "Every 5th of each month" in order to display that information in a view.

Thanks
Cyrille

#6 Nando Vieira disse:
13 Jan 10, 04:13PM

Cyrille, I think the best approach is having a separated model (Schedule) that holds these events and another one (Event) that may or may not be associated to the schedule. This way you can separate the domain and keep things simple. But you can have one single model if the older events aren't important... just use the `scheduled_to` attribute to both view presentation and scheduling seeding.

About your suggestion: this is a pretty nice feature indeed... I'll try to work on it.

#7 Cyrille disse:
14 Jan 10, 05:49AM

Obrigado!

The schedule approach seems a good path.

Cyrille

Deixe um comentário




Este blog usa o Gravatar.


Não é aceito código HTML: adicione-o no pastie.org ou paste.milk-it.net e poste apenas o link.

Se este é seu primeiro comentário, ele terá que ser aprovado antes de ser exibido.