[German Bourdin](http://www.gbourdin.com/)
[@g2k88](http://twitter.com/g2k88)
[gbourdin](https://github.com/gbourdin)
Esta es la parte en la que se rien.
Supongamos que un cliente nos pide una herramienta que le permita crear fechas recurrentes de manera similar a como lo hace Google Calendar.
Nuestro cliente quiere la misma flexibilidad para poder crear repeticiones diarias, semanales, mensuales y anuales.
In [1]:
import datetime
def compute_daily_recurrences(start_date, repeat_every=1,
end_date=None, max_repetitions=None):
dates = []
# Calculemos la cantidad de repeticiones de antemano
if end_date:
repetitions = (
(end_date - start_date).days / repeat_every)
elif max_repetitions:
repetitions = max_repetitions
days_to_next_repetition = datetime.timedelta(days=repeat_every)
next_date = start_date
for x in xrange(repetitions):
next_date = next_date + days_to_next_repetition
dates.append(next_date)
return dates
¿Viste? ¡Fue facil y anda! ¡Probemos!
In [2]:
start_date = datetime.datetime(1900, 1, 1)
end_date = start_date + datetime.timedelta(days=7)
print start_date
In [3]:
compute_daily_recurrences(start_date=start_date, repeat_every=1, end_date=end_date)
Out[3]:
In [4]:
compute_daily_recurrences(start_date=start_date, repeat_every=1, max_repetitions=7)
Out[4]:
In [5]:
compute_daily_recurrences(start_date=start_date, repeat_every=7, max_repetitions=3)
Out[5]:
In [6]:
compute_daily_recurrences(start_date=start_date, repeat_every=28, max_repetitions=12)
Out[6]:
Significa que vamos a necesitar distinguir los días de la semana, y luego cuando calculemos fechas, poder pedir ese día
datetime nos deja identificar a que día de la semana corresponde una fecha!
In [7]:
start_date.weekday()
Out[7]:
In [8]:
MON = 0
TUE = 1
WED = 2
THU = 3
FRI = 4
SAT = 5
SUN = 6
Vamos a tener que a partir de una fecha, poder ir eligiendo dentro de esa misma semana, los siguientes días a partir de lo que se seleccionó.
Supongamos que hoy es miércoles y seleccionaron repetir jueves y viernes durante 3 semanas
¿Cuándo es la próxima fecha? ¿Y la siguiente?
In [9]:
today = datetime.datetime(2014, 9, 17)
today.weekday()
Out[9]:
In [10]:
repeat_on = [THU, FRI]
next_event_on = min([x for x in repeat_on if x >= today.weekday()])
# Pero esto fallaria si repeat_on fuera [MON]!
print next_event_on
In [11]:
days_to_next_event = next_event_on - today.weekday()
print days_to_next_event
In [12]:
next_event = today + datetime.timedelta(days_to_next_event)
print next_event
Bueno, acabamos de ver que vamos a poder saltar al próximo día en el que toca repetir y mas o menos ir haciendo lo que tenemos que hacer, pero:
Así que, ántes de programar esta parte, hay que sentarse un rato largo a pensar bien estos problemas con papel y lapiz
Notemos que no fue un paso tan chiquito desde el primer ejercicio a este, no hay mucha re-usabilidad y hay que introducir muchas cosas nuevas a nuestro código y muchas posibles fuentes de error.
Ántes de escribir mas código, miremos lo que se viene
Tenemos dos nuevas formas de repetición
Que todas nuestras repeticiones tienen que ser en el mismo numero de día que la fecha inicial.
Que todas nuestras repeticiones tienen que ser la misma ocurrencia de ese día pero en meses diferentes, es decir, por ejemplo: el segundo domingo de todos los meses, o el tercer jueves de todos los meses.
Para agregar al problema, recordemos que datetime.timedelta la unidad más grande de tiempo que conoce son 'semanas', así que tampoco podemos pedir datetime.timedelta(months=1) y mucho menos datetime.timedelta(years=1)
Photo Credit: http://communicatebetterblog.com/2013/03/
Dos opciones:
1 - Nos pegamos un tiro en la rodilla y zafamos de trabajar por un tiempo. Mientras tanto, alguien con mas barba y/o menos pelo se va a encargar del tema.
2 - Aprendemos a usar dateutil.rrule y nos ahorramos la lesión.
Simple:
pip install python-dateutil
In [13]:
from dateutil.rrule import *
Es la definición del objeto que computa reglas recurrentes. Su constructor acepta todas las keywords establecidas en el RFC como parámetros (excepto byday, que se renombró a byweekday). El prototipo del constructor es:
rrule(freq)Donde freq es uno de los siguientes: YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, o SECONDLY.
rrule tambien acepta los siguientes parámetros:
rrule(MONTHLY, byweekday=(MO, TU, WE, TH, FR), bysetpos=-1, count=12)Nos dara el último día habil de cada uno de los proximos 12 meses.
Regla diaria para 7 ocurrencias
In [14]:
list(rrule(DAILY, count=7, dtstart=start_date))
Out[14]:
Regla diaria hasta el 7 de enero de 1900
In [15]:
end_date = datetime.datetime(1900, 1, 7)
list(rrule(DAILY, dtstart=start_date, until=end_date))
Out[15]:
Un evento cada 28 días, 6 veces
In [16]:
list(rrule(DAILY, count=6, dtstart=start_date, interval=28))
Out[16]:
Semanalmente, martes y jueves, las proximas 3 semanas
In [17]:
list(rrule(WEEKLY, count=6, byweekday=(TU,TH), dtstart=start_date))
Out[17]:
El primer viernes de cada mes hasta mayo
In [18]:
list(rrule(MONTHLY, byweekday=FR(1), dtstart=start_date,
until=datetime.datetime(1900, 5, 1)))
Out[18]:
Todos los martes 13 entre 2014 y 2016
In [19]:
list(rrule(MONTHLY, byweekday=TU, bymonthday=13,
dtstart=datetime.datetime(2014, 1, 1),
until=datetime.datetime(2016, 12, 31))
)
Out[19]:
Hay muchos ejemplos mas en la documentación, los pueden encontrar en https://labix.org/python-dateutil#head-1f7c5d0c956a96f26aa1de60b861f4d58180c1dd.
La idea para esta charla era solo ver algunos para entender lo que se puede hacer.
Con lo que vimos, parece que va a ser posible y mas mucho más fácil de lo que creiamos definir las funciones que computen las reglas que queriamos, es mas, parece que esta casi todo hecho!
Asi que, ¡Manos a la obra!
¡Empecemos con el que ya habíamos hecho!
In [20]:
def compute_daily_recurrences(start_date, repeat_every=1,
end_date=None, max_repetitions=None):
if end_date:
return(rrule(DAILY, dtstart=start_date, interval=repeat_every,
until=end_date))
elif max_repetitions:
return(rrule(DAILY, dtstart=start_date, interval=repeat_every,
count=max_repetitions))
else:
raise(Exception('Invalid Arguments!'))
In [21]:
compute_daily_recurrences(start_date=start_date, max_repetitions=3)
Out[21]:
Esas no son las fechas que esperabamos, es cualquiera esto de rrule!
Es cierto, rrule no te devuelve una lista de fechas, devuelve algo mejor, un generador, vamos a hacer un poco de trampa para ver las fechas acá
In [22]:
list(compute_daily_recurrences(start_date=start_date, max_repetitions=3))
Out[22]:
Esta era mas díficil, teniamos una lista de días en los cuales se iba a repetir la regla
Aun asi, vamos a ver que es super sencillo ahora
In [23]:
def compute_weekly_recurrences(start_date, repeat_every=1, repeat_on=[],
end_date=None, max_repetitions=None):
if end_date:
return(rrule(WEEKLY, dtstart=start_date, interval=repeat_every,
byweekday=repeat_on, until=end_date))
elif max_repetitions:
return(rrule(WEEKLY, dtstart=start_date, interval=repeat_every,
byweekday=repeat_on, count=max_repetitions))
else:
raise(Exception('Invalid Arguments'))
¡Casi igual que la anterior!
Repeticiones semanales, los martes, 3 eventos
In [24]:
list(compute_weekly_recurrences(start_date, repeat_on=TU, max_repetitions=3))
Out[24]:
Semanales, todos los días de la semana, 10 eventos!
In [25]:
list(compute_weekly_recurrences(start_date, repeat_on=[MO, TU, WE, TH, FR],
max_repetitions=10))
Out[25]:
Teníamos la posibilidad de que sea por día del mes o por día de la semana y encima timedelta no sabe sumar meses!
¡Mas vale, de eso es la charla! Veamos:
In [26]:
from dateutil.rrule import weekday
In [27]:
def compute_monthly_recurrences(start_date, repeat_every=1,
day_of_the_week=False, day_of_the_month=False,
end_date=None, max_repetitions=None):
rule_arguments = dict() # Podemos pasarle un diccionario, mas prolijo
rule_arguments['dtstart'] = start_date
rule_arguments['interval'] = repeat_every
if day_of_the_week:
# Si el numero de semana no existe, (ej: febrero no tiene 5 lunes)
# usamos la ultima ocurrencia del dia en el mes
week_number = (start_date.day / 7) + 1
week_day = start_date.weekday()
rule_arguments['byweekday'] = [weekday(week_day, week_number),
weekday(week_day, -1)]
rule_arguments['bysetpos'] = 1 # Si matchea 2 cosas, la primera
elif day_of_the_month:
# Si el dia del mes no existe (ej: 31), Usamos el ultimo dia del mes
rule_arguments['bymonthday'] = [start_date.day, -1]
rule_arguments['bysetpos'] = 1
if end_date:
rule_arguments['until'] = end_date
else:
rule_arguments['count'] = max_repetitions
return rrule(MONTHLY, **rule_arguments)
In [28]:
list(compute_monthly_recurrences(start_date=datetime.datetime(1900,1,31),
day_of_the_month=True, max_repetitions=4))
Out[28]:
In [29]:
# Enero de 2014 tiene 5 viernes, el ultimo es el 31,
# febrero, marzo y abril tienen 4 y mayo tiene 5
list(compute_monthly_recurrences(start_date=datetime.datetime(2014, 1, 31),
day_of_the_week=True, max_repetitions=5))
Out[29]:
¡Este era el caso más complicado y se hizo muy fácil! ¡Hagamos el último asi nos vamos tranquilos!
Recordemos:
¡Es casi igual a las diarias!
In [30]:
def compute_yearly_recurrences(start_date, repeat_every,
end_date=None, max_repetitions=None):
rule_arguments = dict()
rule_arguments['dtstart'] = start_date
rule_arguments['interval'] = repeat_every
# Necesitamos esto extra para que no nos molesten los bisiestos
rule_arguments['bymonth'] = start_date.month
rule_arguments['bymonthday'] = (start_date.day, -1)
rule_arguments['bysetpos'] = 1
if end_date:
rule_arguments['until'] = end_date
else:
rule_arguments['count'] = max_repetitions
return rrule(YEARLY, **rule_arguments)
El último día de febrero desde 2012 hasta el 2017
In [31]:
start_date = datetime.datetime(2012, 2, 29)
end_date = datetime.datetime(2017, 2, 28)
list(compute_yearly_recurrences(start_date, 1, end_date))
Out[31]:
Mas temprano les contaba que datetime.timedelta, no sabe manejar espacios de tiempo mayores a una semana, así que sumarle un mes a una fecha con eso, es imposible. Entre otras cosas dateutil.relativedelta soluciona esto y nos permite hacer cosas como las siguientes:
In [32]:
from dateutil.relativedelta import relativedelta
print datetime.datetime(2012, 2, 29) + relativedelta(years=1)
print datetime.datetime(2012, 2, 29) + relativedelta(years=4)
In [33]:
print datetime.datetime(1900, 1, 31) + relativedelta(months=1)
print datetime.datetime(1900, 1, 31) + relativedelta(months=2)
print datetime.datetime(1900, 1, 31) + relativedelta(months=3)
relativedelta tiene mucha mas funcionalidad, pero esta fuera del alcance de esta charla.
easter es capaz de calcular la fecha del domingo de pascuas. Existen distintos métodos para hacerlo y cubre 3 de ellos. Si estan interesados, pueden leer la documentacion
In [34]:
from dateutil.easter import easter
print easter(2014)
print easter(2015)
print easter(1492)