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.
Textos escritos por
Comentários #
E quanto à outros intervalos de tempo: bimestre, trimestre, semestre, etc?
Gedean, basta usar um evento mensal com intervalo de 3 ou 6 meses:
Recurrence.monthly(:interval => 3)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?
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
:startse, 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.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
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.
Obrigado!
The schedule approach seems a good path.
Cyrille
Deixe um comentário