Contact: fumanchu@aminus.org

Log in as guest/misc to create tickets

root/recur.py

Revision 52 (checked in by fumanchu, 7 years ago)

Added a link to the project page.

  • Property svn:eol-style set to native
Line 
1 """
2 Iterators for datetime objects.
3
4 This work, including the source code, documentation
5 and related data, is placed into the public domain.
6
7 The original author is Robert Brewer, Amor Ministries.
8 See http://projects.amor.org/misc/wiki/Recur for more docs.
9
10 THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
11 OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
12 MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
13 ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
14 RESULTING FROM THE USE, MODIFICATION, OR
15 REDISTRIBUTION OF THIS SOFTWARE.
16
17 Western-language descriptions of recurrence tend to fall into
18 two distinct types. In order to provide some mnemonic consistency,
19 the base functions are named differently according to these types.
20 However, despite the differing names, every function yields
21 datetime.date or datetime.datetime objects.
22
23 First, there are the declarations which define a unit of time,
24 and then count successive "leaps" of those units. For example,
25 the declaration, "every 4 days," uses a day as the unit, and adds
26 4 to produce each value in the series. The functions which provide
27 these series are named according to the whole unit, in the plural.
28 Examples:
29     "Every 4 days" becomes: days(start, 4, [end])
30     "Every 2 weeks" becomes: weeks(start, 2, [end])
31     "Every 6 hours" becomes: hours(start, 6, [end])
32
33 Second, there are the declarations which define a unit of time,
34 and then count by subdivisions of that unit. For example, the
35 declaration, "the ninth day of each month," uses a month as the
36 whole units and a day as the subdivision. The functions which
37 provide these series are named according to the whole unit,
38 in the singular, prefixed by "each".
39 Examples:
40     "The ninth [day] of each month" becomes: eachmonth(start, 9, [end])
41     "The penultimate [day] of each month" becomes:
42         eachmonth(start, -1, [end])
43     "Every Thursday" becomes "The 3rd [day] of each week" [since
44         datetime.weekday() returns Thursday as the value 3]
45         which becomes: eachweek(start, 3, [end])
46     "08:30:00 on each day" becomes:
47         eachday(start, datetime.time(8, 30), [end])
48 Notice that, in almost every case, the subdivision is understood to be
49 the "next smallest component". In the example above, one might just as
50 well have written, "the ninth of each month," and been understood,
51 since months are "composed of" days (not weeks!). Therefore, our
52 functions do not incorporate this "smaller unit" in the function name.
53 """
54
55 import datetime
56 import re
57 import threading
58
59
60 def sane_date(year, month, day, fixMonth=False):
61     """Return a valid datetime.date even if parameters are out of bounds.
62     
63     If the month param is out of bounds, both it and the year will
64     be modified. If negative, the year will be decremented.
65     
66     If fixMonth is False, and the day param is out of bounds, both the
67     day param and the month will be modified.
68     
69     If fixMonth is True, and the day param is out of bounds, the month
70     will not change, and the day will be set to the appropriate boundary.
71     The month may still, however, modify the year.
72     
73     Examples:
74         sane_date(2003, 2, 1) = datetime.date(2003, 2, 1)
75         sane_date(2003, -10, 13) = datetime.date(2002, 2, 13)
76         sane_date(2003, 12, -5) = datetime.date(2003, 11, 25)
77         sane_date(2003, 1, 35, True) = datetime.date(2003, 1, 31)
78     """
79     while month > 12:
80         month -= 12
81         year += 1
82     while month < 1:
83         month += 12
84         year -= 1
85     if fixMonth:
86         if day < 1:
87             newDate = datetime.date(year, month, 1)
88         else:
89             while True:
90                 try:
91                     newDate = datetime.date(year, month, day)
92                 except ValueError:
93                     day -= 1
94                     if day < 1:
95                         raise ValueError("A valid day for month: %s in "
96                                          "year: %s could not be found",
97                                          (month, year))
98                 else:
99                     break
100     else:
101         if day < 1:
102             # Count backward from the end of the current month.
103             firstOfMonth = sane_date(year, month + 1, 1)
104         else:
105             # Count forward from the first of the current month.
106             firstOfMonth = datetime.date(year, month, 1)
107         newDate = (firstOfMonth + datetime.timedelta(day - 1))
108     return newDate
109
110 def sane_time(day, hour, minute, second):
111     """Return a valid (day, datetime.time) even if parameters are out of bounds.
112     
113     If the hour param is out of bounds, both it and the day will
114     be modified. If negative, the day will be decremented.
115     
116     If the minute param is out of bounds, both it and the hour will
117     be modified. If negative, the hour will be decremented.
118     
119     If the second param is out of bounds, both it and the minute will
120     be modified. If negative, the minute will be decremented.
121     
122     Examples:
123         sane_time(0, 4, 2, 1) = (0, datetime.time(4, 2, 1)
124         sane_time(0, 25, 2, 1) = (1, datetime.time(1, 2, 1)
125         sane_time(0, 4, 1440, 1) = (1, datetime.time(4, 2, 1)
126         sane_time(0, 0, 0, -1) = (-1, datetime.time(23, 59, 59)
127     """
128     while second > 59:
129         second -= 60
130         minute += 1
131     while second < 0:
132         second += 60
133         minute -= 1
134     while minute > 59:
135         minute -= 60
136         hour += 1
137     while minute < 0:
138         minute += 60
139         hour -= 1
140     while hour > 23:
141         hour -= 24
142         day += 1
143     while hour < 0:
144         hour += 24
145         day -= 1
146     newTime = (day, datetime.time(hour, minute, second))
147     return newTime
148
149 def seconds(startDate, frequency=1, endDate=None):
150     """Yield a sequence of datetimes, adding 'frequency' seconds each time.
151     
152     For example:
153         seconds(datetime.datetime(2004, 5, 4, 14, 0), 6)
154     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:00:06,
155                          2004-05-04 14:00:12, ...
156     
157     If startDate has no time component (i.e. if it is a datetime.date),
158     then the first yielded time will be midnight (0:00:00) on that date.
159     
160     If endDate has no time component (i.e. if it is a datetime.date),
161     then the last yielded time will be the last valid time before
162     midnight on that date.
163     
164     For example:
165         seconds(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
166     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:00:15,
167                          2004-05-04 00:00:30, ...
168                                           ... 2004-05-05 23:59:15,
169                          2004-05-05 23:59:30, 2004-05-05 23:59:45.
170     """
171     if not hasattr(startDate, u'time'):
172         startDate = datetime.datetime.combine(startDate, datetime.time(0))
173     while (endDate is None) or (startDate <= endDate):
174         yield startDate
175         startDate += datetime.timedelta(seconds=frequency)
176
177 def eachminute(startDate, seconds=0, endDate=None):
178     """Yield the same time for each minute. Defaults to 0 seconds.
179     
180     Yielded values are datetime.datetime objects.
181     For example:
182         eachminute(datetime.date(2004, 5, 4, 23, 55), 15)
183     yields the sequence: 2004-05-04 23:55:15, 2004-05-04 23:56:15,
184                          2004-05-04 23:57:15, ...
185     
186     If startDate has no time component (i.e. if it is a datetime.date),
187     then the first yielded time will be the first valid time after
188     midnight (0:00:00) on that date.
189     
190     If endDate has no time component (i.e. if it is a datetime.date),
191     then the last yielded time will be the last valid time before
192     midnight on that date.
193     """
194     seconds = int(seconds)
195    
196     if hasattr(startDate, u'time'):
197         days, zerotime = sane_time(0, startDate.hour,
198                                    startDate.minute, seconds)
199         if days < 0 or zerotime < startDate.time():
200             days, zerotime = sane_time(0, startDate.hour,
201                                       startDate.minute + 1, seconds)
202     else:
203         days, zerotime = sane_time(0, 0, 0, seconds)
204     startDate = sane_date(startDate.year, startDate.month,
205                           startDate.day + days)
206     startDate = datetime.datetime.combine(startDate, zerotime)
207    
208     while (endDate is None) or (startDate <= endDate):
209         yield startDate
210         startDate += datetime.timedelta(minutes=1)
211
212 def minutes(startDate, frequency=1, endDate=None):
213     """Yield a sequence of datetimes, adding 'frequency' minutes each time.
214     
215     For example:
216         minutes(datetime.datetime(2004, 5, 4, 14), 30)
217     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:30:00,
218                          2004-05-04 15:00:00, ...
219     
220     If startDate has no time component (i.e. if it is a datetime.date),
221     then the first yielded time will be midnight (0:00:00) on that date.
222     
223     If endDate has no time component (i.e. if it is a datetime.date),
224     then the last yielded time will be the last valid time before
225     midnight on that date.
226     
227     For example:
228         minutes(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
229     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:15:00,
230                          2004-05-04 00:30:00, ...
231                                           ... 2004-05-05 23:15:00,
232                          2004-05-05 23:30:00, 2004-05-05 23:45:00.
233     """
234     if not hasattr(startDate, u'time'):
235         startDate = datetime.datetime.combine(startDate, datetime.time(0))
236     while (endDate is None) or (startDate <= endDate):
237         yield startDate
238         startDate += datetime.timedelta(minutes=frequency)
239
240 def eachhour(startDate, minutes=0, seconds=0, endDate=None):
241     """Yield the same time for each hour. Defaults to 00:00.
242     
243     Yielded values are datetime.datetime objects.
244     For example:
245         eachhour(datetime.date(2004, 5, 4, 6), 15)
246     yields the sequence: 2004-05-04 06:15:00, 2004-05-04 07:15:00,
247                          2004-05-04 08:15:00, ...
248     
249     If startDate has no time component (i.e. if it is a datetime.date),
250     then the first yielded time will be the first valid time after
251     midnight (0:00:00) on that date.
252     
253     If endDate has no time component (i.e. if it is a datetime.date),
254     then the last yielded time will be the last valid time before
255     midnight on that date.
256     """
257     minutes = int(minutes)
258     seconds = int(seconds)
259    
260     if hasattr(startDate, u'time'):
261         zerotime = datetime.time(startDate.hour, minutes, seconds)
262         if zerotime < startDate.time():
263             if zerotime.hour < 23:
264                 zerotime = datetime.time(zerotime.hour + 1, minutes, seconds)
265             else:
266                 zerotime = datetime.time(0, minutes, seconds)
267                 startDate = sane_date(startDate.year, startDate.month,
268                                       startDate.day + 1)
269     else:
270         zerotime = datetime.time(0, minutes, seconds)
271     startDate = datetime.datetime.combine(startDate, zerotime)
272    
273     while (endDate is None) or (startDate <= endDate):
274         yield startDate
275         startDate += datetime.timedelta(hours=1)
276
277 def hours(startDate, frequency=1, endDate=None):
278     """Yield a sequence of datetimes, adding 'frequency' hours each time.
279     
280     For example:
281         hours(datetime.datetime(2004, 5, 4, 14), 6)
282     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 20:00:00,
283                          2004-05-05 2:00:00, ...
284     
285     If startDate has no time component (i.e. if it is a datetime.date),
286     then the first yielded time will be midnight (0:00:00) on that date.
287     
288     If endDate has no time component (i.e. if it is a datetime.date),
289     then the last yielded time will be the last valid time before
290     midnight on that date.
291     
292     For example:
293         hours(datetime.datetime(2004, 5, 4), 8, datetime.datetime(2004, 5, 5))
294     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 08:00:00,
295                          2004-05-04 16:00:00, 2004-05-05 00:00:00,
296                          2004-05-05 08:00:00, 2004-05-05 16:00:00.
297     """
298     if not hasattr(startDate, u'time'):
299         startDate = datetime.datetime.combine(startDate, datetime.time(0))
300     while (endDate is None) or (startDate <= endDate):
301         yield startDate
302         startDate += datetime.timedelta(hours=frequency)
303
304 def time_from_str(timeofday):
305     atoms = timeofday.split(u":")
306     def pop_or_zero():
307         try:
308             return int(atoms.pop(0))
309         except TypeError:
310             raise ValueError("The supplied time '%s' could not be parsed."
311                              % timeofday)
312         except IndexError:
313             return 0
314     hour = pop_or_zero()
315     minute = pop_or_zero()
316     second = pop_or_zero()
317     return datetime.time(hour, minute, second)
318
319 def eachday(startDate, timeofday=None, endDate=None):
320     """Yield the same time-of-day for each day. Defaults to midnight.
321     
322     Yielded values are datetime.datetime objects.
323     For example:
324         eachday(datetime.date(2004, 5, 4), datetime.time(14, 3, 0))
325     yields the sequence: 2004-05-04 14:03:00, 2004-05-05 14:03:00,
326                          2004-05-06 14:03:00, ...
327     
328     timeofday may be a datetime.time, as in the above example, or it
329     may be a string, of the form "hour:min:sec". Seconds and minutes
330     may be omitted if their colon ":" separator is also omitted. So
331     the example above could be rewritten:
332         eachday(datetime.date(2004, 5, 4), "14:03")
333     """
334     if timeofday is None:
335         timeofday = datetime.time(0)
336     elif isinstance(timeofday, (str, unicode)):
337         timeofday = time_from_str(timeofday)
338    
339     # If the timeofday is less than the time of startDate,
340     # don't include the startDate in the results.
341     try:
342         if timeofday < startDate.time():
343             startDate = sane_date(startDate.year, startDate.month,
344                                   startDate.day + 1)
345     except AttributeError:
346         # datetime.date has no time() attribute
347         pass
348     startDate = datetime.datetime.combine(startDate, timeofday)
349    
350     while (endDate is None) or (startDate <= endDate):
351         yield startDate
352         startDate += datetime.timedelta(1)
353
354 def days(startDate, frequency=1, endDate=None):
355     """Yield a sequence of dates, adding 'frequency' days each time.
356     
357     For example:
358         days(datetime.date(2004, 5, 4), 7)
359     yields the sequence: 2004-5-4, 2004-5-11, 2004-5-18, ...
360     """
361     while (endDate is None) or (startDate <= endDate):
362         yield startDate
363         startDate += datetime.timedelta(frequency)
364
365 def eachweek(startDate, weekday=0, endDate=None):
366     """Yield the same day-of-the-week for each week. Defaults to Monday.
367     
368     Yielded values are datetime.date objects.
369     
370     Weekday follows the same days of the week as datetime.weekday().
371     For example:
372         mon, tue, wed, thu, fri, sat, sun = range(7)
373         eachweek(datetime.date(2004, 5, 4), thu)
374     yields the sequence: 2004-5-6, 2004-5-13, 2004-5-20, ...
375     
376     If weekday is out of bounds (0-6), it will be brought in bounds.
377     """
378     weekday = int(weekday)
379     offset = (7 + weekday) - startDate.weekday()
380     while offset > 6:
381         offset -= 7
382     while offset < 0:
383         offset += 7
384     startDate += datetime.timedelta(offset)
385     return days(startDate, 7, endDate)
386
387 def weeks(startDate, frequency=1, endDate=None):
388     """Yield a sequence of dates, adding 'frequency' weeks each time.
389     
390     For example:
391         weeks(datetime.date(2004, 5, 4), 2)
392     yields the sequence: 2004-5-4, 2004-5-18, 2004-6-1, ...
393     """
394     while (endDate is None) or (startDate <= endDate):
395         yield startDate
396         startDate += datetime.timedelta(frequency * 7)
397
398 def eachmonth(startDate, day=1, endDate=None):
399     """Yield the same day of each month. Defaults to the first day.
400     
401     Yielded values are datetime.date objects.
402     
403     If day is a positive number, return that date for each month,
404     starting with startDate. For example:
405         eachmonth(datetime.date(2004, 5, 4), 15)
406     yields the sequence: 2004-5-15, 2004-6-15, 2004-7-15, ...
407     
408     If day is zero or negative, return the same date counting
409     backwards from the end of the month. For example:
410         eachmonth(datetime.date(2004, 5, 4), -5)
411     yields the sequence: 2004-5-26, 2004-6-25, 2004-7-26, ...
412     
413     If day specifies a day which does not appear in every month,
414     then the closest valid date within that month will be used instead.
415     For example:
416         eachmonth(datetime.date(2004, 5, 4), 31)
417     yields the sequence: 2004-5-31, 2004-6-30, 2004-7-31, ...
418     
419     If startDate is greater than what would otherwise be the first date
420     in the sequence, that first item is not yielded; instead, the next
421     item becomes the first item yielded.
422     
423     If endDate is less than what would otherwise be the last date in the
424     sequence, that last item is not yielded, and the sequence ends.
425     """
426     day = int(day)
427     fixmonth = (day > 0)
428     index = 0
429     while True:
430         firstDate = sane_date(startDate.year, startDate.month + index, day, fixmonth)
431         if firstDate >= startDate:
432             break
433         index += 1
434     startDate = firstDate
435    
436     while (endDate is None) or (startDate <= endDate):
437         yield startDate
438         startDate = sane_date(startDate.year, startDate.month + 1, day, fixmonth)
439
440 def months(startDate, frequency=1, endDate=None):
441     """Yield a sequence of dates, adding 'frequency' months each time.
442     
443     For example:
444         months(datetime.date(2004, 5, 4), 3)
445     yields the sequence: 2004-5-4, 2004-8-4, 2004-11-4, ...
446     
447     If the specified startDate contains a day which does not appear
448     in every month, then the closest valid date within that month
449     will be used instead.
450     For example:
451         months(datetime.date(2004, 5, 31), 3)
452     yields the sequence: 2004-5-31, 2004-8-31, 2004-11-30, ...
453     
454     If the frequency parameter is negative, the sequence descends.
455     """
456     idealDay = startDate.day
457     while True:
458         if endDate is not None:
459             if frequency < 0:
460                 if startDate < endDate: break
461             else:
462                 if startDate > endDate: break
463        
464         yield startDate
465         startDate = sane_date(startDate.year, startDate.month + frequency,
466                                idealDay, True)
467
468 def eachyear(startDate, month=1, day=1, endDate=None):
469     """Yield the same day of the year for each year. Defaults to 1/1.
470     
471     Yielded values are datetime.date objects.
472     
473     If day and month are positive numbers, return that day/month for each
474     year, starting with startDate. For example:
475         eachyear(datetime.date(2004, 5, 4), 8, 15)
476     yields the sequence: 2004-8-15, 2005-8-15, 2006-8-15, ...
477     
478     If month is zero or negative, return the same date counting months
479     backwards from the end of the year. For example:
480         eachyear(datetime.date(2004, 5, 4), -2, 15)
481     yields the sequence: 2004-10-15, 2005-10-15, 2006-10-15, ...
482     
483     If day is zero or negative, return the same date counting days
484     backwards from the end of the month. For example:
485         eachyear(datetime.date(2004, 5, 4), -2, -1)
486     yields the sequence: 2004-10-30, 2005-10-30, 2006-10-30, ...
487     
488     If day specifies a day which does not appear in the given month,
489     then the closest valid date within that month will be used instead.
490     For example:
491         eachyear(datetime.date(2004, 5, 4), 5, 31)
492     yields the sequence: 2004-5-30, 2005-5-30, 2006-5-30, ...
493     
494     If startDate is greater than what would otherwise be the first date
495     in the sequence, that first item is not yielded; instead, the next
496     item becomes the first item yielded.
497     
498     If endDate is less than what would otherwise be the last date in the
499     sequence, that last item is not yielded, and the sequence ends.
500     """
501     month = int(month)
502     day = int(day)
503    
504     index = 0
505     while True:
506         curDate = sane_date(startDate.year + index, month, day, True)
507         if curDate >= startDate:
508             break
509         index += 1
510    
511     while (endDate is None) or (curDate <= endDate):
512         yield curDate
513         index += 1
514         curDate = sane_date(startDate.year + index, month, day, True)
515
516 def years(startDate, frequency=1, endDate=None):
517     """Yield a sequence of dates, adding 'frequency' years each time.
518     
519     For example:
520         years(datetime.date(2004, 5, 4), 3)
521     yields the sequence: 2004-5-4, 2007-5-4, 2010-5-4, ...
522     
523     If the specified startDate contains a day which does not appear
524     in every year (i.e. leap years), then the closest valid date
525     within that month will be used instead.
526     
527     For example:
528         years(datetime.date(2004, 2, 29), 3)
529     yields the sequence: 2004-2-29, 2007-2-28, 2010-2-28, ...
530     
531     If the frequency parameter is negative, the sequence descends.
532     """
533     idealDay = startDate.day
534     while True:
535         if endDate is not None:
536             if frequency < 0:
537                 if startDate < endDate: break
538             else:
539                 if startDate > endDate: break
540        
541         yield startDate
542         startDate = sane_date(startDate.year + frequency, startDate.month,
543                                idealDay, True)
544
545 def byunits(startDate, whichUnit, frequency=1, endDate=None):
546     """Dispatch to the appropriate unit handler.
547     
548     This really just exists to help out Locale.series()
549     """
550     frequency = int(frequency)
551     unithandler = (seconds, minutes, hours, days, weeks, months, years)
552     return unithandler[whichUnit](startDate, frequency, endDate)
553
554 def singledate(startDate, year, month=1, day=1, endDate=None):
555     """Yield a single datetime.date if y/m/d occurs between start and end."""
556     year = int(year)
557     month = int(month)
558     day = int(day)
559    
560     curDate = sane_date(year, month, day, True)
561     if curDate < startDate:
562         raise StopIteration
563    
564     if (endDate is None) or (curDate <= endDate):
565         yield curDate
566
567
568 class Locale(object):
569     """Language-specific expression matching.
570     
571     To use a language other than English with Recurrence objects,
572     subclass Locale and override the "patterns" dictionary.
573     """
574    
575     patterns = {byunits: [r"([0-9]+) sec(?:ond)?s?",
576                           r"([0-9]+) min(?:ute)?s?",
577                           r"([0-9]+) hours?",
578                           r"([0-9]+) days?",
579                           r"([0-9]+) weeks?",
580                           r"([0-9]+) months?",
581                           r"([0-9]+) years?",
582                           ],
583                 # \S is any non-whitespace character.
584                 eachday: r"([\S]+) (?:every|each) day",
585                 eachweek: [r"mon(?:days?)?", r"tues?(?:days?)?",
586                            r"wed(?:nesdays?)?", r"thurs?(?:days?)?",
587                            r"fri(?:days?)?", r"sat(?:urdays?)?",
588                            r"sun(?:days?)?",
589                            ],
590                 eachmonth: r"(-?\d+) (?:every|each) month",
591                 # Lookbehind for a digit and separator so we don't
592                 # screw up singledate, below.
593                 eachyear: [r"^(dummy entry to line up indexing)$",
594                            r"(?<!\d\d[/ \-])(?:jan(?:uary)?|0?1)[/ \-]([0-9]+)",
595                            r"(?<!\d\d[/ \-])(?:febr?(?:uary)?|0?2)[/ \-]([0-9]+)",
596                            r"(?<!\d\d[/ \-])(?:mar(?:ch)?|0?3)[/ \-]([0-9]+)",
597                            r"(?<!\d\d[/ \-])(?:apr(?:il)?|0?4)[/ \-]([0-9]+)",
598                            r"(?<!\d\d[/ \-])(?:may|0?5)[/ \-]([0-9]+)",
599                            r"(?<!\d\d[/ \-])(?:june?|0?6)[/ \-]([0-9]+)",
600                            r"(?<!\d\d[/ \-])(?:july?|0?7)[/ \-]([0-9]+)",
601                            r"(?<!\d\d[/ \-])(?:aug(?:ust)?|0?8)[/ \-]([0-9]+)",
602                            r"(?<!\d\d[/ \-])(?:sept?(?:ember)?|0?9)[/ \-]([0-9]+)",
603                            r"(?<!\d\d[/ \-])(?:oct(?:ober)?|10)[/ \-]([0-9]+)",
604                            r"(?<!\d\d[/ \-])(?:nov(?:ember)?|11)[/ \-]([0-9]+)",
605                            r"(?<!\d\d[/ \-])(?:dec(?:ember)?|12)[/ \-]([0-9]+)",
606                            ],
607                 # ISO format (relaxed: 1-digit month and day OK,
608                 # slash or space OK)
609                 singledate: r"(\d\d\d\d)[/ \-]([01]?\d)[/ \-]([0123]?\d)",
610                 }
611     regexes = {}
612    
613     def __init__(self):
614         for key, regSet in self.patterns.items():
615             if isinstance(regSet, list):
616                 self.regexes[key] = [re.compile(x, re.IGNORECASE)
617                                      for x in regSet]
618             else:
619                 self.regexes[key] = re.compile(regSet, re.IGNORECASE)
620    
621     def __call__(self, description):
622         for rule, regSet in self.regexes.items():
623             if isinstance(regSet, list):
624                 for index, regex in enumerate(regSet):
625                     matches = regex.match(description)
626                     if matches:
627                         return rule, (index,) + matches.groups()
628             else:
629                 matches = regSet.match(description)
630                 if matches:
631                     return rule, matches.groups()
632        
633         raise ValueError(u"The supplied description ('%s') "
634                          u"could not be parsed." % description)
635
636 localeEnglish = Locale()
637
638
639 class Recurrence(object):
640     """A recurrence pattern and its iterator.
641     
642     The Recurrence class provides natural-language hooks for common recur
643     operations. The "description" parameter should be a set of keywords in
644     a natural language, which is then looked up in self.locale.regexes.
645     
646     Usage:
647     
648     import datetime, recur
649     
650     firstDate = datetime.date(2004, 1, 7)
651     lastDate = datetime.date(2004, 2, 11)
652     for eachDate in recur.Recurrence(firstDate, "Saturday", lastDate):
653         print eachDate
654     
655     2004-01-10
656     2004-01-17
657     2004-01-24
658     2004-01-31
659     2004-02-07
660     """
661    
662     def __init__(self, startDate=None, description="", endDate=None,
663                  locale=localeEnglish):
664         """
665         If startDate is None (not supplied), then it will be set
666         to the current date and time.
667         
668         Leading and trailing whitespace will be stripped from the
669         description parameter.
670         """
671         if startDate is None:
672             startDate = datetime.datetime.now()
673         description = description.strip()
674        
675         self.startDate = startDate
676         self.description = description
677         self.endDate = endDate
678         self.locale = locale
679         self.function, args = locale(description)
680         self.args = (startDate,) + args + (endDate,)
681         # Form an initial generator, if for no other reason than to test args early.
682         self.reset()
683    
684     def reset(self):
685         try:
686             self.generator = self.function(*self.args)
687         except TypeError, x:
688             x.args += self.args
689             raise
690    
691     def __iter__(self):
692         self.reset()
693         return self
694    
695     def next(self):
696         return self.generator.next()
697
698
699 class Worker(object):
700     """Perform work on a schedule.
701     
702     You must override work(), which is called at each interval.
703     
704     paused: a boolean flag indicating whether or not the Worker's run()
705         method should be executed at each interval. Notice that, even if
706         paused is True, recurring Workers will continue to schedule new
707         threads--they simply won't do anything at run() time.
708     
709     terminated: a boolean flag indicating whether or not the Worker should
710         continue to cycle. If terminated is True, recurring Workers will
711         not schedule new threads.
712     """
713    
714     def __init__(self, recurrence):
715         if isinstance(recurrence, basestring):
716             if recurrence:
717                 recurrence = Recurrence(None, recurrence)
718             else:
719                 recurrence = None
720         self.recurrence = recurrence
721        
722         self.createdate = datetime.datetime.now()
723         self.lastrun = None
724         self.nextrun = None
725         self.paused = False
726         self.terminated = False
727    
728     def motivate(self):
729         """Start a new immediate or recurring thread for work."""
730         self.nextrun = None
731         if not self.terminated:
732             if self.recurrence:
733                 # Start a recurring, timed thread.
734                 now = datetime.datetime.now()
735                 while True:
736                     try:
737                         next = self.recurrence.next()
738                     except StopIteration:
739                         # The recurrence series was exhausted.
740                         return
741                     diff = next - now
742                     diff = (diff.days * 86400) + diff.seconds
743                     if diff >= 0:
744                         self.nextrun = next
745                         break
746                 iv = diff
747                 func = self._cycle
748             else:
749                 # Start a single, non-recurring thread.
750                 iv = 0
751                 func = self.run
752             self.curthread = threading.Timer(iv, func)
753             self.curthread.start()
754    
755     def _cycle(self):
756         """Run the worker on a schedule."""
757         self.motivate()
758         self.run()
759    
760     def run(self):
761         """Prepare for work."""
762         if not self.paused and not self.terminated:
763             self.work()
764             self.lastrun = datetime.datetime.now()
765    
766     def stop(self):
767         """Stop work."""
768         self.terminated = True
769         self.curthread.cancel()
770    
771     def work(self):
772         raise NotImplementedError
773
Note: See TracBrowser for help on using the browser.