jpayne@69
|
1 # -*- coding: utf-8 -*-
|
jpayne@69
|
2 """
|
jpayne@69
|
3 The rrule module offers a small, complete, and very fast, implementation of
|
jpayne@69
|
4 the recurrence rules documented in the
|
jpayne@69
|
5 `iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
jpayne@69
|
6 including support for caching of results.
|
jpayne@69
|
7 """
|
jpayne@69
|
8 import calendar
|
jpayne@69
|
9 import datetime
|
jpayne@69
|
10 import heapq
|
jpayne@69
|
11 import itertools
|
jpayne@69
|
12 import re
|
jpayne@69
|
13 import sys
|
jpayne@69
|
14 from functools import wraps
|
jpayne@69
|
15 # For warning about deprecation of until and count
|
jpayne@69
|
16 from warnings import warn
|
jpayne@69
|
17
|
jpayne@69
|
18 from six import advance_iterator, integer_types
|
jpayne@69
|
19
|
jpayne@69
|
20 from six.moves import _thread, range
|
jpayne@69
|
21
|
jpayne@69
|
22 from ._common import weekday as weekdaybase
|
jpayne@69
|
23
|
jpayne@69
|
24 try:
|
jpayne@69
|
25 from math import gcd
|
jpayne@69
|
26 except ImportError:
|
jpayne@69
|
27 from fractions import gcd
|
jpayne@69
|
28
|
jpayne@69
|
29 __all__ = ["rrule", "rruleset", "rrulestr",
|
jpayne@69
|
30 "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
|
jpayne@69
|
31 "HOURLY", "MINUTELY", "SECONDLY",
|
jpayne@69
|
32 "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
jpayne@69
|
33
|
jpayne@69
|
34 # Every mask is 7 days longer to handle cross-year weekly periods.
|
jpayne@69
|
35 M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
|
jpayne@69
|
36 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
|
jpayne@69
|
37 M365MASK = list(M366MASK)
|
jpayne@69
|
38 M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
|
jpayne@69
|
39 MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
jpayne@69
|
40 MDAY365MASK = list(MDAY366MASK)
|
jpayne@69
|
41 M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
|
jpayne@69
|
42 NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
jpayne@69
|
43 NMDAY365MASK = list(NMDAY366MASK)
|
jpayne@69
|
44 M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
|
jpayne@69
|
45 M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
|
jpayne@69
|
46 WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
|
jpayne@69
|
47 del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
|
jpayne@69
|
48 MDAY365MASK = tuple(MDAY365MASK)
|
jpayne@69
|
49 M365MASK = tuple(M365MASK)
|
jpayne@69
|
50
|
jpayne@69
|
51 FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
|
jpayne@69
|
52
|
jpayne@69
|
53 (YEARLY,
|
jpayne@69
|
54 MONTHLY,
|
jpayne@69
|
55 WEEKLY,
|
jpayne@69
|
56 DAILY,
|
jpayne@69
|
57 HOURLY,
|
jpayne@69
|
58 MINUTELY,
|
jpayne@69
|
59 SECONDLY) = list(range(7))
|
jpayne@69
|
60
|
jpayne@69
|
61 # Imported on demand.
|
jpayne@69
|
62 easter = None
|
jpayne@69
|
63 parser = None
|
jpayne@69
|
64
|
jpayne@69
|
65
|
jpayne@69
|
66 class weekday(weekdaybase):
|
jpayne@69
|
67 """
|
jpayne@69
|
68 This version of weekday does not allow n = 0.
|
jpayne@69
|
69 """
|
jpayne@69
|
70 def __init__(self, wkday, n=None):
|
jpayne@69
|
71 if n == 0:
|
jpayne@69
|
72 raise ValueError("Can't create weekday with n==0")
|
jpayne@69
|
73
|
jpayne@69
|
74 super(weekday, self).__init__(wkday, n)
|
jpayne@69
|
75
|
jpayne@69
|
76
|
jpayne@69
|
77 MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
jpayne@69
|
78
|
jpayne@69
|
79
|
jpayne@69
|
80 def _invalidates_cache(f):
|
jpayne@69
|
81 """
|
jpayne@69
|
82 Decorator for rruleset methods which may invalidate the
|
jpayne@69
|
83 cached length.
|
jpayne@69
|
84 """
|
jpayne@69
|
85 @wraps(f)
|
jpayne@69
|
86 def inner_func(self, *args, **kwargs):
|
jpayne@69
|
87 rv = f(self, *args, **kwargs)
|
jpayne@69
|
88 self._invalidate_cache()
|
jpayne@69
|
89 return rv
|
jpayne@69
|
90
|
jpayne@69
|
91 return inner_func
|
jpayne@69
|
92
|
jpayne@69
|
93
|
jpayne@69
|
94 class rrulebase(object):
|
jpayne@69
|
95 def __init__(self, cache=False):
|
jpayne@69
|
96 if cache:
|
jpayne@69
|
97 self._cache = []
|
jpayne@69
|
98 self._cache_lock = _thread.allocate_lock()
|
jpayne@69
|
99 self._invalidate_cache()
|
jpayne@69
|
100 else:
|
jpayne@69
|
101 self._cache = None
|
jpayne@69
|
102 self._cache_complete = False
|
jpayne@69
|
103 self._len = None
|
jpayne@69
|
104
|
jpayne@69
|
105 def __iter__(self):
|
jpayne@69
|
106 if self._cache_complete:
|
jpayne@69
|
107 return iter(self._cache)
|
jpayne@69
|
108 elif self._cache is None:
|
jpayne@69
|
109 return self._iter()
|
jpayne@69
|
110 else:
|
jpayne@69
|
111 return self._iter_cached()
|
jpayne@69
|
112
|
jpayne@69
|
113 def _invalidate_cache(self):
|
jpayne@69
|
114 if self._cache is not None:
|
jpayne@69
|
115 self._cache = []
|
jpayne@69
|
116 self._cache_complete = False
|
jpayne@69
|
117 self._cache_gen = self._iter()
|
jpayne@69
|
118
|
jpayne@69
|
119 if self._cache_lock.locked():
|
jpayne@69
|
120 self._cache_lock.release()
|
jpayne@69
|
121
|
jpayne@69
|
122 self._len = None
|
jpayne@69
|
123
|
jpayne@69
|
124 def _iter_cached(self):
|
jpayne@69
|
125 i = 0
|
jpayne@69
|
126 gen = self._cache_gen
|
jpayne@69
|
127 cache = self._cache
|
jpayne@69
|
128 acquire = self._cache_lock.acquire
|
jpayne@69
|
129 release = self._cache_lock.release
|
jpayne@69
|
130 while gen:
|
jpayne@69
|
131 if i == len(cache):
|
jpayne@69
|
132 acquire()
|
jpayne@69
|
133 if self._cache_complete:
|
jpayne@69
|
134 break
|
jpayne@69
|
135 try:
|
jpayne@69
|
136 for j in range(10):
|
jpayne@69
|
137 cache.append(advance_iterator(gen))
|
jpayne@69
|
138 except StopIteration:
|
jpayne@69
|
139 self._cache_gen = gen = None
|
jpayne@69
|
140 self._cache_complete = True
|
jpayne@69
|
141 break
|
jpayne@69
|
142 release()
|
jpayne@69
|
143 yield cache[i]
|
jpayne@69
|
144 i += 1
|
jpayne@69
|
145 while i < self._len:
|
jpayne@69
|
146 yield cache[i]
|
jpayne@69
|
147 i += 1
|
jpayne@69
|
148
|
jpayne@69
|
149 def __getitem__(self, item):
|
jpayne@69
|
150 if self._cache_complete:
|
jpayne@69
|
151 return self._cache[item]
|
jpayne@69
|
152 elif isinstance(item, slice):
|
jpayne@69
|
153 if item.step and item.step < 0:
|
jpayne@69
|
154 return list(iter(self))[item]
|
jpayne@69
|
155 else:
|
jpayne@69
|
156 return list(itertools.islice(self,
|
jpayne@69
|
157 item.start or 0,
|
jpayne@69
|
158 item.stop or sys.maxsize,
|
jpayne@69
|
159 item.step or 1))
|
jpayne@69
|
160 elif item >= 0:
|
jpayne@69
|
161 gen = iter(self)
|
jpayne@69
|
162 try:
|
jpayne@69
|
163 for i in range(item+1):
|
jpayne@69
|
164 res = advance_iterator(gen)
|
jpayne@69
|
165 except StopIteration:
|
jpayne@69
|
166 raise IndexError
|
jpayne@69
|
167 return res
|
jpayne@69
|
168 else:
|
jpayne@69
|
169 return list(iter(self))[item]
|
jpayne@69
|
170
|
jpayne@69
|
171 def __contains__(self, item):
|
jpayne@69
|
172 if self._cache_complete:
|
jpayne@69
|
173 return item in self._cache
|
jpayne@69
|
174 else:
|
jpayne@69
|
175 for i in self:
|
jpayne@69
|
176 if i == item:
|
jpayne@69
|
177 return True
|
jpayne@69
|
178 elif i > item:
|
jpayne@69
|
179 return False
|
jpayne@69
|
180 return False
|
jpayne@69
|
181
|
jpayne@69
|
182 # __len__() introduces a large performance penalty.
|
jpayne@69
|
183 def count(self):
|
jpayne@69
|
184 """ Returns the number of recurrences in this set. It will have go
|
jpayne@69
|
185 through the whole recurrence, if this hasn't been done before. """
|
jpayne@69
|
186 if self._len is None:
|
jpayne@69
|
187 for x in self:
|
jpayne@69
|
188 pass
|
jpayne@69
|
189 return self._len
|
jpayne@69
|
190
|
jpayne@69
|
191 def before(self, dt, inc=False):
|
jpayne@69
|
192 """ Returns the last recurrence before the given datetime instance. The
|
jpayne@69
|
193 inc keyword defines what happens if dt is an occurrence. With
|
jpayne@69
|
194 inc=True, if dt itself is an occurrence, it will be returned. """
|
jpayne@69
|
195 if self._cache_complete:
|
jpayne@69
|
196 gen = self._cache
|
jpayne@69
|
197 else:
|
jpayne@69
|
198 gen = self
|
jpayne@69
|
199 last = None
|
jpayne@69
|
200 if inc:
|
jpayne@69
|
201 for i in gen:
|
jpayne@69
|
202 if i > dt:
|
jpayne@69
|
203 break
|
jpayne@69
|
204 last = i
|
jpayne@69
|
205 else:
|
jpayne@69
|
206 for i in gen:
|
jpayne@69
|
207 if i >= dt:
|
jpayne@69
|
208 break
|
jpayne@69
|
209 last = i
|
jpayne@69
|
210 return last
|
jpayne@69
|
211
|
jpayne@69
|
212 def after(self, dt, inc=False):
|
jpayne@69
|
213 """ Returns the first recurrence after the given datetime instance. The
|
jpayne@69
|
214 inc keyword defines what happens if dt is an occurrence. With
|
jpayne@69
|
215 inc=True, if dt itself is an occurrence, it will be returned. """
|
jpayne@69
|
216 if self._cache_complete:
|
jpayne@69
|
217 gen = self._cache
|
jpayne@69
|
218 else:
|
jpayne@69
|
219 gen = self
|
jpayne@69
|
220 if inc:
|
jpayne@69
|
221 for i in gen:
|
jpayne@69
|
222 if i >= dt:
|
jpayne@69
|
223 return i
|
jpayne@69
|
224 else:
|
jpayne@69
|
225 for i in gen:
|
jpayne@69
|
226 if i > dt:
|
jpayne@69
|
227 return i
|
jpayne@69
|
228 return None
|
jpayne@69
|
229
|
jpayne@69
|
230 def xafter(self, dt, count=None, inc=False):
|
jpayne@69
|
231 """
|
jpayne@69
|
232 Generator which yields up to `count` recurrences after the given
|
jpayne@69
|
233 datetime instance, equivalent to `after`.
|
jpayne@69
|
234
|
jpayne@69
|
235 :param dt:
|
jpayne@69
|
236 The datetime at which to start generating recurrences.
|
jpayne@69
|
237
|
jpayne@69
|
238 :param count:
|
jpayne@69
|
239 The maximum number of recurrences to generate. If `None` (default),
|
jpayne@69
|
240 dates are generated until the recurrence rule is exhausted.
|
jpayne@69
|
241
|
jpayne@69
|
242 :param inc:
|
jpayne@69
|
243 If `dt` is an instance of the rule and `inc` is `True`, it is
|
jpayne@69
|
244 included in the output.
|
jpayne@69
|
245
|
jpayne@69
|
246 :yields: Yields a sequence of `datetime` objects.
|
jpayne@69
|
247 """
|
jpayne@69
|
248
|
jpayne@69
|
249 if self._cache_complete:
|
jpayne@69
|
250 gen = self._cache
|
jpayne@69
|
251 else:
|
jpayne@69
|
252 gen = self
|
jpayne@69
|
253
|
jpayne@69
|
254 # Select the comparison function
|
jpayne@69
|
255 if inc:
|
jpayne@69
|
256 comp = lambda dc, dtc: dc >= dtc
|
jpayne@69
|
257 else:
|
jpayne@69
|
258 comp = lambda dc, dtc: dc > dtc
|
jpayne@69
|
259
|
jpayne@69
|
260 # Generate dates
|
jpayne@69
|
261 n = 0
|
jpayne@69
|
262 for d in gen:
|
jpayne@69
|
263 if comp(d, dt):
|
jpayne@69
|
264 if count is not None:
|
jpayne@69
|
265 n += 1
|
jpayne@69
|
266 if n > count:
|
jpayne@69
|
267 break
|
jpayne@69
|
268
|
jpayne@69
|
269 yield d
|
jpayne@69
|
270
|
jpayne@69
|
271 def between(self, after, before, inc=False, count=1):
|
jpayne@69
|
272 """ Returns all the occurrences of the rrule between after and before.
|
jpayne@69
|
273 The inc keyword defines what happens if after and/or before are
|
jpayne@69
|
274 themselves occurrences. With inc=True, they will be included in the
|
jpayne@69
|
275 list, if they are found in the recurrence set. """
|
jpayne@69
|
276 if self._cache_complete:
|
jpayne@69
|
277 gen = self._cache
|
jpayne@69
|
278 else:
|
jpayne@69
|
279 gen = self
|
jpayne@69
|
280 started = False
|
jpayne@69
|
281 l = []
|
jpayne@69
|
282 if inc:
|
jpayne@69
|
283 for i in gen:
|
jpayne@69
|
284 if i > before:
|
jpayne@69
|
285 break
|
jpayne@69
|
286 elif not started:
|
jpayne@69
|
287 if i >= after:
|
jpayne@69
|
288 started = True
|
jpayne@69
|
289 l.append(i)
|
jpayne@69
|
290 else:
|
jpayne@69
|
291 l.append(i)
|
jpayne@69
|
292 else:
|
jpayne@69
|
293 for i in gen:
|
jpayne@69
|
294 if i >= before:
|
jpayne@69
|
295 break
|
jpayne@69
|
296 elif not started:
|
jpayne@69
|
297 if i > after:
|
jpayne@69
|
298 started = True
|
jpayne@69
|
299 l.append(i)
|
jpayne@69
|
300 else:
|
jpayne@69
|
301 l.append(i)
|
jpayne@69
|
302 return l
|
jpayne@69
|
303
|
jpayne@69
|
304
|
jpayne@69
|
305 class rrule(rrulebase):
|
jpayne@69
|
306 """
|
jpayne@69
|
307 That's the base of the rrule operation. It accepts all the keywords
|
jpayne@69
|
308 defined in the RFC as its constructor parameters (except byday,
|
jpayne@69
|
309 which was renamed to byweekday) and more. The constructor prototype is::
|
jpayne@69
|
310
|
jpayne@69
|
311 rrule(freq)
|
jpayne@69
|
312
|
jpayne@69
|
313 Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
|
jpayne@69
|
314 or SECONDLY.
|
jpayne@69
|
315
|
jpayne@69
|
316 .. note::
|
jpayne@69
|
317 Per RFC section 3.3.10, recurrence instances falling on invalid dates
|
jpayne@69
|
318 and times are ignored rather than coerced:
|
jpayne@69
|
319
|
jpayne@69
|
320 Recurrence rules may generate recurrence instances with an invalid
|
jpayne@69
|
321 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
|
jpayne@69
|
322 on a day where the local time is moved forward by an hour at 1:00
|
jpayne@69
|
323 AM). Such recurrence instances MUST be ignored and MUST NOT be
|
jpayne@69
|
324 counted as part of the recurrence set.
|
jpayne@69
|
325
|
jpayne@69
|
326 This can lead to possibly surprising behavior when, for example, the
|
jpayne@69
|
327 start date occurs at the end of the month:
|
jpayne@69
|
328
|
jpayne@69
|
329 >>> from dateutil.rrule import rrule, MONTHLY
|
jpayne@69
|
330 >>> from datetime import datetime
|
jpayne@69
|
331 >>> start_date = datetime(2014, 12, 31)
|
jpayne@69
|
332 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
|
jpayne@69
|
333 ... # doctest: +NORMALIZE_WHITESPACE
|
jpayne@69
|
334 [datetime.datetime(2014, 12, 31, 0, 0),
|
jpayne@69
|
335 datetime.datetime(2015, 1, 31, 0, 0),
|
jpayne@69
|
336 datetime.datetime(2015, 3, 31, 0, 0),
|
jpayne@69
|
337 datetime.datetime(2015, 5, 31, 0, 0)]
|
jpayne@69
|
338
|
jpayne@69
|
339 Additionally, it supports the following keyword arguments:
|
jpayne@69
|
340
|
jpayne@69
|
341 :param dtstart:
|
jpayne@69
|
342 The recurrence start. Besides being the base for the recurrence,
|
jpayne@69
|
343 missing parameters in the final recurrence instances will also be
|
jpayne@69
|
344 extracted from this date. If not given, datetime.now() will be used
|
jpayne@69
|
345 instead.
|
jpayne@69
|
346 :param interval:
|
jpayne@69
|
347 The interval between each freq iteration. For example, when using
|
jpayne@69
|
348 YEARLY, an interval of 2 means once every two years, but with HOURLY,
|
jpayne@69
|
349 it means once every two hours. The default interval is 1.
|
jpayne@69
|
350 :param wkst:
|
jpayne@69
|
351 The week start day. Must be one of the MO, TU, WE constants, or an
|
jpayne@69
|
352 integer, specifying the first day of the week. This will affect
|
jpayne@69
|
353 recurrences based on weekly periods. The default week start is got
|
jpayne@69
|
354 from calendar.firstweekday(), and may be modified by
|
jpayne@69
|
355 calendar.setfirstweekday().
|
jpayne@69
|
356 :param count:
|
jpayne@69
|
357 If given, this determines how many occurrences will be generated.
|
jpayne@69
|
358
|
jpayne@69
|
359 .. note::
|
jpayne@69
|
360 As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
jpayne@69
|
361 with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
jpayne@69
|
362 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
jpayne@69
|
363 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
jpayne@69
|
364 **must not** occur in the same call to ``rrule``.
|
jpayne@69
|
365 :param until:
|
jpayne@69
|
366 If given, this must be a datetime instance specifying the upper-bound
|
jpayne@69
|
367 limit of the recurrence. The last recurrence in the rule is the greatest
|
jpayne@69
|
368 datetime that is less than or equal to the value specified in the
|
jpayne@69
|
369 ``until`` parameter.
|
jpayne@69
|
370
|
jpayne@69
|
371 .. note::
|
jpayne@69
|
372 As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
jpayne@69
|
373 with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
jpayne@69
|
374 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
jpayne@69
|
375 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
jpayne@69
|
376 **must not** occur in the same call to ``rrule``.
|
jpayne@69
|
377 :param bysetpos:
|
jpayne@69
|
378 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
379 positive or negative. Each given integer will specify an occurrence
|
jpayne@69
|
380 number, corresponding to the nth occurrence of the rule inside the
|
jpayne@69
|
381 frequency period. For example, a bysetpos of -1 if combined with a
|
jpayne@69
|
382 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
|
jpayne@69
|
383 result in the last work day of every month.
|
jpayne@69
|
384 :param bymonth:
|
jpayne@69
|
385 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
386 meaning the months to apply the recurrence to.
|
jpayne@69
|
387 :param bymonthday:
|
jpayne@69
|
388 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
389 meaning the month days to apply the recurrence to.
|
jpayne@69
|
390 :param byyearday:
|
jpayne@69
|
391 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
392 meaning the year days to apply the recurrence to.
|
jpayne@69
|
393 :param byeaster:
|
jpayne@69
|
394 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
395 positive or negative. Each integer will define an offset from the
|
jpayne@69
|
396 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
|
jpayne@69
|
397 Sunday itself. This is an extension to the RFC specification.
|
jpayne@69
|
398 :param byweekno:
|
jpayne@69
|
399 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
400 meaning the week numbers to apply the recurrence to. Week numbers
|
jpayne@69
|
401 have the meaning described in ISO8601, that is, the first week of
|
jpayne@69
|
402 the year is that containing at least four days of the new year.
|
jpayne@69
|
403 :param byweekday:
|
jpayne@69
|
404 If given, it must be either an integer (0 == MO), a sequence of
|
jpayne@69
|
405 integers, one of the weekday constants (MO, TU, etc), or a sequence
|
jpayne@69
|
406 of these constants. When given, these variables will define the
|
jpayne@69
|
407 weekdays where the recurrence will be applied. It's also possible to
|
jpayne@69
|
408 use an argument n for the weekday instances, which will mean the nth
|
jpayne@69
|
409 occurrence of this weekday in the period. For example, with MONTHLY,
|
jpayne@69
|
410 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
|
jpayne@69
|
411 first friday of the month where the recurrence happens. Notice that in
|
jpayne@69
|
412 the RFC documentation, this is specified as BYDAY, but was renamed to
|
jpayne@69
|
413 avoid the ambiguity of that keyword.
|
jpayne@69
|
414 :param byhour:
|
jpayne@69
|
415 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
416 meaning the hours to apply the recurrence to.
|
jpayne@69
|
417 :param byminute:
|
jpayne@69
|
418 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
419 meaning the minutes to apply the recurrence to.
|
jpayne@69
|
420 :param bysecond:
|
jpayne@69
|
421 If given, it must be either an integer, or a sequence of integers,
|
jpayne@69
|
422 meaning the seconds to apply the recurrence to.
|
jpayne@69
|
423 :param cache:
|
jpayne@69
|
424 If given, it must be a boolean value specifying to enable or disable
|
jpayne@69
|
425 caching of results. If you will use the same rrule instance multiple
|
jpayne@69
|
426 times, enabling caching will improve the performance considerably.
|
jpayne@69
|
427 """
|
jpayne@69
|
428 def __init__(self, freq, dtstart=None,
|
jpayne@69
|
429 interval=1, wkst=None, count=None, until=None, bysetpos=None,
|
jpayne@69
|
430 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
|
jpayne@69
|
431 byweekno=None, byweekday=None,
|
jpayne@69
|
432 byhour=None, byminute=None, bysecond=None,
|
jpayne@69
|
433 cache=False):
|
jpayne@69
|
434 super(rrule, self).__init__(cache)
|
jpayne@69
|
435 global easter
|
jpayne@69
|
436 if not dtstart:
|
jpayne@69
|
437 if until and until.tzinfo:
|
jpayne@69
|
438 dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
|
jpayne@69
|
439 else:
|
jpayne@69
|
440 dtstart = datetime.datetime.now().replace(microsecond=0)
|
jpayne@69
|
441 elif not isinstance(dtstart, datetime.datetime):
|
jpayne@69
|
442 dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
|
jpayne@69
|
443 else:
|
jpayne@69
|
444 dtstart = dtstart.replace(microsecond=0)
|
jpayne@69
|
445 self._dtstart = dtstart
|
jpayne@69
|
446 self._tzinfo = dtstart.tzinfo
|
jpayne@69
|
447 self._freq = freq
|
jpayne@69
|
448 self._interval = interval
|
jpayne@69
|
449 self._count = count
|
jpayne@69
|
450
|
jpayne@69
|
451 # Cache the original byxxx rules, if they are provided, as the _byxxx
|
jpayne@69
|
452 # attributes do not necessarily map to the inputs, and this can be
|
jpayne@69
|
453 # a problem in generating the strings. Only store things if they've
|
jpayne@69
|
454 # been supplied (the string retrieval will just use .get())
|
jpayne@69
|
455 self._original_rule = {}
|
jpayne@69
|
456
|
jpayne@69
|
457 if until and not isinstance(until, datetime.datetime):
|
jpayne@69
|
458 until = datetime.datetime.fromordinal(until.toordinal())
|
jpayne@69
|
459 self._until = until
|
jpayne@69
|
460
|
jpayne@69
|
461 if self._dtstart and self._until:
|
jpayne@69
|
462 if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
|
jpayne@69
|
463 # According to RFC5545 Section 3.3.10:
|
jpayne@69
|
464 # https://tools.ietf.org/html/rfc5545#section-3.3.10
|
jpayne@69
|
465 #
|
jpayne@69
|
466 # > If the "DTSTART" property is specified as a date with UTC
|
jpayne@69
|
467 # > time or a date with local time and time zone reference,
|
jpayne@69
|
468 # > then the UNTIL rule part MUST be specified as a date with
|
jpayne@69
|
469 # > UTC time.
|
jpayne@69
|
470 raise ValueError(
|
jpayne@69
|
471 'RRULE UNTIL values must be specified in UTC when DTSTART '
|
jpayne@69
|
472 'is timezone-aware'
|
jpayne@69
|
473 )
|
jpayne@69
|
474
|
jpayne@69
|
475 if count is not None and until:
|
jpayne@69
|
476 warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
jpayne@69
|
477 " and has been deprecated in dateutil. Future versions will "
|
jpayne@69
|
478 "raise an error.", DeprecationWarning)
|
jpayne@69
|
479
|
jpayne@69
|
480 if wkst is None:
|
jpayne@69
|
481 self._wkst = calendar.firstweekday()
|
jpayne@69
|
482 elif isinstance(wkst, integer_types):
|
jpayne@69
|
483 self._wkst = wkst
|
jpayne@69
|
484 else:
|
jpayne@69
|
485 self._wkst = wkst.weekday
|
jpayne@69
|
486
|
jpayne@69
|
487 if bysetpos is None:
|
jpayne@69
|
488 self._bysetpos = None
|
jpayne@69
|
489 elif isinstance(bysetpos, integer_types):
|
jpayne@69
|
490 if bysetpos == 0 or not (-366 <= bysetpos <= 366):
|
jpayne@69
|
491 raise ValueError("bysetpos must be between 1 and 366, "
|
jpayne@69
|
492 "or between -366 and -1")
|
jpayne@69
|
493 self._bysetpos = (bysetpos,)
|
jpayne@69
|
494 else:
|
jpayne@69
|
495 self._bysetpos = tuple(bysetpos)
|
jpayne@69
|
496 for pos in self._bysetpos:
|
jpayne@69
|
497 if pos == 0 or not (-366 <= pos <= 366):
|
jpayne@69
|
498 raise ValueError("bysetpos must be between 1 and 366, "
|
jpayne@69
|
499 "or between -366 and -1")
|
jpayne@69
|
500
|
jpayne@69
|
501 if self._bysetpos:
|
jpayne@69
|
502 self._original_rule['bysetpos'] = self._bysetpos
|
jpayne@69
|
503
|
jpayne@69
|
504 if (byweekno is None and byyearday is None and bymonthday is None and
|
jpayne@69
|
505 byweekday is None and byeaster is None):
|
jpayne@69
|
506 if freq == YEARLY:
|
jpayne@69
|
507 if bymonth is None:
|
jpayne@69
|
508 bymonth = dtstart.month
|
jpayne@69
|
509 self._original_rule['bymonth'] = None
|
jpayne@69
|
510 bymonthday = dtstart.day
|
jpayne@69
|
511 self._original_rule['bymonthday'] = None
|
jpayne@69
|
512 elif freq == MONTHLY:
|
jpayne@69
|
513 bymonthday = dtstart.day
|
jpayne@69
|
514 self._original_rule['bymonthday'] = None
|
jpayne@69
|
515 elif freq == WEEKLY:
|
jpayne@69
|
516 byweekday = dtstart.weekday()
|
jpayne@69
|
517 self._original_rule['byweekday'] = None
|
jpayne@69
|
518
|
jpayne@69
|
519 # bymonth
|
jpayne@69
|
520 if bymonth is None:
|
jpayne@69
|
521 self._bymonth = None
|
jpayne@69
|
522 else:
|
jpayne@69
|
523 if isinstance(bymonth, integer_types):
|
jpayne@69
|
524 bymonth = (bymonth,)
|
jpayne@69
|
525
|
jpayne@69
|
526 self._bymonth = tuple(sorted(set(bymonth)))
|
jpayne@69
|
527
|
jpayne@69
|
528 if 'bymonth' not in self._original_rule:
|
jpayne@69
|
529 self._original_rule['bymonth'] = self._bymonth
|
jpayne@69
|
530
|
jpayne@69
|
531 # byyearday
|
jpayne@69
|
532 if byyearday is None:
|
jpayne@69
|
533 self._byyearday = None
|
jpayne@69
|
534 else:
|
jpayne@69
|
535 if isinstance(byyearday, integer_types):
|
jpayne@69
|
536 byyearday = (byyearday,)
|
jpayne@69
|
537
|
jpayne@69
|
538 self._byyearday = tuple(sorted(set(byyearday)))
|
jpayne@69
|
539 self._original_rule['byyearday'] = self._byyearday
|
jpayne@69
|
540
|
jpayne@69
|
541 # byeaster
|
jpayne@69
|
542 if byeaster is not None:
|
jpayne@69
|
543 if not easter:
|
jpayne@69
|
544 from dateutil import easter
|
jpayne@69
|
545 if isinstance(byeaster, integer_types):
|
jpayne@69
|
546 self._byeaster = (byeaster,)
|
jpayne@69
|
547 else:
|
jpayne@69
|
548 self._byeaster = tuple(sorted(byeaster))
|
jpayne@69
|
549
|
jpayne@69
|
550 self._original_rule['byeaster'] = self._byeaster
|
jpayne@69
|
551 else:
|
jpayne@69
|
552 self._byeaster = None
|
jpayne@69
|
553
|
jpayne@69
|
554 # bymonthday
|
jpayne@69
|
555 if bymonthday is None:
|
jpayne@69
|
556 self._bymonthday = ()
|
jpayne@69
|
557 self._bynmonthday = ()
|
jpayne@69
|
558 else:
|
jpayne@69
|
559 if isinstance(bymonthday, integer_types):
|
jpayne@69
|
560 bymonthday = (bymonthday,)
|
jpayne@69
|
561
|
jpayne@69
|
562 bymonthday = set(bymonthday) # Ensure it's unique
|
jpayne@69
|
563
|
jpayne@69
|
564 self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
|
jpayne@69
|
565 self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
|
jpayne@69
|
566
|
jpayne@69
|
567 # Storing positive numbers first, then negative numbers
|
jpayne@69
|
568 if 'bymonthday' not in self._original_rule:
|
jpayne@69
|
569 self._original_rule['bymonthday'] = tuple(
|
jpayne@69
|
570 itertools.chain(self._bymonthday, self._bynmonthday))
|
jpayne@69
|
571
|
jpayne@69
|
572 # byweekno
|
jpayne@69
|
573 if byweekno is None:
|
jpayne@69
|
574 self._byweekno = None
|
jpayne@69
|
575 else:
|
jpayne@69
|
576 if isinstance(byweekno, integer_types):
|
jpayne@69
|
577 byweekno = (byweekno,)
|
jpayne@69
|
578
|
jpayne@69
|
579 self._byweekno = tuple(sorted(set(byweekno)))
|
jpayne@69
|
580
|
jpayne@69
|
581 self._original_rule['byweekno'] = self._byweekno
|
jpayne@69
|
582
|
jpayne@69
|
583 # byweekday / bynweekday
|
jpayne@69
|
584 if byweekday is None:
|
jpayne@69
|
585 self._byweekday = None
|
jpayne@69
|
586 self._bynweekday = None
|
jpayne@69
|
587 else:
|
jpayne@69
|
588 # If it's one of the valid non-sequence types, convert to a
|
jpayne@69
|
589 # single-element sequence before the iterator that builds the
|
jpayne@69
|
590 # byweekday set.
|
jpayne@69
|
591 if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
|
jpayne@69
|
592 byweekday = (byweekday,)
|
jpayne@69
|
593
|
jpayne@69
|
594 self._byweekday = set()
|
jpayne@69
|
595 self._bynweekday = set()
|
jpayne@69
|
596 for wday in byweekday:
|
jpayne@69
|
597 if isinstance(wday, integer_types):
|
jpayne@69
|
598 self._byweekday.add(wday)
|
jpayne@69
|
599 elif not wday.n or freq > MONTHLY:
|
jpayne@69
|
600 self._byweekday.add(wday.weekday)
|
jpayne@69
|
601 else:
|
jpayne@69
|
602 self._bynweekday.add((wday.weekday, wday.n))
|
jpayne@69
|
603
|
jpayne@69
|
604 if not self._byweekday:
|
jpayne@69
|
605 self._byweekday = None
|
jpayne@69
|
606 elif not self._bynweekday:
|
jpayne@69
|
607 self._bynweekday = None
|
jpayne@69
|
608
|
jpayne@69
|
609 if self._byweekday is not None:
|
jpayne@69
|
610 self._byweekday = tuple(sorted(self._byweekday))
|
jpayne@69
|
611 orig_byweekday = [weekday(x) for x in self._byweekday]
|
jpayne@69
|
612 else:
|
jpayne@69
|
613 orig_byweekday = ()
|
jpayne@69
|
614
|
jpayne@69
|
615 if self._bynweekday is not None:
|
jpayne@69
|
616 self._bynweekday = tuple(sorted(self._bynweekday))
|
jpayne@69
|
617 orig_bynweekday = [weekday(*x) for x in self._bynweekday]
|
jpayne@69
|
618 else:
|
jpayne@69
|
619 orig_bynweekday = ()
|
jpayne@69
|
620
|
jpayne@69
|
621 if 'byweekday' not in self._original_rule:
|
jpayne@69
|
622 self._original_rule['byweekday'] = tuple(itertools.chain(
|
jpayne@69
|
623 orig_byweekday, orig_bynweekday))
|
jpayne@69
|
624
|
jpayne@69
|
625 # byhour
|
jpayne@69
|
626 if byhour is None:
|
jpayne@69
|
627 if freq < HOURLY:
|
jpayne@69
|
628 self._byhour = {dtstart.hour}
|
jpayne@69
|
629 else:
|
jpayne@69
|
630 self._byhour = None
|
jpayne@69
|
631 else:
|
jpayne@69
|
632 if isinstance(byhour, integer_types):
|
jpayne@69
|
633 byhour = (byhour,)
|
jpayne@69
|
634
|
jpayne@69
|
635 if freq == HOURLY:
|
jpayne@69
|
636 self._byhour = self.__construct_byset(start=dtstart.hour,
|
jpayne@69
|
637 byxxx=byhour,
|
jpayne@69
|
638 base=24)
|
jpayne@69
|
639 else:
|
jpayne@69
|
640 self._byhour = set(byhour)
|
jpayne@69
|
641
|
jpayne@69
|
642 self._byhour = tuple(sorted(self._byhour))
|
jpayne@69
|
643 self._original_rule['byhour'] = self._byhour
|
jpayne@69
|
644
|
jpayne@69
|
645 # byminute
|
jpayne@69
|
646 if byminute is None:
|
jpayne@69
|
647 if freq < MINUTELY:
|
jpayne@69
|
648 self._byminute = {dtstart.minute}
|
jpayne@69
|
649 else:
|
jpayne@69
|
650 self._byminute = None
|
jpayne@69
|
651 else:
|
jpayne@69
|
652 if isinstance(byminute, integer_types):
|
jpayne@69
|
653 byminute = (byminute,)
|
jpayne@69
|
654
|
jpayne@69
|
655 if freq == MINUTELY:
|
jpayne@69
|
656 self._byminute = self.__construct_byset(start=dtstart.minute,
|
jpayne@69
|
657 byxxx=byminute,
|
jpayne@69
|
658 base=60)
|
jpayne@69
|
659 else:
|
jpayne@69
|
660 self._byminute = set(byminute)
|
jpayne@69
|
661
|
jpayne@69
|
662 self._byminute = tuple(sorted(self._byminute))
|
jpayne@69
|
663 self._original_rule['byminute'] = self._byminute
|
jpayne@69
|
664
|
jpayne@69
|
665 # bysecond
|
jpayne@69
|
666 if bysecond is None:
|
jpayne@69
|
667 if freq < SECONDLY:
|
jpayne@69
|
668 self._bysecond = ((dtstart.second,))
|
jpayne@69
|
669 else:
|
jpayne@69
|
670 self._bysecond = None
|
jpayne@69
|
671 else:
|
jpayne@69
|
672 if isinstance(bysecond, integer_types):
|
jpayne@69
|
673 bysecond = (bysecond,)
|
jpayne@69
|
674
|
jpayne@69
|
675 self._bysecond = set(bysecond)
|
jpayne@69
|
676
|
jpayne@69
|
677 if freq == SECONDLY:
|
jpayne@69
|
678 self._bysecond = self.__construct_byset(start=dtstart.second,
|
jpayne@69
|
679 byxxx=bysecond,
|
jpayne@69
|
680 base=60)
|
jpayne@69
|
681 else:
|
jpayne@69
|
682 self._bysecond = set(bysecond)
|
jpayne@69
|
683
|
jpayne@69
|
684 self._bysecond = tuple(sorted(self._bysecond))
|
jpayne@69
|
685 self._original_rule['bysecond'] = self._bysecond
|
jpayne@69
|
686
|
jpayne@69
|
687 if self._freq >= HOURLY:
|
jpayne@69
|
688 self._timeset = None
|
jpayne@69
|
689 else:
|
jpayne@69
|
690 self._timeset = []
|
jpayne@69
|
691 for hour in self._byhour:
|
jpayne@69
|
692 for minute in self._byminute:
|
jpayne@69
|
693 for second in self._bysecond:
|
jpayne@69
|
694 self._timeset.append(
|
jpayne@69
|
695 datetime.time(hour, minute, second,
|
jpayne@69
|
696 tzinfo=self._tzinfo))
|
jpayne@69
|
697 self._timeset.sort()
|
jpayne@69
|
698 self._timeset = tuple(self._timeset)
|
jpayne@69
|
699
|
jpayne@69
|
700 def __str__(self):
|
jpayne@69
|
701 """
|
jpayne@69
|
702 Output a string that would generate this RRULE if passed to rrulestr.
|
jpayne@69
|
703 This is mostly compatible with RFC5545, except for the
|
jpayne@69
|
704 dateutil-specific extension BYEASTER.
|
jpayne@69
|
705 """
|
jpayne@69
|
706
|
jpayne@69
|
707 output = []
|
jpayne@69
|
708 h, m, s = [None] * 3
|
jpayne@69
|
709 if self._dtstart:
|
jpayne@69
|
710 output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
|
jpayne@69
|
711 h, m, s = self._dtstart.timetuple()[3:6]
|
jpayne@69
|
712
|
jpayne@69
|
713 parts = ['FREQ=' + FREQNAMES[self._freq]]
|
jpayne@69
|
714 if self._interval != 1:
|
jpayne@69
|
715 parts.append('INTERVAL=' + str(self._interval))
|
jpayne@69
|
716
|
jpayne@69
|
717 if self._wkst:
|
jpayne@69
|
718 parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
|
jpayne@69
|
719
|
jpayne@69
|
720 if self._count is not None:
|
jpayne@69
|
721 parts.append('COUNT=' + str(self._count))
|
jpayne@69
|
722
|
jpayne@69
|
723 if self._until:
|
jpayne@69
|
724 parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
|
jpayne@69
|
725
|
jpayne@69
|
726 if self._original_rule.get('byweekday') is not None:
|
jpayne@69
|
727 # The str() method on weekday objects doesn't generate
|
jpayne@69
|
728 # RFC5545-compliant strings, so we should modify that.
|
jpayne@69
|
729 original_rule = dict(self._original_rule)
|
jpayne@69
|
730 wday_strings = []
|
jpayne@69
|
731 for wday in original_rule['byweekday']:
|
jpayne@69
|
732 if wday.n:
|
jpayne@69
|
733 wday_strings.append('{n:+d}{wday}'.format(
|
jpayne@69
|
734 n=wday.n,
|
jpayne@69
|
735 wday=repr(wday)[0:2]))
|
jpayne@69
|
736 else:
|
jpayne@69
|
737 wday_strings.append(repr(wday))
|
jpayne@69
|
738
|
jpayne@69
|
739 original_rule['byweekday'] = wday_strings
|
jpayne@69
|
740 else:
|
jpayne@69
|
741 original_rule = self._original_rule
|
jpayne@69
|
742
|
jpayne@69
|
743 partfmt = '{name}={vals}'
|
jpayne@69
|
744 for name, key in [('BYSETPOS', 'bysetpos'),
|
jpayne@69
|
745 ('BYMONTH', 'bymonth'),
|
jpayne@69
|
746 ('BYMONTHDAY', 'bymonthday'),
|
jpayne@69
|
747 ('BYYEARDAY', 'byyearday'),
|
jpayne@69
|
748 ('BYWEEKNO', 'byweekno'),
|
jpayne@69
|
749 ('BYDAY', 'byweekday'),
|
jpayne@69
|
750 ('BYHOUR', 'byhour'),
|
jpayne@69
|
751 ('BYMINUTE', 'byminute'),
|
jpayne@69
|
752 ('BYSECOND', 'bysecond'),
|
jpayne@69
|
753 ('BYEASTER', 'byeaster')]:
|
jpayne@69
|
754 value = original_rule.get(key)
|
jpayne@69
|
755 if value:
|
jpayne@69
|
756 parts.append(partfmt.format(name=name, vals=(','.join(str(v)
|
jpayne@69
|
757 for v in value))))
|
jpayne@69
|
758
|
jpayne@69
|
759 output.append('RRULE:' + ';'.join(parts))
|
jpayne@69
|
760 return '\n'.join(output)
|
jpayne@69
|
761
|
jpayne@69
|
762 def replace(self, **kwargs):
|
jpayne@69
|
763 """Return new rrule with same attributes except for those attributes given new
|
jpayne@69
|
764 values by whichever keyword arguments are specified."""
|
jpayne@69
|
765 new_kwargs = {"interval": self._interval,
|
jpayne@69
|
766 "count": self._count,
|
jpayne@69
|
767 "dtstart": self._dtstart,
|
jpayne@69
|
768 "freq": self._freq,
|
jpayne@69
|
769 "until": self._until,
|
jpayne@69
|
770 "wkst": self._wkst,
|
jpayne@69
|
771 "cache": False if self._cache is None else True }
|
jpayne@69
|
772 new_kwargs.update(self._original_rule)
|
jpayne@69
|
773 new_kwargs.update(kwargs)
|
jpayne@69
|
774 return rrule(**new_kwargs)
|
jpayne@69
|
775
|
jpayne@69
|
776 def _iter(self):
|
jpayne@69
|
777 year, month, day, hour, minute, second, weekday, yearday, _ = \
|
jpayne@69
|
778 self._dtstart.timetuple()
|
jpayne@69
|
779
|
jpayne@69
|
780 # Some local variables to speed things up a bit
|
jpayne@69
|
781 freq = self._freq
|
jpayne@69
|
782 interval = self._interval
|
jpayne@69
|
783 wkst = self._wkst
|
jpayne@69
|
784 until = self._until
|
jpayne@69
|
785 bymonth = self._bymonth
|
jpayne@69
|
786 byweekno = self._byweekno
|
jpayne@69
|
787 byyearday = self._byyearday
|
jpayne@69
|
788 byweekday = self._byweekday
|
jpayne@69
|
789 byeaster = self._byeaster
|
jpayne@69
|
790 bymonthday = self._bymonthday
|
jpayne@69
|
791 bynmonthday = self._bynmonthday
|
jpayne@69
|
792 bysetpos = self._bysetpos
|
jpayne@69
|
793 byhour = self._byhour
|
jpayne@69
|
794 byminute = self._byminute
|
jpayne@69
|
795 bysecond = self._bysecond
|
jpayne@69
|
796
|
jpayne@69
|
797 ii = _iterinfo(self)
|
jpayne@69
|
798 ii.rebuild(year, month)
|
jpayne@69
|
799
|
jpayne@69
|
800 getdayset = {YEARLY: ii.ydayset,
|
jpayne@69
|
801 MONTHLY: ii.mdayset,
|
jpayne@69
|
802 WEEKLY: ii.wdayset,
|
jpayne@69
|
803 DAILY: ii.ddayset,
|
jpayne@69
|
804 HOURLY: ii.ddayset,
|
jpayne@69
|
805 MINUTELY: ii.ddayset,
|
jpayne@69
|
806 SECONDLY: ii.ddayset}[freq]
|
jpayne@69
|
807
|
jpayne@69
|
808 if freq < HOURLY:
|
jpayne@69
|
809 timeset = self._timeset
|
jpayne@69
|
810 else:
|
jpayne@69
|
811 gettimeset = {HOURLY: ii.htimeset,
|
jpayne@69
|
812 MINUTELY: ii.mtimeset,
|
jpayne@69
|
813 SECONDLY: ii.stimeset}[freq]
|
jpayne@69
|
814 if ((freq >= HOURLY and
|
jpayne@69
|
815 self._byhour and hour not in self._byhour) or
|
jpayne@69
|
816 (freq >= MINUTELY and
|
jpayne@69
|
817 self._byminute and minute not in self._byminute) or
|
jpayne@69
|
818 (freq >= SECONDLY and
|
jpayne@69
|
819 self._bysecond and second not in self._bysecond)):
|
jpayne@69
|
820 timeset = ()
|
jpayne@69
|
821 else:
|
jpayne@69
|
822 timeset = gettimeset(hour, minute, second)
|
jpayne@69
|
823
|
jpayne@69
|
824 total = 0
|
jpayne@69
|
825 count = self._count
|
jpayne@69
|
826 while True:
|
jpayne@69
|
827 # Get dayset with the right frequency
|
jpayne@69
|
828 dayset, start, end = getdayset(year, month, day)
|
jpayne@69
|
829
|
jpayne@69
|
830 # Do the "hard" work ;-)
|
jpayne@69
|
831 filtered = False
|
jpayne@69
|
832 for i in dayset[start:end]:
|
jpayne@69
|
833 if ((bymonth and ii.mmask[i] not in bymonth) or
|
jpayne@69
|
834 (byweekno and not ii.wnomask[i]) or
|
jpayne@69
|
835 (byweekday and ii.wdaymask[i] not in byweekday) or
|
jpayne@69
|
836 (ii.nwdaymask and not ii.nwdaymask[i]) or
|
jpayne@69
|
837 (byeaster and not ii.eastermask[i]) or
|
jpayne@69
|
838 ((bymonthday or bynmonthday) and
|
jpayne@69
|
839 ii.mdaymask[i] not in bymonthday and
|
jpayne@69
|
840 ii.nmdaymask[i] not in bynmonthday) or
|
jpayne@69
|
841 (byyearday and
|
jpayne@69
|
842 ((i < ii.yearlen and i+1 not in byyearday and
|
jpayne@69
|
843 -ii.yearlen+i not in byyearday) or
|
jpayne@69
|
844 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
|
jpayne@69
|
845 -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
|
jpayne@69
|
846 dayset[i] = None
|
jpayne@69
|
847 filtered = True
|
jpayne@69
|
848
|
jpayne@69
|
849 # Output results
|
jpayne@69
|
850 if bysetpos and timeset:
|
jpayne@69
|
851 poslist = []
|
jpayne@69
|
852 for pos in bysetpos:
|
jpayne@69
|
853 if pos < 0:
|
jpayne@69
|
854 daypos, timepos = divmod(pos, len(timeset))
|
jpayne@69
|
855 else:
|
jpayne@69
|
856 daypos, timepos = divmod(pos-1, len(timeset))
|
jpayne@69
|
857 try:
|
jpayne@69
|
858 i = [x for x in dayset[start:end]
|
jpayne@69
|
859 if x is not None][daypos]
|
jpayne@69
|
860 time = timeset[timepos]
|
jpayne@69
|
861 except IndexError:
|
jpayne@69
|
862 pass
|
jpayne@69
|
863 else:
|
jpayne@69
|
864 date = datetime.date.fromordinal(ii.yearordinal+i)
|
jpayne@69
|
865 res = datetime.datetime.combine(date, time)
|
jpayne@69
|
866 if res not in poslist:
|
jpayne@69
|
867 poslist.append(res)
|
jpayne@69
|
868 poslist.sort()
|
jpayne@69
|
869 for res in poslist:
|
jpayne@69
|
870 if until and res > until:
|
jpayne@69
|
871 self._len = total
|
jpayne@69
|
872 return
|
jpayne@69
|
873 elif res >= self._dtstart:
|
jpayne@69
|
874 if count is not None:
|
jpayne@69
|
875 count -= 1
|
jpayne@69
|
876 if count < 0:
|
jpayne@69
|
877 self._len = total
|
jpayne@69
|
878 return
|
jpayne@69
|
879 total += 1
|
jpayne@69
|
880 yield res
|
jpayne@69
|
881 else:
|
jpayne@69
|
882 for i in dayset[start:end]:
|
jpayne@69
|
883 if i is not None:
|
jpayne@69
|
884 date = datetime.date.fromordinal(ii.yearordinal + i)
|
jpayne@69
|
885 for time in timeset:
|
jpayne@69
|
886 res = datetime.datetime.combine(date, time)
|
jpayne@69
|
887 if until and res > until:
|
jpayne@69
|
888 self._len = total
|
jpayne@69
|
889 return
|
jpayne@69
|
890 elif res >= self._dtstart:
|
jpayne@69
|
891 if count is not None:
|
jpayne@69
|
892 count -= 1
|
jpayne@69
|
893 if count < 0:
|
jpayne@69
|
894 self._len = total
|
jpayne@69
|
895 return
|
jpayne@69
|
896
|
jpayne@69
|
897 total += 1
|
jpayne@69
|
898 yield res
|
jpayne@69
|
899
|
jpayne@69
|
900 # Handle frequency and interval
|
jpayne@69
|
901 fixday = False
|
jpayne@69
|
902 if freq == YEARLY:
|
jpayne@69
|
903 year += interval
|
jpayne@69
|
904 if year > datetime.MAXYEAR:
|
jpayne@69
|
905 self._len = total
|
jpayne@69
|
906 return
|
jpayne@69
|
907 ii.rebuild(year, month)
|
jpayne@69
|
908 elif freq == MONTHLY:
|
jpayne@69
|
909 month += interval
|
jpayne@69
|
910 if month > 12:
|
jpayne@69
|
911 div, mod = divmod(month, 12)
|
jpayne@69
|
912 month = mod
|
jpayne@69
|
913 year += div
|
jpayne@69
|
914 if month == 0:
|
jpayne@69
|
915 month = 12
|
jpayne@69
|
916 year -= 1
|
jpayne@69
|
917 if year > datetime.MAXYEAR:
|
jpayne@69
|
918 self._len = total
|
jpayne@69
|
919 return
|
jpayne@69
|
920 ii.rebuild(year, month)
|
jpayne@69
|
921 elif freq == WEEKLY:
|
jpayne@69
|
922 if wkst > weekday:
|
jpayne@69
|
923 day += -(weekday+1+(6-wkst))+self._interval*7
|
jpayne@69
|
924 else:
|
jpayne@69
|
925 day += -(weekday-wkst)+self._interval*7
|
jpayne@69
|
926 weekday = wkst
|
jpayne@69
|
927 fixday = True
|
jpayne@69
|
928 elif freq == DAILY:
|
jpayne@69
|
929 day += interval
|
jpayne@69
|
930 fixday = True
|
jpayne@69
|
931 elif freq == HOURLY:
|
jpayne@69
|
932 if filtered:
|
jpayne@69
|
933 # Jump to one iteration before next day
|
jpayne@69
|
934 hour += ((23-hour)//interval)*interval
|
jpayne@69
|
935
|
jpayne@69
|
936 if byhour:
|
jpayne@69
|
937 ndays, hour = self.__mod_distance(value=hour,
|
jpayne@69
|
938 byxxx=self._byhour,
|
jpayne@69
|
939 base=24)
|
jpayne@69
|
940 else:
|
jpayne@69
|
941 ndays, hour = divmod(hour+interval, 24)
|
jpayne@69
|
942
|
jpayne@69
|
943 if ndays:
|
jpayne@69
|
944 day += ndays
|
jpayne@69
|
945 fixday = True
|
jpayne@69
|
946
|
jpayne@69
|
947 timeset = gettimeset(hour, minute, second)
|
jpayne@69
|
948 elif freq == MINUTELY:
|
jpayne@69
|
949 if filtered:
|
jpayne@69
|
950 # Jump to one iteration before next day
|
jpayne@69
|
951 minute += ((1439-(hour*60+minute))//interval)*interval
|
jpayne@69
|
952
|
jpayne@69
|
953 valid = False
|
jpayne@69
|
954 rep_rate = (24*60)
|
jpayne@69
|
955 for j in range(rep_rate // gcd(interval, rep_rate)):
|
jpayne@69
|
956 if byminute:
|
jpayne@69
|
957 nhours, minute = \
|
jpayne@69
|
958 self.__mod_distance(value=minute,
|
jpayne@69
|
959 byxxx=self._byminute,
|
jpayne@69
|
960 base=60)
|
jpayne@69
|
961 else:
|
jpayne@69
|
962 nhours, minute = divmod(minute+interval, 60)
|
jpayne@69
|
963
|
jpayne@69
|
964 div, hour = divmod(hour+nhours, 24)
|
jpayne@69
|
965 if div:
|
jpayne@69
|
966 day += div
|
jpayne@69
|
967 fixday = True
|
jpayne@69
|
968 filtered = False
|
jpayne@69
|
969
|
jpayne@69
|
970 if not byhour or hour in byhour:
|
jpayne@69
|
971 valid = True
|
jpayne@69
|
972 break
|
jpayne@69
|
973
|
jpayne@69
|
974 if not valid:
|
jpayne@69
|
975 raise ValueError('Invalid combination of interval and ' +
|
jpayne@69
|
976 'byhour resulting in empty rule.')
|
jpayne@69
|
977
|
jpayne@69
|
978 timeset = gettimeset(hour, minute, second)
|
jpayne@69
|
979 elif freq == SECONDLY:
|
jpayne@69
|
980 if filtered:
|
jpayne@69
|
981 # Jump to one iteration before next day
|
jpayne@69
|
982 second += (((86399 - (hour * 3600 + minute * 60 + second))
|
jpayne@69
|
983 // interval) * interval)
|
jpayne@69
|
984
|
jpayne@69
|
985 rep_rate = (24 * 3600)
|
jpayne@69
|
986 valid = False
|
jpayne@69
|
987 for j in range(0, rep_rate // gcd(interval, rep_rate)):
|
jpayne@69
|
988 if bysecond:
|
jpayne@69
|
989 nminutes, second = \
|
jpayne@69
|
990 self.__mod_distance(value=second,
|
jpayne@69
|
991 byxxx=self._bysecond,
|
jpayne@69
|
992 base=60)
|
jpayne@69
|
993 else:
|
jpayne@69
|
994 nminutes, second = divmod(second+interval, 60)
|
jpayne@69
|
995
|
jpayne@69
|
996 div, minute = divmod(minute+nminutes, 60)
|
jpayne@69
|
997 if div:
|
jpayne@69
|
998 hour += div
|
jpayne@69
|
999 div, hour = divmod(hour, 24)
|
jpayne@69
|
1000 if div:
|
jpayne@69
|
1001 day += div
|
jpayne@69
|
1002 fixday = True
|
jpayne@69
|
1003
|
jpayne@69
|
1004 if ((not byhour or hour in byhour) and
|
jpayne@69
|
1005 (not byminute or minute in byminute) and
|
jpayne@69
|
1006 (not bysecond or second in bysecond)):
|
jpayne@69
|
1007 valid = True
|
jpayne@69
|
1008 break
|
jpayne@69
|
1009
|
jpayne@69
|
1010 if not valid:
|
jpayne@69
|
1011 raise ValueError('Invalid combination of interval, ' +
|
jpayne@69
|
1012 'byhour and byminute resulting in empty' +
|
jpayne@69
|
1013 ' rule.')
|
jpayne@69
|
1014
|
jpayne@69
|
1015 timeset = gettimeset(hour, minute, second)
|
jpayne@69
|
1016
|
jpayne@69
|
1017 if fixday and day > 28:
|
jpayne@69
|
1018 daysinmonth = calendar.monthrange(year, month)[1]
|
jpayne@69
|
1019 if day > daysinmonth:
|
jpayne@69
|
1020 while day > daysinmonth:
|
jpayne@69
|
1021 day -= daysinmonth
|
jpayne@69
|
1022 month += 1
|
jpayne@69
|
1023 if month == 13:
|
jpayne@69
|
1024 month = 1
|
jpayne@69
|
1025 year += 1
|
jpayne@69
|
1026 if year > datetime.MAXYEAR:
|
jpayne@69
|
1027 self._len = total
|
jpayne@69
|
1028 return
|
jpayne@69
|
1029 daysinmonth = calendar.monthrange(year, month)[1]
|
jpayne@69
|
1030 ii.rebuild(year, month)
|
jpayne@69
|
1031
|
jpayne@69
|
1032 def __construct_byset(self, start, byxxx, base):
|
jpayne@69
|
1033 """
|
jpayne@69
|
1034 If a `BYXXX` sequence is passed to the constructor at the same level as
|
jpayne@69
|
1035 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
|
jpayne@69
|
1036 specifications which cannot be reached given some starting conditions.
|
jpayne@69
|
1037
|
jpayne@69
|
1038 This occurs whenever the interval is not coprime with the base of a
|
jpayne@69
|
1039 given unit and the difference between the starting position and the
|
jpayne@69
|
1040 ending position is not coprime with the greatest common denominator
|
jpayne@69
|
1041 between the interval and the base. For example, with a FREQ of hourly
|
jpayne@69
|
1042 starting at 17:00 and an interval of 4, the only valid values for
|
jpayne@69
|
1043 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
|
jpayne@69
|
1044 coprime.
|
jpayne@69
|
1045
|
jpayne@69
|
1046 :param start:
|
jpayne@69
|
1047 Specifies the starting position.
|
jpayne@69
|
1048 :param byxxx:
|
jpayne@69
|
1049 An iterable containing the list of allowed values.
|
jpayne@69
|
1050 :param base:
|
jpayne@69
|
1051 The largest allowable value for the specified frequency (e.g.
|
jpayne@69
|
1052 24 hours, 60 minutes).
|
jpayne@69
|
1053
|
jpayne@69
|
1054 This does not preserve the type of the iterable, returning a set, since
|
jpayne@69
|
1055 the values should be unique and the order is irrelevant, this will
|
jpayne@69
|
1056 speed up later lookups.
|
jpayne@69
|
1057
|
jpayne@69
|
1058 In the event of an empty set, raises a :exception:`ValueError`, as this
|
jpayne@69
|
1059 results in an empty rrule.
|
jpayne@69
|
1060 """
|
jpayne@69
|
1061
|
jpayne@69
|
1062 cset = set()
|
jpayne@69
|
1063
|
jpayne@69
|
1064 # Support a single byxxx value.
|
jpayne@69
|
1065 if isinstance(byxxx, integer_types):
|
jpayne@69
|
1066 byxxx = (byxxx, )
|
jpayne@69
|
1067
|
jpayne@69
|
1068 for num in byxxx:
|
jpayne@69
|
1069 i_gcd = gcd(self._interval, base)
|
jpayne@69
|
1070 # Use divmod rather than % because we need to wrap negative nums.
|
jpayne@69
|
1071 if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
|
jpayne@69
|
1072 cset.add(num)
|
jpayne@69
|
1073
|
jpayne@69
|
1074 if len(cset) == 0:
|
jpayne@69
|
1075 raise ValueError("Invalid rrule byxxx generates an empty set.")
|
jpayne@69
|
1076
|
jpayne@69
|
1077 return cset
|
jpayne@69
|
1078
|
jpayne@69
|
1079 def __mod_distance(self, value, byxxx, base):
|
jpayne@69
|
1080 """
|
jpayne@69
|
1081 Calculates the next value in a sequence where the `FREQ` parameter is
|
jpayne@69
|
1082 specified along with a `BYXXX` parameter at the same "level"
|
jpayne@69
|
1083 (e.g. `HOURLY` specified with `BYHOUR`).
|
jpayne@69
|
1084
|
jpayne@69
|
1085 :param value:
|
jpayne@69
|
1086 The old value of the component.
|
jpayne@69
|
1087 :param byxxx:
|
jpayne@69
|
1088 The `BYXXX` set, which should have been generated by
|
jpayne@69
|
1089 `rrule._construct_byset`, or something else which checks that a
|
jpayne@69
|
1090 valid rule is present.
|
jpayne@69
|
1091 :param base:
|
jpayne@69
|
1092 The largest allowable value for the specified frequency (e.g.
|
jpayne@69
|
1093 24 hours, 60 minutes).
|
jpayne@69
|
1094
|
jpayne@69
|
1095 If a valid value is not found after `base` iterations (the maximum
|
jpayne@69
|
1096 number before the sequence would start to repeat), this raises a
|
jpayne@69
|
1097 :exception:`ValueError`, as no valid values were found.
|
jpayne@69
|
1098
|
jpayne@69
|
1099 This returns a tuple of `divmod(n*interval, base)`, where `n` is the
|
jpayne@69
|
1100 smallest number of `interval` repetitions until the next specified
|
jpayne@69
|
1101 value in `byxxx` is found.
|
jpayne@69
|
1102 """
|
jpayne@69
|
1103 accumulator = 0
|
jpayne@69
|
1104 for ii in range(1, base + 1):
|
jpayne@69
|
1105 # Using divmod() over % to account for negative intervals
|
jpayne@69
|
1106 div, value = divmod(value + self._interval, base)
|
jpayne@69
|
1107 accumulator += div
|
jpayne@69
|
1108 if value in byxxx:
|
jpayne@69
|
1109 return (accumulator, value)
|
jpayne@69
|
1110
|
jpayne@69
|
1111
|
jpayne@69
|
1112 class _iterinfo(object):
|
jpayne@69
|
1113 __slots__ = ["rrule", "lastyear", "lastmonth",
|
jpayne@69
|
1114 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
|
jpayne@69
|
1115 "mmask", "mrange", "mdaymask", "nmdaymask",
|
jpayne@69
|
1116 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
|
jpayne@69
|
1117
|
jpayne@69
|
1118 def __init__(self, rrule):
|
jpayne@69
|
1119 for attr in self.__slots__:
|
jpayne@69
|
1120 setattr(self, attr, None)
|
jpayne@69
|
1121 self.rrule = rrule
|
jpayne@69
|
1122
|
jpayne@69
|
1123 def rebuild(self, year, month):
|
jpayne@69
|
1124 # Every mask is 7 days longer to handle cross-year weekly periods.
|
jpayne@69
|
1125 rr = self.rrule
|
jpayne@69
|
1126 if year != self.lastyear:
|
jpayne@69
|
1127 self.yearlen = 365 + calendar.isleap(year)
|
jpayne@69
|
1128 self.nextyearlen = 365 + calendar.isleap(year + 1)
|
jpayne@69
|
1129 firstyday = datetime.date(year, 1, 1)
|
jpayne@69
|
1130 self.yearordinal = firstyday.toordinal()
|
jpayne@69
|
1131 self.yearweekday = firstyday.weekday()
|
jpayne@69
|
1132
|
jpayne@69
|
1133 wday = datetime.date(year, 1, 1).weekday()
|
jpayne@69
|
1134 if self.yearlen == 365:
|
jpayne@69
|
1135 self.mmask = M365MASK
|
jpayne@69
|
1136 self.mdaymask = MDAY365MASK
|
jpayne@69
|
1137 self.nmdaymask = NMDAY365MASK
|
jpayne@69
|
1138 self.wdaymask = WDAYMASK[wday:]
|
jpayne@69
|
1139 self.mrange = M365RANGE
|
jpayne@69
|
1140 else:
|
jpayne@69
|
1141 self.mmask = M366MASK
|
jpayne@69
|
1142 self.mdaymask = MDAY366MASK
|
jpayne@69
|
1143 self.nmdaymask = NMDAY366MASK
|
jpayne@69
|
1144 self.wdaymask = WDAYMASK[wday:]
|
jpayne@69
|
1145 self.mrange = M366RANGE
|
jpayne@69
|
1146
|
jpayne@69
|
1147 if not rr._byweekno:
|
jpayne@69
|
1148 self.wnomask = None
|
jpayne@69
|
1149 else:
|
jpayne@69
|
1150 self.wnomask = [0]*(self.yearlen+7)
|
jpayne@69
|
1151 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
|
jpayne@69
|
1152 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
|
jpayne@69
|
1153 if no1wkst >= 4:
|
jpayne@69
|
1154 no1wkst = 0
|
jpayne@69
|
1155 # Number of days in the year, plus the days we got
|
jpayne@69
|
1156 # from last year.
|
jpayne@69
|
1157 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
|
jpayne@69
|
1158 else:
|
jpayne@69
|
1159 # Number of days in the year, minus the days we
|
jpayne@69
|
1160 # left in last year.
|
jpayne@69
|
1161 wyearlen = self.yearlen-no1wkst
|
jpayne@69
|
1162 div, mod = divmod(wyearlen, 7)
|
jpayne@69
|
1163 numweeks = div+mod//4
|
jpayne@69
|
1164 for n in rr._byweekno:
|
jpayne@69
|
1165 if n < 0:
|
jpayne@69
|
1166 n += numweeks+1
|
jpayne@69
|
1167 if not (0 < n <= numweeks):
|
jpayne@69
|
1168 continue
|
jpayne@69
|
1169 if n > 1:
|
jpayne@69
|
1170 i = no1wkst+(n-1)*7
|
jpayne@69
|
1171 if no1wkst != firstwkst:
|
jpayne@69
|
1172 i -= 7-firstwkst
|
jpayne@69
|
1173 else:
|
jpayne@69
|
1174 i = no1wkst
|
jpayne@69
|
1175 for j in range(7):
|
jpayne@69
|
1176 self.wnomask[i] = 1
|
jpayne@69
|
1177 i += 1
|
jpayne@69
|
1178 if self.wdaymask[i] == rr._wkst:
|
jpayne@69
|
1179 break
|
jpayne@69
|
1180 if 1 in rr._byweekno:
|
jpayne@69
|
1181 # Check week number 1 of next year as well
|
jpayne@69
|
1182 # TODO: Check -numweeks for next year.
|
jpayne@69
|
1183 i = no1wkst+numweeks*7
|
jpayne@69
|
1184 if no1wkst != firstwkst:
|
jpayne@69
|
1185 i -= 7-firstwkst
|
jpayne@69
|
1186 if i < self.yearlen:
|
jpayne@69
|
1187 # If week starts in next year, we
|
jpayne@69
|
1188 # don't care about it.
|
jpayne@69
|
1189 for j in range(7):
|
jpayne@69
|
1190 self.wnomask[i] = 1
|
jpayne@69
|
1191 i += 1
|
jpayne@69
|
1192 if self.wdaymask[i] == rr._wkst:
|
jpayne@69
|
1193 break
|
jpayne@69
|
1194 if no1wkst:
|
jpayne@69
|
1195 # Check last week number of last year as
|
jpayne@69
|
1196 # well. If no1wkst is 0, either the year
|
jpayne@69
|
1197 # started on week start, or week number 1
|
jpayne@69
|
1198 # got days from last year, so there are no
|
jpayne@69
|
1199 # days from last year's last week number in
|
jpayne@69
|
1200 # this year.
|
jpayne@69
|
1201 if -1 not in rr._byweekno:
|
jpayne@69
|
1202 lyearweekday = datetime.date(year-1, 1, 1).weekday()
|
jpayne@69
|
1203 lno1wkst = (7-lyearweekday+rr._wkst) % 7
|
jpayne@69
|
1204 lyearlen = 365+calendar.isleap(year-1)
|
jpayne@69
|
1205 if lno1wkst >= 4:
|
jpayne@69
|
1206 lno1wkst = 0
|
jpayne@69
|
1207 lnumweeks = 52+(lyearlen +
|
jpayne@69
|
1208 (lyearweekday-rr._wkst) % 7) % 7//4
|
jpayne@69
|
1209 else:
|
jpayne@69
|
1210 lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
|
jpayne@69
|
1211 else:
|
jpayne@69
|
1212 lnumweeks = -1
|
jpayne@69
|
1213 if lnumweeks in rr._byweekno:
|
jpayne@69
|
1214 for i in range(no1wkst):
|
jpayne@69
|
1215 self.wnomask[i] = 1
|
jpayne@69
|
1216
|
jpayne@69
|
1217 if (rr._bynweekday and (month != self.lastmonth or
|
jpayne@69
|
1218 year != self.lastyear)):
|
jpayne@69
|
1219 ranges = []
|
jpayne@69
|
1220 if rr._freq == YEARLY:
|
jpayne@69
|
1221 if rr._bymonth:
|
jpayne@69
|
1222 for month in rr._bymonth:
|
jpayne@69
|
1223 ranges.append(self.mrange[month-1:month+1])
|
jpayne@69
|
1224 else:
|
jpayne@69
|
1225 ranges = [(0, self.yearlen)]
|
jpayne@69
|
1226 elif rr._freq == MONTHLY:
|
jpayne@69
|
1227 ranges = [self.mrange[month-1:month+1]]
|
jpayne@69
|
1228 if ranges:
|
jpayne@69
|
1229 # Weekly frequency won't get here, so we may not
|
jpayne@69
|
1230 # care about cross-year weekly periods.
|
jpayne@69
|
1231 self.nwdaymask = [0]*self.yearlen
|
jpayne@69
|
1232 for first, last in ranges:
|
jpayne@69
|
1233 last -= 1
|
jpayne@69
|
1234 for wday, n in rr._bynweekday:
|
jpayne@69
|
1235 if n < 0:
|
jpayne@69
|
1236 i = last+(n+1)*7
|
jpayne@69
|
1237 i -= (self.wdaymask[i]-wday) % 7
|
jpayne@69
|
1238 else:
|
jpayne@69
|
1239 i = first+(n-1)*7
|
jpayne@69
|
1240 i += (7-self.wdaymask[i]+wday) % 7
|
jpayne@69
|
1241 if first <= i <= last:
|
jpayne@69
|
1242 self.nwdaymask[i] = 1
|
jpayne@69
|
1243
|
jpayne@69
|
1244 if rr._byeaster:
|
jpayne@69
|
1245 self.eastermask = [0]*(self.yearlen+7)
|
jpayne@69
|
1246 eyday = easter.easter(year).toordinal()-self.yearordinal
|
jpayne@69
|
1247 for offset in rr._byeaster:
|
jpayne@69
|
1248 self.eastermask[eyday+offset] = 1
|
jpayne@69
|
1249
|
jpayne@69
|
1250 self.lastyear = year
|
jpayne@69
|
1251 self.lastmonth = month
|
jpayne@69
|
1252
|
jpayne@69
|
1253 def ydayset(self, year, month, day):
|
jpayne@69
|
1254 return list(range(self.yearlen)), 0, self.yearlen
|
jpayne@69
|
1255
|
jpayne@69
|
1256 def mdayset(self, year, month, day):
|
jpayne@69
|
1257 dset = [None]*self.yearlen
|
jpayne@69
|
1258 start, end = self.mrange[month-1:month+1]
|
jpayne@69
|
1259 for i in range(start, end):
|
jpayne@69
|
1260 dset[i] = i
|
jpayne@69
|
1261 return dset, start, end
|
jpayne@69
|
1262
|
jpayne@69
|
1263 def wdayset(self, year, month, day):
|
jpayne@69
|
1264 # We need to handle cross-year weeks here.
|
jpayne@69
|
1265 dset = [None]*(self.yearlen+7)
|
jpayne@69
|
1266 i = datetime.date(year, month, day).toordinal()-self.yearordinal
|
jpayne@69
|
1267 start = i
|
jpayne@69
|
1268 for j in range(7):
|
jpayne@69
|
1269 dset[i] = i
|
jpayne@69
|
1270 i += 1
|
jpayne@69
|
1271 # if (not (0 <= i < self.yearlen) or
|
jpayne@69
|
1272 # self.wdaymask[i] == self.rrule._wkst):
|
jpayne@69
|
1273 # This will cross the year boundary, if necessary.
|
jpayne@69
|
1274 if self.wdaymask[i] == self.rrule._wkst:
|
jpayne@69
|
1275 break
|
jpayne@69
|
1276 return dset, start, i
|
jpayne@69
|
1277
|
jpayne@69
|
1278 def ddayset(self, year, month, day):
|
jpayne@69
|
1279 dset = [None] * self.yearlen
|
jpayne@69
|
1280 i = datetime.date(year, month, day).toordinal() - self.yearordinal
|
jpayne@69
|
1281 dset[i] = i
|
jpayne@69
|
1282 return dset, i, i + 1
|
jpayne@69
|
1283
|
jpayne@69
|
1284 def htimeset(self, hour, minute, second):
|
jpayne@69
|
1285 tset = []
|
jpayne@69
|
1286 rr = self.rrule
|
jpayne@69
|
1287 for minute in rr._byminute:
|
jpayne@69
|
1288 for second in rr._bysecond:
|
jpayne@69
|
1289 tset.append(datetime.time(hour, minute, second,
|
jpayne@69
|
1290 tzinfo=rr._tzinfo))
|
jpayne@69
|
1291 tset.sort()
|
jpayne@69
|
1292 return tset
|
jpayne@69
|
1293
|
jpayne@69
|
1294 def mtimeset(self, hour, minute, second):
|
jpayne@69
|
1295 tset = []
|
jpayne@69
|
1296 rr = self.rrule
|
jpayne@69
|
1297 for second in rr._bysecond:
|
jpayne@69
|
1298 tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
|
jpayne@69
|
1299 tset.sort()
|
jpayne@69
|
1300 return tset
|
jpayne@69
|
1301
|
jpayne@69
|
1302 def stimeset(self, hour, minute, second):
|
jpayne@69
|
1303 return (datetime.time(hour, minute, second,
|
jpayne@69
|
1304 tzinfo=self.rrule._tzinfo),)
|
jpayne@69
|
1305
|
jpayne@69
|
1306
|
jpayne@69
|
1307 class rruleset(rrulebase):
|
jpayne@69
|
1308 """ The rruleset type allows more complex recurrence setups, mixing
|
jpayne@69
|
1309 multiple rules, dates, exclusion rules, and exclusion dates. The type
|
jpayne@69
|
1310 constructor takes the following keyword arguments:
|
jpayne@69
|
1311
|
jpayne@69
|
1312 :param cache: If True, caching of results will be enabled, improving
|
jpayne@69
|
1313 performance of multiple queries considerably. """
|
jpayne@69
|
1314
|
jpayne@69
|
1315 class _genitem(object):
|
jpayne@69
|
1316 def __init__(self, genlist, gen):
|
jpayne@69
|
1317 try:
|
jpayne@69
|
1318 self.dt = advance_iterator(gen)
|
jpayne@69
|
1319 genlist.append(self)
|
jpayne@69
|
1320 except StopIteration:
|
jpayne@69
|
1321 pass
|
jpayne@69
|
1322 self.genlist = genlist
|
jpayne@69
|
1323 self.gen = gen
|
jpayne@69
|
1324
|
jpayne@69
|
1325 def __next__(self):
|
jpayne@69
|
1326 try:
|
jpayne@69
|
1327 self.dt = advance_iterator(self.gen)
|
jpayne@69
|
1328 except StopIteration:
|
jpayne@69
|
1329 if self.genlist[0] is self:
|
jpayne@69
|
1330 heapq.heappop(self.genlist)
|
jpayne@69
|
1331 else:
|
jpayne@69
|
1332 self.genlist.remove(self)
|
jpayne@69
|
1333 heapq.heapify(self.genlist)
|
jpayne@69
|
1334
|
jpayne@69
|
1335 next = __next__
|
jpayne@69
|
1336
|
jpayne@69
|
1337 def __lt__(self, other):
|
jpayne@69
|
1338 return self.dt < other.dt
|
jpayne@69
|
1339
|
jpayne@69
|
1340 def __gt__(self, other):
|
jpayne@69
|
1341 return self.dt > other.dt
|
jpayne@69
|
1342
|
jpayne@69
|
1343 def __eq__(self, other):
|
jpayne@69
|
1344 return self.dt == other.dt
|
jpayne@69
|
1345
|
jpayne@69
|
1346 def __ne__(self, other):
|
jpayne@69
|
1347 return self.dt != other.dt
|
jpayne@69
|
1348
|
jpayne@69
|
1349 def __init__(self, cache=False):
|
jpayne@69
|
1350 super(rruleset, self).__init__(cache)
|
jpayne@69
|
1351 self._rrule = []
|
jpayne@69
|
1352 self._rdate = []
|
jpayne@69
|
1353 self._exrule = []
|
jpayne@69
|
1354 self._exdate = []
|
jpayne@69
|
1355
|
jpayne@69
|
1356 @_invalidates_cache
|
jpayne@69
|
1357 def rrule(self, rrule):
|
jpayne@69
|
1358 """ Include the given :py:class:`rrule` instance in the recurrence set
|
jpayne@69
|
1359 generation. """
|
jpayne@69
|
1360 self._rrule.append(rrule)
|
jpayne@69
|
1361
|
jpayne@69
|
1362 @_invalidates_cache
|
jpayne@69
|
1363 def rdate(self, rdate):
|
jpayne@69
|
1364 """ Include the given :py:class:`datetime` instance in the recurrence
|
jpayne@69
|
1365 set generation. """
|
jpayne@69
|
1366 self._rdate.append(rdate)
|
jpayne@69
|
1367
|
jpayne@69
|
1368 @_invalidates_cache
|
jpayne@69
|
1369 def exrule(self, exrule):
|
jpayne@69
|
1370 """ Include the given rrule instance in the recurrence set exclusion
|
jpayne@69
|
1371 list. Dates which are part of the given recurrence rules will not
|
jpayne@69
|
1372 be generated, even if some inclusive rrule or rdate matches them.
|
jpayne@69
|
1373 """
|
jpayne@69
|
1374 self._exrule.append(exrule)
|
jpayne@69
|
1375
|
jpayne@69
|
1376 @_invalidates_cache
|
jpayne@69
|
1377 def exdate(self, exdate):
|
jpayne@69
|
1378 """ Include the given datetime instance in the recurrence set
|
jpayne@69
|
1379 exclusion list. Dates included that way will not be generated,
|
jpayne@69
|
1380 even if some inclusive rrule or rdate matches them. """
|
jpayne@69
|
1381 self._exdate.append(exdate)
|
jpayne@69
|
1382
|
jpayne@69
|
1383 def _iter(self):
|
jpayne@69
|
1384 rlist = []
|
jpayne@69
|
1385 self._rdate.sort()
|
jpayne@69
|
1386 self._genitem(rlist, iter(self._rdate))
|
jpayne@69
|
1387 for gen in [iter(x) for x in self._rrule]:
|
jpayne@69
|
1388 self._genitem(rlist, gen)
|
jpayne@69
|
1389 exlist = []
|
jpayne@69
|
1390 self._exdate.sort()
|
jpayne@69
|
1391 self._genitem(exlist, iter(self._exdate))
|
jpayne@69
|
1392 for gen in [iter(x) for x in self._exrule]:
|
jpayne@69
|
1393 self._genitem(exlist, gen)
|
jpayne@69
|
1394 lastdt = None
|
jpayne@69
|
1395 total = 0
|
jpayne@69
|
1396 heapq.heapify(rlist)
|
jpayne@69
|
1397 heapq.heapify(exlist)
|
jpayne@69
|
1398 while rlist:
|
jpayne@69
|
1399 ritem = rlist[0]
|
jpayne@69
|
1400 if not lastdt or lastdt != ritem.dt:
|
jpayne@69
|
1401 while exlist and exlist[0] < ritem:
|
jpayne@69
|
1402 exitem = exlist[0]
|
jpayne@69
|
1403 advance_iterator(exitem)
|
jpayne@69
|
1404 if exlist and exlist[0] is exitem:
|
jpayne@69
|
1405 heapq.heapreplace(exlist, exitem)
|
jpayne@69
|
1406 if not exlist or ritem != exlist[0]:
|
jpayne@69
|
1407 total += 1
|
jpayne@69
|
1408 yield ritem.dt
|
jpayne@69
|
1409 lastdt = ritem.dt
|
jpayne@69
|
1410 advance_iterator(ritem)
|
jpayne@69
|
1411 if rlist and rlist[0] is ritem:
|
jpayne@69
|
1412 heapq.heapreplace(rlist, ritem)
|
jpayne@69
|
1413 self._len = total
|
jpayne@69
|
1414
|
jpayne@69
|
1415
|
jpayne@69
|
1416
|
jpayne@69
|
1417
|
jpayne@69
|
1418 class _rrulestr(object):
|
jpayne@69
|
1419 """ Parses a string representation of a recurrence rule or set of
|
jpayne@69
|
1420 recurrence rules.
|
jpayne@69
|
1421
|
jpayne@69
|
1422 :param s:
|
jpayne@69
|
1423 Required, a string defining one or more recurrence rules.
|
jpayne@69
|
1424
|
jpayne@69
|
1425 :param dtstart:
|
jpayne@69
|
1426 If given, used as the default recurrence start if not specified in the
|
jpayne@69
|
1427 rule string.
|
jpayne@69
|
1428
|
jpayne@69
|
1429 :param cache:
|
jpayne@69
|
1430 If set ``True`` caching of results will be enabled, improving
|
jpayne@69
|
1431 performance of multiple queries considerably.
|
jpayne@69
|
1432
|
jpayne@69
|
1433 :param unfold:
|
jpayne@69
|
1434 If set ``True`` indicates that a rule string is split over more
|
jpayne@69
|
1435 than one line and should be joined before processing.
|
jpayne@69
|
1436
|
jpayne@69
|
1437 :param forceset:
|
jpayne@69
|
1438 If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
|
jpayne@69
|
1439 be returned.
|
jpayne@69
|
1440
|
jpayne@69
|
1441 :param compatible:
|
jpayne@69
|
1442 If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
|
jpayne@69
|
1443
|
jpayne@69
|
1444 :param ignoretz:
|
jpayne@69
|
1445 If set ``True``, time zones in parsed strings are ignored and a naive
|
jpayne@69
|
1446 :class:`datetime.datetime` object is returned.
|
jpayne@69
|
1447
|
jpayne@69
|
1448 :param tzids:
|
jpayne@69
|
1449 If given, a callable or mapping used to retrieve a
|
jpayne@69
|
1450 :class:`datetime.tzinfo` from a string representation.
|
jpayne@69
|
1451 Defaults to :func:`dateutil.tz.gettz`.
|
jpayne@69
|
1452
|
jpayne@69
|
1453 :param tzinfos:
|
jpayne@69
|
1454 Additional time zone names / aliases which may be present in a string
|
jpayne@69
|
1455 representation. See :func:`dateutil.parser.parse` for more
|
jpayne@69
|
1456 information.
|
jpayne@69
|
1457
|
jpayne@69
|
1458 :return:
|
jpayne@69
|
1459 Returns a :class:`dateutil.rrule.rruleset` or
|
jpayne@69
|
1460 :class:`dateutil.rrule.rrule`
|
jpayne@69
|
1461 """
|
jpayne@69
|
1462
|
jpayne@69
|
1463 _freq_map = {"YEARLY": YEARLY,
|
jpayne@69
|
1464 "MONTHLY": MONTHLY,
|
jpayne@69
|
1465 "WEEKLY": WEEKLY,
|
jpayne@69
|
1466 "DAILY": DAILY,
|
jpayne@69
|
1467 "HOURLY": HOURLY,
|
jpayne@69
|
1468 "MINUTELY": MINUTELY,
|
jpayne@69
|
1469 "SECONDLY": SECONDLY}
|
jpayne@69
|
1470
|
jpayne@69
|
1471 _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
|
jpayne@69
|
1472 "FR": 4, "SA": 5, "SU": 6}
|
jpayne@69
|
1473
|
jpayne@69
|
1474 def _handle_int(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1475 rrkwargs[name.lower()] = int(value)
|
jpayne@69
|
1476
|
jpayne@69
|
1477 def _handle_int_list(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1478 rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
|
jpayne@69
|
1479
|
jpayne@69
|
1480 _handle_INTERVAL = _handle_int
|
jpayne@69
|
1481 _handle_COUNT = _handle_int
|
jpayne@69
|
1482 _handle_BYSETPOS = _handle_int_list
|
jpayne@69
|
1483 _handle_BYMONTH = _handle_int_list
|
jpayne@69
|
1484 _handle_BYMONTHDAY = _handle_int_list
|
jpayne@69
|
1485 _handle_BYYEARDAY = _handle_int_list
|
jpayne@69
|
1486 _handle_BYEASTER = _handle_int_list
|
jpayne@69
|
1487 _handle_BYWEEKNO = _handle_int_list
|
jpayne@69
|
1488 _handle_BYHOUR = _handle_int_list
|
jpayne@69
|
1489 _handle_BYMINUTE = _handle_int_list
|
jpayne@69
|
1490 _handle_BYSECOND = _handle_int_list
|
jpayne@69
|
1491
|
jpayne@69
|
1492 def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1493 rrkwargs["freq"] = self._freq_map[value]
|
jpayne@69
|
1494
|
jpayne@69
|
1495 def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1496 global parser
|
jpayne@69
|
1497 if not parser:
|
jpayne@69
|
1498 from dateutil import parser
|
jpayne@69
|
1499 try:
|
jpayne@69
|
1500 rrkwargs["until"] = parser.parse(value,
|
jpayne@69
|
1501 ignoretz=kwargs.get("ignoretz"),
|
jpayne@69
|
1502 tzinfos=kwargs.get("tzinfos"))
|
jpayne@69
|
1503 except ValueError:
|
jpayne@69
|
1504 raise ValueError("invalid until date")
|
jpayne@69
|
1505
|
jpayne@69
|
1506 def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1507 rrkwargs["wkst"] = self._weekday_map[value]
|
jpayne@69
|
1508
|
jpayne@69
|
1509 def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
|
jpayne@69
|
1510 """
|
jpayne@69
|
1511 Two ways to specify this: +1MO or MO(+1)
|
jpayne@69
|
1512 """
|
jpayne@69
|
1513 l = []
|
jpayne@69
|
1514 for wday in value.split(','):
|
jpayne@69
|
1515 if '(' in wday:
|
jpayne@69
|
1516 # If it's of the form TH(+1), etc.
|
jpayne@69
|
1517 splt = wday.split('(')
|
jpayne@69
|
1518 w = splt[0]
|
jpayne@69
|
1519 n = int(splt[1][:-1])
|
jpayne@69
|
1520 elif len(wday):
|
jpayne@69
|
1521 # If it's of the form +1MO
|
jpayne@69
|
1522 for i in range(len(wday)):
|
jpayne@69
|
1523 if wday[i] not in '+-0123456789':
|
jpayne@69
|
1524 break
|
jpayne@69
|
1525 n = wday[:i] or None
|
jpayne@69
|
1526 w = wday[i:]
|
jpayne@69
|
1527 if n:
|
jpayne@69
|
1528 n = int(n)
|
jpayne@69
|
1529 else:
|
jpayne@69
|
1530 raise ValueError("Invalid (empty) BYDAY specification.")
|
jpayne@69
|
1531
|
jpayne@69
|
1532 l.append(weekdays[self._weekday_map[w]](n))
|
jpayne@69
|
1533 rrkwargs["byweekday"] = l
|
jpayne@69
|
1534
|
jpayne@69
|
1535 _handle_BYDAY = _handle_BYWEEKDAY
|
jpayne@69
|
1536
|
jpayne@69
|
1537 def _parse_rfc_rrule(self, line,
|
jpayne@69
|
1538 dtstart=None,
|
jpayne@69
|
1539 cache=False,
|
jpayne@69
|
1540 ignoretz=False,
|
jpayne@69
|
1541 tzinfos=None):
|
jpayne@69
|
1542 if line.find(':') != -1:
|
jpayne@69
|
1543 name, value = line.split(':')
|
jpayne@69
|
1544 if name != "RRULE":
|
jpayne@69
|
1545 raise ValueError("unknown parameter name")
|
jpayne@69
|
1546 else:
|
jpayne@69
|
1547 value = line
|
jpayne@69
|
1548 rrkwargs = {}
|
jpayne@69
|
1549 for pair in value.split(';'):
|
jpayne@69
|
1550 name, value = pair.split('=')
|
jpayne@69
|
1551 name = name.upper()
|
jpayne@69
|
1552 value = value.upper()
|
jpayne@69
|
1553 try:
|
jpayne@69
|
1554 getattr(self, "_handle_"+name)(rrkwargs, name, value,
|
jpayne@69
|
1555 ignoretz=ignoretz,
|
jpayne@69
|
1556 tzinfos=tzinfos)
|
jpayne@69
|
1557 except AttributeError:
|
jpayne@69
|
1558 raise ValueError("unknown parameter '%s'" % name)
|
jpayne@69
|
1559 except (KeyError, ValueError):
|
jpayne@69
|
1560 raise ValueError("invalid '%s': %s" % (name, value))
|
jpayne@69
|
1561 return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
|
jpayne@69
|
1562
|
jpayne@69
|
1563 def _parse_date_value(self, date_value, parms, rule_tzids,
|
jpayne@69
|
1564 ignoretz, tzids, tzinfos):
|
jpayne@69
|
1565 global parser
|
jpayne@69
|
1566 if not parser:
|
jpayne@69
|
1567 from dateutil import parser
|
jpayne@69
|
1568
|
jpayne@69
|
1569 datevals = []
|
jpayne@69
|
1570 value_found = False
|
jpayne@69
|
1571 TZID = None
|
jpayne@69
|
1572
|
jpayne@69
|
1573 for parm in parms:
|
jpayne@69
|
1574 if parm.startswith("TZID="):
|
jpayne@69
|
1575 try:
|
jpayne@69
|
1576 tzkey = rule_tzids[parm.split('TZID=')[-1]]
|
jpayne@69
|
1577 except KeyError:
|
jpayne@69
|
1578 continue
|
jpayne@69
|
1579 if tzids is None:
|
jpayne@69
|
1580 from . import tz
|
jpayne@69
|
1581 tzlookup = tz.gettz
|
jpayne@69
|
1582 elif callable(tzids):
|
jpayne@69
|
1583 tzlookup = tzids
|
jpayne@69
|
1584 else:
|
jpayne@69
|
1585 tzlookup = getattr(tzids, 'get', None)
|
jpayne@69
|
1586 if tzlookup is None:
|
jpayne@69
|
1587 msg = ('tzids must be a callable, mapping, or None, '
|
jpayne@69
|
1588 'not %s' % tzids)
|
jpayne@69
|
1589 raise ValueError(msg)
|
jpayne@69
|
1590
|
jpayne@69
|
1591 TZID = tzlookup(tzkey)
|
jpayne@69
|
1592 continue
|
jpayne@69
|
1593
|
jpayne@69
|
1594 # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
|
jpayne@69
|
1595 # only once.
|
jpayne@69
|
1596 if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
|
jpayne@69
|
1597 raise ValueError("unsupported parm: " + parm)
|
jpayne@69
|
1598 else:
|
jpayne@69
|
1599 if value_found:
|
jpayne@69
|
1600 msg = ("Duplicate value parameter found in: " + parm)
|
jpayne@69
|
1601 raise ValueError(msg)
|
jpayne@69
|
1602 value_found = True
|
jpayne@69
|
1603
|
jpayne@69
|
1604 for datestr in date_value.split(','):
|
jpayne@69
|
1605 date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
|
jpayne@69
|
1606 if TZID is not None:
|
jpayne@69
|
1607 if date.tzinfo is None:
|
jpayne@69
|
1608 date = date.replace(tzinfo=TZID)
|
jpayne@69
|
1609 else:
|
jpayne@69
|
1610 raise ValueError('DTSTART/EXDATE specifies multiple timezone')
|
jpayne@69
|
1611 datevals.append(date)
|
jpayne@69
|
1612
|
jpayne@69
|
1613 return datevals
|
jpayne@69
|
1614
|
jpayne@69
|
1615 def _parse_rfc(self, s,
|
jpayne@69
|
1616 dtstart=None,
|
jpayne@69
|
1617 cache=False,
|
jpayne@69
|
1618 unfold=False,
|
jpayne@69
|
1619 forceset=False,
|
jpayne@69
|
1620 compatible=False,
|
jpayne@69
|
1621 ignoretz=False,
|
jpayne@69
|
1622 tzids=None,
|
jpayne@69
|
1623 tzinfos=None):
|
jpayne@69
|
1624 global parser
|
jpayne@69
|
1625 if compatible:
|
jpayne@69
|
1626 forceset = True
|
jpayne@69
|
1627 unfold = True
|
jpayne@69
|
1628
|
jpayne@69
|
1629 TZID_NAMES = dict(map(
|
jpayne@69
|
1630 lambda x: (x.upper(), x),
|
jpayne@69
|
1631 re.findall('TZID=(?P<name>[^:]+):', s)
|
jpayne@69
|
1632 ))
|
jpayne@69
|
1633 s = s.upper()
|
jpayne@69
|
1634 if not s.strip():
|
jpayne@69
|
1635 raise ValueError("empty string")
|
jpayne@69
|
1636 if unfold:
|
jpayne@69
|
1637 lines = s.splitlines()
|
jpayne@69
|
1638 i = 0
|
jpayne@69
|
1639 while i < len(lines):
|
jpayne@69
|
1640 line = lines[i].rstrip()
|
jpayne@69
|
1641 if not line:
|
jpayne@69
|
1642 del lines[i]
|
jpayne@69
|
1643 elif i > 0 and line[0] == " ":
|
jpayne@69
|
1644 lines[i-1] += line[1:]
|
jpayne@69
|
1645 del lines[i]
|
jpayne@69
|
1646 else:
|
jpayne@69
|
1647 i += 1
|
jpayne@69
|
1648 else:
|
jpayne@69
|
1649 lines = s.split()
|
jpayne@69
|
1650 if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
|
jpayne@69
|
1651 s.startswith('RRULE:'))):
|
jpayne@69
|
1652 return self._parse_rfc_rrule(lines[0], cache=cache,
|
jpayne@69
|
1653 dtstart=dtstart, ignoretz=ignoretz,
|
jpayne@69
|
1654 tzinfos=tzinfos)
|
jpayne@69
|
1655 else:
|
jpayne@69
|
1656 rrulevals = []
|
jpayne@69
|
1657 rdatevals = []
|
jpayne@69
|
1658 exrulevals = []
|
jpayne@69
|
1659 exdatevals = []
|
jpayne@69
|
1660 for line in lines:
|
jpayne@69
|
1661 if not line:
|
jpayne@69
|
1662 continue
|
jpayne@69
|
1663 if line.find(':') == -1:
|
jpayne@69
|
1664 name = "RRULE"
|
jpayne@69
|
1665 value = line
|
jpayne@69
|
1666 else:
|
jpayne@69
|
1667 name, value = line.split(':', 1)
|
jpayne@69
|
1668 parms = name.split(';')
|
jpayne@69
|
1669 if not parms:
|
jpayne@69
|
1670 raise ValueError("empty property name")
|
jpayne@69
|
1671 name = parms[0]
|
jpayne@69
|
1672 parms = parms[1:]
|
jpayne@69
|
1673 if name == "RRULE":
|
jpayne@69
|
1674 for parm in parms:
|
jpayne@69
|
1675 raise ValueError("unsupported RRULE parm: "+parm)
|
jpayne@69
|
1676 rrulevals.append(value)
|
jpayne@69
|
1677 elif name == "RDATE":
|
jpayne@69
|
1678 for parm in parms:
|
jpayne@69
|
1679 if parm != "VALUE=DATE-TIME":
|
jpayne@69
|
1680 raise ValueError("unsupported RDATE parm: "+parm)
|
jpayne@69
|
1681 rdatevals.append(value)
|
jpayne@69
|
1682 elif name == "EXRULE":
|
jpayne@69
|
1683 for parm in parms:
|
jpayne@69
|
1684 raise ValueError("unsupported EXRULE parm: "+parm)
|
jpayne@69
|
1685 exrulevals.append(value)
|
jpayne@69
|
1686 elif name == "EXDATE":
|
jpayne@69
|
1687 exdatevals.extend(
|
jpayne@69
|
1688 self._parse_date_value(value, parms,
|
jpayne@69
|
1689 TZID_NAMES, ignoretz,
|
jpayne@69
|
1690 tzids, tzinfos)
|
jpayne@69
|
1691 )
|
jpayne@69
|
1692 elif name == "DTSTART":
|
jpayne@69
|
1693 dtvals = self._parse_date_value(value, parms, TZID_NAMES,
|
jpayne@69
|
1694 ignoretz, tzids, tzinfos)
|
jpayne@69
|
1695 if len(dtvals) != 1:
|
jpayne@69
|
1696 raise ValueError("Multiple DTSTART values specified:" +
|
jpayne@69
|
1697 value)
|
jpayne@69
|
1698 dtstart = dtvals[0]
|
jpayne@69
|
1699 else:
|
jpayne@69
|
1700 raise ValueError("unsupported property: "+name)
|
jpayne@69
|
1701 if (forceset or len(rrulevals) > 1 or rdatevals
|
jpayne@69
|
1702 or exrulevals or exdatevals):
|
jpayne@69
|
1703 if not parser and (rdatevals or exdatevals):
|
jpayne@69
|
1704 from dateutil import parser
|
jpayne@69
|
1705 rset = rruleset(cache=cache)
|
jpayne@69
|
1706 for value in rrulevals:
|
jpayne@69
|
1707 rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
jpayne@69
|
1708 ignoretz=ignoretz,
|
jpayne@69
|
1709 tzinfos=tzinfos))
|
jpayne@69
|
1710 for value in rdatevals:
|
jpayne@69
|
1711 for datestr in value.split(','):
|
jpayne@69
|
1712 rset.rdate(parser.parse(datestr,
|
jpayne@69
|
1713 ignoretz=ignoretz,
|
jpayne@69
|
1714 tzinfos=tzinfos))
|
jpayne@69
|
1715 for value in exrulevals:
|
jpayne@69
|
1716 rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
jpayne@69
|
1717 ignoretz=ignoretz,
|
jpayne@69
|
1718 tzinfos=tzinfos))
|
jpayne@69
|
1719 for value in exdatevals:
|
jpayne@69
|
1720 rset.exdate(value)
|
jpayne@69
|
1721 if compatible and dtstart:
|
jpayne@69
|
1722 rset.rdate(dtstart)
|
jpayne@69
|
1723 return rset
|
jpayne@69
|
1724 else:
|
jpayne@69
|
1725 return self._parse_rfc_rrule(rrulevals[0],
|
jpayne@69
|
1726 dtstart=dtstart,
|
jpayne@69
|
1727 cache=cache,
|
jpayne@69
|
1728 ignoretz=ignoretz,
|
jpayne@69
|
1729 tzinfos=tzinfos)
|
jpayne@69
|
1730
|
jpayne@69
|
1731 def __call__(self, s, **kwargs):
|
jpayne@69
|
1732 return self._parse_rfc(s, **kwargs)
|
jpayne@69
|
1733
|
jpayne@69
|
1734
|
jpayne@69
|
1735 rrulestr = _rrulestr()
|
jpayne@69
|
1736
|
jpayne@69
|
1737 # vim:ts=4:sw=4:et
|