Evitando lesiones autoinfligidas usando rrule

(o algún titulo mas aburrido)

[German Bourdin](http://www.gbourdin.com/)

[@g2k88](http://twitter.com/g2k88)

[gbourdin](https://github.com/gbourdin)

german.bourdin@gmail.com

Objetivos de esta charla

  1. Entender la complejidad de diseñar algoritmos que computan fechas basadas en reglas de recurrencia
  2. Entender como rrule soluciona estos problemas de manera simple e intuitiva a partir de un caso de estudio "real"
  3. Explorar algunas de las otras herramientas provistas por la libreria dateutil para solucionar de forma trivial problemas que involucren fechas

Motivación

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.

¿Cómo lo hace google calendar?

Repeticiones Diarias

Repeticiones Semanales

Repeticiones Semanales

Repeticiones Semanales

Repeticiones Semanales

Repeticiones Mensuales

Repeticiones Anuales

Resolviendo el problema de forma naive

  • Esto no es tan difícil, python tiene todo lo que necesito en datetime
  • Voy a contruir una solución con lo que trae python y te vas a arrepentir de haber hecho esta charla
  • Empecemos con las recurrencias diarias, estos problemas se hacen fáciles si empezas con un caso base y vas agrandandolo para cubrir lo que necesitas.
  • Recurrencias diarias - Usando únicamente built-ins

    ¿Qué hace falta?

    • Una fecha de comienzo
    • La cantidad de días entre evento y evento
    • Una fecha de finalización, o
    • Una cantidad de veces para repetir antes de finalizar

    ¡Eso es fácil!

    Recurrencias diarias - Usando únicamente built-ins

    
    
    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
    
    
    
    
    1900-01-01 00:00:00
    
    
    
    In [3]:
    compute_daily_recurrences(start_date=start_date, repeat_every=1, end_date=end_date)
    
    
    
    
    Out[3]:
    [datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 1, 6, 0, 0),
     datetime.datetime(1900, 1, 7, 0, 0),
     datetime.datetime(1900, 1, 8, 0, 0)]
    
    
    In [4]:
    compute_daily_recurrences(start_date=start_date, repeat_every=1, max_repetitions=7)
    
    
    
    
    Out[4]:
    [datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 1, 6, 0, 0),
     datetime.datetime(1900, 1, 7, 0, 0),
     datetime.datetime(1900, 1, 8, 0, 0)]
    
    
    In [5]:
    compute_daily_recurrences(start_date=start_date, repeat_every=7, max_repetitions=3)
    
    
    
    
    Out[5]:
    [datetime.datetime(1900, 1, 8, 0, 0),
     datetime.datetime(1900, 1, 15, 0, 0),
     datetime.datetime(1900, 1, 22, 0, 0)]
    
    
    In [6]:
    compute_daily_recurrences(start_date=start_date, repeat_every=28, max_repetitions=12)
    
    
    
    
    Out[6]:
    [datetime.datetime(1900, 1, 29, 0, 0),
     datetime.datetime(1900, 2, 26, 0, 0),
     datetime.datetime(1900, 3, 26, 0, 0),
     datetime.datetime(1900, 4, 23, 0, 0),
     datetime.datetime(1900, 5, 21, 0, 0),
     datetime.datetime(1900, 6, 18, 0, 0),
     datetime.datetime(1900, 7, 16, 0, 0),
     datetime.datetime(1900, 8, 13, 0, 0),
     datetime.datetime(1900, 9, 10, 0, 0),
     datetime.datetime(1900, 10, 8, 0, 0),
     datetime.datetime(1900, 11, 5, 0, 0),
     datetime.datetime(1900, 12, 3, 0, 0)]

    Recurrencias semanales - Usando únicamente built-ins

    Recordemos

    Recurrencias semanales - Usando únicamente built-ins

    ¿Qué vamos a necesitar?

    • Una fecha de comienzo
    • La cantidad de semanas entre grupo de eventos y grupo de eventos
    • Una lista de días de la semana durante los cuales debemos repetir
    • Una fecha de finalización, o
    • Una cantidad de veces para repetir antes de finalizar

    Recurrencias semanales - Usando únicamente built-ins

    ¿Qué cambia?

    • Una lista de días de la semana durante los cuales debemos repetir

    Significa que vamos a necesitar distinguir los días de la semana, y luego cuando calculemos fechas, poder pedir ese día

    Recurrencias semanales - Usando únicamente built-ins

    Bueno. pero eso no es tan difícil

    datetime nos deja identificar a que día de la semana corresponde una fecha!

    
    
    In [7]:
    start_date.weekday()
    
    
    
    
    Out[7]:
    0
    
    
    In [8]:
    MON = 0
    TUE = 1
    WED = 2
    THU = 3
    FRI = 4
    SAT = 5
    SUN = 6
    

    Recurrencias semanales - Usando únicamente built-ins

    Genial, pensemos en el algoritmo entonces:

    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?

    Recurrencias semanales - Usando únicamente built-ins

    
    
    In [9]:
    today = datetime.datetime(2014, 05, 28)
    today.weekday()
    
    
    
    
    Out[9]:
    2
    
    
    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
    
    
    
    
    3
    
    
    
    In [11]:
    days_to_next_event = next_event_on - today.weekday()
    print days_to_next_event
    
    
    
    
    1
    
    
    
    In [12]:
    next_event = today + datetime.timedelta(days_to_next_event)
    print next_event
    
    
    
    
    2014-05-29 00:00:00
    

    Recurrencias semanales - Usando únicamente built-ins

    No entiendo. ¿Qué quisiste probar?

    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:

    • Vamos a necesitar hacer algo de lógica para ver si el próximo día es en la semana actual o en la próxima
    • Ántes de saltar a la próxima semana, tenemos que ver si se nos acabaron las repeticiones
    • Si nos dieron una fecha de finalización, ya no sabemos de entrada cuantas veces vamos a repetir. ¡Tenemos que tener cuidado siempre de no pasarnos!
    • Tenemos que pensar a que día de la semana siguiente vamos a saltar cuando cambiemos de semana (al lunes probablemente)
    • Y no nos tenemos, olvidar ántes de saltar a la siguiente semana, que es probable que haya que saltar mas de una semana si se eligió que se repita semanalmente cada X semanas con X > 1

    Así que, ántes de programar esta parte, hay que sentarse un rato largo a pensar bien estos problemas con papel y lapiz

    Recurrencias semanales - Usando únicamente built-ins

    Ok, la lógica es un poco complicada, pero se puede hacer

    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

    Recurrencias mensuales

    Recordemos

    Recurrencias mensuales

    ¿Qué cambia?

    Tenemos dos nuevas formas de repetición

    Por día del mes

    ¿Qué significa?

    Que todas nuestras repeticiones tienen que ser en el mismo numero de día que la fecha inicial.

    • Caso común: 15 de marzo. El próximo evento es el 15 de abril, fácil!
    • Caso mas complicado: 31 de marzo. ¿El próximo evento es el 30 de abril o el 31 de mayo?
    • Caso feo: 31 de enero. ¿El próximo evento es el 28 de febrero?
    • Caso horrible: 29 de febrero. ¿Qué va a pasar dentro de 12 repeticiones, y dentro de 12 x 4?

    Por día de la semana

    ¿Qué significa?

    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.

    • Caso común: Si elegí el primer lunes de enero, todas las repeticiones son el primer lunes del mes
    • Caso problemático: Elegí el quinto lunes del mes, pero el siguiente mes solo tiene 4 lunes, qué hago?

    Recurrencias mensuales

    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)

    ¿Y Ahora?

    ¿Y Ahora?

    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.

    rrule

    ¿Qué es?

    ¿Cómo lo obtengo?

    Simple:

    pip install python-dateutil

    ¿Como lo uso?

    
    
    In [13]:
    from dateutil.rrule import *
    

    rrule

    rrule type

    Es la base de la operación rrule. Acepta todas las keywords establecidas en el RFC como constructores (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

    rrule type (constructores)

    rrule tambien acepta los siguientes constructores:

  • cache: Si se provee, debe ser un booleano (True o False indicando si los resultados seran cacheados o no, mejora mucho la performance si la misma regla se usa mas de una vez.
  • dtstart: La fecha de inicio para la recurrencia. Si no se provee, utiliza datetime.now()
  • interval: Es el intervalo entre cada iteración de freq. Por ejemplo, si usamos YEARLY un intervalo de 2 significa una vez cada dos años, si usamos DAILY, es una vez cada dos días, etc.
  • wkst: El día de comienzo de la semana, debe ser una de las siguientes constantes: MO, TU, WE, o un entero especificando el primer día de la semana. Afectara recurrencias basadas en periodos semanales. El valor predeterminado para esto se obtiene de calendar.firstweekday() (usualmente el lunes), y se puede modificar usando calendar.setfirstweekday()
  • count: Cuantas ocurrencias van a ser generadas. Debe ser un entero.
  • until: Si se especifica, debe ser una instancia de datetime que indicara el limite para la recurrencia. Si un evento coincide con este valor, esa sera la última ocurrencia.
  • rrule

    rrule type (constructores)

  • bysetpos: Si se provee debe ser un entero o una secuencia de enteros positivos o negativos. Cada entero especifica un numero de ocurrencia correspondiente a la nth ocurrencia de la regla dentro del periodo de frecuencia. Por ejemplo:
  • 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.

  • bymonth: Si se provee debe ser un entero o secuencia de enteros que indicará los meses a los cuales se aplicará la recurrencia
  • bymonthday: Si se provee debe ser un entero o secuencia de enteros que indicará los días del mes a los cuales aplicar la recurrencia.
  • byyearday: Si se provee debe ser un entero o secuencia de enteros que indicará los días del año a los cuales se aplicará la recurrencia.
  • rrule

    rrule type (constructores)

  • byweekno: Si se provee debe ser un entero o secuencia de enteros. Indica los numeros de semana a los cuales aplicar la recurrencia. El significado de 'numero de semana' esta especificado en ISO8601 (la primera semana del año es aquella que contiene al menos 4 días del nuevo año).
  • byweekday: Si se provee, debe ser un entero (0 == MO) o secuencia de enteros, una de las constantes de weekday (MO, TU, etc) o una secuencia de ellas. Cuando se provea, define los días de la semana a los cuales se aplicará la recurrencia. Tambien es posible usar un argumento n para la instancia de weekday que indicará la nth ocurrencia de ese weekday en el periodo. Por ejemplo: usando byweekday=FR(+1) combinado con MONTHLY o YEARLY y BYMONTH, nos dara el primer viernes del mes donde ocurre la recurrencia.
  • byhour: Si se provee, debe ser un entero o secuencia de enteros indicando las horas a las cuales se aplicarará la recurrencia.
  • byminute: Igual que byhour pero para minutos
  • bysecond: Igual que byhour pero para segundos
  • byeaster: Si se provee debe ser un entero o lista de enteros, indicarán un offset desde el domingo de pascuas. Usar 0 para este offset dara el domingo de pascuas mismo.
  • rrule

    Algunos ejemplos

    Regla diaria para 7 ocurrencias

    
    
    In [14]:
    list(rrule(DAILY, count=7, dtstart=start_date))
    
    
    
    
    Out[14]:
    [datetime.datetime(1900, 1, 1, 0, 0),
     datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 1, 6, 0, 0),
     datetime.datetime(1900, 1, 7, 0, 0)]

    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]:
    [datetime.datetime(1900, 1, 1, 0, 0),
     datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 1, 6, 0, 0),
     datetime.datetime(1900, 1, 7, 0, 0)]

    rrule

    Algunos ejemplos

    Un evento cada 28 días, 6 veces

    
    
    In [16]:
    list(rrule(DAILY, count=6, dtstart=start_date, interval=28))
    
    
    
    
    Out[16]:
    [datetime.datetime(1900, 1, 1, 0, 0),
     datetime.datetime(1900, 1, 29, 0, 0),
     datetime.datetime(1900, 2, 26, 0, 0),
     datetime.datetime(1900, 3, 26, 0, 0),
     datetime.datetime(1900, 4, 23, 0, 0),
     datetime.datetime(1900, 5, 21, 0, 0)]

    Semanalmente, martes y jueves, las proximas 3 semanas

    
    
    In [17]:
    list(rrule(WEEKLY, count=6, byweekday=(TU,TH), dtstart=start_date))
    
    
    
    
    Out[17]:
    [datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 9, 0, 0),
     datetime.datetime(1900, 1, 11, 0, 0),
     datetime.datetime(1900, 1, 16, 0, 0),
     datetime.datetime(1900, 1, 18, 0, 0)]

    rrule

    Algunos Ejemplos

    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]:
    [datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 2, 2, 0, 0),
     datetime.datetime(1900, 3, 2, 0, 0),
     datetime.datetime(1900, 4, 6, 0, 0)]

    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]:
    [datetime.datetime(2014, 5, 13, 0, 0),
     datetime.datetime(2015, 1, 13, 0, 0),
     datetime.datetime(2015, 10, 13, 0, 0),
     datetime.datetime(2016, 9, 13, 0, 0),
     datetime.datetime(2016, 12, 13, 0, 0)]

    rrule

    Algunos ejemplos

    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.

    rrule

    ¿Y ahora?

    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!

    Recurrencias diarias - Usando rrule

    ¡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!'))
    

    Recurrencias diarias - Usando rrule

    ¡Eso fue mucho mas fácil que antes! ¡Probémoslo!

    
    
    In [21]:
    compute_daily_recurrences(start_date=start_date, max_repetitions=3)
    
    
    
    
    Out[21]:
    <dateutil.rrule.rrule at 0x94724ac>

    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]:
    [datetime.datetime(1900, 1, 1, 0, 0),
     datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0)]

    Recurrencias semanales - Usando rrule

    Recordemos

    Esta era mas díficil, teniamos una lista de días en los cuales se iba a repetir la regla

    Recurrencias semanales - Usando rrule

    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!

    Recurrencias semanales - Usando rrule

    ¡A probar!

    Repeticiones semanales, los martes, 3 eventos

    
    
    In [24]:
    list(compute_weekly_recurrences(start_date, repeat_on=TU, max_repetitions=3))
    
    
    
    
    Out[24]:
    [datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 9, 0, 0),
     datetime.datetime(1900, 1, 16, 0, 0)]

    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]:
    [datetime.datetime(1900, 1, 1, 0, 0),
     datetime.datetime(1900, 1, 2, 0, 0),
     datetime.datetime(1900, 1, 3, 0, 0),
     datetime.datetime(1900, 1, 4, 0, 0),
     datetime.datetime(1900, 1, 5, 0, 0),
     datetime.datetime(1900, 1, 8, 0, 0),
     datetime.datetime(1900, 1, 9, 0, 0),
     datetime.datetime(1900, 1, 10, 0, 0),
     datetime.datetime(1900, 1, 11, 0, 0),
     datetime.datetime(1900, 1, 12, 0, 0)]

    Recurrencias mensuales - Usando rrule

    Recordemos

    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!

    Recurrencias mensuales - Usando rrule

    ¿Se puede implementar?

    ¡Mas vale, de eso es la charla! Veamos:

    Recurrencias mensuales - Usando rrule

    
    
    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)
    

    Recurrencias mensuales - Usando rrule

    ¡Probemos!

    
    
    In [28]:
    list(compute_monthly_recurrences(start_date=datetime.datetime(1900,1,31),
                                     day_of_the_month=True, max_repetitions=4))
    
    
    
    
    Out[28]:
    [datetime.datetime(1900, 1, 31, 0, 0),
     datetime.datetime(1900, 2, 28, 0, 0),
     datetime.datetime(1900, 3, 31, 0, 0),
     datetime.datetime(1900, 4, 30, 0, 0)]
    
    
    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]:
    [datetime.datetime(2014, 1, 31, 0, 0),
     datetime.datetime(2014, 2, 28, 0, 0),
     datetime.datetime(2014, 3, 28, 0, 0),
     datetime.datetime(2014, 4, 25, 0, 0),
     datetime.datetime(2014, 5, 30, 0, 0)]

    Recurrencias mensuales - Usando rrule

    ¡Este era el caso más complicado y se hizo muy fácil! ¡Hagamos el último asi nos vamos tranquilos!

    Recurrencia anual - Usando rrule

    Recordemos:

    ¡Es casi igual a las diarias!

    Recurrencia anual - Usando rrule

    ¿Cómo es el código?

    
    
    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)
    

    Recurrencia anual - Usando rrule

    ¡Probemos!

    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]:
    [datetime.datetime(2012, 2, 29, 0, 0),
     datetime.datetime(2013, 2, 28, 0, 0),
     datetime.datetime(2014, 2, 28, 0, 0),
     datetime.datetime(2015, 2, 28, 0, 0),
     datetime.datetime(2016, 2, 29, 0, 0),
     datetime.datetime(2017, 2, 28, 0, 0)]

    Algunas otras herramientas del dateutil

    relativedelta

    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)
    
    
    
    
    2013-02-28 00:00:00
    2016-02-29 00:00:00
    
    
    
    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)
    
    
    
    
    1900-02-28 00:00:00
    1900-03-31 00:00:00
    1900-04-30 00:00:00
    

    relativedelta tiene mucha mas funcionalidad, pero esta fuera del alcance de esta charla.

    Algunas otras herramientas del dateutil

    easter

    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)
    
    
    
    
    2014-04-20
    2015-04-05
    1492-03-27
    

    Conclusión

  • No es imposible implementar el codigo que solucione este problema con las cosas que trae de fábrica python. Con un poco de paciencia se hace, después de todo, dateutil y rrule están escritos en python
  • Sin embargo, no hay que re-inventar la rueda. rrule existe para solucionar este tipo de problemas, lo hace bien y sigue un standard
  • En particular, para imitar el funcionamiento de Google Calendar rrule es la herramienta perfecta. Acabamos de ver una implementación (de juguete) que demuestra esto.
  • Más Información