Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

root/tags/1.4.0/recur.py

Revision 165 (checked in by fumanchu, 3 years ago)

Now distributing recur module with dejavu.

  • 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 orginal author is Robert Brewer, Amor Ministries.
8
9 THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
10 OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
11 MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
12 ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
13 RESULTING FROM THE USE, MODIFICATION, OR
14 REDISTRIBUTION OF THIS SOFTWARE.
15
16 Western-language descriptions of recurrence tend to fall into
17 two distinct types. In order to provide some mnemonic consistency,
18 the base functions are named differently according to these types.
19 However, despite the differing names, every function yields
20 datetime.date or datetime.datetime objects.
21
22 First, there are the declarations which define a unit of time,
23 and then count successive "leaps" of those units. For example,
24 the declaration, "every 4 days," uses a day as the unit, and adds
25 4 to produce each value in the series. The functions which provide
26 these series are named according to the whole unit, in the plural.
27 Examples:
28     "Every 4 days" becomes: days(start, 4, [end])
29     "Every 2 weeks" becomes: weeks(start, 2, [end])
30     "Every 6 hours" becomes: hours(start, 6, [end])
31
32 Second, there are the declarations which define a unit of time,
33 and then count by subdivisions of that unit. For example, the
34 declaration, "the ninth day of each month," uses a month as the
35 whole units and a day as the subdivision. The functions which
36 provide these series are named according to the whole unit,
37 in the singular, prefixed by "each".
38 Examples:
39     "The ninth [day] of each month" becomes: eachmonth(start, 9, [end])
40     "The penultimate [day] of each month" becomes:
41         eachmonth(start, -1, [end])
42     "Every Thursday" becomes "The 3rd [day] of each week" [since
43         datetime.weekday() returns Thursday as the value 3]
44         which becomes: eachweek(start, 3, [end])
45     "08:30:00 on each day" becomes:
46         eachday(start, datetime.time(8, 30), [end])
47 Notice that, in almost every case, the subdivision is understood to be
48 the "next smallest component". In the example above, one might just as
49 well have written, "the ninth of each month," and been understood,
50 since months are "composed of" days (not weeks!). Therefore, our
51 functions do not incorporate this "smaller unit" in the function name.
52 """
53
54 import datetime
55 import re
56 import threading
57
58
59 def sane_date(year, month, day, fixMonth=False):
60     """Return a valid datetime.date even if parameters are out of bounds.
61     
62     If the month param is out of bounds, both it and the year will
63     be modified. If negative, the year will be decremented.
64     
65     If fixMonth is False, and the day param is out of bounds, both the
66     day param and the month will be modified.
67     
68     If fixMonth is True, and the day param is out of bounds, the month
69     will not change, and the day will be set to the appropriate boundary.
70     The month may still, however, modify the year.
71     
72     Examples:
73         sane_date(2003, 2, 1) = datetime.date(2003, 2, 1)
74         sane_date(2003, -10, 13) = datetime.date(2002, 2, 13)
75         sane_date(2003, 12, -5) = datetime.date(2003, 11, 25)
76         sane_date(2003, 1, 35, True) = datetime.date(2003, 1, 31)
77     """
78     while month > 12:
79         month -= 12
80         year += 1
81     while month < 1:
82         month += 12
83         year -= 1
84     if fixMonth:
85         if day < 1:
86             newDate = datetime.date(year, month, 1)
87         else:
88             while True:
89                 try:
90                     newDate = datetime.date(year, month, day)
91                 except ValueError:
92                     day -= 1
93                     if day < 1:
94                         raise ValueError("A valid day for month: %s in "
95                                          "year: %s could not be found",
96                                          (month, year))
97                 else:
98                     break
99     else:
100         if day < 1:
101             # Count backward from the end of the current month.
102             firstOfMonth = sane_date(year, month + 1, 1)
103         else:
104             # Count forward from the first of the current month.
105             firstOfMonth = datetime.date(year, month, 1)
106         newDate = (firstOfMonth + datetime.timedelta(day - 1))
107     return newDate
108
109 def sane_time(day, hour, minute, second):
110     """Return a valid (day, datetime.time) even if parameters are out of bounds.
111     
112     If the hour param is out of bounds, both it and the day will
113     be modified. If negative, the day will be decremented.
114     
115     If the minute param is out of bounds, both it and the hour will
116     be modified. If negative, the hour will be decremented.
117     
118     If the second param is out of bounds, both it and the minute will
119     be modified. If negative, the minute will be decremented.
120     
121     Examples:
122         sane_time(0, 4, 2, 1) = (0, datetime.time(4, 2, 1)
123         sane_time(0, 25, 2, 1) = (1, datetime.time(1, 2, 1)
124         sane_time(0, 4, 1440, 1) = (1, datetime.time(4, 2, 1)
125         sane_time(0, 0, 0, -1) = (-1, datetime.time(23, 59, 59)
126     """
127     while second > 59:
128         second -= 60
129         minute += 1
130     while second < 0:
131         second += 60
132         minute -= 1
133     while minute > 59:
134         minute -= 60
135         hour += 1
136     while minute < 0:
137         minute += 60
138         hour -= 1
139     while hour > 23:
140         hour -= 24
141         day += 1
142     while hour < 0:
143         hour += 24
144         day -= 1
145     newTime = (day, datetime.time(hour, minute, second))
146     return newTime
147
148 def seconds(startDate, frequency=1, endDate=None):
149     """Yield a sequence of datetimes, adding 'frequency' seconds each time.
150     
151     For example:
152         seconds(datetime.datetime(2004, 5, 4, 14, 0), 6)
153     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:00:06,
154                          2004-05-04 14:00:12, ...
155     
156     If startDate has no time component (i.e. if it is a datetime.date),
157     then the first yielded time will be midnight (0:00:00) on that date.
158     
159     If endDate has no time component (i.e. if it is a datetime.date),
160     then the last yielded time will be the last valid time before
161     midnight on that date.
162     
163     For example:
164         seconds(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
165     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:00:15,
166                          2004-05-04 00:00:30, ...
167                                           ... 2004-05-05 23:59:15,
168                          2004-05-05 23:59:30, 2004-05-05 23:59:45.
169     """
170     if not hasattr(startDate, u'time'):
171         startDate = datetime.datetime.combine(startDate, datetime.time(0))
172     while (endDate is None) or (startDate <= endDate):
173         yield startDate
174         startDate += datetime.timedelta(seconds=frequency)
175
176 def eachminute(startDate, seconds=0, endDate=None):
177     """Yield the same time for each minute. Defaults to 0 seconds.
178     
179     Yielded values are datetime.datetime objects.
180     For example:
181         eachminute(datetime.date(2004, 5, 4, 23, 55), 15)
182     yields the sequence: 2004-05-04 23:55:15, 2004-05-04 23:56:15,
183                          2004-05-04 23:57:15, ...
184     
185     If startDate has no time component (i.e. if it is a datetime.date),
186     then the first yielded time will be the first valid time after
187     midnight (0:00:00) on that date.
188     
189     If endDate has no time component (i.e. if it is a datetime.date),
190     then the last yielded time will be the last valid time before
191     midnight on that date.
192     """
193     seconds = int(seconds)
194    
195     if hasattr(startDate, u'time'):
196         days, zerotime = sane_time(0, startDate.hour,
197                                    startDate.minute, seconds)
198         if days < 0 or zerotime < startDate.time():
199             days, zerotime = sane_time(0, startDate.hour,
200                                       startDate.minute + 1, seconds)
201     else:
202         days, zerotime = sane_time(0, 0, 0, seconds)
203     startDate = sane_date(startDate.year, startDate.month,
204                           startDate.day + days)
205     startDate = datetime.datetime.combine(startDate, zerotime)
206    
207     while (endDate is None) or (startDate <= endDate):
208         yield startDate
209         startDate += datetime.timedelta(minutes=1)
210
211 def minutes(startDate, frequency=1, endDate=None):
212     """Yield a sequence of datetimes, adding 'frequency' minutes each time.
213     
214     For example:
215         minutes(datetime.datetime(2004, 5, 4, 14), 30)
216     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:30:00,
217                          2004-05-04 15:00:00, ...
218     
219     If startDate has no time component (i.e. if it is a datetime.date),
220     then the first yielded time will be midnight (0:00:00) on that date.
221     
222     If endDate has no time component (i.e. if it is a datetime.date),
223     then the last yielded time will be the last valid time before
224     midnight on that date.
225     
226     For example:
227         minutes(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
228     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:15:00,
229                          2004-05-04 00:30:00, ...
230                                           ... 2004-05-05 23:15:00,
231                          2004-05-05 23:30:00, 2004-05-05 23:45:00.
232     """
233     if not hasattr(startDate, u'time'):
234         startDate = datetime.datetime.combine(startDate, datetime.time(0))
235     while (endDate is None) or (startDate <= endDate):
236         yield startDate
237         startDate += datetime.timedelta(minutes=frequency)
238
239 def eachhour(startDate, minutes=0, seconds=0, endDate=None):
240     """Yield the same time for each hour. Defaults to 00:00.
241     
242     Yielded values are datetime.datetime objects.
243     For example:
244         eachhour(datetime.date(2004, 5, 4, 6), 15)
245     yields the sequence: 2004-05-04 06:15:00, 2004-05-04 07:15:00,
246                          2004-05-04 08:15:00, ...
247     
248     If startDate has no time component (i.e. if it is a datetime.date),
249     then the first yielded time will be the first valid time after
250     midnight (0:00:00) on that date.
251     
252     If endDate has no time component (i.e. if it is a datetime.date),
253     then the last yielded time will be the last valid time before
254     midnight on that date.
255     """
256     minutes = int(minutes)
257     seconds = int(seconds)
258    
259     if hasattr(startDate, u'time'):
260         zerotime = datetime.time(startDate.hour, minutes, seconds)
261         if zerotime < startDate.time():
262             if zerotime.hour < 23:
263                 zerotime = datetime.time(zerotime.hour + 1, minutes, seconds)
264             else:
265                 zerotime = datetime.time(0, minutes, seconds)
266                 startDate = sane_date(startDate.year, startDate.month,
267                                       startDate.day + 1)
268     else:
269         zerotime = datetime.time(0, minutes, seconds)
270     startDate = datetime.datetime.combine(startDate, zerotime)
271    
272     while (endDate is None) or (startDate <= endDate):
273         yield startDate
274         startDate += datetime.timedelta(hours=1)
275
276 def hours(startDate, frequency=1, endDate=None):
277     """Yield a sequence of datetimes, adding 'frequency' hours each time.
278     
279     For example:
280         hours(datetime.datetime(2004, 5, 4, 14), 6)
281     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 20:00:00,
282                          2004-05-05 2:00:00, ...
283     
284     If startDate has no time component (i.e. if it is a datetime.date),
285     then the first yielded time will be midnight (0:00:00) on that date.
286     
287     If endDate has no time component (i.e. if it is a datetime.date),
288     then the last yielded time will be the last valid time before
289     midnight on that date.
290     
291     For example:
292         hours(datetime.datetime(2004, 5, 4), 8, datetime.datetime(2004, 5, 5))
293     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 08:00:00,
294                          2004-05-04 16:00:00, 2004-05-05 00:00:00,
295                          2004-05-05 08:00:00, 2004-05-05 16:00:00.
296     """
297     if not hasattr(startDate, u'time'):
298         startDate = datetime.datetime.combine(startDate, datetime.time(0))
299     while (endDate is None) or (startDate <= endDate):
300         yield startDate
301         startDate += datetime.timedelta(hours=frequency)
302
303 def time_from_str(timeofday):
304     atoms = timeofday.split(u":")
305     def pop_or_zero():
306         try:
307             return int(atoms.pop(0))
308         except TypeError:
309             raise ValueError("The supplied time '%s' could not be parsed."
310                              % timeofday)
311         except IndexError:
312             return 0
313     hour = pop_or_zero()
314     minute = pop_or_zero()
315     second = pop_or_zero()
316     return datetime.time(hour, minute, second)
317
318 def eachday(startDate, timeofday=None, endDate=None):
319     """Yield the same time-of-day for each day. Defaults to midnight.
320     
321     Yielded values are datetime.datetime objects.
322     For example:
323         eachday(datetime.date(2004, 5, 4), datetime.time(14, 3, 0))
324     yields the sequence: 2004-05-04 14:03:00, 2004-05-05 14:03:00,
325                          2004-05-06 14:03:00, ...
326     
327     timeofday may be a datetime.time, as in the above example, or it
328     may be a string, of the form "hour:min:sec". Seconds and minutes
329     may be omitted if their colon ":" separator is also omitted. So
330     the example above could be rewritten:
331         eachday(datetime.date(2004, 5, 4), "14:03")
332     """
333     if timeofday is None:
334         timeofday = datetime.time(0)
335     elif isinstance(timeofday, (str, unicode)):
336         timeofday = time_from_str(timeofday)
337    
338     # If the timeofday is less than the time of startDate,
339     # don't include the startDate in the results.
340     try:
341         if timeofday < startDate.time():
342             startDate = sane_date(startDate.year, startDate.month,
343                                   startDate.day + 1)
344     except AttributeError:
345         # datetime.date has no time() attribute
346         pass
347     startDate = datetime.datetime.combine(startDate, timeofday)
348    
349     while (endDate is None) or (startDate <= endDate):
350         yield startDate
351         startDate += datetime.timedelta(1)
352
353 def days(startDate, frequency=1, endDate=None):
354     """Yield a sequence of dates, adding 'frequency' days each time.
355     
356     For example:
357         days(datetime.date(2004, 5, 4), 7)
358     yields the sequence: 2004-5-4, 2004-5-11, 2004-5-18, ...
359     """
360     while (endDate is None) or (startDate <= endDate):
361         yield startDate
362         startDate += datetime.timedelta(frequency)
363
364 def eachweek(startDate, weekday=0, endDate=None):
365     """Yield the same day-of-the-week for each week. Defaults to Monday.
366     
367     Yielded values are datetime.date objects.
368     
369     Weekday follows the same days of the week as datetime.weekday().
370     For example:
371         mon, tue, wed, thu, fri, sat, sun = range(7)
372         eachweek(datetime.date(2004, 5, 4), thu)
373     yields the sequence: 2004-5-6, 2004-5-13, 2004-5-20, ...
374     
375     If weekday is out of bounds (0-6), it will be brought in bounds.
376     """
377     weekday = int(weekday)
378     offset = (7 + weekday) - startDate.weekday()
379     while offset > 6:
380         offset -= 7
381     while offset < 0:
382         offset += 7
383     startDate += datetime.timedelta(offset)
384     return days(startDate, 7, endDate)
385
386 def weeks(startDate, frequency=1, endDate=None):
387     """Yield a sequence of dates, adding 'frequency' weeks each time.
388     
389     For example:
390         weeks(datetime.date(2004, 5, 4), 2)
391     yields the sequence: 2004-5-4, 2004-5-18, 2004-6-1, ...
392     """
393     while (endDate is None) or (startDate <= endDate):
394         yield startDate
395         startDate += datetime.timedelta(frequency * 7)
396
397 def eachmonth(startDate, day=1, endDate=None):
398     """Yield the same day of each month. Defaults to the first day.
399     
400     Yielded values are datetime.date objects.
401     
402     If day is a positive number, return that date for each month,
403     starting with startDate. For example:
404         eachmonth(datetime.date(2004, 5, 4), 15)
405     yields the sequence: 2004-5-15, 2004-6-15, 2004-7-15, ...
406     
407     If day is zero or negative, return the same date counting
408     backwards from the end of the month. For example:
409         eachmonth(datetime.date(2004, 5, 4), -5)
410     yields the sequence: 2004-5-26, 2004-6-25, 2004-7-26, ...
411     
412     If day specifies a day which does not appear in every month,
413     then the closest valid date within that month will be used instead.
414     For example:
415         eachmonth(datetime.date(2004, 5, 4), 31)
416     yields the sequence: 2004-5-31, 2004-6-30, 2004-7-31, ...
417     
418     If startDate is greater than what would otherwise be the first date
419     in the sequence, that first item is not yielded; instead, the next
420     item becomes the first item yielded.
421     
422     If endDate is less than what would otherwise be the last date in the
423     sequence, that last item is not yielded, and the sequence ends.
424     """
425     day = int(day)
426     fixmonth = (day > 0)
427     index = 0
428     while True:
429         firstDate = sane_date(startDate.year, startDate.month + index, day, fixmonth)
430         if firstDate >= startDate:
431             break
432         index += 1
433     startDate = firstDate
434    
435     while (endDate is None) or (startDate <= endDate):
436         yield startDate
437         startDate = sane_date(startDate.year, startDate.month + 1, day, fixmonth)
438
439 def months(startDate, frequency=1, endDate=None):
440     """Yield a sequence of dates, adding 'frequency' months each time.
441     
442     For example:
443         months(datetime.date(2004, 5, 4), 3)
444     yields the sequence: 2004-5-4, 2004-8-4, 2004-11-4, ...
445     
446     If the specified startDate contains a day which does not appear
447     in every month, then the closest valid date within that month
448     will be used instead.
449     For example:
450         months(datetime.date(2004, 5, 31), 3)
451     yields the sequence: 2004-5-31, 2004-8-31, 2004-11-30, ...
452     
453     If the frequency parameter is negative, the sequence descends.
454     """
455     idealDay = startDate.day
456     while True:
457         if endDate is not None:
458             if frequency < 0:
459                 if startDate < endDate: break
460             else:
461                 if startDate > endDate: break
462        
463         yield startDate
464         startDate = sane_date(startDate.year, startDate.month + frequency,
465                                idealDay, True)
466
467 def eachyear(startDate, month=1, day=1, endDate=None):
468     """Yield the same day of the year for each year. Defaults to 1/1.
469     
470     Yielded values are datetime.date objects.
471     
472     If day and month are positive numbers, return that day/month for each
473     year, starting with startDate. For example:
474         eachyear(datetime.date(2004, 5, 4), 8, 15)
475     yields the sequence: 2004-8-15, 2005-8-15, 2006-8-15, ...
476     
477     If month is zero or negative, return the same date counting months
478     backwards from the end of the year. For example:
479         eachyear(datetime.date(2004, 5, 4), -2, 15)
480     yields the sequence: 2004-10-15, 2005-10-15, 2006-10-15, ...
481     
482     If day is zero or negative, return the same date counting days
483     backwards from the end of the month. For example:
484         eachyear(datetime.date(2004, 5, 4), -2, -1)
485     yields the sequence: 2004-10-30, 2005-10-30, 2006-10-30, ...
486     
487     If day specifies a day which does not appear in the given month,
488     then the closest valid date within that month will be used instead.
489     For example:
490         eachyear(datetime.date(2004, 5, 4), 5, 31)
491     yields the sequence: 2004-5-30, 2005-5-30, 2006-5-30, ...
492     
493     If startDate is greater than what would otherwise be the first date
494     in the sequence, that first item is not yielded; instead, the next
495     item becomes the first item yielded.
496     
497     If endDate is less than what would otherwise be the last date in the
498     sequence, that last item is not yielded, and the sequence ends.
499     """
500     month = int(month)
501     day = int(day)
502    
503     index = 0
504     while True:
505         curDate = sane_date(startDate.year + index, month, day, True)
506         if curDate >= startDate:
507             break
508         index += 1
509    
510     while (endDate is None) or (curDate <= endDate):
511         yield curDate
512         index += 1
513         curDate = sane_date(startDate.year + index, month, day, True)
514
515 def years(startDate, frequency=1, endDate=None):
516     """Yield a sequence of dates, adding 'frequency' years each time.
517     
518     For example:
519         years(datetime.date(2004, 5, 4), 3)
520     yields the sequence: 2004-5-4, 2007-5-4, 2010-5-4, ...
521     
522     If the specified startDate contains a day which does not appear
523     in every year (i.e. leap years), then the closest valid date
524     within that month will be used instead.
525     
526     For example:
527         years(datetime.date(2004, 2, 29), 3)
528     yields the sequence: 2004-2-29, 2007-2-28, 2010-2-28, ...
529     
530     If the frequency parameter is negative, the sequence descends.
531     """
532     idealDay = startDate.day
533     while True:
534         if endDate is not None:
535             if frequency < 0:
536                 if startDate < endDate: break
537             else:
538                 if startDate > endDate: break
539        
540         yield startDate
541         startDate = sane_date(startDate.year + frequency, startDate.month,
542                                idealDay, True)
543
544 def byunits(startDate, whichUnit, frequency=1, endDate=None):
545     """Dispatch to the appropriate unit handler.
546     
547     This really just exists to help out Locale.series()
548     """
549     frequency = int(frequency)
550     unithandler = (seconds, minutes, hours, days, weeks, months, years)
551     return unithandler[whichUnit](startDate, frequency, endDate)
552
553 def singledate(startDate, year, month=1, day=1, endDate=None):
554     """Yield a single datetime.date if y/m/d occurs between start and end."""
555     year = int(year)
556     month = int(month)
557     day = int(day)
558    
559     curDate = sane_date(year, month, day, True)
560     if curDate < startDate:
561         raise StopIteration
562    
563     if (endDate is None) or (curDate <= endDate):
564         yield curDate
565
566
567 class Locale(object):
568     """Language-specific expression matching.
569     
570     To use a language other than English with Recurrence objects,
571     subclass Locale and override the "patterns" dictionary.
572     """
573    
574     patterns = {byunits: [r"([0-9]+) sec(?:ond)?s?",
575                           r"([0-9]+) min(?:ute)?s?",
576                           r"([0-9]+) hours?",