annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/dateutil/rrule.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
rev   line source
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