annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/dateutil/rrule.py @ 68:5028fdace37b

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