~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/python-dateutil-0.4/dateutil/rrule.py

  • Committer: Robert Collins
  • Date: 2005-09-13 12:39:26 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913123926-b72242bdacc1ae52
create the output directory

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Copyright (c) 2003  Gustavo Niemeyer <niemeyer@conectiva.com>
 
3
 
 
4
This module offers extensions to the standard python 2.3+
 
5
datetime module.
 
6
"""
 
7
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
 
8
__license__ = "PSF License"
 
9
 
 
10
import itertools
 
11
import datetime
 
12
import calendar
 
13
import thread
 
14
import sys
 
15
 
 
16
__all__ = ["rrule", "rruleset", "rrulestr",
 
17
           "FREQ_YEARLY", "FREQ_MONTHLY", "FREQ_WEEKLY", "FREQ_DAILY",
 
18
           "FREQ_HOURLY", "FREQ_MINUTELY", "FREQ_SECONDLY",
 
19
           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
 
20
 
 
21
# Every mask is 7 days longer to handle cross-year weekly periods.
 
22
M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+
 
23
                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
 
24
M365MASK = list(M366MASK)
 
25
M29, M30, M31 = range(1,30), range(1,31), range(1,32)
 
26
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
 
27
MDAY365MASK = list(MDAY366MASK)
 
28
M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0)
 
29
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
 
30
NMDAY365MASK = list(NMDAY366MASK)
 
31
M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366)
 
32
M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365)
 
33
WDAYMASK = [0,1,2,3,4,5,6]*55
 
34
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
 
35
MDAY365MASK = tuple(MDAY365MASK)
 
36
M365MASK = tuple(M365MASK)
 
37
 
 
38
(FREQ_YEARLY,
 
39
 FREQ_MONTHLY,
 
40
 FREQ_WEEKLY,
 
41
 FREQ_DAILY,
 
42
 FREQ_HOURLY,
 
43
 FREQ_MINUTELY,
 
44
 FREQ_SECONDLY) = range(7)
 
45
 
 
46
# Imported on demand.
 
47
easter = None
 
48
parser = None
 
49
 
 
50
class weekday(object):
 
51
    __slots__ = ["weekday", "n"]
 
52
 
 
53
    def __init__(self, weekday, n=0):
 
54
        self.weekday = weekday
 
55
        self.n = n
 
56
 
 
57
    def __call__(self, n):
 
58
        if n == self.n:
 
59
            return self
 
60
        else:
 
61
            return self.__class__(self.weekday, n)
 
62
 
 
63
    def __eq__(self, other):
 
64
        try:
 
65
            if self.weekday != other.weekday or self.n != other.n:
 
66
                return False
 
67
        except AttributeError:
 
68
            return False
 
69
        return True
 
70
 
 
71
    def __repr__(self):
 
72
        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
 
73
        if not self.n:
 
74
            return s
 
75
        else:
 
76
            return "%s(%+d)" % (s, self.n)
 
77
 
 
78
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
 
79
 
 
80
class rrulebase:
 
81
    def __init__(self, cache=False):
 
82
        if cache:
 
83
            self._cache = []
 
84
            self._cache_lock = thread.allocate_lock()
 
85
            self._cache_gen  = self._iter()
 
86
            self._cache_complete = False
 
87
        else:
 
88
            self._cache = None
 
89
            self._cache_complete = False
 
90
        self._len = None
 
91
 
 
92
    def __iter__(self):
 
93
        if self._cache_complete:
 
94
            return iter(self._cache)
 
95
        elif self._cache is None:
 
96
            return self._iter()
 
97
        else:
 
98
            return self._iter_cached()
 
99
 
 
100
    def _iter_cached(self):
 
101
        i = 0
 
102
        gen = self._cache_gen
 
103
        cache = self._cache
 
104
        acquire = self._cache_lock.acquire
 
105
        release = self._cache_lock.release
 
106
        while gen:
 
107
            if i == len(cache):
 
108
                acquire()
 
109
                if self._cache_complete:
 
110
                    break
 
111
                try:
 
112
                    for j in range(10):
 
113
                        cache.append(gen.next())
 
114
                except StopIteration:
 
115
                    self._cache_gen = gen = None
 
116
                    self._cache_complete = True
 
117
                    break
 
118
                release()
 
119
            yield cache[i]
 
120
            i += 1
 
121
        while i < self._len:
 
122
            yield cache[i]
 
123
            i += 1
 
124
 
 
125
    def __getitem__(self, item):
 
126
        if self._cache_complete:
 
127
            return self._cache[item]
 
128
        elif isinstance(item, slice):
 
129
            if item.step and item.step < 0:
 
130
                return list(iter(self))[item]
 
131
            else:
 
132
                return list(itertools.islice(self,
 
133
                                             item.start or 0,
 
134
                                             item.stop or sys.maxint,
 
135
                                             item.step or 1))
 
136
        elif item >= 0:
 
137
            gen = iter(self)
 
138
            try:
 
139
                for i in range(item+1):
 
140
                    res = gen.next()
 
141
            except StopIteration:
 
142
                raise IndexError
 
143
            return res
 
144
        else:
 
145
            return list(iter(self))[item]
 
146
 
 
147
    def __contains__(self, item):
 
148
        if self._cache_complete:
 
149
            return item in self._cache
 
150
        else:
 
151
            for i in self:
 
152
                if i == item:
 
153
                    return True
 
154
        return False
 
155
 
 
156
    # __len__() introduces a large performance penality.
 
157
    def count(self):
 
158
        if self._len is None:
 
159
            for x in self: pass
 
160
        return self._len
 
161
 
 
162
    def before(self, dt, inc=False):
 
163
        if self._cache_complete:
 
164
            gen = self._cache
 
165
        else:
 
166
            gen = self
 
167
        last = None
 
168
        if inc:
 
169
            for i in gen:
 
170
                if i > dt:
 
171
                    break
 
172
                last = i
 
173
        else:
 
174
            for i in gen:
 
175
                if i >= dt:
 
176
                    break
 
177
                last = i
 
178
        return last
 
179
 
 
180
    def after(self, dt, inc=False):
 
181
        if self._cache_complete:
 
182
            gen = self._cache
 
183
        else:
 
184
            gen = self
 
185
        if inc:
 
186
            for i in gen:
 
187
                if i >= dt:
 
188
                    return i
 
189
        else:
 
190
            for i in gen:
 
191
                if i > dt:
 
192
                    return i
 
193
        return None
 
194
 
 
195
    def between(self, after, before, inc=False):
 
196
        if self._cache_complete:
 
197
            gen = self._cache
 
198
        else:
 
199
            gen = self
 
200
        started = False
 
201
        l = []
 
202
        if inc:
 
203
            for i in gen:
 
204
                if not started:
 
205
                    if i >= after:
 
206
                        started = True
 
207
                        l.append(i)
 
208
                else:
 
209
                    if i > before:
 
210
                        break
 
211
                    l.append(i)
 
212
        else:
 
213
            for i in gen:
 
214
                if not started:
 
215
                    if i > after:
 
216
                        started = True
 
217
                        l.append(i)
 
218
                else:
 
219
                    if i >= before:
 
220
                        break
 
221
                    l.append(i)
 
222
        return l
 
223
 
 
224
class rrule(rrulebase):
 
225
    def __init__(self, freq, dtstart=None,
 
226
                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
 
227
                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
 
228
                 byweekno=None, byweekday=None,
 
229
                 byhour=None, byminute=None, bysecond=None,
 
230
                 cache=False):
 
231
        rrulebase.__init__(self, cache)
 
232
        global easter
 
233
        if not dtstart:
 
234
            dtstart = datetime.datetime.now().replace(microsecond=0)
 
235
        elif not isinstance(dtstart, datetime.datetime):
 
236
            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
 
237
        else:
 
238
            dtstart = dtstart.replace(microsecond=0)
 
239
        self._dtstart = dtstart
 
240
        self._tzinfo = dtstart.tzinfo
 
241
        self._freq = freq
 
242
        self._interval = interval
 
243
        self._count = count
 
244
        if until and not isinstance(until, datetime.datetime):
 
245
            until = datetime.datetime.fromordinal(until.toordinal())
 
246
        self._until = until
 
247
        if wkst is None:
 
248
            self._wkst = calendar.firstweekday()
 
249
        elif type(wkst) is int:
 
250
            self._wkst = wkst
 
251
        else:
 
252
            self._wkst = wkst.weekday
 
253
        if bysetpos is None:
 
254
            self._bysetpos = None
 
255
        elif type(bysetpos) is int:
 
256
            self._bysetpos = (bysetpos,)
 
257
        else:
 
258
            self._bysetpos = tuple(bysetpos)
 
259
        if not (byweekno or byyearday or bymonthday or
 
260
                byweekday is not None or byeaster is not None):
 
261
            if freq == FREQ_YEARLY:
 
262
                if not bymonth:
 
263
                    bymonth = dtstart.month
 
264
                bymonthday = dtstart.day
 
265
            elif freq == FREQ_MONTHLY:
 
266
                bymonthday = dtstart.day
 
267
            elif freq == FREQ_WEEKLY:
 
268
                byweekday = dtstart.weekday()
 
269
        # bymonth
 
270
        if not bymonth:
 
271
            self._bymonth = None
 
272
        elif type(bymonth) is int:
 
273
            self._bymonth = (bymonth,)
 
274
        else:
 
275
            self._bymonth = tuple(bymonth)
 
276
        # byyearday
 
277
        if not byyearday:
 
278
            self._byyearday = None
 
279
        elif type(byyearday) is int:
 
280
            self._byyearday = (byyearday,)
 
281
        else:
 
282
            self._byyearday = tuple(byyearday)
 
283
        # byeaster
 
284
        if byeaster is not None:
 
285
            if not easter:
 
286
                from dateutil import easter
 
287
            if type(byeaster) is int:
 
288
                self._byeaster = (byeaster,)
 
289
            else:
 
290
                self._byeaster = tuple(byeaster)
 
291
        else:
 
292
            self._byeaster = None
 
293
        # bymonthay
 
294
        if not bymonthday:
 
295
            self._bymonthday = ()
 
296
            self._bynmonthday = ()
 
297
        elif type(bymonthday) is int:
 
298
            if bymonthday < 0:
 
299
                self._bynmonthday = (bymonthday,)
 
300
                self._bymonthday = ()
 
301
            else:
 
302
                self._bymonthday = (bymonthday,)
 
303
                self._bynmonthday = ()
 
304
        else:
 
305
            self._bymonthday = tuple([x for x in bymonthday if x > 0])
 
306
            self._bynmonthday = tuple([x for x in bymonthday if x < 0])
 
307
        # byweekno
 
308
        if byweekno is None:
 
309
            self._byweekno = None
 
310
        elif type(byweekno) is int:
 
311
            self._byweekno = (byweekno,)
 
312
        else:
 
313
            self._byweekno = tuple(byweekno)
 
314
        # byweekday / bynweekday
 
315
        if byweekday is None:
 
316
            self._byweekday = None
 
317
            self._bynweekday = None
 
318
        elif type(byweekday) is int:
 
319
            self._byweekday = (byweekday,)
 
320
            self._bynweekday = None
 
321
        elif hasattr(byweekday, "n"):
 
322
            if byweekday.n == 0 or freq > FREQ_MONTHLY:
 
323
                self._byweekday = (byweekday.weekday,)
 
324
                self._bynweekday = None
 
325
            else:
 
326
                self._bynweekday = ((byweekday.weekday, byweekday.n),)
 
327
                self._byweekday = None
 
328
        else:
 
329
            self._byweekday = []
 
330
            self._bynweekday = []
 
331
            for wday in byweekday:
 
332
                if type(wday) is int:
 
333
                    self._byweekday.append(wday)
 
334
                elif wday.n == 0 or freq > FREQ_MONTHLY:
 
335
                    self._byweekday.append(wday.weekday)
 
336
                else:
 
337
                    self._bynweekday.append((wday.weekday, wday.n))
 
338
            self._byweekday = tuple(self._byweekday)
 
339
            self._bynweekday = tuple(self._bynweekday)
 
340
            if not self._byweekday:
 
341
                self._byweekday = None
 
342
            elif not self._bynweekday:
 
343
                self._bynweekday = None
 
344
        # byhour
 
345
        if byhour is None:
 
346
            if freq < FREQ_HOURLY:
 
347
                self._byhour = (dtstart.hour,)
 
348
            else:
 
349
                self._byhour = None
 
350
        elif type(byhour) is int:
 
351
            self._byhour = (byhour,)
 
352
        else:
 
353
            self._byhour = tuple(byhour)
 
354
        # byminute
 
355
        if byminute is None:
 
356
            if freq < FREQ_MINUTELY:
 
357
                self._byminute = (dtstart.minute,)
 
358
            else:
 
359
                self._byminute = None
 
360
        elif type(byminute) is int:
 
361
            self._byminute = (byminute,)
 
362
        else:
 
363
            self._byminute = tuple(byminute)
 
364
        # bysecond
 
365
        if bysecond is None:
 
366
            if freq < FREQ_SECONDLY:
 
367
                self._bysecond = (dtstart.second,)
 
368
            else:
 
369
                self._bysecond = None
 
370
        elif type(bysecond) is int:
 
371
            self._bysecond = (bysecond,)
 
372
        else:
 
373
            self._bysecond = tuple(bysecond)
 
374
 
 
375
        if self._freq >= FREQ_HOURLY:
 
376
            self._timeset = None
 
377
        else:
 
378
            self._timeset = []
 
379
            for hour in self._byhour:
 
380
                for minute in self._byminute:
 
381
                    for second in self._bysecond:
 
382
                        self._timeset.append(
 
383
                                datetime.time(hour, minute, second,
 
384
                                                    tzinfo=self._tzinfo))
 
385
            self._timeset.sort()
 
386
            self._timeset = tuple(self._timeset)
 
387
 
 
388
    def _iter(self):
 
389
        year, month, day, hour, minute, second, weekday, yearday, _ = \
 
390
            self._dtstart.timetuple()
 
391
 
 
392
        # Some local variables to speed things up a bit
 
393
        freq = self._freq
 
394
        interval = self._interval
 
395
        wkst = self._wkst
 
396
        until = self._until
 
397
        bymonth = self._bymonth
 
398
        byweekno = self._byweekno
 
399
        byyearday = self._byyearday
 
400
        byweekday = self._byweekday
 
401
        byeaster = self._byeaster
 
402
        bymonthday = self._bymonthday
 
403
        bynmonthday = self._bynmonthday
 
404
        bysetpos = self._bysetpos
 
405
        byhour = self._byhour
 
406
        byminute = self._byminute
 
407
        bysecond = self._bysecond
 
408
 
 
409
        ii = _iterinfo(self)
 
410
        ii.rebuild(year, month)
 
411
 
 
412
        getdayset = {FREQ_YEARLY:ii.ydayset,
 
413
                     FREQ_MONTHLY:ii.mdayset,
 
414
                     FREQ_WEEKLY:ii.wdayset,
 
415
                     FREQ_DAILY:ii.ddayset,
 
416
                     FREQ_HOURLY:ii.ddayset,
 
417
                     FREQ_MINUTELY:ii.ddayset,
 
418
                     FREQ_SECONDLY:ii.ddayset}[freq]
 
419
        
 
420
        if freq < FREQ_HOURLY:
 
421
            timeset = self._timeset
 
422
        else:
 
423
            gettimeset = {FREQ_HOURLY:ii.htimeset,
 
424
                          FREQ_MINUTELY:ii.mtimeset,
 
425
                          FREQ_SECONDLY:ii.stimeset}[freq]
 
426
            if ((freq >= FREQ_HOURLY and
 
427
                 self._byhour and hour not in self._byhour) or
 
428
                (freq >= FREQ_MINUTELY and
 
429
                 self._byminute and minute not in self._byminute) or
 
430
                (freq >= FREQ_SECONDLY and
 
431
                 self._bysecond and minute not in self._bysecond)):
 
432
                timeset = ()
 
433
            else:
 
434
                timeset = gettimeset(hour, minute, second)
 
435
 
 
436
        total = 0
 
437
        count = self._count
 
438
        while True:
 
439
            # Get dayset with the right frequency
 
440
            dayset, start, end = getdayset(year, month, day)
 
441
 
 
442
            # Do the "hard" work ;-)
 
443
            filtered = False
 
444
            for i in dayset[start:end]:
 
445
                if ((bymonth and ii.mmask[i] not in bymonth) or
 
446
                    (byweekno and not ii.wnomask[i]) or
 
447
                    (byyearday and (i%ii.yearlen)+1 not in byyearday) or
 
448
                    (byweekday and ii.wdaymask[i] not in byweekday) or
 
449
                    (ii.nwdaymask and not ii.nwdaymask[i]) or
 
450
                    (byeaster and not ii.eastermask[i]) or
 
451
                    ((bymonthday or bynmonthday) and
 
452
                     ii.mdaymask[i] not in bymonthday and
 
453
                     ii.nmdaymask[i] not in bynmonthday)):
 
454
                    dayset[i] = None
 
455
                    filtered = True
 
456
 
 
457
            # Output results
 
458
            if bysetpos and timeset:
 
459
                poslist = []
 
460
                for pos in bysetpos:
 
461
                    if pos < 0:
 
462
                        daypos, timepos = divmod(pos, len(timeset))
 
463
                    else:
 
464
                        daypos, timepos = divmod(pos-1, len(timeset))
 
465
                    try:
 
466
                        i = [x for x in dayset[start:end]
 
467
                                if x is not None][daypos]
 
468
                        time = timeset[timepos]
 
469
                    except IndexError:
 
470
                        pass
 
471
                    else:
 
472
                        date = datetime.date.fromordinal(ii.yearordinal+i)
 
473
                        res = datetime.datetime.combine(date, time)
 
474
                        if res not in poslist:
 
475
                            poslist.append(res)
 
476
                poslist.sort()
 
477
                for res in poslist:
 
478
                    if until and res > until:
 
479
                        self._len = total
 
480
                        return
 
481
                    elif res >= self._dtstart:
 
482
                        total += 1
 
483
                        yield res
 
484
                        if count:
 
485
                            count -= 1
 
486
                            if not count:
 
487
                                self._len = total
 
488
                                return
 
489
            else:
 
490
                for i in dayset[start:end]:
 
491
                    if i is not None:
 
492
                        date = datetime.date.fromordinal(ii.yearordinal+i)
 
493
                        for time in timeset:
 
494
                            res = datetime.datetime.combine(date, time)
 
495
                            if until and res > until:
 
496
                                self._len = total
 
497
                                return
 
498
                            elif res >= self._dtstart:
 
499
                                total += 1
 
500
                                yield res
 
501
                                if count:
 
502
                                    count -= 1
 
503
                                    if not count:
 
504
                                        self._len = total
 
505
                                        return
 
506
 
 
507
            # Handle frequency and interval
 
508
            fixday = False
 
509
            if freq == FREQ_YEARLY:
 
510
                year += interval
 
511
                if year > datetime.MAXYEAR:
 
512
                    self._len = total
 
513
                    return
 
514
                ii.rebuild(year, month)
 
515
            elif freq == FREQ_MONTHLY:
 
516
                month += interval
 
517
                if month > 12:
 
518
                    div, mod = divmod(month, 12)
 
519
                    month = mod
 
520
                    year += div
 
521
                    if month == 0:
 
522
                        month = 12
 
523
                        year -= 1
 
524
                    if year > datetime.MAXYEAR:
 
525
                        self._len = total
 
526
                        return
 
527
                ii.rebuild(year, month)
 
528
            elif freq == FREQ_WEEKLY:
 
529
                if wkst > weekday:
 
530
                    day += -(weekday+1+(6-wkst))+self._interval*7
 
531
                else:
 
532
                    day += -(weekday-wkst)+self._interval*7
 
533
                weekday = wkst
 
534
                fixday = True
 
535
            elif freq == FREQ_DAILY:
 
536
                day += interval
 
537
                fixday = True
 
538
            elif freq == FREQ_HOURLY:
 
539
                if filtered:
 
540
                    # Jump to one iteration before next day
 
541
                    hour += ((23-hour)//interval)*interval
 
542
                while True:
 
543
                    hour += interval
 
544
                    div, mod = divmod(hour, 24)
 
545
                    if div:
 
546
                        hour = mod
 
547
                        day += div
 
548
                        fixday = True
 
549
                    if not byhour or hour in byhour:
 
550
                        break
 
551
                timeset = gettimeset(hour, minute, second)
 
552
            elif freq == FREQ_MINUTELY:
 
553
                if filtered:
 
554
                    # Jump to one iteration before next day
 
555
                    minute += ((1439-(hour*60+minute))//interval)*interval
 
556
                while True:
 
557
                    minute += interval
 
558
                    div, mod = divmod(minute, 60)
 
559
                    if div:
 
560
                        minute = mod
 
561
                        hour += div
 
562
                        div, mod = divmod(hour, 24)
 
563
                        if div:
 
564
                            hour = mod
 
565
                            day += div
 
566
                            fixday = True
 
567
                            filtered = False
 
568
                    if ((not byhour or hour in byhour) and
 
569
                        (not byminute or minute in byminute)):
 
570
                        break
 
571
                timeset = gettimeset(hour, minute, second)
 
572
            elif freq == FREQ_SECONDLY:
 
573
                if filtered:
 
574
                    # Jump to one iteration before next day
 
575
                    second += (((86399-(hour*3600+minute*60+second))
 
576
                                //interval)*interval)
 
577
                while True:
 
578
                    second += self._interval
 
579
                    div, mod = divmod(second, 60)
 
580
                    if div:
 
581
                        second = mod
 
582
                        minute += div
 
583
                        div, mod = divmod(minute, 60)
 
584
                        if div:
 
585
                            minute = mod
 
586
                            hour += div
 
587
                            div, mod = divmod(hour, 24)
 
588
                            if div:
 
589
                                hour = mod
 
590
                                day += div
 
591
                                fixday = True
 
592
                    if ((not byhour or hour in byhour) and
 
593
                        (not byminute or minute in byminute) and
 
594
                        (not bysecond or second in bysecond)):
 
595
                        break
 
596
                timeset = gettimeset(hour, minute, second)
 
597
 
 
598
            if fixday and day > 28:
 
599
                daysinmonth = calendar.monthrange(year, month)[1]
 
600
                if day > daysinmonth:
 
601
                    while day > daysinmonth:
 
602
                        day -= daysinmonth
 
603
                        month += 1
 
604
                        if month == 13:
 
605
                            month = 1
 
606
                            year += 1
 
607
                            if year > datetime.MAXYEAR:
 
608
                                self._len = total
 
609
                                return
 
610
                        daysinmonth = calendar.monthrange(year, month)[1]
 
611
                    ii.rebuild(year, month)
 
612
 
 
613
class _iterinfo(object):
 
614
    __slots__ = ["rrule", "lastyear", "lastmonth",
 
615
                 "yearlen", "yearordinal", "yearweekday",
 
616
                 "mmask", "mrange", "mdaymask", "nmdaymask",
 
617
                 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
 
618
 
 
619
    def __init__(self, rrule):
 
620
        for attr in self.__slots__:
 
621
            setattr(self, attr, None)
 
622
        self.rrule = rrule
 
623
 
 
624
    def rebuild(self, year, month):
 
625
        # Every mask is 7 days longer to handle cross-year weekly periods.
 
626
        rr = self.rrule
 
627
        if year != self.lastyear:
 
628
            self.yearlen = 365+calendar.isleap(year)
 
629
            firstyday = datetime.date(year, 1, 1)
 
630
            self.yearordinal = firstyday.toordinal()
 
631
            self.yearweekday = firstyday.weekday()
 
632
 
 
633
            wday = datetime.date(year, 1, 1).weekday()
 
634
            if self.yearlen == 365:
 
635
                self.mmask = M365MASK
 
636
                self.mdaymask = MDAY365MASK
 
637
                self.nmdaymask = NMDAY365MASK
 
638
                self.wdaymask = WDAYMASK[wday:]
 
639
                self.mrange = M365RANGE
 
640
            else:
 
641
                self.mmask = M366MASK
 
642
                self.mdaymask = MDAY366MASK
 
643
                self.nmdaymask = NMDAY366MASK
 
644
                self.wdaymask = WDAYMASK[wday:]
 
645
                self.mrange = M366RANGE
 
646
 
 
647
            if not rr._byweekno:
 
648
                self.wnomask = None
 
649
            else:
 
650
                self.wnomask = [0]*(self.yearlen+7)
 
651
                #no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
 
652
                no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7
 
653
                if no1wkst >= 4:
 
654
                    no1wkst = 0
 
655
                    # Number of days in the year, plus the days we got
 
656
                    # from last year.
 
657
                    wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7
 
658
                else:
 
659
                    # Number of days in the year, minus the days we
 
660
                    # left in last year.
 
661
                    wyearlen = self.yearlen-no1wkst
 
662
                div, mod = divmod(wyearlen, 7)
 
663
                numweeks = div+mod//4
 
664
                for n in rr._byweekno:
 
665
                    if n < 0:
 
666
                        n += numweeks+1
 
667
                    if not (0 < n <= numweeks):
 
668
                        continue
 
669
                    if n > 1:
 
670
                        i = no1wkst+(n-1)*7
 
671
                        if no1wkst != firstwkst:
 
672
                            i -= 7-firstwkst
 
673
                    else:
 
674
                        i = no1wkst
 
675
                    for j in range(7):
 
676
                        self.wnomask[i] = 1
 
677
                        i += 1
 
678
                        if self.wdaymask[i] == rr._wkst:
 
679
                            break
 
680
                if 1 in rr._byweekno:
 
681
                    # Check week number 1 of next year as well
 
682
                    # TODO: Check -numweeks for next year.
 
683
                    i = no1wkst+numweeks*7
 
684
                    if no1wkst != firstwkst:
 
685
                        i -= 7-firstwkst
 
686
                    if i < self.yearlen:
 
687
                        # If week starts in next year, we
 
688
                        # don't care about it.
 
689
                        for j in range(7):
 
690
                            self.wnomask[i] = 1
 
691
                            i += 1
 
692
                            if self.wdaymask[i] == rr._wkst:
 
693
                                break
 
694
                if no1wkst:
 
695
                    # Check last week number of last year as
 
696
                    # well. If no1wkst is 0, either the year
 
697
                    # started on week start, or week number 1
 
698
                    # got days from last year, so there are no
 
699
                    # days from last year's last week number in
 
700
                    # this year.
 
701
                    if -1 not in rr._byweekno:
 
702
                        lyearweekday = datetime.date(year-1,1,1).weekday()
 
703
                        lno1wkst = (7-lyearweekday+rr._wkst)%7
 
704
                        lyearlen = 365+calendar.isleap(year-1)
 
705
                        if lno1wkst >= 4:
 
706
                            lno1wkst = 0
 
707
                            lnumweeks = 52+(lyearlen+
 
708
                                           (lyearweekday-rr._wkst)%7)%7//4
 
709
                        else:
 
710
                            lnumweeks = 52+(self.yearlen-no1wkst)%7//4
 
711
                    else:
 
712
                        lnumweeks = -1
 
713
                    if lnumweeks in rr._byweekno:
 
714
                        for i in range(no1wkst):
 
715
                            self.wnomask[i] = 1
 
716
 
 
717
        if (rr._bynweekday and
 
718
            (month != self.lastmonth or year != self.lastyear)):
 
719
            ranges = []
 
720
            if rr._freq == FREQ_YEARLY:
 
721
                if rr._bymonth:
 
722
                    for month in rr._bymonth:
 
723
                        ranges.append(self.mrange[month-1:month+1])
 
724
                else:
 
725
                    ranges = [(0, self.yearlen)]
 
726
            elif rr._freq == FREQ_MONTHLY:
 
727
                ranges = [self.mrange[month-1:month+1]]
 
728
            if ranges:
 
729
                # Weekly frequency won't get here, so we may not
 
730
                # care about cross-year weekly periods.
 
731
                self.nwdaymask = [0]*self.yearlen
 
732
                for first, last in ranges:
 
733
                    last -= 1
 
734
                    for wday, n in rr._bynweekday:
 
735
                        if n < 0:
 
736
                            i = last+(n+1)*7
 
737
                            i -= (self.wdaymask[i]-wday)%7
 
738
                        else:
 
739
                            i = first+(n-1)*7
 
740
                            i += (7-self.wdaymask[i]+wday)%7
 
741
                        if first <= i <= last:
 
742
                            self.nwdaymask[i] = 1
 
743
 
 
744
        if rr._byeaster:
 
745
            self.eastermask = [0]*(self.yearlen+7)
 
746
            eyday = easter.easter(year).toordinal()-self.yearordinal
 
747
            for offset in rr._byeaster:
 
748
                self.eastermask[eyday+offset] = 1
 
749
 
 
750
        self.lastyear = year
 
751
        self.lastmonth = month
 
752
 
 
753
    def ydayset(self, year, month, day):
 
754
        return range(self.yearlen), 0, self.yearlen
 
755
 
 
756
    def mdayset(self, year, month, day):
 
757
        set = [None]*self.yearlen
 
758
        start, end = self.mrange[month-1:month+1]
 
759
        for i in range(start, end):
 
760
            set[i] = i
 
761
        return set, start, end
 
762
 
 
763
    def wdayset(self, year, month, day):
 
764
        # We need to handle cross-year weeks here.
 
765
        set = [None]*(self.yearlen+7)
 
766
        i = datetime.date(year, month, day).toordinal()-self.yearordinal
 
767
        start = i
 
768
        for j in range(7):
 
769
            set[i] = i
 
770
            i += 1
 
771
            #if (not (0 <= i < self.yearlen) or
 
772
            #    self.wdaymask[i] == self.rrule._wkst):
 
773
            # This will cross the year boundary, if necessary.
 
774
            if self.wdaymask[i] == self.rrule._wkst:
 
775
                break
 
776
        return set, start, i
 
777
 
 
778
    def ddayset(self, year, month, day):
 
779
        set = [None]*self.yearlen
 
780
        i = datetime.date(year, month, day).toordinal()-self.yearordinal
 
781
        set[i] = i
 
782
        return set, i, i+1
 
783
 
 
784
    def htimeset(self, hour, minute, second):
 
785
        set = []
 
786
        rr = self.rrule
 
787
        for minute in rr._byminute:
 
788
            for second in rr._bysecond:
 
789
                set.append(datetime.time(hour, minute, second,
 
790
                                         tzinfo=rr._tzinfo))
 
791
        set.sort()
 
792
        return set
 
793
 
 
794
    def mtimeset(self, hour, minute, second):
 
795
        set = []
 
796
        rr = self.rrule
 
797
        for second in rr._bysecond:
 
798
            set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
 
799
        set.sort()
 
800
        return set
 
801
 
 
802
    def stimeset(self, hour, minute, second):
 
803
        return (datetime.time(hour, minute, second,
 
804
                tzinfo=self.rrule._tzinfo),)
 
805
 
 
806
 
 
807
class rruleset(rrulebase):
 
808
 
 
809
    class _genitem:
 
810
        def __init__(self, genlist, gen):
 
811
            try:
 
812
                self.dt = gen()
 
813
                genlist.append(self)
 
814
            except StopIteration:
 
815
                pass
 
816
            self.genlist = genlist
 
817
            self.gen = gen
 
818
 
 
819
        def next(self):
 
820
            try:
 
821
                self.dt = self.gen()
 
822
            except StopIteration:
 
823
                self.genlist.remove(self)
 
824
 
 
825
        def __cmp__(self, other):
 
826
            return cmp(self.dt, other.dt)
 
827
 
 
828
    def __init__(self, cache=False):
 
829
        rrulebase.__init__(self, cache)
 
830
        self._rrule = []
 
831
        self._rdate = []
 
832
        self._exrule = []
 
833
        self._exdate = []
 
834
 
 
835
    def rrule(self, rrule):
 
836
        self._rrule.append(rrule)
 
837
    
 
838
    def rdate(self, rdate):
 
839
        self._rdate.append(rdate)
 
840
 
 
841
    def exrule(self, exrule):
 
842
        self._exrule.append(exrule)
 
843
 
 
844
    def exdate(self, exdate):
 
845
        self._exdate.append(exdate)
 
846
 
 
847
    def _iter(self):
 
848
        rlist = []
 
849
        self._genitem(rlist, iter(self._rdate).next)
 
850
        for gen in [iter(x).next for x in self._rrule]:
 
851
            self._genitem(rlist, gen)
 
852
        rlist.sort()
 
853
        exlist = []
 
854
        self._genitem(exlist, iter(self._exdate).next)
 
855
        for gen in [iter(x).next for x in self._exrule]:
 
856
            self._genitem(exlist, gen)
 
857
        exlist.sort()
 
858
        lastdt = None
 
859
        total = 0
 
860
        while rlist:
 
861
            ritem = rlist[0]
 
862
            if not lastdt or lastdt != ritem.dt:
 
863
                while exlist and exlist[0] < ritem:
 
864
                    exlist[0].next()
 
865
                    exlist.sort()
 
866
                if not exlist or ritem != exlist[0]:
 
867
                    total += 1
 
868
                    yield ritem.dt
 
869
                lastdt = ritem.dt
 
870
            ritem.next()
 
871
            rlist.sort()
 
872
        self._len = total
 
873
 
 
874
class _rrulestr:
 
875
 
 
876
    _freq_map = {"YEARLY": FREQ_YEARLY,
 
877
                 "MONTHLY": FREQ_MONTHLY,
 
878
                 "WEEKLY": FREQ_WEEKLY,
 
879
                 "DAILY": FREQ_DAILY,
 
880
                 "HOURLY": FREQ_HOURLY,
 
881
                 "MINUTELY": FREQ_MINUTELY,
 
882
                 "SECONDLY": FREQ_SECONDLY}
 
883
 
 
884
    _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6}
 
885
 
 
886
    def _handle_int(self, rrkwargs, name, value, **kwargs):
 
887
        rrkwargs[name.lower()] = int(value)
 
888
 
 
889
    def _handle_int_list(self, rrkwargs, name, value, **kwargs):
 
890
        rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
 
891
 
 
892
    _handle_INTERVAL   = _handle_int
 
893
    _handle_COUNT      = _handle_int
 
894
    _handle_BYSETPOS   = _handle_int_list
 
895
    _handle_BYMONTH    = _handle_int_list
 
896
    _handle_BYMONTHDAY = _handle_int_list
 
897
    _handle_BYYEARDAY  = _handle_int_list
 
898
    _handle_BYEASTER   = _handle_int_list
 
899
    _handle_BYWEEKNO   = _handle_int_list
 
900
    _handle_BYHOUR     = _handle_int_list
 
901
    _handle_BYMINUTE   = _handle_int_list
 
902
    _handle_BYSECOND   = _handle_int_list
 
903
 
 
904
    def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
 
905
        rrkwargs["freq"] = self._freq_map[value]
 
906
 
 
907
    def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
 
908
        global parser
 
909
        if not parser:
 
910
            from dateutil import parser
 
911
        try:
 
912
            rrkwargs["until"] = parser.parse(value,
 
913
                                           ignoretz=kwargs.get("ignoretz"),
 
914
                                           tzinfos=kwargs.get("tzinfos"))
 
915
        except ValueError:
 
916
            raise ValueError, "invalid until date"
 
917
 
 
918
    def _handle_WKST(self, rrkwargs, name, value, **kwargs):
 
919
        rrkwargs["wkst"] = self._weekday_map[value]
 
920
 
 
921
    def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg):
 
922
        l = []
 
923
        for wday in value.split(','):
 
924
            for i in range(len(wday)):
 
925
                if wday[i] not in '+-0123456789':
 
926
                    break
 
927
            n = wday[:i] or 0
 
928
            w = wday[i:]
 
929
            if n: n = int(n)
 
930
            l.append(weekdays[self._weekday_map[w]](n))
 
931
        rrkwargs["byweekday"] = l
 
932
 
 
933
    _handle_BYDAY = _handle_BYWEEKDAY
 
934
 
 
935
    def _parse_rfc_rrule(self, line,
 
936
                         dtstart=None,
 
937
                         cache=False,
 
938
                         ignoretz=False,
 
939
                         tzinfos=None):
 
940
        if line.find(':') != -1:
 
941
            name, value = line.split(':')
 
942
            if name != "RRULE":
 
943
                raise ValueError, "unknown parameter name"
 
944
        else:
 
945
            value = line
 
946
        rrkwargs = {}
 
947
        for pair in value.split(';'):
 
948
            name, value = pair.split('=')
 
949
            name = name.upper()
 
950
            value = value.upper()
 
951
            try:
 
952
                getattr(self, "_handle_"+name)(rrkwargs, name, value,
 
953
                                               ignoretz=ignoretz,
 
954
                                               tzinfos=tzinfos)
 
955
            except AttributeError:
 
956
                raise "unknown parameter '%s'" % name
 
957
            except (KeyError, ValueError):
 
958
                raise "invalid '%s': %s" % (name, value)
 
959
        return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
 
960
 
 
961
    def _parse_rfc(self, s,
 
962
                   dtstart=None,
 
963
                   cache=False,
 
964
                   unfold=False,
 
965
                   forceset=False,
 
966
                   compatible=False,
 
967
                   ignoretz=False,
 
968
                   tzinfos=None):
 
969
        global parser
 
970
        if compatible:
 
971
            forceset = True
 
972
            unfold = True
 
973
        s = s.upper()
 
974
        if not s.strip():
 
975
            raise ValueError, "empty string"
 
976
        if unfold:
 
977
            lines = s.splitlines()
 
978
            i = 0
 
979
            while i < len(lines):
 
980
                line = lines[i].rstrip()
 
981
                if not line:
 
982
                    del lines[i]
 
983
                elif i > 0 and line[0] == " ":
 
984
                    lines[i-1] += line[1:]
 
985
                    del lines[i]
 
986
                else:
 
987
                    i += 1
 
988
        else:
 
989
            lines = s.split()
 
990
        if (not forceset and len(lines) == 1 and
 
991
            (s.find(':') == -1 or s.startswith('RRULE:'))):
 
992
            return self._parse_rfc_rrule(lines[0], cache=cache,
 
993
                                         dtstart=dtstart, ignoretz=ignoretz,
 
994
                                         tzinfos=tzinfos)
 
995
        else:
 
996
            rrulevals = []
 
997
            rdatevals = []
 
998
            exrulevals = []
 
999
            exdatevals = []
 
1000
            for line in lines:
 
1001
                if not line:
 
1002
                    continue
 
1003
                if line.find(':') == -1:
 
1004
                    name = "RRULE"
 
1005
                    value = line
 
1006
                else:
 
1007
                    name, value = line.split(':', 1)
 
1008
                parms = name.split(';')
 
1009
                if not parms:
 
1010
                    raise ValueError, "empty property name"
 
1011
                name = parms[0]
 
1012
                parms = parms[1:]
 
1013
                if name == "RRULE":
 
1014
                    for parm in parms:
 
1015
                        raise ValueError, "unsupported RRULE parm: "+parm
 
1016
                    rrulevals.append(value)
 
1017
                elif name == "RDATE":
 
1018
                    for parm in parms:
 
1019
                        if parm != "VALUE=DATE-TIME":
 
1020
                            raise ValueError, "unsupported RDATE parm: "+parm
 
1021
                    rdatevals.append(value)
 
1022
                elif name == "EXRULE":
 
1023
                    for parm in parms:
 
1024
                        raise ValueError, "unsupported EXRULE parm: "+parm
 
1025
                    exrulevals.append(value)
 
1026
                elif name == "EXDATE":
 
1027
                    for parm in parms:
 
1028
                        if parm != "VALUE=DATE-TIME":
 
1029
                            raise ValueError, "unsupported RDATE parm: "+parm
 
1030
                    exdatevals.append(value)
 
1031
                elif name == "DTSTART":
 
1032
                    for parm in parms:
 
1033
                        raise ValueError, "unsupported DTSTART parm: "+parm
 
1034
                    if not parser:
 
1035
                        from dateutil import parser
 
1036
                    dtstart = parser.parse(value, ignoretz=ignoretz,
 
1037
                                           tzinfos=tzinfos)
 
1038
                else:
 
1039
                    raise ValueError, "unsupported property: "+name
 
1040
            if (forceset or len(rrulevals) > 1 or
 
1041
                rdatevals or exrulevals or exdatevals):
 
1042
                if not parser and (rdatevals or exdatevals):
 
1043
                    from dateutil import parser
 
1044
                set = rruleset(cache=cache)
 
1045
                for value in rrulevals:
 
1046
                    set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
 
1047
                                                    ignoretz=ignoretz,
 
1048
                                                    tzinfos=tzinfos))
 
1049
                for value in rdatevals:
 
1050
                    for datestr in value.split(','):
 
1051
                        set.rdate(parser.parse(datestr,
 
1052
                                               ignoretz=ignoretz,
 
1053
                                               tzinfos=tzinfos))
 
1054
                for value in exrulevals:
 
1055
                    set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
 
1056
                                                     ignoretz=ignoretz,
 
1057
                                                     tzinfos=tzinfos))
 
1058
                for value in exdatevals:
 
1059
                    for datestr in value.split(','):
 
1060
                        set.exdate(parser.parse(datestr,
 
1061
                                                ignoretz=ignoretz,
 
1062
                                                tzinfos=tzinfos))
 
1063
                if compatible and dtstart:
 
1064
                    set.rdate(dtstart)
 
1065
                return set
 
1066
            else:
 
1067
                return self._parse_rfc_rrule(rrulevals[0],
 
1068
                                             dtstart=dtstart,
 
1069
                                             cache=cache,
 
1070
                                             ignoretz=ignoretz,
 
1071
                                             tzinfos=tzinfos)
 
1072
 
 
1073
    def __call__(self, s, **kwargs):
 
1074
        return self._parse_rfc(s, **kwargs)
 
1075
 
 
1076
rrulestr = _rrulestr()
 
1077
 
 
1078
# vim:ts=4:sw=4:et