============================ plone.event recurrence tests ============================ recurrence_sequence_ical - rrulestr test ======================================== Basic ----- When no recurrence rule is given, the start date should be returned. >>> from plone.event.recurrence import recurrence_sequence_ical >>> from datetime import datetime >>> dates = recurrence_sequence_ical(start=datetime(2010,01,01,0,0)) >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=)] Timezone naive dates are converted to utc. >>> dates = recurrence_sequence_ical( ... start=datetime(2010,01,01,0,0), ... recrule="""RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5""") >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 11, 0, 0, tzinfo=), datetime.datetime(2010, 1, 21, 0, 0, tzinfo=), datetime.datetime(2010, 1, 31, 0, 0, tzinfo=), datetime.datetime(2010, 2, 10, 0, 0, tzinfo=)] The same with the timezone set beforehand. >>> import pytz >>> at = pytz.timezone('Europe/Vienna') >>> dates = recurrence_sequence_ical( ... start=at.localize(datetime(2010,01,01,0,0)), ... recrule="""RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5""") >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 11, 0, 0, tzinfo=), datetime.datetime(2010, 1, 21, 0, 0, tzinfo=), datetime.datetime(2010, 1, 31, 0, 0, tzinfo=), datetime.datetime(2010, 2, 10, 0, 0, tzinfo=)] Get the integer representations from this recurrence rule >>> dates = recurrence_sequence_ical( ... start=at.localize(datetime(2010,01,01,0,0)), ... recrule="""RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5""") >>> from plone.event.recurrence import recurrence_int_sequence >>> list(recurrence_int_sequence(dates)) [1076762820, 1076777220, 1076791620, 1076806020, 1076820420] Crossing Daylight Saving Time Boundaries ---------------------------------------- Timezone aware Daylight Saving Time dates crossing >>> start = at.localize(datetime(2010,10,28,9,0)) >>> rulestr = """RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5""" >>> dates = recurrence_sequence_ical(start, recrule=rulestr) >>> list(dates) [datetime.datetime(2010, 10, 28, 9, 0, tzinfo=), datetime.datetime(2010, 10, 29, 9, 0, tzinfo=), datetime.datetime(2010, 10, 30, 9, 0, tzinfo=), datetime.datetime(2010, 10, 31, 9, 0, tzinfo=), datetime.datetime(2010, 11, 1, 9, 0, tzinfo=)] Note, that recurrence_sequence_ical calculates occurences timezone naively and applies timezones afterwards. This leads into a problem in corner cases: >>> list(recurrence_sequence_ical( ... start=at.localize(datetime(2010,10,30,23,0,0,0)), ... recrule="""RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=7""" ... )) [datetime.datetime(2010, 10, 30, 23, 0, tzinfo=), datetime.datetime(2010, 10, 31, 0, 0, tzinfo=), datetime.datetime(2010, 10, 31, 1, 0, tzinfo=), datetime.datetime(2010, 10, 31, 2, 0, tzinfo=), datetime.datetime(2010, 10, 31, 3, 0, tzinfo=), datetime.datetime(2010, 10, 31, 4, 0, tzinfo=), datetime.datetime(2010, 10, 31, 5, 0, tzinfo=)] Here is an hour missing, because at 3:00 in the morning, the clock is turned an hour back. Correct would be: [datetime.datetime(2010, 10, 30, 23, 0, tzinfo=), datetime.datetime(2010, 10, 31, 0, 0, tzinfo=), datetime.datetime(2010, 10, 31, 1, 0, tzinfo=), datetime.datetime(2010, 10, 31, 2, 0, tzinfo=), datetime.datetime(2010, 10, 31, 2, 0, tzinfo=), datetime.datetime(2010, 10, 31, 3, 0, tzinfo=), datetime.datetime(2010, 10, 31, 4, 0, tzinfo=)] Multiple Ruleset ---------------- >>> start = at.localize(datetime(2010,01,01,0,0)) >>> rulestr = """RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20100110T000000 ... RDATE:20100120T000000 ... EXRULE:FREQ=DAILY;INTERVAL=2;UNTIL=20100110T000000 ... EXDATE:20100102T000000""" >>> dates = recurrence_sequence_ical(start, recrule=rulestr) >>> list(dates) [datetime.datetime(2010, 1, 4, 0, 0, tzinfo=), datetime.datetime(2010, 1, 6, 0, 0, tzinfo=), datetime.datetime(2010, 1, 8, 0, 0, tzinfo=), datetime.datetime(2010, 1, 10, 0, 0, tzinfo=), datetime.datetime(2010, 1, 20, 0, 0, tzinfo=)] Limiting number of occurences ----------------------------- Until (date until recurrence happens) and count (Number of occurences) can also be given in recurrence_sequence_ical, instead of defining it in the rrule. But defining it in a rrule gives more flexibility since you can set it for each rrule individually. If MAXCOUNT is exceeded, recurrence generation will stop regardless of any other setting. ...init >>> rulestr = """RRULE:FREQ=DAILY;INTERVAL=1""" >>> start = at.localize(datetime(2010,01,01,0,0)) >>> until = at.localize(datetime(2010,01,02,0,0)) ...until >>> dates = recurrence_sequence_ical(start, recrule=rulestr, until=until) >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 2, 0, 0, tzinfo=)] ...count >>> dates = recurrence_sequence_ical(start, recrule=rulestr, count=3) >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 2, 0, 0, tzinfo=), datetime.datetime(2010, 1, 3, 0, 0, tzinfo=)] ...until and count You can mix both until and count. What happens earlier will stop recurrence >>> dates = recurrence_sequence_ical(start, rulestr, until=until, count=3) >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 2, 0, 0, tzinfo=)] ...MAXCOUNT is important to prevent (unintentional/intentional) abuse. It's a constant, is configurable (TODO) or - for this test - set from outside... >>> from plone.event import recurrence >>> orig_maxcount = recurrence.MAXCOUNT >>> recurrence.MAXCOUNT = 5 >>> dates = recurrence_sequence_ical(start, rulestr) >>> list(dates) [datetime.datetime(2010, 1, 1, 0, 0, tzinfo=), datetime.datetime(2010, 1, 2, 0, 0, tzinfo=), datetime.datetime(2010, 1, 3, 0, 0, tzinfo=), datetime.datetime(2010, 1, 4, 0, 0, tzinfo=), datetime.datetime(2010, 1, 5, 0, 0, tzinfo=)] >>> recurrence.MAXCOUNT = orig_maxcount recurrence_sequence_timedelta test ================================== >>> from plone.event.utils import DSTKEEP, DSTADJUST, DSTAUTO >>> from plone.event.recurrence import recurrence_sequence_timedelta >>> from datetime import timedelta The simplest case is to just have a one-time event. >>> start = datetime(2008,01,01,0,0,0,0,pytz.timezone('CET')) >>> dates = recurrence_sequence_timedelta(start) >>> list(dates) [datetime.datetime(2008, 1, 1, 0, 0, tzinfo=)] We want all hours of a day: >>> until = start + timedelta(days=1) - timedelta(microseconds=1) >>> dates = recurrence_sequence_timedelta(start, delta=60, until=until) >>> list(dates) [datetime.datetime(2008, 1, 1, 0, 0, tzinfo=), datetime.datetime(2008, 1, 1, 1, 0, tzinfo=), datetime.datetime(2008, 1, 1, 2, 0, tzinfo=), datetime.datetime(2008, 1, 1, 3, 0, tzinfo=), datetime.datetime(2008, 1, 1, 4, 0, tzinfo=), datetime.datetime(2008, 1, 1, 5, 0, tzinfo=), datetime.datetime(2008, 1, 1, 6, 0, tzinfo=), datetime.datetime(2008, 1, 1, 7, 0, tzinfo=), datetime.datetime(2008, 1, 1, 8, 0, tzinfo=), datetime.datetime(2008, 1, 1, 9, 0, tzinfo=), datetime.datetime(2008, 1, 1, 10, 0, tzinfo=), datetime.datetime(2008, 1, 1, 11, 0, tzinfo=), datetime.datetime(2008, 1, 1, 12, 0, tzinfo=), datetime.datetime(2008, 1, 1, 13, 0, tzinfo=), datetime.datetime(2008, 1, 1, 14, 0, tzinfo=), datetime.datetime(2008, 1, 1, 15, 0, tzinfo=), datetime.datetime(2008, 1, 1, 16, 0, tzinfo=), datetime.datetime(2008, 1, 1, 17, 0, tzinfo=), datetime.datetime(2008, 1, 1, 18, 0, tzinfo=), datetime.datetime(2008, 1, 1, 19, 0, tzinfo=), datetime.datetime(2008, 1, 1, 20, 0, tzinfo=), datetime.datetime(2008, 1, 1, 21, 0, tzinfo=), datetime.datetime(2008, 1, 1, 22, 0, tzinfo=), datetime.datetime(2008, 1, 1, 23, 0, tzinfo=)] different daylight saving time behaviours ----------------------------------------- First lets change from winter to summertime (EU rules with CET): >>> start = datetime(2008,03,29,21,0,0,0,pytz.timezone('CET')) >>> until = datetime(2008,03,30,21,0,0,0,pytz.timezone('CET')) - timedelta(microseconds=1) >>> from plone.event.utils import utcoffset_normalize, DSTADJUST >>> until = utcoffset_normalize(until, dstmode=DSTADJUST) >>> until datetime.datetime(2008, 3, 30, 20, 59, 59, 999999, tzinfo=) >>> dates = recurrence_sequence_timedelta(start, delta=60, until=until, dst=DSTADJUST) >>> dates = list(dates) >>> len(dates) 24 This sequence has one hour to much, so DSTADJUST on a granularity below one day is wrong! 1:00 CET is the same as 2:00 CEST! >>> dates [datetime.datetime(2008, 3, 29, 21, 0, tzinfo=), datetime.datetime(2008, 3, 29, 22, 0, tzinfo=), datetime.datetime(2008, 3, 29, 23, 0, tzinfo=), datetime.datetime(2008, 3, 30, 0, 0, tzinfo=), datetime.datetime(2008, 3, 30, 1, 0, tzinfo=), datetime.datetime(2008, 3, 30, 2, 0, tzinfo=), datetime.datetime(2008, 3, 30, 3, 0, tzinfo=), datetime.datetime(2008, 3, 30, 4, 0, tzinfo=), datetime.datetime(2008, 3, 30, 5, 0, tzinfo=), datetime.datetime(2008, 3, 30, 6, 0, tzinfo=), datetime.datetime(2008, 3, 30, 7, 0, tzinfo=), datetime.datetime(2008, 3, 30, 8, 0, tzinfo=), datetime.datetime(2008, 3, 30, 9, 0, tzinfo=), datetime.datetime(2008, 3, 30, 10, 0, tzinfo=), datetime.datetime(2008, 3, 30, 11, 0, tzinfo=), datetime.datetime(2008, 3, 30, 12, 0, tzinfo=), datetime.datetime(2008, 3, 30, 13, 0, tzinfo=), datetime.datetime(2008, 3, 30, 14, 0, tzinfo=), datetime.datetime(2008, 3, 30, 15, 0, tzinfo=), datetime.datetime(2008, 3, 30, 16, 0, tzinfo=), datetime.datetime(2008, 3, 30, 17, 0, tzinfo=), datetime.datetime(2008, 3, 30, 18, 0, tzinfo=), datetime.datetime(2008, 3, 30, 19, 0, tzinfo=), datetime.datetime(2008, 3, 30, 20, 0, tzinfo=)] Anyway, we want this behaviour on a day-level, see next example, a event should recurr every day 11:00am. >>> start = datetime(2008,03,29,11,0,0,0,pytz.timezone('CET')) >>> until = datetime(2008,03,31,11,0,0,0,pytz.timezone('CET')) >>> until = utcoffset_normalize(until, dstmode=DSTADJUST) >>> dates = recurrence_sequence_timedelta(start, delta=24*60, until=until, dst=DSTADJUST) >>> list(dates) [datetime.datetime(2008, 3, 29, 11, 0, tzinfo=), datetime.datetime(2008, 3, 30, 11, 0, tzinfo=), datetime.datetime(2008, 3, 31, 11, 0, tzinfo=)] see what DSTKEEP does: >>> dates = recurrence_sequence_timedelta(start, delta=24*60, until=until, dst=DSTKEEP) >>> list(dates) [datetime.datetime(2008, 3, 29, 11, 0, tzinfo=), datetime.datetime(2008, 3, 30, 12, 0, tzinfo=)] Here the correct behaviour on day level with DSTKEEP. >>> start = datetime(2008,03,29,21,0,0,0,pytz.timezone('CET')) >>> until = datetime(2008,03,30,21,0,0,0,pytz.timezone('CET')) - timedelta(microseconds=1) >>> until = utcoffset_normalize(until, dstmode=DSTADJUST) >>> dates = recurrence_sequence_timedelta(start, delta=60, until=until, dst=DSTKEEP) >>> dates = list(dates) >>> len(dates) 23 >>> dates [datetime.datetime(2008, 3, 29, 21, 0, tzinfo=), datetime.datetime(2008, 3, 29, 22, 0, tzinfo=), datetime.datetime(2008, 3, 29, 23, 0, tzinfo=), datetime.datetime(2008, 3, 30, 0, 0, tzinfo=), datetime.datetime(2008, 3, 30, 1, 0, tzinfo=), datetime.datetime(2008, 3, 30, 3, 0, tzinfo=), datetime.datetime(2008, 3, 30, 4, 0, tzinfo=), datetime.datetime(2008, 3, 30, 5, 0, tzinfo=), datetime.datetime(2008, 3, 30, 6, 0, tzinfo=), datetime.datetime(2008, 3, 30, 7, 0, tzinfo=), datetime.datetime(2008, 3, 30, 8, 0, tzinfo=), datetime.datetime(2008, 3, 30, 9, 0, tzinfo=), datetime.datetime(2008, 3, 30, 10, 0, tzinfo=), datetime.datetime(2008, 3, 30, 11, 0, tzinfo=), datetime.datetime(2008, 3, 30, 12, 0, tzinfo=), datetime.datetime(2008, 3, 30, 13, 0, tzinfo=), datetime.datetime(2008, 3, 30, 14, 0, tzinfo=), datetime.datetime(2008, 3, 30, 15, 0, tzinfo=), datetime.datetime(2008, 3, 30, 16, 0, tzinfo=), datetime.datetime(2008, 3, 30, 17, 0, tzinfo=), datetime.datetime(2008, 3, 30, 18, 0, tzinfo=), datetime.datetime(2008, 3, 30, 19, 0, tzinfo=), datetime.datetime(2008, 3, 30, 20, 0, tzinfo=)] Usally we want an clever behaviour, dependend on delta. This is implemented with DSTAUTO, which is the default behaviour. Here the correct behaviour on day level or above: DSTADJUST is choosen. >>> start = datetime(2008,03,29,11,0,0,0,pytz.timezone('CET')) >>> until = datetime(2008,03,31,11,0,0,0,pytz.timezone('CET')) >>> until = utcoffset_normalize(until, dstmode=DSTADJUST) >>> until datetime.datetime(2008, 3, 31, 11, 0, tzinfo=) >>> dates = recurrence_sequence_timedelta(start, delta=24*60, until=until) >>> list(dates) [datetime.datetime(2008, 3, 29, 11, 0, tzinfo=), datetime.datetime(2008, 3, 30, 11, 0, tzinfo=), datetime.datetime(2008, 3, 31, 11, 0, tzinfo=)] The correct behaviour on below day level: DSTKEEP is choosen. >>> start = datetime(2008,03,29,21,0,0,0,pytz.timezone('CET')) >>> until = datetime(2008,03,30,21,0,0,0,pytz.timezone('CET')) - timedelta(microseconds=1) >>> until = utcoffset_normalize(until, dstmode=DSTADJUST) >>> dates = recurrence_sequence_timedelta(start, delta=60, until=until) >>> len(list(dates)) 23 recurringIntSequence -------------------- taking the above results in integers if recurringIntSequence is called: >>> from plone.event.recurrence import recurrence_int_sequence >>> dates = recurrence_sequence_timedelta(start, 60, until) >>> list(recurrence_int_sequence(dates)) [1075822320, 1075822380, 1075822440, 1075822500, 1075822560, 1075822620, 1075822680, 1075822740, 1075822800, 1075822860, 1075822920, 1075822980, 1075823040, 1075823100, 1075823160, 1075823220, 1075823280, 1075823340, 1075823400, 1075823460, 1075823520, 1075823580, 1075823640] Zope DateTime support ===================== Now some tests with Zopes ugly DateTime >>> from DateTime import DateTime First with UTC >>> DT = DateTime('2008-08-26 23:59:00 GMT+0') >>> dt = datetime(2008, 8, 26, 23, 59, 0, 0, pytz.timezone('utc')) >>> seqDT = list(recurrence_int_sequence(recurrence_sequence_timedelta(DT, 0, None))) >>> seqdt = list(recurrence_int_sequence(recurrence_sequence_timedelta(dt, 0, None))) >>> seqDT[0] == seqdt[0] True Now with GMT+2 (CET with DST) >>> DT = DateTime('2008-08-26 23:59:00 GMT+2') >>> dt = datetime(2008, 8, 26, 23, 59, 0, 0, pytz.timezone('CET')) >>> seqDT = list(recurrence_sequence_timedelta(DT, 0, None)) seqDT shows now a hour of 21 because it's converted to UTC >>> seqDT [datetime.datetime(2008, 8, 26, 21, 59, tzinfo=)] >>> seqdt = list(recurrence_sequence_timedelta(dt, 0, None)) >>> seqdt [datetime.datetime(2008, 8, 26, 23, 59, tzinfo=)] Comparison of two datetime with same time related to UTC but in different timezones works. >>> seqDT[0] == seqdt[0] True Does integer represenation work? >>> seqDT = list(recurrence_int_sequence(recurrence_sequence_timedelta(DT, 0, None))) >>> seqdt = list(recurrence_int_sequence(recurrence_sequence_timedelta(dt, 0, None))) Integer sequences are the same anyways, because dates are converted to UTC first. >>> seqDT[0] == seqdt[0] True