summaryrefslogtreecommitdiff
path: root/src/dateutil
diff options
context:
space:
mode:
Diffstat (limited to 'src/dateutil')
-rw-r--r--src/dateutil/__init__.py24
-rw-r--r--src/dateutil/_common.py43
-rw-r--r--src/dateutil/easter.py89
-rw-r--r--src/dateutil/parser/__init__.py61
-rw-r--r--src/dateutil/parser/_parser.py1613
-rw-r--r--src/dateutil/parser/isoparser.py416
-rw-r--r--src/dateutil/relativedelta.py599
-rw-r--r--src/dateutil/rrule.py1737
-rw-r--r--src/dateutil/test/__init__.py0
-rw-r--r--src/dateutil/test/_common.py233
-rw-r--r--src/dateutil/test/conftest.py41
-rw-r--r--src/dateutil/test/property/test_isoparse_prop.py27
-rw-r--r--src/dateutil/test/property/test_parser_prop.py22
-rw-r--r--src/dateutil/test/property/test_tz_prop.py35
-rw-r--r--src/dateutil/test/test_easter.py93
-rw-r--r--src/dateutil/test/test_import_star.py33
-rw-r--r--src/dateutil/test/test_imports.py240
-rw-r--r--src/dateutil/test/test_internals.py91
-rw-r--r--src/dateutil/test/test_isoparser.py509
-rw-r--r--src/dateutil/test/test_parser.py964
-rw-r--r--src/dateutil/test/test_relativedelta.py706
-rw-r--r--src/dateutil/test/test_rrule.py4914
-rw-r--r--src/dateutil/test/test_tz.py2811
-rw-r--r--src/dateutil/test/test_utils.py52
-rw-r--r--src/dateutil/tz/__init__.py12
-rw-r--r--src/dateutil/tz/_common.py419
-rw-r--r--src/dateutil/tz/_factories.py80
-rw-r--r--src/dateutil/tz/tz.py1849
-rw-r--r--src/dateutil/tz/win.py370
-rw-r--r--src/dateutil/tzwin.py2
-rw-r--r--src/dateutil/utils.py71
-rw-r--r--src/dateutil/zoneinfo/__init__.py167
-rw-r--r--src/dateutil/zoneinfo/rebuild.py75
33 files changed, 18398 insertions, 0 deletions
diff --git a/src/dateutil/__init__.py b/src/dateutil/__init__.py
new file mode 100644
index 0000000..a2c19c0
--- /dev/null
+++ b/src/dateutil/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+import sys
+
+try:
+ from ._version import version as __version__
+except ImportError:
+ __version__ = 'unknown'
+
+__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
+ 'utils', 'zoneinfo']
+
+def __getattr__(name):
+ import importlib
+
+ if name in __all__:
+ return importlib.import_module("." + name, __name__)
+ raise AttributeError(
+ "module {!r} has not attribute {!r}".format(__name__, name)
+ )
+
+
+def __dir__():
+ # __dir__ should include all the lazy-importable modules as well.
+ return [x for x in globals() if x not in sys.modules] + __all__
diff --git a/src/dateutil/_common.py b/src/dateutil/_common.py
new file mode 100644
index 0000000..4eb2659
--- /dev/null
+++ b/src/dateutil/_common.py
@@ -0,0 +1,43 @@
+"""
+Common code used in multiple modules.
+"""
+
+
+class weekday(object):
+ __slots__ = ["weekday", "n"]
+
+ def __init__(self, weekday, n=None):
+ self.weekday = weekday
+ self.n = n
+
+ def __call__(self, n):
+ if n == self.n:
+ return self
+ else:
+ return self.__class__(self.weekday, n)
+
+ def __eq__(self, other):
+ try:
+ if self.weekday != other.weekday or self.n != other.n:
+ return False
+ except AttributeError:
+ return False
+ return True
+
+ def __hash__(self):
+ return hash((
+ self.weekday,
+ self.n,
+ ))
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+ if not self.n:
+ return s
+ else:
+ return "%s(%+d)" % (s, self.n)
+
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/easter.py b/src/dateutil/easter.py
new file mode 100644
index 0000000..f74d1f7
--- /dev/null
+++ b/src/dateutil/easter.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a generic Easter computing method for any given year, using
+Western, Orthodox or Julian algorithms.
+"""
+
+import datetime
+
+__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
+
+EASTER_JULIAN = 1
+EASTER_ORTHODOX = 2
+EASTER_WESTERN = 3
+
+
+def easter(year, method=EASTER_WESTERN):
+ """
+ This method was ported from the work done by GM Arts,
+ on top of the algorithm by Claus Tondering, which was
+ based in part on the algorithm of Ouding (1940), as
+ quoted in "Explanatory Supplement to the Astronomical
+ Almanac", P. Kenneth Seidelmann, editor.
+
+ This algorithm implements three different Easter
+ calculation methods:
+
+ 1. Original calculation in Julian calendar, valid in
+ dates after 326 AD
+ 2. Original method, with date converted to Gregorian
+ calendar, valid in years 1583 to 4099
+ 3. Revised method, in Gregorian calendar, valid in
+ years 1583 to 4099 as well
+
+ These methods are represented by the constants:
+
+ * ``EASTER_JULIAN = 1``
+ * ``EASTER_ORTHODOX = 2``
+ * ``EASTER_WESTERN = 3``
+
+ The default method is method 3.
+
+ More about the algorithm may be found at:
+
+ `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
+
+ and
+
+ `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
+
+ """
+
+ if not (1 <= method <= 3):
+ raise ValueError("invalid method")
+
+ # g - Golden year - 1
+ # c - Century
+ # h - (23 - Epact) mod 30
+ # i - Number of days from March 21 to Paschal Full Moon
+ # j - Weekday for PFM (0=Sunday, etc)
+ # p - Number of days from March 21 to Sunday on or before PFM
+ # (-6 to 28 methods 1 & 3, to 56 for method 2)
+ # e - Extra days to add for method 2 (converting Julian
+ # date to Gregorian date)
+
+ y = year
+ g = y % 19
+ e = 0
+ if method < 3:
+ # Old method
+ i = (19*g + 15) % 30
+ j = (y + y//4 + i) % 7
+ if method == 2:
+ # Extra dates to convert Julian to Gregorian date
+ e = 10
+ if y > 1600:
+ e = e + y//100 - 16 - (y//100 - 16)//4
+ else:
+ # New method
+ c = y//100
+ h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
+ i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
+ j = (y + y//4 + i + 2 - c + c//4) % 7
+
+ # p can be from -6 to 56 corresponding to dates 22 March to 23 May
+ # (later dates apply to method 2, although 23 May never actually occurs)
+ p = i - j + e
+ d = 1 + (p + 27 + (p + 6)//40) % 31
+ m = 3 + (p + 26)//30
+ return datetime.date(int(y), int(m), int(d))
diff --git a/src/dateutil/parser/__init__.py b/src/dateutil/parser/__init__.py
new file mode 100644
index 0000000..d174b0e
--- /dev/null
+++ b/src/dateutil/parser/__init__.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+from ._parser import parse, parser, parserinfo, ParserError
+from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
+from ._parser import UnknownTimezoneWarning
+
+from ._parser import __doc__
+
+from .isoparser import isoparser, isoparse
+
+__all__ = ['parse', 'parser', 'parserinfo',
+ 'isoparse', 'isoparser',
+ 'ParserError',
+ 'UnknownTimezoneWarning']
+
+
+###
+# Deprecate portions of the private interface so that downstream code that
+# is improperly relying on it is given *some* notice.
+
+
+def __deprecated_private_func(f):
+ from functools import wraps
+ import warnings
+
+ msg = ('{name} is a private function and may break without warning, '
+ 'it will be moved and or renamed in future versions.')
+ msg = msg.format(name=f.__name__)
+
+ @wraps(f)
+ def deprecated_func(*args, **kwargs):
+ warnings.warn(msg, DeprecationWarning)
+ return f(*args, **kwargs)
+
+ return deprecated_func
+
+def __deprecate_private_class(c):
+ import warnings
+
+ msg = ('{name} is a private class and may break without warning, '
+ 'it will be moved and or renamed in future versions.')
+ msg = msg.format(name=c.__name__)
+
+ class private_class(c):
+ __doc__ = c.__doc__
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn(msg, DeprecationWarning)
+ super(private_class, self).__init__(*args, **kwargs)
+
+ private_class.__name__ = c.__name__
+
+ return private_class
+
+
+from ._parser import _timelex, _resultbase
+from ._parser import _tzparser, _parsetz
+
+_timelex = __deprecate_private_class(_timelex)
+_tzparser = __deprecate_private_class(_tzparser)
+_resultbase = __deprecate_private_class(_resultbase)
+_parsetz = __deprecated_private_func(_parsetz)
diff --git a/src/dateutil/parser/_parser.py b/src/dateutil/parser/_parser.py
new file mode 100644
index 0000000..37d1663
--- /dev/null
+++ b/src/dateutil/parser/_parser.py
@@ -0,0 +1,1613 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a generic date/time string parser which is able to parse
+most known formats to represent a date and/or time.
+
+This module attempts to be forgiving with regards to unlikely input formats,
+returning a datetime object even for dates which are ambiguous. If an element
+of a date/time stamp is omitted, the following rules are applied:
+
+- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour
+ on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
+ specified.
+- If a time zone is omitted, a timezone-naive datetime is returned.
+
+If any other elements are missing, they are taken from the
+:class:`datetime.datetime` object passed to the parameter ``default``. If this
+results in a day number exceeding the valid number of days per month, the
+value falls back to the end of the month.
+
+Additional resources about date/time string formats can be found below:
+
+- `A summary of the international standard date and time notation
+ <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
+- `W3C Date and Time Formats <https://www.w3.org/TR/NOTE-datetime>`_
+- `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_
+- `CPAN ParseDate module
+ <https://metacpan.org/pod/release/MUIR/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_
+- `Java SimpleDateFormat Class
+ <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_
+"""
+from __future__ import unicode_literals
+
+import datetime
+import re
+import string
+import time
+import warnings
+
+from calendar import monthrange
+from io import StringIO
+
+import six
+from six import integer_types, text_type
+
+from decimal import Decimal
+
+from warnings import warn
+
+from .. import relativedelta
+from .. import tz
+
+__all__ = ["parse", "parserinfo", "ParserError"]
+
+
+# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth
+# making public and/or figuring out if there is something we can
+# take off their plate.
+class _timelex(object):
+ # Fractional seconds are sometimes split by a comma
+ _split_decimal = re.compile("([.,])")
+
+ def __init__(self, instream):
+ if isinstance(instream, (bytes, bytearray)):
+ instream = instream.decode()
+
+ if isinstance(instream, text_type):
+ instream = StringIO(instream)
+ elif getattr(instream, 'read', None) is None:
+ raise TypeError('Parser must be a string or character stream, not '
+ '{itype}'.format(itype=instream.__class__.__name__))
+
+ self.instream = instream
+ self.charstack = []
+ self.tokenstack = []
+ self.eof = False
+
+ def get_token(self):
+ """
+ This function breaks the time string into lexical units (tokens), which
+ can be parsed by the parser. Lexical units are demarcated by changes in
+ the character set, so any continuous string of letters is considered
+ one unit, any continuous string of numbers is considered one unit.
+
+ The main complication arises from the fact that dots ('.') can be used
+ both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
+ "4:30:21.447"). As such, it is necessary to read the full context of
+ any dot-separated strings before breaking it into tokens; as such, this
+ function maintains a "token stack", for when the ambiguous context
+ demands that multiple tokens be parsed at once.
+ """
+ if self.tokenstack:
+ return self.tokenstack.pop(0)
+
+ seenletters = False
+ token = None
+ state = None
+
+ while not self.eof:
+ # We only realize that we've reached the end of a token when we
+ # find a character that's not part of the current token - since
+ # that character may be part of the next token, it's stored in the
+ # charstack.
+ if self.charstack:
+ nextchar = self.charstack.pop(0)
+ else:
+ nextchar = self.instream.read(1)
+ while nextchar == '\x00':
+ nextchar = self.instream.read(1)
+
+ if not nextchar:
+ self.eof = True
+ break
+ elif not state:
+ # First character of the token - determines if we're starting
+ # to parse a word, a number or something else.
+ token = nextchar
+ if self.isword(nextchar):
+ state = 'a'
+ elif self.isnum(nextchar):
+ state = '0'
+ elif self.isspace(nextchar):
+ token = ' '
+ break # emit token
+ else:
+ break # emit token
+ elif state == 'a':
+ # If we've already started reading a word, we keep reading
+ # letters until we find something that's not part of a word.
+ seenletters = True
+ if self.isword(nextchar):
+ token += nextchar
+ elif nextchar == '.':
+ token += nextchar
+ state = 'a.'
+ else:
+ self.charstack.append(nextchar)
+ break # emit token
+ elif state == '0':
+ # If we've already started reading a number, we keep reading
+ # numbers until we find something that doesn't fit.
+ if self.isnum(nextchar):
+ token += nextchar
+ elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
+ token += nextchar
+ state = '0.'
+ else:
+ self.charstack.append(nextchar)
+ break # emit token
+ elif state == 'a.':
+ # If we've seen some letters and a dot separator, continue
+ # parsing, and the tokens will be broken up later.
+ seenletters = True
+ if nextchar == '.' or self.isword(nextchar):
+ token += nextchar
+ elif self.isnum(nextchar) and token[-1] == '.':
+ token += nextchar
+ state = '0.'
+ else:
+ self.charstack.append(nextchar)
+ break # emit token
+ elif state == '0.':
+ # If we've seen at least one dot separator, keep going, we'll
+ # break up the tokens later.
+ if nextchar == '.' or self.isnum(nextchar):
+ token += nextchar
+ elif self.isword(nextchar) and token[-1] == '.':
+ token += nextchar
+ state = 'a.'
+ else:
+ self.charstack.append(nextchar)
+ break # emit token
+
+ if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
+ token[-1] in '.,')):
+ l = self._split_decimal.split(token)
+ token = l[0]
+ for tok in l[1:]:
+ if tok:
+ self.tokenstack.append(tok)
+
+ if state == '0.' and token.count('.') == 0:
+ token = token.replace(',', '.')
+
+ return token
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ token = self.get_token()
+ if token is None:
+ raise StopIteration
+
+ return token
+
+ def next(self):
+ return self.__next__() # Python 2.x support
+
+ @classmethod
+ def split(cls, s):
+ return list(cls(s))
+
+ @classmethod
+ def isword(cls, nextchar):
+ """ Whether or not the next character is part of a word """
+ return nextchar.isalpha()
+
+ @classmethod
+ def isnum(cls, nextchar):
+ """ Whether the next character is part of a number """
+ return nextchar.isdigit()
+
+ @classmethod
+ def isspace(cls, nextchar):
+ """ Whether the next character is whitespace """
+ return nextchar.isspace()
+
+
+class _resultbase(object):
+
+ def __init__(self):
+ for attr in self.__slots__:
+ setattr(self, attr, None)
+
+ def _repr(self, classname):
+ l = []
+ for attr in self.__slots__:
+ value = getattr(self, attr)
+ if value is not None:
+ l.append("%s=%s" % (attr, repr(value)))
+ return "%s(%s)" % (classname, ", ".join(l))
+
+ def __len__(self):
+ return (sum(getattr(self, attr) is not None
+ for attr in self.__slots__))
+
+ def __repr__(self):
+ return self._repr(self.__class__.__name__)
+
+
+class parserinfo(object):
+ """
+ Class which handles what inputs are accepted. Subclass this to customize
+ the language and acceptable values for each parameter.
+
+ :param dayfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+ ``yearfirst`` is set to ``True``, this distinguishes between YDM
+ and YMD. Default is ``False``.
+
+ :param yearfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+ to be the year, otherwise the last number is taken to be the year.
+ Default is ``False``.
+ """
+
+ # m from a.m/p.m, t from ISO T separator
+ JUMP = [" ", ".", ",", ";", "-", "/", "'",
+ "at", "on", "and", "ad", "m", "t", "of",
+ "st", "nd", "rd", "th"]
+
+ WEEKDAYS = [("Mon", "Monday"),
+ ("Tue", "Tuesday"), # TODO: "Tues"
+ ("Wed", "Wednesday"),
+ ("Thu", "Thursday"), # TODO: "Thurs"
+ ("Fri", "Friday"),
+ ("Sat", "Saturday"),
+ ("Sun", "Sunday")]
+ MONTHS = [("Jan", "January"),
+ ("Feb", "February"), # TODO: "Febr"
+ ("Mar", "March"),
+ ("Apr", "April"),
+ ("May", "May"),
+ ("Jun", "June"),
+ ("Jul", "July"),
+ ("Aug", "August"),
+ ("Sep", "Sept", "September"),
+ ("Oct", "October"),
+ ("Nov", "November"),
+ ("Dec", "December")]
+ HMS = [("h", "hour", "hours"),
+ ("m", "minute", "minutes"),
+ ("s", "second", "seconds")]
+ AMPM = [("am", "a"),
+ ("pm", "p")]
+ UTCZONE = ["UTC", "GMT", "Z", "z"]
+ PERTAIN = ["of"]
+ TZOFFSET = {}
+ # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
+ # "Anno Domini", "Year of Our Lord"]
+
+ def __init__(self, dayfirst=False, yearfirst=False):
+ self._jump = self._convert(self.JUMP)
+ self._weekdays = self._convert(self.WEEKDAYS)
+ self._months = self._convert(self.MONTHS)
+ self._hms = self._convert(self.HMS)
+ self._ampm = self._convert(self.AMPM)
+ self._utczone = self._convert(self.UTCZONE)
+ self._pertain = self._convert(self.PERTAIN)
+
+ self.dayfirst = dayfirst
+ self.yearfirst = yearfirst
+
+ self._year = time.localtime().tm_year
+ self._century = self._year // 100 * 100
+
+ def _convert(self, lst):
+ dct = {}
+ for i, v in enumerate(lst):
+ if isinstance(v, tuple):
+ for v in v:
+ dct[v.lower()] = i
+ else:
+ dct[v.lower()] = i
+ return dct
+
+ def jump(self, name):
+ return name.lower() in self._jump
+
+ def weekday(self, name):
+ try:
+ return self._weekdays[name.lower()]
+ except KeyError:
+ pass
+ return None
+
+ def month(self, name):
+ try:
+ return self._months[name.lower()] + 1
+ except KeyError:
+ pass
+ return None
+
+ def hms(self, name):
+ try:
+ return self._hms[name.lower()]
+ except KeyError:
+ return None
+
+ def ampm(self, name):
+ try:
+ return self._ampm[name.lower()]
+ except KeyError:
+ return None
+
+ def pertain(self, name):
+ return name.lower() in self._pertain
+
+ def utczone(self, name):
+ return name.lower() in self._utczone
+
+ def tzoffset(self, name):
+ if name in self._utczone:
+ return 0
+
+ return self.TZOFFSET.get(name)
+
+ def convertyear(self, year, century_specified=False):
+ """
+ Converts two-digit years to year within [-50, 49]
+ range of self._year (current local time)
+ """
+
+ # Function contract is that the year is always positive
+ assert year >= 0
+
+ if year < 100 and not century_specified:
+ # assume current century to start
+ year += self._century
+
+ if year >= self._year + 50: # if too far in future
+ year -= 100
+ elif year < self._year - 50: # if too far in past
+ year += 100
+
+ return year
+
+ def validate(self, res):
+ # move to info
+ if res.year is not None:
+ res.year = self.convertyear(res.year, res.century_specified)
+
+ if ((res.tzoffset == 0 and not res.tzname) or
+ (res.tzname == 'Z' or res.tzname == 'z')):
+ res.tzname = "UTC"
+ res.tzoffset = 0
+ elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
+ res.tzoffset = 0
+ return True
+
+
+class _ymd(list):
+ def __init__(self, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+ self.century_specified = False
+ self.dstridx = None
+ self.mstridx = None
+ self.ystridx = None
+
+ @property
+ def has_year(self):
+ return self.ystridx is not None
+
+ @property
+ def has_month(self):
+ return self.mstridx is not None
+
+ @property
+ def has_day(self):
+ return self.dstridx is not None
+
+ def could_be_day(self, value):
+ if self.has_day:
+ return False
+ elif not self.has_month:
+ return 1 <= value <= 31
+ elif not self.has_year:
+ # Be permissive, assume leap year
+ month = self[self.mstridx]
+ return 1 <= value <= monthrange(2000, month)[1]
+ else:
+ month = self[self.mstridx]
+ year = self[self.ystridx]
+ return 1 <= value <= monthrange(year, month)[1]
+
+ def append(self, val, label=None):
+ if hasattr(val, '__len__'):
+ if val.isdigit() and len(val) > 2:
+ self.century_specified = True
+ if label not in [None, 'Y']: # pragma: no cover
+ raise ValueError(label)
+ label = 'Y'
+ elif val > 100:
+ self.century_specified = True
+ if label not in [None, 'Y']: # pragma: no cover
+ raise ValueError(label)
+ label = 'Y'
+
+ super(self.__class__, self).append(int(val))
+
+ if label == 'M':
+ if self.has_month:
+ raise ValueError('Month is already set')
+ self.mstridx = len(self) - 1
+ elif label == 'D':
+ if self.has_day:
+ raise ValueError('Day is already set')
+ self.dstridx = len(self) - 1
+ elif label == 'Y':
+ if self.has_year:
+ raise ValueError('Year is already set')
+ self.ystridx = len(self) - 1
+
+ def _resolve_from_stridxs(self, strids):
+ """
+ Try to resolve the identities of year/month/day elements using
+ ystridx, mstridx, and dstridx, if enough of these are specified.
+ """
+ if len(self) == 3 and len(strids) == 2:
+ # we can back out the remaining stridx value
+ missing = [x for x in range(3) if x not in strids.values()]
+ key = [x for x in ['y', 'm', 'd'] if x not in strids]
+ assert len(missing) == len(key) == 1
+ key = key[0]
+ val = missing[0]
+ strids[key] = val
+
+ assert len(self) == len(strids) # otherwise this should not be called
+ out = {key: self[strids[key]] for key in strids}
+ return (out.get('y'), out.get('m'), out.get('d'))
+
+ def resolve_ymd(self, yearfirst, dayfirst):
+ len_ymd = len(self)
+ year, month, day = (None, None, None)
+
+ strids = (('y', self.ystridx),
+ ('m', self.mstridx),
+ ('d', self.dstridx))
+
+ strids = {key: val for key, val in strids if val is not None}
+ if (len(self) == len(strids) > 0 or
+ (len(self) == 3 and len(strids) == 2)):
+ return self._resolve_from_stridxs(strids)
+
+ mstridx = self.mstridx
+
+ if len_ymd > 3:
+ raise ValueError("More than three YMD values")
+ elif len_ymd == 1 or (mstridx is not None and len_ymd == 2):
+ # One member, or two members with a month string
+ if mstridx is not None:
+ month = self[mstridx]
+ # since mstridx is 0 or 1, self[mstridx-1] always
+ # looks up the other element
+ other = self[mstridx - 1]
+ else:
+ other = self[0]
+
+ if len_ymd > 1 or mstridx is None:
+ if other > 31:
+ year = other
+ else:
+ day = other
+
+ elif len_ymd == 2:
+ # Two members with numbers
+ if self[0] > 31:
+ # 99-01
+ year, month = self
+ elif self[1] > 31:
+ # 01-99
+ month, year = self
+ elif dayfirst and self[1] <= 12:
+ # 13-01
+ day, month = self
+ else:
+ # 01-13
+ month, day = self
+
+ elif len_ymd == 3:
+ # Three members
+ if mstridx == 0:
+ if self[1] > 31:
+ # Apr-2003-25
+ month, year, day = self
+ else:
+ month, day, year = self
+ elif mstridx == 1:
+ if self[0] > 31 or (yearfirst and self[2] <= 31):
+ # 99-Jan-01
+ year, month, day = self
+ else:
+ # 01-Jan-01
+ # Give precedence to day-first, since
+ # two-digit years is usually hand-written.
+ day, month, year = self
+
+ elif mstridx == 2:
+ # WTF!?
+ if self[1] > 31:
+ # 01-99-Jan
+ day, year, month = self
+ else:
+ # 99-01-Jan
+ year, day, month = self
+
+ else:
+ if (self[0] > 31 or
+ self.ystridx == 0 or
+ (yearfirst and self[1] <= 12 and self[2] <= 31)):
+ # 99-01-01
+ if dayfirst and self[2] <= 12:
+ year, day, month = self
+ else:
+ year, month, day = self
+ elif self[0] > 12 or (dayfirst and self[1] <= 12):
+ # 13-01-01
+ day, month, year = self
+ else:
+ # 01-13-01
+ month, day, year = self
+
+ return year, month, day
+
+
+class parser(object):
+ def __init__(self, info=None):
+ self.info = info or parserinfo()
+
+ def parse(self, timestr, default=None,
+ ignoretz=False, tzinfos=None, **kwargs):
+ """
+ Parse the date/time string into a :class:`datetime.datetime` object.
+
+ :param timestr:
+ Any date/time string using the supported formats.
+
+ :param default:
+ The default datetime object, if this is a datetime object and not
+ ``None``, elements specified in ``timestr`` replace elements in the
+ default object.
+
+ :param ignoretz:
+ If set ``True``, time zones in parsed strings are ignored and a
+ naive :class:`datetime.datetime` object is returned.
+
+ :param tzinfos:
+ Additional time zone names / aliases which may be present in the
+ string. This argument maps time zone names (and optionally offsets
+ from those time zones) to time zones. This parameter can be a
+ dictionary with timezone aliases mapping time zone names to time
+ zones or a function taking two parameters (``tzname`` and
+ ``tzoffset``) and returning a time zone.
+
+ The timezones to which the names are mapped can be an integer
+ offset from UTC in seconds or a :class:`tzinfo` object.
+
+ .. doctest::
+ :options: +NORMALIZE_WHITESPACE
+
+ >>> from dateutil.parser import parse
+ >>> from dateutil.tz import gettz
+ >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
+ >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+ datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
+ >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+ datetime.datetime(2012, 1, 19, 17, 21,
+ tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+ This parameter is ignored if ``ignoretz`` is set.
+
+ :param \\*\\*kwargs:
+ Keyword arguments as passed to ``_parse()``.
+
+ :return:
+ Returns a :class:`datetime.datetime` object or, if the
+ ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+ first element being a :class:`datetime.datetime` object, the second
+ a tuple containing the fuzzy tokens.
+
+ :raises ParserError:
+ Raised for invalid or unknown string format, if the provided
+ :class:`tzinfo` is not in a valid format, or if an invalid date
+ would be created.
+
+ :raises TypeError:
+ Raised for non-string or character stream input.
+
+ :raises OverflowError:
+ Raised if the parsed date exceeds the largest valid C integer on
+ your system.
+ """
+
+ if default is None:
+ default = datetime.datetime.now().replace(hour=0, minute=0,
+ second=0, microsecond=0)
+
+ res, skipped_tokens = self._parse(timestr, **kwargs)
+
+ if res is None:
+ raise ParserError("Unknown string format: %s", timestr)
+
+ if len(res) == 0:
+ raise ParserError("String does not contain a date: %s", timestr)
+
+ try:
+ ret = self._build_naive(res, default)
+ except ValueError as e:
+ six.raise_from(ParserError(str(e) + ": %s", timestr), e)
+
+ if not ignoretz:
+ ret = self._build_tzaware(ret, res, tzinfos)
+
+ if kwargs.get('fuzzy_with_tokens', False):
+ return ret, skipped_tokens
+ else:
+ return ret
+
+ class _result(_resultbase):
+ __slots__ = ["year", "month", "day", "weekday",
+ "hour", "minute", "second", "microsecond",
+ "tzname", "tzoffset", "ampm","any_unused_tokens"]
+
+ def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
+ fuzzy_with_tokens=False):
+ """
+ Private method which performs the heavy lifting of parsing, called from
+ ``parse()``, which passes on its ``kwargs`` to this function.
+
+ :param timestr:
+ The string to parse.
+
+ :param dayfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+ ``yearfirst`` is set to ``True``, this distinguishes between YDM
+ and YMD. If set to ``None``, this value is retrieved from the
+ current :class:`parserinfo` object (which itself defaults to
+ ``False``).
+
+ :param yearfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+ to be the year, otherwise the last number is taken to be the year.
+ If this is set to ``None``, the value is retrieved from the current
+ :class:`parserinfo` object (which itself defaults to ``False``).
+
+ :param fuzzy:
+ Whether to allow fuzzy parsing, allowing for string like "Today is
+ January 1, 2047 at 8:21:00AM".
+
+ :param fuzzy_with_tokens:
+ If ``True``, ``fuzzy`` is automatically set to True, and the parser
+ will return a tuple where the first element is the parsed
+ :class:`datetime.datetime` datetimestamp and the second element is
+ a tuple containing the portions of the string which were ignored:
+
+ .. doctest::
+
+ >>> from dateutil.parser import parse
+ >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+ (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+ """
+ if fuzzy_with_tokens:
+ fuzzy = True
+
+ info = self.info
+
+ if dayfirst is None:
+ dayfirst = info.dayfirst
+
+ if yearfirst is None:
+ yearfirst = info.yearfirst
+
+ res = self._result()
+ l = _timelex.split(timestr) # Splits the timestr into tokens
+
+ skipped_idxs = []
+
+ # year/month/day list
+ ymd = _ymd()
+
+ len_l = len(l)
+ i = 0
+ try:
+ while i < len_l:
+
+ # Check if it's a number
+ value_repr = l[i]
+ try:
+ value = float(value_repr)
+ except ValueError:
+ value = None
+
+ if value is not None:
+ # Numeric token
+ i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy)
+
+ # Check weekday
+ elif info.weekday(l[i]) is not None:
+ value = info.weekday(l[i])
+ res.weekday = value
+
+ # Check month name
+ elif info.month(l[i]) is not None:
+ value = info.month(l[i])
+ ymd.append(value, 'M')
+
+ if i + 1 < len_l:
+ if l[i + 1] in ('-', '/'):
+ # Jan-01[-99]
+ sep = l[i + 1]
+ ymd.append(l[i + 2])
+
+ if i + 3 < len_l and l[i + 3] == sep:
+ # Jan-01-99
+ ymd.append(l[i + 4])
+ i += 2
+
+ i += 2
+
+ elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and
+ info.pertain(l[i + 2])):
+ # Jan of 01
+ # In this case, 01 is clearly year
+ if l[i + 4].isdigit():
+ # Convert it here to become unambiguous
+ value = int(l[i + 4])
+ year = str(info.convertyear(value))
+ ymd.append(year, 'Y')
+ else:
+ # Wrong guess
+ pass
+ # TODO: not hit in tests
+ i += 4
+
+ # Check am/pm
+ elif info.ampm(l[i]) is not None:
+ value = info.ampm(l[i])
+ val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy)
+
+ if val_is_ampm:
+ res.hour = self._adjust_ampm(res.hour, value)
+ res.ampm = value
+
+ elif fuzzy:
+ skipped_idxs.append(i)
+
+ # Check for a timezone name
+ elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]):
+ res.tzname = l[i]
+ res.tzoffset = info.tzoffset(res.tzname)
+
+ # Check for something like GMT+3, or BRST+3. Notice
+ # that it doesn't mean "I am 3 hours after GMT", but
+ # "my time +3 is GMT". If found, we reverse the
+ # logic so that timezone parsing code will get it
+ # right.
+ if i + 1 < len_l and l[i + 1] in ('+', '-'):
+ l[i + 1] = ('+', '-')[l[i + 1] == '+']
+ res.tzoffset = None
+ if info.utczone(res.tzname):
+ # With something like GMT+3, the timezone
+ # is *not* GMT.
+ res.tzname = None
+
+ # Check for a numbered timezone
+ elif res.hour is not None and l[i] in ('+', '-'):
+ signal = (-1, 1)[l[i] == '+']
+ len_li = len(l[i + 1])
+
+ # TODO: check that l[i + 1] is integer?
+ if len_li == 4:
+ # -0300
+ hour_offset = int(l[i + 1][:2])
+ min_offset = int(l[i + 1][2:])
+ elif i + 2 < len_l and l[i + 2] == ':':
+ # -03:00
+ hour_offset = int(l[i + 1])
+ min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like?
+ i += 2
+ elif len_li <= 2:
+ # -[0]3
+ hour_offset = int(l[i + 1][:2])
+ min_offset = 0
+ else:
+ raise ValueError(timestr)
+
+ res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60)
+
+ # Look for a timezone name between parenthesis
+ if (i + 5 < len_l and
+ info.jump(l[i + 2]) and l[i + 3] == '(' and
+ l[i + 5] == ')' and
+ 3 <= len(l[i + 4]) and
+ self._could_be_tzname(res.hour, res.tzname,
+ None, l[i + 4])):
+ # -0300 (BRST)
+ res.tzname = l[i + 4]
+ i += 4
+
+ i += 1
+
+ # Check jumps
+ elif not (info.jump(l[i]) or fuzzy):
+ raise ValueError(timestr)
+
+ else:
+ skipped_idxs.append(i)
+ i += 1
+
+ # Process year/month/day
+ year, month, day = ymd.resolve_ymd(yearfirst, dayfirst)
+
+ res.century_specified = ymd.century_specified
+ res.year = year
+ res.month = month
+ res.day = day
+
+ except (IndexError, ValueError):
+ return None, None
+
+ if not info.validate(res):
+ return None, None
+
+ if fuzzy_with_tokens:
+ skipped_tokens = self._recombine_skipped(l, skipped_idxs)
+ return res, tuple(skipped_tokens)
+ else:
+ return res, None
+
+ def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy):
+ # Token is a number
+ value_repr = tokens[idx]
+ try:
+ value = self._to_decimal(value_repr)
+ except Exception as e:
+ six.raise_from(ValueError('Unknown numeric token'), e)
+
+ len_li = len(value_repr)
+
+ len_l = len(tokens)
+
+ if (len(ymd) == 3 and len_li in (2, 4) and
+ res.hour is None and
+ (idx + 1 >= len_l or
+ (tokens[idx + 1] != ':' and
+ info.hms(tokens[idx + 1]) is None))):
+ # 19990101T23[59]
+ s = tokens[idx]
+ res.hour = int(s[:2])
+
+ if len_li == 4:
+ res.minute = int(s[2:])
+
+ elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6):
+ # YYMMDD or HHMMSS[.ss]
+ s = tokens[idx]
+
+ if not ymd and '.' not in tokens[idx]:
+ ymd.append(s[:2])
+ ymd.append(s[2:4])
+ ymd.append(s[4:])
+ else:
+ # 19990101T235959[.59]
+
+ # TODO: Check if res attributes already set.
+ res.hour = int(s[:2])
+ res.minute = int(s[2:4])
+ res.second, res.microsecond = self._parsems(s[4:])
+
+ elif len_li in (8, 12, 14):
+ # YYYYMMDD
+ s = tokens[idx]
+ ymd.append(s[:4], 'Y')
+ ymd.append(s[4:6])
+ ymd.append(s[6:8])
+
+ if len_li > 8:
+ res.hour = int(s[8:10])
+ res.minute = int(s[10:12])
+
+ if len_li > 12:
+ res.second = int(s[12:])
+
+ elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None:
+ # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+ hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True)
+ (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx)
+ if hms is not None:
+ # TODO: checking that hour/minute/second are not
+ # already set?
+ self._assign_hms(res, value_repr, hms)
+
+ elif idx + 2 < len_l and tokens[idx + 1] == ':':
+ # HH:MM[:SS[.ss]]
+ res.hour = int(value)
+ value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this?
+ (res.minute, res.second) = self._parse_min_sec(value)
+
+ if idx + 4 < len_l and tokens[idx + 3] == ':':
+ res.second, res.microsecond = self._parsems(tokens[idx + 4])
+
+ idx += 2
+
+ idx += 2
+
+ elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'):
+ sep = tokens[idx + 1]
+ ymd.append(value_repr)
+
+ if idx + 2 < len_l and not info.jump(tokens[idx + 2]):
+ if tokens[idx + 2].isdigit():
+ # 01-01[-01]
+ ymd.append(tokens[idx + 2])
+ else:
+ # 01-Jan[-01]
+ value = info.month(tokens[idx + 2])
+
+ if value is not None:
+ ymd.append(value, 'M')
+ else:
+ raise ValueError()
+
+ if idx + 3 < len_l and tokens[idx + 3] == sep:
+ # We have three members
+ value = info.month(tokens[idx + 4])
+
+ if value is not None:
+ ymd.append(value, 'M')
+ else:
+ ymd.append(tokens[idx + 4])
+ idx += 2
+
+ idx += 1
+ idx += 1
+
+ elif idx + 1 >= len_l or info.jump(tokens[idx + 1]):
+ if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None:
+ # 12 am
+ hour = int(value)
+ res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2]))
+ idx += 1
+ else:
+ # Year, month or day
+ ymd.append(value)
+ idx += 1
+
+ elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24):
+ # 12am
+ hour = int(value)
+ res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1]))
+ idx += 1
+
+ elif ymd.could_be_day(value):
+ ymd.append(value)
+
+ elif not fuzzy:
+ raise ValueError()
+
+ return idx
+
+ def _find_hms_idx(self, idx, tokens, info, allow_jump):
+ len_l = len(tokens)
+
+ if idx+1 < len_l and info.hms(tokens[idx+1]) is not None:
+ # There is an "h", "m", or "s" label following this token. We take
+ # assign the upcoming label to the current token.
+ # e.g. the "12" in 12h"
+ hms_idx = idx + 1
+
+ elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and
+ info.hms(tokens[idx+2]) is not None):
+ # There is a space and then an "h", "m", or "s" label.
+ # e.g. the "12" in "12 h"
+ hms_idx = idx + 2
+
+ elif idx > 0 and info.hms(tokens[idx-1]) is not None:
+ # There is a "h", "m", or "s" preceding this token. Since neither
+ # of the previous cases was hit, there is no label following this
+ # token, so we use the previous label.
+ # e.g. the "04" in "12h04"
+ hms_idx = idx-1
+
+ elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and
+ info.hms(tokens[idx-2]) is not None):
+ # If we are looking at the final token, we allow for a
+ # backward-looking check to skip over a space.
+ # TODO: Are we sure this is the right condition here?
+ hms_idx = idx - 2
+
+ else:
+ hms_idx = None
+
+ return hms_idx
+
+ def _assign_hms(self, res, value_repr, hms):
+ # See GH issue #427, fixing float rounding
+ value = self._to_decimal(value_repr)
+
+ if hms == 0:
+ # Hour
+ res.hour = int(value)
+ if value % 1:
+ res.minute = int(60*(value % 1))
+
+ elif hms == 1:
+ (res.minute, res.second) = self._parse_min_sec(value)
+
+ elif hms == 2:
+ (res.second, res.microsecond) = self._parsems(value_repr)
+
+ def _could_be_tzname(self, hour, tzname, tzoffset, token):
+ return (hour is not None and
+ tzname is None and
+ tzoffset is None and
+ len(token) <= 5 and
+ (all(x in string.ascii_uppercase for x in token)
+ or token in self.info.UTCZONE))
+
+ def _ampm_valid(self, hour, ampm, fuzzy):
+ """
+ For fuzzy parsing, 'a' or 'am' (both valid English words)
+ may erroneously trigger the AM/PM flag. Deal with that
+ here.
+ """
+ val_is_ampm = True
+
+ # If there's already an AM/PM flag, this one isn't one.
+ if fuzzy and ampm is not None:
+ val_is_ampm = False
+
+ # If AM/PM is found and hour is not, raise a ValueError
+ if hour is None:
+ if fuzzy:
+ val_is_ampm = False
+ else:
+ raise ValueError('No hour specified with AM or PM flag.')
+ elif not 0 <= hour <= 12:
+ # If AM/PM is found, it's a 12 hour clock, so raise
+ # an error for invalid range
+ if fuzzy:
+ val_is_ampm = False
+ else:
+ raise ValueError('Invalid hour specified for 12-hour clock.')
+
+ return val_is_ampm
+
+ def _adjust_ampm(self, hour, ampm):
+ if hour < 12 and ampm == 1:
+ hour += 12
+ elif hour == 12 and ampm == 0:
+ hour = 0
+ return hour
+
+ def _parse_min_sec(self, value):
+ # TODO: Every usage of this function sets res.second to the return
+ # value. Are there any cases where second will be returned as None and
+ # we *don't* want to set res.second = None?
+ minute = int(value)
+ second = None
+
+ sec_remainder = value % 1
+ if sec_remainder:
+ second = int(60 * sec_remainder)
+ return (minute, second)
+
+ def _parse_hms(self, idx, tokens, info, hms_idx):
+ # TODO: Is this going to admit a lot of false-positives for when we
+ # just happen to have digits and "h", "m" or "s" characters in non-date
+ # text? I guess hex hashes won't have that problem, but there's plenty
+ # of random junk out there.
+ if hms_idx is None:
+ hms = None
+ new_idx = idx
+ elif hms_idx > idx:
+ hms = info.hms(tokens[hms_idx])
+ new_idx = hms_idx
+ else:
+ # Looking backwards, increment one.
+ hms = info.hms(tokens[hms_idx]) + 1
+ new_idx = idx
+
+ return (new_idx, hms)
+
+ # ------------------------------------------------------------------
+ # Handling for individual tokens. These are kept as methods instead
+ # of functions for the sake of customizability via subclassing.
+
+ def _parsems(self, value):
+ """Parse a I[.F] seconds value into (seconds, microseconds)."""
+ if "." not in value:
+ return int(value), 0
+ else:
+ i, f = value.split(".")
+ return int(i), int(f.ljust(6, "0")[:6])
+
+ def _to_decimal(self, val):
+ try:
+ decimal_value = Decimal(val)
+ # See GH 662, edge case, infinite value should not be converted
+ # via `_to_decimal`
+ if not decimal_value.is_finite():
+ raise ValueError("Converted decimal value is infinite or NaN")
+ except Exception as e:
+ msg = "Could not convert %s to decimal" % val
+ six.raise_from(ValueError(msg), e)
+ else:
+ return decimal_value
+
+ # ------------------------------------------------------------------
+ # Post-Parsing construction of datetime output. These are kept as
+ # methods instead of functions for the sake of customizability via
+ # subclassing.
+
+ def _build_tzinfo(self, tzinfos, tzname, tzoffset):
+ if callable(tzinfos):
+ tzdata = tzinfos(tzname, tzoffset)
+ else:
+ tzdata = tzinfos.get(tzname)
+ # handle case where tzinfo is paased an options that returns None
+ # eg tzinfos = {'BRST' : None}
+ if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
+ tzinfo = tzdata
+ elif isinstance(tzdata, text_type):
+ tzinfo = tz.tzstr(tzdata)
+ elif isinstance(tzdata, integer_types):
+ tzinfo = tz.tzoffset(tzname, tzdata)
+ else:
+ raise TypeError("Offset must be tzinfo subclass, tz string, "
+ "or int offset.")
+ return tzinfo
+
+ def _build_tzaware(self, naive, res, tzinfos):
+ if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)):
+ tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
+ aware = naive.replace(tzinfo=tzinfo)
+ aware = self._assign_tzname(aware, res.tzname)
+
+ elif res.tzname and res.tzname in time.tzname:
+ aware = naive.replace(tzinfo=tz.tzlocal())
+
+ # Handle ambiguous local datetime
+ aware = self._assign_tzname(aware, res.tzname)
+
+ # This is mostly relevant for winter GMT zones parsed in the UK
+ if (aware.tzname() != res.tzname and
+ res.tzname in self.info.UTCZONE):
+ aware = aware.replace(tzinfo=tz.UTC)
+
+ elif res.tzoffset == 0:
+ aware = naive.replace(tzinfo=tz.UTC)
+
+ elif res.tzoffset:
+ aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+
+ elif not res.tzname and not res.tzoffset:
+ # i.e. no timezone information was found.
+ aware = naive
+
+ elif res.tzname:
+ # tz-like string was parsed but we don't know what to do
+ # with it
+ warnings.warn("tzname {tzname} identified but not understood. "
+ "Pass `tzinfos` argument in order to correctly "
+ "return a timezone-aware datetime. In a future "
+ "version, this will raise an "
+ "exception.".format(tzname=res.tzname),
+ category=UnknownTimezoneWarning)
+ aware = naive
+
+ return aware
+
+ def _build_naive(self, res, default):
+ repl = {}
+ for attr in ("year", "month", "day", "hour",
+ "minute", "second", "microsecond"):
+ value = getattr(res, attr)
+ if value is not None:
+ repl[attr] = value
+
+ if 'day' not in repl:
+ # If the default day exceeds the last day of the month, fall back
+ # to the end of the month.
+ cyear = default.year if res.year is None else res.year
+ cmonth = default.month if res.month is None else res.month
+ cday = default.day if res.day is None else res.day
+
+ if cday > monthrange(cyear, cmonth)[1]:
+ repl['day'] = monthrange(cyear, cmonth)[1]
+
+ naive = default.replace(**repl)
+
+ if res.weekday is not None and not res.day:
+ naive = naive + relativedelta.relativedelta(weekday=res.weekday)
+
+ return naive
+
+ def _assign_tzname(self, dt, tzname):
+ if dt.tzname() != tzname:
+ new_dt = tz.enfold(dt, fold=1)
+ if new_dt.tzname() == tzname:
+ return new_dt
+
+ return dt
+
+ def _recombine_skipped(self, tokens, skipped_idxs):
+ """
+ >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
+ >>> skipped_idxs = [0, 1, 2, 5]
+ >>> _recombine_skipped(tokens, skipped_idxs)
+ ["foo bar", "baz"]
+ """
+ skipped_tokens = []
+ for i, idx in enumerate(sorted(skipped_idxs)):
+ if i > 0 and idx - 1 == skipped_idxs[i - 1]:
+ skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
+ else:
+ skipped_tokens.append(tokens[idx])
+
+ return skipped_tokens
+
+
+DEFAULTPARSER = parser()
+
+
+def parse(timestr, parserinfo=None, **kwargs):
+ """
+
+ Parse a string in one of the supported formats, using the
+ ``parserinfo`` parameters.
+
+ :param timestr:
+ A string containing a date/time stamp.
+
+ :param parserinfo:
+ A :class:`parserinfo` object containing parameters for the parser.
+ If ``None``, the default arguments to the :class:`parserinfo`
+ constructor are used.
+
+ The ``**kwargs`` parameter takes the following keyword arguments:
+
+ :param default:
+ The default datetime object, if this is a datetime object and not
+ ``None``, elements specified in ``timestr`` replace elements in the
+ default object.
+
+ :param ignoretz:
+ If set ``True``, time zones in parsed strings are ignored and a naive
+ :class:`datetime` object is returned.
+
+ :param tzinfos:
+ Additional time zone names / aliases which may be present in the
+ string. This argument maps time zone names (and optionally offsets
+ from those time zones) to time zones. This parameter can be a
+ dictionary with timezone aliases mapping time zone names to time
+ zones or a function taking two parameters (``tzname`` and
+ ``tzoffset``) and returning a time zone.
+
+ The timezones to which the names are mapped can be an integer
+ offset from UTC in seconds or a :class:`tzinfo` object.
+
+ .. doctest::
+ :options: +NORMALIZE_WHITESPACE
+
+ >>> from dateutil.parser import parse
+ >>> from dateutil.tz import gettz
+ >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
+ >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+ datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
+ >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+ datetime.datetime(2012, 1, 19, 17, 21,
+ tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+ This parameter is ignored if ``ignoretz`` is set.
+
+ :param dayfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+ ``yearfirst`` is set to ``True``, this distinguishes between YDM and
+ YMD. If set to ``None``, this value is retrieved from the current
+ :class:`parserinfo` object (which itself defaults to ``False``).
+
+ :param yearfirst:
+ Whether to interpret the first value in an ambiguous 3-integer date
+ (e.g. 01/05/09) as the year. If ``True``, the first number is taken to
+ be the year, otherwise the last number is taken to be the year. If
+ this is set to ``None``, the value is retrieved from the current
+ :class:`parserinfo` object (which itself defaults to ``False``).
+
+ :param fuzzy:
+ Whether to allow fuzzy parsing, allowing for string like "Today is
+ January 1, 2047 at 8:21:00AM".
+
+ :param fuzzy_with_tokens:
+ If ``True``, ``fuzzy`` is automatically set to True, and the parser
+ will return a tuple where the first element is the parsed
+ :class:`datetime.datetime` datetimestamp and the second element is
+ a tuple containing the portions of the string which were ignored:
+
+ .. doctest::
+
+ >>> from dateutil.parser import parse
+ >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+ (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+ :return:
+ Returns a :class:`datetime.datetime` object or, if the
+ ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+ first element being a :class:`datetime.datetime` object, the second
+ a tuple containing the fuzzy tokens.
+
+ :raises ParserError:
+ Raised for invalid or unknown string formats, if the provided
+ :class:`tzinfo` is not in a valid format, or if an invalid date would
+ be created.
+
+ :raises OverflowError:
+ Raised if the parsed date exceeds the largest valid C integer on
+ your system.
+ """
+ if parserinfo:
+ return parser(parserinfo).parse(timestr, **kwargs)
+ else:
+ return DEFAULTPARSER.parse(timestr, **kwargs)
+
+
+class _tzparser(object):
+
+ class _result(_resultbase):
+
+ __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
+ "start", "end"]
+
+ class _attr(_resultbase):
+ __slots__ = ["month", "week", "weekday",
+ "yday", "jyday", "day", "time"]
+
+ def __repr__(self):
+ return self._repr("")
+
+ def __init__(self):
+ _resultbase.__init__(self)
+ self.start = self._attr()
+ self.end = self._attr()
+
+ def parse(self, tzstr):
+ res = self._result()
+ l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x]
+ used_idxs = list()
+ try:
+
+ len_l = len(l)
+
+ i = 0
+ while i < len_l:
+ # BRST+3[BRDT[+2]]
+ j = i
+ while j < len_l and not [x for x in l[j]
+ if x in "0123456789:,-+"]:
+ j += 1
+ if j != i:
+ if not res.stdabbr:
+ offattr = "stdoffset"
+ res.stdabbr = "".join(l[i:j])
+ else:
+ offattr = "dstoffset"
+ res.dstabbr = "".join(l[i:j])
+
+ for ii in range(j):
+ used_idxs.append(ii)
+ i = j
+ if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
+ "0123456789")):
+ if l[i] in ('+', '-'):
+ # Yes, that's right. See the TZ variable
+ # documentation.
+ signal = (1, -1)[l[i] == '+']
+ used_idxs.append(i)
+ i += 1
+ else:
+ signal = -1
+ len_li = len(l[i])
+ if len_li == 4:
+ # -0300
+ setattr(res, offattr, (int(l[i][:2]) * 3600 +
+ int(l[i][2:]) * 60) * signal)
+ elif i + 1 < len_l and l[i + 1] == ':':
+ # -03:00
+ setattr(res, offattr,
+ (int(l[i]) * 3600 +
+ int(l[i + 2]) * 60) * signal)
+ used_idxs.append(i)
+ i += 2
+ elif len_li <= 2:
+ # -[0]3
+ setattr(res, offattr,
+ int(l[i][:2]) * 3600 * signal)
+ else:
+ return None
+ used_idxs.append(i)
+ i += 1
+ if res.dstabbr:
+ break
+ else:
+ break
+
+
+ if i < len_l:
+ for j in range(i, len_l):
+ if l[j] == ';':
+ l[j] = ','
+
+ assert l[i] == ','
+
+ i += 1
+
+ if i >= len_l:
+ pass
+ elif (8 <= l.count(',') <= 9 and
+ not [y for x in l[i:] if x != ','
+ for y in x if y not in "0123456789+-"]):
+ # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
+ for x in (res.start, res.end):
+ x.month = int(l[i])
+ used_idxs.append(i)
+ i += 2
+ if l[i] == '-':
+ value = int(l[i + 1]) * -1
+ used_idxs.append(i)
+ i += 1
+ else:
+ value = int(l[i])
+ used_idxs.append(i)
+ i += 2
+ if value:
+ x.week = value
+ x.weekday = (int(l[i]) - 1) % 7
+ else:
+ x.day = int(l[i])
+ used_idxs.append(i)
+ i += 2
+ x.time = int(l[i])
+ used_idxs.append(i)
+ i += 2
+ if i < len_l:
+ if l[i] in ('-', '+'):
+ signal = (-1, 1)[l[i] == "+"]
+ used_idxs.append(i)
+ i += 1
+ else:
+ signal = 1
+ used_idxs.append(i)
+ res.dstoffset = (res.stdoffset + int(l[i]) * signal)
+
+ # This was a made-up format that is not in normal use
+ warn(('Parsed time zone "%s"' % tzstr) +
+ 'is in a non-standard dateutil-specific format, which ' +
+ 'is now deprecated; support for parsing this format ' +
+ 'will be removed in future versions. It is recommended ' +
+ 'that you switch to a standard format like the GNU ' +
+ 'TZ variable format.', tz.DeprecatedTzFormatWarning)
+ elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
+ not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
+ '.', '-', ':')
+ for y in x if y not in "0123456789"]):
+ for x in (res.start, res.end):
+ if l[i] == 'J':
+ # non-leap year day (1 based)
+ used_idxs.append(i)
+ i += 1
+ x.jyday = int(l[i])
+ elif l[i] == 'M':
+ # month[-.]week[-.]weekday
+ used_idxs.append(i)
+ i += 1
+ x.month = int(l[i])
+ used_idxs.append(i)
+ i += 1
+ assert l[i] in ('-', '.')
+ used_idxs.append(i)
+ i += 1
+ x.week = int(l[i])
+ if x.week == 5:
+ x.week = -1
+ used_idxs.append(i)
+ i += 1
+ assert l[i] in ('-', '.')
+ used_idxs.append(i)
+ i += 1
+ x.weekday = (int(l[i]) - 1) % 7
+ else:
+ # year day (zero based)
+ x.yday = int(l[i]) + 1
+
+ used_idxs.append(i)
+ i += 1
+
+ if i < len_l and l[i] == '/':
+ used_idxs.append(i)
+ i += 1
+ # start time
+ len_li = len(l[i])
+ if len_li == 4:
+ # -0300
+ x.time = (int(l[i][:2]) * 3600 +
+ int(l[i][2:]) * 60)
+ elif i + 1 < len_l and l[i + 1] == ':':
+ # -03:00
+ x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
+ used_idxs.append(i)
+ i += 2
+ if i + 1 < len_l and l[i + 1] == ':':
+ used_idxs.append(i)
+ i += 2
+ x.time += int(l[i])
+ elif len_li <= 2:
+ # -[0]3
+ x.time = (int(l[i][:2]) * 3600)
+ else:
+ return None
+ used_idxs.append(i)
+ i += 1
+
+ assert i == len_l or l[i] == ','
+
+ i += 1
+
+ assert i >= len_l
+
+ except (IndexError, ValueError, AssertionError):
+ return None
+
+ unused_idxs = set(range(len_l)).difference(used_idxs)
+ res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"})
+ return res
+
+
+DEFAULTTZPARSER = _tzparser()
+
+
+def _parsetz(tzstr):
+ return DEFAULTTZPARSER.parse(tzstr)
+
+
+class ParserError(ValueError):
+ """Exception subclass used for any failure to parse a datetime string.
+
+ This is a subclass of :py:exc:`ValueError`, and should be raised any time
+ earlier versions of ``dateutil`` would have raised ``ValueError``.
+
+ .. versionadded:: 2.8.1
+ """
+ def __str__(self):
+ try:
+ return self.args[0] % self.args[1:]
+ except (TypeError, IndexError):
+ return super(ParserError, self).__str__()
+
+ def __repr__(self):
+ args = ", ".join("'%s'" % arg for arg in self.args)
+ return "%s(%s)" % (self.__class__.__name__, args)
+
+
+class UnknownTimezoneWarning(RuntimeWarning):
+ """Raised when the parser finds a timezone it cannot parse into a tzinfo.
+
+ .. versionadded:: 2.7.0
+ """
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/parser/isoparser.py b/src/dateutil/parser/isoparser.py
new file mode 100644
index 0000000..5d7bee3
--- /dev/null
+++ b/src/dateutil/parser/isoparser.py
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a parser for ISO-8601 strings
+
+It is intended to support all valid date, time and datetime formats per the
+ISO-8601 specification.
+
+..versionadded:: 2.7.0
+"""
+from datetime import datetime, timedelta, time, date
+import calendar
+from dateutil import tz
+
+from functools import wraps
+
+import re
+import six
+
+__all__ = ["isoparse", "isoparser"]
+
+
+def _takes_ascii(f):
+ @wraps(f)
+ def func(self, str_in, *args, **kwargs):
+ # If it's a stream, read the whole thing
+ str_in = getattr(str_in, 'read', lambda: str_in)()
+
+ # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
+ if isinstance(str_in, six.text_type):
+ # ASCII is the same in UTF-8
+ try:
+ str_in = str_in.encode('ascii')
+ except UnicodeEncodeError as e:
+ msg = 'ISO-8601 strings should contain only ASCII characters'
+ six.raise_from(ValueError(msg), e)
+
+ return f(self, str_in, *args, **kwargs)
+
+ return func
+
+
+class isoparser(object):
+ def __init__(self, sep=None):
+ """
+ :param sep:
+ A single character that separates date and time portions. If
+ ``None``, the parser will accept any single character.
+ For strict ISO-8601 adherence, pass ``'T'``.
+ """
+ if sep is not None:
+ if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
+ raise ValueError('Separator must be a single, non-numeric ' +
+ 'ASCII character')
+
+ sep = sep.encode('ascii')
+
+ self._sep = sep
+
+ @_takes_ascii
+ def isoparse(self, dt_str):
+ """
+ Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
+
+ An ISO-8601 datetime string consists of a date portion, followed
+ optionally by a time portion - the date and time portions are separated
+ by a single character separator, which is ``T`` in the official
+ standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
+ combined with a time portion.
+
+ Supported date formats are:
+
+ Common:
+
+ - ``YYYY``
+ - ``YYYY-MM`` or ``YYYYMM``
+ - ``YYYY-MM-DD`` or ``YYYYMMDD``
+
+ Uncommon:
+
+ - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
+ - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
+
+ The ISO week and day numbering follows the same logic as
+ :func:`datetime.date.isocalendar`.
+
+ Supported time formats are:
+
+ - ``hh``
+ - ``hh:mm`` or ``hhmm``
+ - ``hh:mm:ss`` or ``hhmmss``
+ - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
+
+ Midnight is a special case for `hh`, as the standard supports both
+ 00:00 and 24:00 as a representation. The decimal separator can be
+ either a dot or a comma.
+
+
+ .. caution::
+
+ Support for fractional components other than seconds is part of the
+ ISO-8601 standard, but is not currently implemented in this parser.
+
+ Supported time zone offset formats are:
+
+ - `Z` (UTC)
+ - `±HH:MM`
+ - `±HHMM`
+ - `±HH`
+
+ Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
+ with the exception of UTC, which will be represented as
+ :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
+ as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
+
+ :param dt_str:
+ A string or stream containing only an ISO-8601 datetime string
+
+ :return:
+ Returns a :class:`datetime.datetime` representing the string.
+ Unspecified components default to their lowest value.
+
+ .. warning::
+
+ As of version 2.7.0, the strictness of the parser should not be
+ considered a stable part of the contract. Any valid ISO-8601 string
+ that parses correctly with the default settings will continue to
+ parse correctly in future versions, but invalid strings that
+ currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
+ guaranteed to continue failing in future versions if they encode
+ a valid date.
+
+ .. versionadded:: 2.7.0
+ """
+ components, pos = self._parse_isodate(dt_str)
+
+ if len(dt_str) > pos:
+ if self._sep is None or dt_str[pos:pos + 1] == self._sep:
+ components += self._parse_isotime(dt_str[pos + 1:])
+ else:
+ raise ValueError('String contains unknown ISO components')
+
+ if len(components) > 3 and components[3] == 24:
+ components[3] = 0
+ return datetime(*components) + timedelta(days=1)
+
+ return datetime(*components)
+
+ @_takes_ascii
+ def parse_isodate(self, datestr):
+ """
+ Parse the date portion of an ISO string.
+
+ :param datestr:
+ The string portion of an ISO string, without a separator
+
+ :return:
+ Returns a :class:`datetime.date` object
+ """
+ components, pos = self._parse_isodate(datestr)
+ if pos < len(datestr):
+ raise ValueError('String contains unknown ISO ' +
+ 'components: {!r}'.format(datestr.decode('ascii')))
+ return date(*components)
+
+ @_takes_ascii
+ def parse_isotime(self, timestr):
+ """
+ Parse the time portion of an ISO string.
+
+ :param timestr:
+ The time portion of an ISO string, without a separator
+
+ :return:
+ Returns a :class:`datetime.time` object
+ """
+ components = self._parse_isotime(timestr)
+ if components[0] == 24:
+ components[0] = 0
+ return time(*components)
+
+ @_takes_ascii
+ def parse_tzstr(self, tzstr, zero_as_utc=True):
+ """
+ Parse a valid ISO time zone string.
+
+ See :func:`isoparser.isoparse` for details on supported formats.
+
+ :param tzstr:
+ A string representing an ISO time zone offset
+
+ :param zero_as_utc:
+ Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
+
+ :return:
+ Returns :class:`dateutil.tz.tzoffset` for offsets and
+ :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
+ specified) offsets equivalent to UTC.
+ """
+ return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
+
+ # Constants
+ _DATE_SEP = b'-'
+ _TIME_SEP = b':'
+ _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
+
+ def _parse_isodate(self, dt_str):
+ try:
+ return self._parse_isodate_common(dt_str)
+ except ValueError:
+ return self._parse_isodate_uncommon(dt_str)
+
+ def _parse_isodate_common(self, dt_str):
+ len_str = len(dt_str)
+ components = [1, 1, 1]
+
+ if len_str < 4:
+ raise ValueError('ISO string too short')
+
+ # Year
+ components[0] = int(dt_str[0:4])
+ pos = 4
+ if pos >= len_str:
+ return components, pos
+
+ has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
+ if has_sep:
+ pos += 1
+
+ # Month
+ if len_str - pos < 2:
+ raise ValueError('Invalid common month')
+
+ components[1] = int(dt_str[pos:pos + 2])
+ pos += 2
+
+ if pos >= len_str:
+ if has_sep:
+ return components, pos
+ else:
+ raise ValueError('Invalid ISO format')
+
+ if has_sep:
+ if dt_str[pos:pos + 1] != self._DATE_SEP:
+ raise ValueError('Invalid separator in ISO string')
+ pos += 1
+
+ # Day
+ if len_str - pos < 2:
+ raise ValueError('Invalid common day')
+ components[2] = int(dt_str[pos:pos + 2])
+ return components, pos + 2
+
+ def _parse_isodate_uncommon(self, dt_str):
+ if len(dt_str) < 4:
+ raise ValueError('ISO string too short')
+
+ # All ISO formats start with the year
+ year = int(dt_str[0:4])
+
+ has_sep = dt_str[4:5] == self._DATE_SEP
+
+ pos = 4 + has_sep # Skip '-' if it's there
+ if dt_str[pos:pos + 1] == b'W':
+ # YYYY-?Www-?D?
+ pos += 1
+ weekno = int(dt_str[pos:pos + 2])
+ pos += 2
+
+ dayno = 1
+ if len(dt_str) > pos:
+ if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
+ raise ValueError('Inconsistent use of dash separator')
+
+ pos += has_sep
+
+ dayno = int(dt_str[pos:pos + 1])
+ pos += 1
+
+ base_date = self._calculate_weekdate(year, weekno, dayno)
+ else:
+ # YYYYDDD or YYYY-DDD
+ if len(dt_str) - pos < 3:
+ raise ValueError('Invalid ordinal day')
+
+ ordinal_day = int(dt_str[pos:pos + 3])
+ pos += 3
+
+ if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
+ raise ValueError('Invalid ordinal day' +
+ ' {} for year {}'.format(ordinal_day, year))
+
+ base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
+
+ components = [base_date.year, base_date.month, base_date.day]
+ return components, pos
+
+ def _calculate_weekdate(self, year, week, day):
+ """
+ Calculate the day of corresponding to the ISO year-week-day calendar.
+
+ This function is effectively the inverse of
+ :func:`datetime.date.isocalendar`.
+
+ :param year:
+ The year in the ISO calendar
+
+ :param week:
+ The week in the ISO calendar - range is [1, 53]
+
+ :param day:
+ The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
+
+ :return:
+ Returns a :class:`datetime.date`
+ """
+ if not 0 < week < 54:
+ raise ValueError('Invalid week: {}'.format(week))
+
+ if not 0 < day < 8: # Range is 1-7
+ raise ValueError('Invalid weekday: {}'.format(day))
+
+ # Get week 1 for the specific year:
+ jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
+ week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
+
+ # Now add the specific number of weeks and days to get what we want
+ week_offset = (week - 1) * 7 + (day - 1)
+ return week_1 + timedelta(days=week_offset)
+
+ def _parse_isotime(self, timestr):
+ len_str = len(timestr)
+ components = [0, 0, 0, 0, None]
+ pos = 0
+ comp = -1
+
+ if len_str < 2:
+ raise ValueError('ISO time too short')
+
+ has_sep = False
+
+ while pos < len_str and comp < 5:
+ comp += 1
+
+ if timestr[pos:pos + 1] in b'-+Zz':
+ # Detect time zone boundary
+ components[-1] = self._parse_tzstr(timestr[pos:])
+ pos = len_str
+ break
+
+ if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
+ has_sep = True
+ pos += 1
+ elif comp == 2 and has_sep:
+ if timestr[pos:pos+1] != self._TIME_SEP:
+ raise ValueError('Inconsistent use of colon separator')
+ pos += 1
+
+ if comp < 3:
+ # Hour, minute, second
+ components[comp] = int(timestr[pos:pos + 2])
+ pos += 2
+
+ if comp == 3:
+ # Fraction of a second
+ frac = self._FRACTION_REGEX.match(timestr[pos:])
+ if not frac:
+ continue
+
+ us_str = frac.group(1)[:6] # Truncate to microseconds
+ components[comp] = int(us_str) * 10**(6 - len(us_str))
+ pos += len(frac.group())
+
+ if pos < len_str:
+ raise ValueError('Unused components in ISO string')
+
+ if components[0] == 24:
+ # Standard supports 00:00 and 24:00 as representations of midnight
+ if any(component != 0 for component in components[1:4]):
+ raise ValueError('Hour may only be 24 at 24:00:00.000')
+
+ return components
+
+ def _parse_tzstr(self, tzstr, zero_as_utc=True):
+ if tzstr == b'Z' or tzstr == b'z':
+ return tz.UTC
+
+ if len(tzstr) not in {3, 5, 6}:
+ raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
+
+ if tzstr[0:1] == b'-':
+ mult = -1
+ elif tzstr[0:1] == b'+':
+ mult = 1
+ else:
+ raise ValueError('Time zone offset requires sign')
+
+ hours = int(tzstr[1:3])
+ if len(tzstr) == 3:
+ minutes = 0
+ else:
+ minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
+
+ if zero_as_utc and hours == 0 and minutes == 0:
+ return tz.UTC
+ else:
+ if minutes > 59:
+ raise ValueError('Invalid minutes in time zone offset')
+
+ if hours > 23:
+ raise ValueError('Invalid hours in time zone offset')
+
+ return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
+
+
+DEFAULT_ISOPARSER = isoparser()
+isoparse = DEFAULT_ISOPARSER.isoparse
diff --git a/src/dateutil/relativedelta.py b/src/dateutil/relativedelta.py
new file mode 100644
index 0000000..a9e85f7
--- /dev/null
+++ b/src/dateutil/relativedelta.py
@@ -0,0 +1,599 @@
+# -*- coding: utf-8 -*-
+import datetime
+import calendar
+
+import operator
+from math import copysign
+
+from six import integer_types
+from warnings import warn
+
+from ._common import weekday
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
+
+__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+
+class relativedelta(object):
+ """
+ The relativedelta type is designed to be applied to an existing datetime and
+ can replace specific components of that datetime, or represents an interval
+ of time.
+
+ It is based on the specification of the excellent work done by M.-A. Lemburg
+ in his
+ `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
+ However, notice that this type does *NOT* implement the same algorithm as
+ his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
+
+ There are two different ways to build a relativedelta instance. The
+ first one is passing it two date/datetime classes::
+
+ relativedelta(datetime1, datetime2)
+
+ The second one is passing it any number of the following keyword arguments::
+
+ relativedelta(arg1=x,arg2=y,arg3=z...)
+
+ year, month, day, hour, minute, second, microsecond:
+ Absolute information (argument is singular); adding or subtracting a
+ relativedelta with absolute information does not perform an arithmetic
+ operation, but rather REPLACES the corresponding value in the
+ original datetime with the value(s) in relativedelta.
+
+ years, months, weeks, days, hours, minutes, seconds, microseconds:
+ Relative information, may be negative (argument is plural); adding
+ or subtracting a relativedelta with relative information performs
+ the corresponding arithmetic operation on the original datetime value
+ with the information in the relativedelta.
+
+ weekday:
+ One of the weekday instances (MO, TU, etc) available in the
+ relativedelta module. These instances may receive a parameter N,
+ specifying the Nth weekday, which could be positive or negative
+ (like MO(+1) or MO(-2)). Not specifying it is the same as specifying
+ +1. You can also use an integer, where 0=MO. This argument is always
+ relative e.g. if the calculated date is already Monday, using MO(1)
+ or MO(-1) won't change the day. To effectively make it absolute, use
+ it in combination with the day argument (e.g. day=1, MO(1) for first
+ Monday of the month).
+
+ leapdays:
+ Will add given days to the date found, if year is a leap
+ year, and the date found is post 28 of february.
+
+ yearday, nlyearday:
+ Set the yearday or the non-leap year day (jump leap days).
+ These are converted to day/month/leapdays information.
+
+ There are relative and absolute forms of the keyword
+ arguments. The plural is relative, and the singular is
+ absolute. For each argument in the order below, the absolute form
+ is applied first (by setting each attribute to that value) and
+ then the relative form (by adding the value to the attribute).
+
+ The order of attributes considered when this relativedelta is
+ added to a datetime is:
+
+ 1. Year
+ 2. Month
+ 3. Day
+ 4. Hours
+ 5. Minutes
+ 6. Seconds
+ 7. Microseconds
+
+ Finally, weekday is applied, using the rule described above.
+
+ For example
+
+ >>> from datetime import datetime
+ >>> from dateutil.relativedelta import relativedelta, MO
+ >>> dt = datetime(2018, 4, 9, 13, 37, 0)
+ >>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
+ >>> dt + delta
+ datetime.datetime(2018, 4, 2, 14, 37)
+
+ First, the day is set to 1 (the first of the month), then 25 hours
+ are added, to get to the 2nd day and 14th hour, finally the
+ weekday is applied, but since the 2nd is already a Monday there is
+ no effect.
+
+ """
+
+ def __init__(self, dt1=None, dt2=None,
+ years=0, months=0, days=0, leapdays=0, weeks=0,
+ hours=0, minutes=0, seconds=0, microseconds=0,
+ year=None, month=None, day=None, weekday=None,
+ yearday=None, nlyearday=None,
+ hour=None, minute=None, second=None, microsecond=None):
+
+ if dt1 and dt2:
+ # datetime is a subclass of date. So both must be date
+ if not (isinstance(dt1, datetime.date) and
+ isinstance(dt2, datetime.date)):
+ raise TypeError("relativedelta only diffs datetime/date")
+
+ # We allow two dates, or two datetimes, so we coerce them to be
+ # of the same type
+ if (isinstance(dt1, datetime.datetime) !=
+ isinstance(dt2, datetime.datetime)):
+ if not isinstance(dt1, datetime.datetime):
+ dt1 = datetime.datetime.fromordinal(dt1.toordinal())
+ elif not isinstance(dt2, datetime.datetime):
+ dt2 = datetime.datetime.fromordinal(dt2.toordinal())
+
+ self.years = 0
+ self.months = 0
+ self.days = 0
+ self.leapdays = 0
+ self.hours = 0
+ self.minutes = 0
+ self.seconds = 0
+ self.microseconds = 0
+ self.year = None
+ self.month = None
+ self.day = None
+ self.weekday = None
+ self.hour = None
+ self.minute = None
+ self.second = None
+ self.microsecond = None
+ self._has_time = 0
+
+ # Get year / month delta between the two
+ months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
+ self._set_months(months)
+
+ # Remove the year/month delta so the timedelta is just well-defined
+ # time units (seconds, days and microseconds)
+ dtm = self.__radd__(dt2)
+
+ # If we've overshot our target, make an adjustment
+ if dt1 < dt2:
+ compare = operator.gt
+ increment = 1
+ else:
+ compare = operator.lt
+ increment = -1
+
+ while compare(dt1, dtm):
+ months += increment
+ self._set_months(months)
+ dtm = self.__radd__(dt2)
+
+ # Get the timedelta between the "months-adjusted" date and dt1
+ delta = dt1 - dtm
+ self.seconds = delta.seconds + delta.days * 86400
+ self.microseconds = delta.microseconds
+ else:
+ # Check for non-integer values in integer-only quantities
+ if any(x is not None and x != int(x) for x in (years, months)):
+ raise ValueError("Non-integer years and months are "
+ "ambiguous and not currently supported.")
+
+ # Relative information
+ self.years = int(years)
+ self.months = int(months)
+ self.days = days + weeks * 7
+ self.leapdays = leapdays
+ self.hours = hours
+ self.minutes = minutes
+ self.seconds = seconds
+ self.microseconds = microseconds
+
+ # Absolute information
+ self.year = year
+ self.month = month
+ self.day = day
+ self.hour = hour
+ self.minute = minute
+ self.second = second
+ self.microsecond = microsecond
+
+ if any(x is not None and int(x) != x
+ for x in (year, month, day, hour,
+ minute, second, microsecond)):
+ # For now we'll deprecate floats - later it'll be an error.
+ warn("Non-integer value passed as absolute information. " +
+ "This is not a well-defined condition and will raise " +
+ "errors in future versions.", DeprecationWarning)
+
+ if isinstance(weekday, integer_types):
+ self.weekday = weekdays[weekday]
+ else:
+ self.weekday = weekday
+
+ yday = 0
+ if nlyearday:
+ yday = nlyearday
+ elif yearday:
+ yday = yearday
+ if yearday > 59:
+ self.leapdays = -1
+ if yday:
+ ydayidx = [31, 59, 90, 120, 151, 181, 212,
+ 243, 273, 304, 334, 366]
+ for idx, ydays in enumerate(ydayidx):
+ if yday <= ydays:
+ self.month = idx+1
+ if idx == 0:
+ self.day = yday
+ else:
+ self.day = yday-ydayidx[idx-1]
+ break
+ else:
+ raise ValueError("invalid year day (%d)" % yday)
+
+ self._fix()
+
+ def _fix(self):
+ if abs(self.microseconds) > 999999:
+ s = _sign(self.microseconds)
+ div, mod = divmod(self.microseconds * s, 1000000)
+ self.microseconds = mod * s
+ self.seconds += div * s
+ if abs(self.seconds) > 59:
+ s = _sign(self.seconds)
+ div, mod = divmod(self.seconds * s, 60)
+ self.seconds = mod * s
+ self.minutes += div * s
+ if abs(self.minutes) > 59:
+ s = _sign(self.minutes)
+ div, mod = divmod(self.minutes * s, 60)
+ self.minutes = mod * s
+ self.hours += div * s
+ if abs(self.hours) > 23:
+ s = _sign(self.hours)
+ div, mod = divmod(self.hours * s, 24)
+ self.hours = mod * s
+ self.days += div * s
+ if abs(self.months) > 11:
+ s = _sign(self.months)
+ div, mod = divmod(self.months * s, 12)
+ self.months = mod * s
+ self.years += div * s
+ if (self.hours or self.minutes or self.seconds or self.microseconds
+ or self.hour is not None or self.minute is not None or
+ self.second is not None or self.microsecond is not None):
+ self._has_time = 1
+ else:
+ self._has_time = 0
+
+ @property
+ def weeks(self):
+ return int(self.days / 7.0)
+
+ @weeks.setter
+ def weeks(self, value):
+ self.days = self.days - (self.weeks * 7) + value * 7
+
+ def _set_months(self, months):
+ self.months = months
+ if abs(self.months) > 11:
+ s = _sign(self.months)
+ div, mod = divmod(self.months * s, 12)
+ self.months = mod * s
+ self.years = div * s
+ else:
+ self.years = 0
+
+ def normalized(self):
+ """
+ Return a version of this object represented entirely using integer
+ values for the relative attributes.
+
+ >>> relativedelta(days=1.5, hours=2).normalized()
+ relativedelta(days=+1, hours=+14)
+
+ :return:
+ Returns a :class:`dateutil.relativedelta.relativedelta` object.
+ """
+ # Cascade remainders down (rounding each to roughly nearest microsecond)
+ days = int(self.days)
+
+ hours_f = round(self.hours + 24 * (self.days - days), 11)
+ hours = int(hours_f)
+
+ minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
+ minutes = int(minutes_f)
+
+ seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
+ seconds = int(seconds_f)
+
+ microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
+
+ # Constructor carries overflow back up with call to _fix()
+ return self.__class__(years=self.years, months=self.months,
+ days=days, hours=hours, minutes=minutes,
+ seconds=seconds, microseconds=microseconds,
+ leapdays=self.leapdays, year=self.year,
+ month=self.month, day=self.day,
+ weekday=self.weekday, hour=self.hour,
+ minute=self.minute, second=self.second,
+ microsecond=self.microsecond)
+
+ def __add__(self, other):
+ if isinstance(other, relativedelta):
+ return self.__class__(years=other.years + self.years,
+ months=other.months + self.months,
+ days=other.days + self.days,
+ hours=other.hours + self.hours,
+ minutes=other.minutes + self.minutes,
+ seconds=other.seconds + self.seconds,
+ microseconds=(other.microseconds +
+ self.microseconds),
+ leapdays=other.leapdays or self.leapdays,
+ year=(other.year if other.year is not None
+ else self.year),
+ month=(other.month if other.month is not None
+ else self.month),
+ day=(other.day if other.day is not None
+ else self.day),
+ weekday=(other.weekday if other.weekday is not None
+ else self.weekday),
+ hour=(other.hour if other.hour is not None
+ else self.hour),
+ minute=(other.minute if other.minute is not None
+ else self.minute),
+ second=(other.second if other.second is not None
+ else self.second),
+ microsecond=(other.microsecond if other.microsecond
+ is not None else
+ self.microsecond))
+ if isinstance(other, datetime.timedelta):
+ return self.__class__(years=self.years,
+ months=self.months,
+ days=self.days + other.days,
+ hours=self.hours,
+ minutes=self.minutes,
+ seconds=self.seconds + other.seconds,
+ microseconds=self.microseconds + other.microseconds,
+ leapdays=self.leapdays,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ weekday=self.weekday,
+ hour=self.hour,
+ minute=self.minute,
+ second=self.second,
+ microsecond=self.microsecond)
+ if not isinstance(other, datetime.date):
+ return NotImplemented
+ elif self._has_time and not isinstance(other, datetime.datetime):
+ other = datetime.datetime.fromordinal(other.toordinal())
+ year = (self.year or other.year)+self.years
+ month = self.month or other.month
+ if self.months:
+ assert 1 <= abs(self.months) <= 12
+ month += self.months
+ if month > 12:
+ year += 1
+ month -= 12
+ elif month < 1:
+ year -= 1
+ month += 12
+ day = min(calendar.monthrange(year, month)[1],
+ self.day or other.day)
+ repl = {"year": year, "month": month, "day": day}
+ for attr in ["hour", "minute", "second", "microsecond"]:
+ value = getattr(self, attr)
+ if value is not None:
+ repl[attr] = value
+ days = self.days
+ if self.leapdays and month > 2 and calendar.isleap(year):
+ days += self.leapdays
+ ret = (other.replace(**repl)
+ + datetime.timedelta(days=days,
+ hours=self.hours,
+ minutes=self.minutes,
+ seconds=self.seconds,
+ microseconds=self.microseconds))
+ if self.weekday:
+ weekday, nth = self.weekday.weekday, self.weekday.n or 1
+ jumpdays = (abs(nth) - 1) * 7
+ if nth > 0:
+ jumpdays += (7 - ret.weekday() + weekday) % 7
+ else:
+ jumpdays += (ret.weekday() - weekday) % 7
+ jumpdays *= -1
+ ret += datetime.timedelta(days=jumpdays)
+ return ret
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __rsub__(self, other):
+ return self.__neg__().__radd__(other)
+
+ def __sub__(self, other):
+ if not isinstance(other, relativedelta):
+ return NotImplemented # In case the other object defines __rsub__
+ return self.__class__(years=self.years - other.years,
+ months=self.months - other.months,
+ days=self.days - other.days,
+ hours=self.hours - other.hours,
+ minutes=self.minutes - other.minutes,
+ seconds=self.seconds - other.seconds,
+ microseconds=self.microseconds - other.microseconds,
+ leapdays=self.leapdays or other.leapdays,
+ year=(self.year if self.year is not None
+ else other.year),
+ month=(self.month if self.month is not None else
+ other.month),
+ day=(self.day if self.day is not None else
+ other.day),
+ weekday=(self.weekday if self.weekday is not None else
+ other.weekday),
+ hour=(self.hour if self.hour is not None else
+ other.hour),
+ minute=(self.minute if self.minute is not None else
+ other.minute),
+ second=(self.second if self.second is not None else
+ other.second),
+ microsecond=(self.microsecond if self.microsecond
+ is not None else
+ other.microsecond))
+
+ def __abs__(self):
+ return self.__class__(years=abs(self.years),
+ months=abs(self.months),
+ days=abs(self.days),
+ hours=abs(self.hours),
+ minutes=abs(self.minutes),
+ seconds=abs(self.seconds),
+ microseconds=abs(self.microseconds),
+ leapdays=self.leapdays,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ weekday=self.weekday,
+ hour=self.hour,
+ minute=self.minute,
+ second=self.second,
+ microsecond=self.microsecond)
+
+ def __neg__(self):
+ return self.__class__(years=-self.years,
+ months=-self.months,
+ days=-self.days,
+ hours=-self.hours,
+ minutes=-self.minutes,
+ seconds=-self.seconds,
+ microseconds=-self.microseconds,
+ leapdays=self.leapdays,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ weekday=self.weekday,
+ hour=self.hour,
+ minute=self.minute,
+ second=self.second,
+ microsecond=self.microsecond)
+
+ def __bool__(self):
+ return not (not self.years and
+ not self.months and
+ not self.days and
+ not self.hours and
+ not self.minutes and
+ not self.seconds and
+ not self.microseconds and
+ not self.leapdays and
+ self.year is None and
+ self.month is None and
+ self.day is None and
+ self.weekday is None and
+ self.hour is None and
+ self.minute is None and
+ self.second is None and
+ self.microsecond is None)
+ # Compatibility with Python 2.x
+ __nonzero__ = __bool__
+
+ def __mul__(self, other):
+ try:
+ f = float(other)
+ except TypeError:
+ return NotImplemented
+
+ return self.__class__(years=int(self.years * f),
+ months=int(self.months * f),
+ days=int(self.days * f),
+ hours=int(self.hours * f),
+ minutes=int(self.minutes * f),
+ seconds=int(self.seconds * f),
+ microseconds=int(self.microseconds * f),
+ leapdays=self.leapdays,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ weekday=self.weekday,
+ hour=self.hour,
+ minute=self.minute,
+ second=self.second,
+ microsecond=self.microsecond)
+
+ __rmul__ = __mul__
+
+ def __eq__(self, other):
+ if not isinstance(other, relativedelta):
+ return NotImplemented
+ if self.weekday or other.weekday:
+ if not self.weekday or not other.weekday:
+ return False
+ if self.weekday.weekday != other.weekday.weekday:
+ return False
+ n1, n2 = self.weekday.n, other.weekday.n
+ if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
+ return False
+ return (self.years == other.years and
+ self.months == other.months and
+ self.days == other.days and
+ self.hours == other.hours and
+ self.minutes == other.minutes and
+ self.seconds == other.seconds and
+ self.microseconds == other.microseconds and
+ self.leapdays == other.leapdays and
+ self.year == other.year and
+ self.month == other.month and
+ self.day == other.day and
+ self.hour == other.hour and
+ self.minute == other.minute and
+ self.second == other.second and
+ self.microsecond == other.microsecond)
+
+ def __hash__(self):
+ return hash((
+ self.weekday,
+ self.years,
+ self.months,
+ self.days,
+ self.hours,
+ self.minutes,
+ self.seconds,
+ self.microseconds,
+ self.leapdays,
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ ))
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __div__(self, other):
+ try:
+ reciprocal = 1 / float(other)
+ except TypeError:
+ return NotImplemented
+
+ return self.__mul__(reciprocal)
+
+ __truediv__ = __div__
+
+ def __repr__(self):
+ l = []
+ for attr in ["years", "months", "days", "leapdays",
+ "hours", "minutes", "seconds", "microseconds"]:
+ value = getattr(self, attr)
+ if value:
+ l.append("{attr}={value:+g}".format(attr=attr, value=value))
+ for attr in ["year", "month", "day", "weekday",
+ "hour", "minute", "second", "microsecond"]:
+ value = getattr(self, attr)
+ if value is not None:
+ l.append("{attr}={value}".format(attr=attr, value=repr(value)))
+ return "{classname}({attrs})".format(classname=self.__class__.__name__,
+ attrs=", ".join(l))
+
+
+def _sign(x):
+ return int(copysign(1, x))
+
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/rrule.py b/src/dateutil/rrule.py
new file mode 100644
index 0000000..571a0d2
--- /dev/null
+++ b/src/dateutil/rrule.py
@@ -0,0 +1,1737 @@
+# -*- coding: utf-8 -*-
+"""
+The rrule module offers a small, complete, and very fast, implementation of
+the recurrence rules documented in the
+`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
+including support for caching of results.
+"""
+import calendar
+import datetime
+import heapq
+import itertools
+import re
+import sys
+from functools import wraps
+# For warning about deprecation of until and count
+from warnings import warn
+
+from six import advance_iterator, integer_types
+
+from six.moves import _thread, range
+
+from ._common import weekday as weekdaybase
+
+try:
+ from math import gcd
+except ImportError:
+ from fractions import gcd
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+ "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+ "HOURLY", "MINUTELY", "SECONDLY",
+ "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
+ [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
+M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
+WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = list(range(7))
+
+# Imported on demand.
+easter = None
+parser = None
+
+
+class weekday(weekdaybase):
+ """
+ This version of weekday does not allow n = 0.
+ """
+ def __init__(self, wkday, n=None):
+ if n == 0:
+ raise ValueError("Can't create weekday with n==0")
+
+ super(weekday, self).__init__(wkday, n)
+
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
+
+
+def _invalidates_cache(f):
+ """
+ Decorator for rruleset methods which may invalidate the
+ cached length.
+ """
+ @wraps(f)
+ def inner_func(self, *args, **kwargs):
+ rv = f(self, *args, **kwargs)
+ self._invalidate_cache()
+ return rv
+
+ return inner_func
+
+
+class rrulebase(object):
+ def __init__(self, cache=False):
+ if cache:
+ self._cache = []
+ self._cache_lock = _thread.allocate_lock()
+ self._invalidate_cache()
+ else:
+ self._cache = None
+ self._cache_complete = False
+ self._len = None
+
+ def __iter__(self):
+ if self._cache_complete:
+ return iter(self._cache)
+ elif self._cache is None:
+ return self._iter()
+ else:
+ return self._iter_cached()
+
+ def _invalidate_cache(self):
+ if self._cache is not None:
+ self._cache = []
+ self._cache_complete = False
+ self._cache_gen = self._iter()
+
+ if self._cache_lock.locked():
+ self._cache_lock.release()
+
+ self._len = None
+
+ def _iter_cached(self):
+ i = 0
+ gen = self._cache_gen
+ cache = self._cache
+ acquire = self._cache_lock.acquire
+ release = self._cache_lock.release
+ while gen:
+ if i == len(cache):
+ acquire()
+ if self._cache_complete:
+ break
+ try:
+ for j in range(10):
+ cache.append(advance_iterator(gen))
+ except StopIteration:
+ self._cache_gen = gen = None
+ self._cache_complete = True
+ break
+ release()
+ yield cache[i]
+ i += 1
+ while i < self._len:
+ yield cache[i]
+ i += 1
+
+ def __getitem__(self, item):
+ if self._cache_complete:
+ return self._cache[item]
+ elif isinstance(item, slice):
+ if item.step and item.step < 0:
+ return list(iter(self))[item]
+ else:
+ return list(itertools.islice(self,
+ item.start or 0,
+ item.stop or sys.maxsize,
+ item.step or 1))
+ elif item >= 0:
+ gen = iter(self)
+ try:
+ for i in range(item+1):
+ res = advance_iterator(gen)
+ except StopIteration:
+ raise IndexError
+ return res
+ else:
+ return list(iter(self))[item]
+
+ def __contains__(self, item):
+ if self._cache_complete:
+ return item in self._cache
+ else:
+ for i in self:
+ if i == item:
+ return True
+ elif i > item:
+ return False
+ return False
+
+ # __len__() introduces a large performance penalty.
+ def count(self):
+ """ Returns the number of recurrences in this set. It will have go
+ through the whole recurrence, if this hasn't been done before. """
+ if self._len is None:
+ for x in self:
+ pass
+ return self._len
+
+ def before(self, dt, inc=False):
+ """ Returns the last recurrence before the given datetime instance. The
+ inc keyword defines what happens if dt is an occurrence. With
+ inc=True, if dt itself is an occurrence, it will be returned. """
+ if self._cache_complete:
+ gen = self._cache
+ else:
+ gen = self
+ last = None
+ if inc:
+ for i in gen:
+ if i > dt:
+ break
+ last = i
+ else:
+ for i in gen:
+ if i >= dt:
+ break
+ last = i
+ return last
+
+ def after(self, dt, inc=False):
+ """ Returns the first recurrence after the given datetime instance. The
+ inc keyword defines what happens if dt is an occurrence. With
+ inc=True, if dt itself is an occurrence, it will be returned. """
+ if self._cache_complete:
+ gen = self._cache
+ else:
+ gen = self
+ if inc:
+ for i in gen:
+ if i >= dt:
+ return i
+ else:
+ for i in gen:
+ if i > dt:
+ return i
+ return None
+
+ def xafter(self, dt, count=None, inc=False):
+ """
+ Generator which yields up to `count` recurrences after the given
+ datetime instance, equivalent to `after`.
+
+ :param dt:
+ The datetime at which to start generating recurrences.
+
+ :param count:
+ The maximum number of recurrences to generate. If `None` (default),
+ dates are generated until the recurrence rule is exhausted.
+
+ :param inc:
+ If `dt` is an instance of the rule and `inc` is `True`, it is
+ included in the output.
+
+ :yields: Yields a sequence of `datetime` objects.
+ """
+
+ if self._cache_complete:
+ gen = self._cache
+ else:
+ gen = self
+
+ # Select the comparison function
+ if inc:
+ comp = lambda dc, dtc: dc >= dtc
+ else:
+ comp = lambda dc, dtc: dc > dtc
+
+ # Generate dates
+ n = 0
+ for d in gen:
+ if comp(d, dt):
+ if count is not None:
+ n += 1
+ if n > count:
+ break
+
+ yield d
+
+ def between(self, after, before, inc=False, count=1):
+ """ Returns all the occurrences of the rrule between after and before.
+ The inc keyword defines what happens if after and/or before are
+ themselves occurrences. With inc=True, they will be included in the
+ list, if they are found in the recurrence set. """
+ if self._cache_complete:
+ gen = self._cache
+ else:
+ gen = self
+ started = False
+ l = []
+ if inc:
+ for i in gen:
+ if i > before:
+ break
+ elif not started:
+ if i >= after:
+ started = True
+ l.append(i)
+ else:
+ l.append(i)
+ else:
+ for i in gen:
+ if i >= before:
+ break
+ elif not started:
+ if i > after:
+ started = True
+ l.append(i)
+ else:
+ l.append(i)
+ return l
+
+
+class rrule(rrulebase):
+ """
+ That's the base of the rrule operation. It accepts all the keywords
+ defined in the RFC as its constructor parameters (except byday,
+ which was renamed to byweekday) and more. The constructor prototype is::
+
+ rrule(freq)
+
+ Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
+ or SECONDLY.
+
+ .. note::
+ Per RFC section 3.3.10, recurrence instances falling on invalid dates
+ and times are ignored rather than coerced:
+
+ Recurrence rules may generate recurrence instances with an invalid
+ date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
+ on a day where the local time is moved forward by an hour at 1:00
+ AM). Such recurrence instances MUST be ignored and MUST NOT be
+ counted as part of the recurrence set.
+
+ This can lead to possibly surprising behavior when, for example, the
+ start date occurs at the end of the month:
+
+ >>> from dateutil.rrule import rrule, MONTHLY
+ >>> from datetime import datetime
+ >>> start_date = datetime(2014, 12, 31)
+ >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
+ ... # doctest: +NORMALIZE_WHITESPACE
+ [datetime.datetime(2014, 12, 31, 0, 0),
+ datetime.datetime(2015, 1, 31, 0, 0),
+ datetime.datetime(2015, 3, 31, 0, 0),
+ datetime.datetime(2015, 5, 31, 0, 0)]
+
+ Additionally, it supports the following keyword arguments:
+
+ :param dtstart:
+ The recurrence start. Besides being the base for the recurrence,
+ missing parameters in the final recurrence instances will also be
+ extracted from this date. If not given, datetime.now() will be used
+ instead.
+ :param interval:
+ The interval between each freq iteration. For example, when using
+ YEARLY, an interval of 2 means once every two years, but with HOURLY,
+ it means once every two hours. The default interval is 1.
+ :param wkst:
+ The week start day. Must be one of the MO, TU, WE constants, or an
+ integer, specifying the first day of the week. This will affect
+ recurrences based on weekly periods. The default week start is got
+ from calendar.firstweekday(), and may be modified by
+ calendar.setfirstweekday().
+ :param count:
+ If given, this determines how many occurrences will be generated.
+
+ .. note::
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
+ :param until:
+ If given, this must be a datetime instance specifying the upper-bound
+ limit of the recurrence. The last recurrence in the rule is the greatest
+ datetime that is less than or equal to the value specified in the
+ ``until`` parameter.
+
+ .. note::
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
+ :param bysetpos:
+ If given, it must be either an integer, or a sequence of integers,
+ positive or negative. Each given integer will specify an occurrence
+ number, corresponding to the nth occurrence of the rule inside the
+ frequency period. For example, a bysetpos of -1 if combined with a
+ MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
+ result in the last work day of every month.
+ :param bymonth:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the months to apply the recurrence to.
+ :param bymonthday:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the month days to apply the recurrence to.
+ :param byyearday:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the year days to apply the recurrence to.
+ :param byeaster:
+ If given, it must be either an integer, or a sequence of integers,
+ positive or negative. Each integer will define an offset from the
+ Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+ Sunday itself. This is an extension to the RFC specification.
+ :param byweekno:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the week numbers to apply the recurrence to. Week numbers
+ have the meaning described in ISO8601, that is, the first week of
+ the year is that containing at least four days of the new year.
+ :param byweekday:
+ If given, it must be either an integer (0 == MO), a sequence of
+ integers, one of the weekday constants (MO, TU, etc), or a sequence
+ of these constants. When given, these variables will define the
+ weekdays where the recurrence will be applied. It's also possible to
+ use an argument n for the weekday instances, which will mean the nth
+ occurrence of this weekday in the period. For example, with MONTHLY,
+ or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
+ first friday of the month where the recurrence happens. Notice that in
+ the RFC documentation, this is specified as BYDAY, but was renamed to
+ avoid the ambiguity of that keyword.
+ :param byhour:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the hours to apply the recurrence to.
+ :param byminute:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the minutes to apply the recurrence to.
+ :param bysecond:
+ If given, it must be either an integer, or a sequence of integers,
+ meaning the seconds to apply the recurrence to.
+ :param cache:
+ If given, it must be a boolean value specifying to enable or disable
+ caching of results. If you will use the same rrule instance multiple
+ times, enabling caching will improve the performance considerably.
+ """
+ def __init__(self, freq, dtstart=None,
+ interval=1, wkst=None, count=None, until=None, bysetpos=None,
+ bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+ byweekno=None, byweekday=None,
+ byhour=None, byminute=None, bysecond=None,
+ cache=False):
+ super(rrule, self).__init__(cache)
+ global easter
+ if not dtstart:
+ if until and until.tzinfo:
+ dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+ else:
+ dtstart = datetime.datetime.now().replace(microsecond=0)
+ elif not isinstance(dtstart, datetime.datetime):
+ dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+ else:
+ dtstart = dtstart.replace(microsecond=0)
+ self._dtstart = dtstart
+ self._tzinfo = dtstart.tzinfo
+ self._freq = freq
+ self._interval = interval
+ self._count = count
+
+ # Cache the original byxxx rules, if they are provided, as the _byxxx
+ # attributes do not necessarily map to the inputs, and this can be
+ # a problem in generating the strings. Only store things if they've
+ # been supplied (the string retrieval will just use .get())
+ self._original_rule = {}
+
+ if until and not isinstance(until, datetime.datetime):
+ until = datetime.datetime.fromordinal(until.toordinal())
+ self._until = until
+
+ if self._dtstart and self._until:
+ if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
+ # According to RFC5545 Section 3.3.10:
+ # https://tools.ietf.org/html/rfc5545#section-3.3.10
+ #
+ # > If the "DTSTART" property is specified as a date with UTC
+ # > time or a date with local time and time zone reference,
+ # > then the UNTIL rule part MUST be specified as a date with
+ # > UTC time.
+ raise ValueError(
+ 'RRULE UNTIL values must be specified in UTC when DTSTART '
+ 'is timezone-aware'
+ )
+
+ if count is not None and until:
+ warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
+ " and has been deprecated in dateutil. Future versions will "
+ "raise an error.", DeprecationWarning)
+
+ if wkst is None:
+ self._wkst = calendar.firstweekday()
+ elif isinstance(wkst, integer_types):
+ self._wkst = wkst
+ else:
+ self._wkst = wkst.weekday
+
+ if bysetpos is None:
+ self._bysetpos = None
+ elif isinstance(bysetpos, integer_types):
+ if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+ raise ValueError("bysetpos must be between 1 and 366, "
+ "or between -366 and -1")
+ self._bysetpos = (bysetpos,)
+ else:
+ self._bysetpos = tuple(bysetpos)
+ for pos in self._bysetpos:
+ if pos == 0 or not (-366 <= pos <= 366):
+ raise ValueError("bysetpos must be between 1 and 366, "
+ "or between -366 and -1")
+
+ if self._bysetpos:
+ self._original_rule['bysetpos'] = self._bysetpos
+
+ if (byweekno is None and byyearday is None and bymonthday is None and
+ byweekday is None and byeaster is None):
+ if freq == YEARLY:
+ if bymonth is None:
+ bymonth = dtstart.month
+ self._original_rule['bymonth'] = None
+ bymonthday = dtstart.day
+ self._original_rule['bymonthday'] = None
+ elif freq == MONTHLY:
+ bymonthday = dtstart.day
+ self._original_rule['bymonthday'] = None
+ elif freq == WEEKLY:
+ byweekday = dtstart.weekday()
+ self._original_rule['byweekday'] = None
+
+ # bymonth
+ if bymonth is None:
+ self._bymonth = None
+ else:
+ if isinstance(bymonth, integer_types):
+ bymonth = (bymonth,)
+
+ self._bymonth = tuple(sorted(set(bymonth)))
+
+ if 'bymonth' not in self._original_rule:
+ self._original_rule['bymonth'] = self._bymonth
+
+ # byyearday
+ if byyearday is None:
+ self._byyearday = None
+ else:
+ if isinstance(byyearday, integer_types):
+ byyearday = (byyearday,)
+
+ self._byyearday = tuple(sorted(set(byyearday)))
+ self._original_rule['byyearday'] = self._byyearday
+
+ # byeaster
+ if byeaster is not None:
+ if not easter:
+ from dateutil import easter
+ if isinstance(byeaster, integer_types):
+ self._byeaster = (byeaster,)
+ else:
+ self._byeaster = tuple(sorted(byeaster))
+
+ self._original_rule['byeaster'] = self._byeaster
+ else:
+ self._byeaster = None
+
+ # bymonthday
+ if bymonthday is None:
+ self._bymonthday = ()
+ self._bynmonthday = ()
+ else:
+ if isinstance(bymonthday, integer_types):
+ bymonthday = (bymonthday,)
+
+ bymonthday = set(bymonthday) # Ensure it's unique
+
+ self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
+ self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
+
+ # Storing positive numbers first, then negative numbers
+ if 'bymonthday' not in self._original_rule:
+ self._original_rule['bymonthday'] = tuple(
+ itertools.chain(self._bymonthday, self._bynmonthday))
+
+ # byweekno
+ if byweekno is None:
+ self._byweekno = None
+ else:
+ if isinstance(byweekno, integer_types):
+ byweekno = (byweekno,)
+
+ self._byweekno = tuple(sorted(set(byweekno)))
+
+ self._original_rule['byweekno'] = self._byweekno
+
+ # byweekday / bynweekday
+ if byweekday is None:
+ self._byweekday = None
+ self._bynweekday = None
+ else:
+ # If it's one of the valid non-sequence types, convert to a
+ # single-element sequence before the iterator that builds the
+ # byweekday set.
+ if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
+ byweekday = (byweekday,)
+
+ self._byweekday = set()
+ self._bynweekday = set()
+ for wday in byweekday:
+ if isinstance(wday, integer_types):
+ self._byweekday.add(wday)
+ elif not wday.n or freq > MONTHLY:
+ self._byweekday.add(wday.weekday)
+ else:
+ self._bynweekday.add((wday.weekday, wday.n))
+
+ if not self._byweekday:
+ self._byweekday = None
+ elif not self._bynweekday:
+ self._bynweekday = None
+
+ if self._byweekday is not None:
+ self._byweekday = tuple(sorted(self._byweekday))
+ orig_byweekday = [weekday(x) for x in self._byweekday]
+ else:
+ orig_byweekday = ()
+
+ if self._bynweekday is not None:
+ self._bynweekday = tuple(sorted(self._bynweekday))
+ orig_bynweekday = [weekday(*x) for x in self._bynweekday]
+ else:
+ orig_bynweekday = ()
+
+ if 'byweekday' not in self._original_rule:
+ self._original_rule['byweekday'] = tuple(itertools.chain(
+ orig_byweekday, orig_bynweekday))
+
+ # byhour
+ if byhour is None:
+ if freq < HOURLY:
+ self._byhour = {dtstart.hour}
+ else:
+ self._byhour = None
+ else:
+ if isinstance(byhour, integer_types):
+ byhour = (byhour,)
+
+ if freq == HOURLY:
+ self._byhour = self.__construct_byset(start=dtstart.hour,
+ byxxx=byhour,
+ base=24)
+ else:
+ self._byhour = set(byhour)
+
+ self._byhour = tuple(sorted(self._byhour))
+ self._original_rule['byhour'] = self._byhour
+
+ # byminute
+ if byminute is None:
+ if freq < MINUTELY:
+ self._byminute = {dtstart.minute}
+ else:
+ self._byminute = None
+ else:
+ if isinstance(byminute, integer_types):
+ byminute = (byminute,)
+
+ if freq == MINUTELY:
+ self._byminute = self.__construct_byset(start=dtstart.minute,
+ byxxx=byminute,
+ base=60)
+ else:
+ self._byminute = set(byminute)
+
+ self._byminute = tuple(sorted(self._byminute))
+ self._original_rule['byminute'] = self._byminute
+
+ # bysecond
+ if bysecond is None:
+ if freq < SECONDLY:
+ self._bysecond = ((dtstart.second,))
+ else:
+ self._bysecond = None
+ else:
+ if isinstance(bysecond, integer_types):
+ bysecond = (bysecond,)
+
+ self._bysecond = set(bysecond)
+
+ if freq == SECONDLY:
+ self._bysecond = self.__construct_byset(start=dtstart.second,
+ byxxx=bysecond,
+ base=60)
+ else:
+ self._bysecond = set(bysecond)
+
+ self._bysecond = tuple(sorted(self._bysecond))
+ self._original_rule['bysecond'] = self._bysecond
+
+ if self._freq >= HOURLY:
+ self._timeset = None
+ else:
+ self._timeset = []
+ for hour in self._byhour:
+ for minute in self._byminute:
+ for second in self._bysecond:
+ self._timeset.append(
+ datetime.time(hour, minute, second,
+ tzinfo=self._tzinfo))
+ self._timeset.sort()
+ self._timeset = tuple(self._timeset)
+
+ def __str__(self):
+ """
+ Output a string that would generate this RRULE if passed to rrulestr.
+ This is mostly compatible with RFC5545, except for the
+ dateutil-specific extension BYEASTER.
+ """
+
+ output = []
+ h, m, s = [None] * 3
+ if self._dtstart:
+ output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
+ h, m, s = self._dtstart.timetuple()[3:6]
+
+ parts = ['FREQ=' + FREQNAMES[self._freq]]
+ if self._interval != 1:
+ parts.append('INTERVAL=' + str(self._interval))
+
+ if self._wkst:
+ parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
+
+ if self._count is not None:
+ parts.append('COUNT=' + str(self._count))
+
+ if self._until:
+ parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
+
+ if self._original_rule.get('byweekday') is not None:
+ # The str() method on weekday objects doesn't generate
+ # RFC5545-compliant strings, so we should modify that.
+ original_rule = dict(self._original_rule)
+ wday_strings = []
+ for wday in original_rule['byweekday']:
+ if wday.n:
+ wday_strings.append('{n:+d}{wday}'.format(
+ n=wday.n,
+ wday=repr(wday)[0:2]))
+ else:
+ wday_strings.append(repr(wday))
+
+ original_rule['byweekday'] = wday_strings
+ else:
+ original_rule = self._original_rule
+
+ partfmt = '{name}={vals}'
+ for name, key in [('BYSETPOS', 'bysetpos'),
+ ('BYMONTH', 'bymonth'),
+ ('BYMONTHDAY', 'bymonthday'),
+ ('BYYEARDAY', 'byyearday'),
+ ('BYWEEKNO', 'byweekno'),
+ ('BYDAY', 'byweekday'),
+ ('BYHOUR', 'byhour'),
+ ('BYMINUTE', 'byminute'),
+ ('BYSECOND', 'bysecond'),
+ ('BYEASTER', 'byeaster')]:
+ value = original_rule.get(key)
+ if value:
+ parts.append(partfmt.format(name=name, vals=(','.join(str(v)
+ for v in value))))
+
+ output.append('RRULE:' + ';'.join(parts))
+ return '\n'.join(output)
+
+ def replace(self, **kwargs):
+ """Return new rrule with same attributes except for those attributes given new
+ values by whichever keyword arguments are specified."""
+ new_kwargs = {"interval": self._interval,
+ "count": self._count,
+ "dtstart": self._dtstart,
+ "freq": self._freq,
+ "until": self._until,
+ "wkst": self._wkst,
+ "cache": False if self._cache is None else True }
+ new_kwargs.update(self._original_rule)
+ new_kwargs.update(kwargs)
+ return rrule(**new_kwargs)
+
+ def _iter(self):
+ year, month, day, hour, minute, second, weekday, yearday, _ = \
+ self._dtstart.timetuple()
+
+ # Some local variables to speed things up a bit
+ freq = self._freq
+ interval = self._interval
+ wkst = self._wkst
+ until = self._until
+ bymonth = self._bymonth
+ byweekno = self._byweekno
+ byyearday = self._byyearday
+ byweekday = self._byweekday
+ byeaster = self._byeaster
+ bymonthday = self._bymonthday
+ bynmonthday = self._bynmonthday
+ bysetpos = self._bysetpos
+ byhour = self._byhour
+ byminute = self._byminute
+ bysecond = self._bysecond
+
+ ii = _iterinfo(self)
+ ii.rebuild(year, month)
+
+ getdayset = {YEARLY: ii.ydayset,
+ MONTHLY: ii.mdayset,
+ WEEKLY: ii.wdayset,
+ DAILY: ii.ddayset,
+ HOURLY: ii.ddayset,
+ MINUTELY: ii.ddayset,
+ SECONDLY: ii.ddayset}[freq]
+
+ if freq < HOURLY:
+ timeset = self._timeset
+ else:
+ gettimeset = {HOURLY: ii.htimeset,
+ MINUTELY: ii.mtimeset,
+ SECONDLY: ii.stimeset}[freq]
+ if ((freq >= HOURLY and
+ self._byhour and hour not in self._byhour) or
+ (freq >= MINUTELY and
+ self._byminute and minute not in self._byminute) or
+ (freq >= SECONDLY and
+ self._bysecond and second not in self._bysecond)):
+ timeset = ()
+ else:
+ timeset = gettimeset(hour, minute, second)
+
+ total = 0
+ count = self._count
+ while True:
+ # Get dayset with the right frequency
+ dayset, start, end = getdayset(year, month, day)
+
+ # Do the "hard" work ;-)
+ filtered = False
+ for i in dayset[start:end]:
+ if ((bymonth and ii.mmask[i] not in bymonth) or
+ (byweekno and not ii.wnomask[i]) or
+ (byweekday and ii.wdaymask[i] not in byweekday) or
+ (ii.nwdaymask and not ii.nwdaymask[i]) or
+ (byeaster and not ii.eastermask[i]) or
+ ((bymonthday or bynmonthday) and
+ ii.mdaymask[i] not in bymonthday and
+ ii.nmdaymask[i] not in bynmonthday) or
+ (byyearday and
+ ((i < ii.yearlen and i+1 not in byyearday and
+ -ii.yearlen+i not in byyearday) or
+ (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
+ -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
+ dayset[i] = None
+ filtered = True
+
+ # Output results
+ if bysetpos and timeset:
+ poslist = []
+ for pos in bysetpos:
+ if pos < 0:
+ daypos, timepos = divmod(pos, len(timeset))
+ else:
+ daypos, timepos = divmod(pos-1, len(timeset))
+ try:
+ i = [x for x in dayset[start:end]
+ if x is not None][daypos]
+ time = timeset[timepos]
+ except IndexError:
+ pass
+ else:
+ date = datetime.date.fromordinal(ii.yearordinal+i)
+ res = datetime.datetime.combine(date, time)
+ if res not in poslist:
+ poslist.append(res)
+ poslist.sort()
+ for res in poslist:
+ if until and res > until:
+ self._len = total
+ return
+ elif res >= self._dtstart:
+ if count is not None:
+ count -= 1
+ if count < 0:
+ self._len = total
+ return
+ total += 1
+ yield res
+ else:
+ for i in dayset[start:end]:
+ if i is not None:
+ date = datetime.date.fromordinal(ii.yearordinal + i)
+ for time in timeset:
+ res = datetime.datetime.combine(date, time)
+ if until and res > until:
+ self._len = total
+ return
+ elif res >= self._dtstart:
+ if count is not None:
+ count -= 1
+ if count < 0:
+ self._len = total
+ return
+
+ total += 1
+ yield res
+
+ # Handle frequency and interval
+ fixday = False
+ if freq == YEARLY:
+ year += interval
+ if year > datetime.MAXYEAR:
+ self._len = total
+ return
+ ii.rebuild(year, month)
+ elif freq == MONTHLY:
+ month += interval
+ if month > 12:
+ div, mod = divmod(month, 12)
+ month = mod
+ year += div
+ if month == 0:
+ month = 12
+ year -= 1
+ if year > datetime.MAXYEAR:
+ self._len = total
+ return
+ ii.rebuild(year, month)
+ elif freq == WEEKLY:
+ if wkst > weekday:
+ day += -(weekday+1+(6-wkst))+self._interval*7
+ else:
+ day += -(weekday-wkst)+self._interval*7
+ weekday = wkst
+ fixday = True
+ elif freq == DAILY:
+ day += interval
+ fixday = True
+ elif freq == HOURLY:
+ if filtered:
+ # Jump to one iteration before next day
+ hour += ((23-hour)//interval)*interval
+
+ if byhour:
+ ndays, hour = self.__mod_distance(value=hour,
+ byxxx=self._byhour,
+ base=24)
+ else:
+ ndays, hour = divmod(hour+interval, 24)
+
+ if ndays:
+ day += ndays
+ fixday = True
+
+ timeset = gettimeset(hour, minute, second)
+ elif freq == MINUTELY:
+ if filtered:
+ # Jump to one iteration before next day
+ minute += ((1439-(hour*60+minute))//interval)*interval
+
+ valid = False
+ rep_rate = (24*60)
+ for j in range(rep_rate // gcd(interval, rep_rate)):
+ if byminute:
+ nhours, minute = \
+ self.__mod_distance(value=minute,
+ byxxx=self._byminute,
+ base=60)
+ else:
+ nhours, minute = divmod(minute+interval, 60)
+
+ div, hour = divmod(hour+nhours, 24)
+ if div:
+ day += div
+ fixday = True
+ filtered = False
+
+ if not byhour or hour in byhour:
+ valid = True
+ break
+
+ if not valid:
+ raise ValueError('Invalid combination of interval and ' +
+ 'byhour resulting in empty rule.')
+
+ timeset = gettimeset(hour, minute, second)
+ elif freq == SECONDLY:
+ if filtered:
+ # Jump to one iteration before next day
+ second += (((86399 - (hour * 3600 + minute * 60 + second))
+ // interval) * interval)
+
+ rep_rate = (24 * 3600)
+ valid = False
+ for j in range(0, rep_rate // gcd(interval, rep_rate)):
+ if bysecond:
+ nminutes, second = \
+ self.__mod_distance(value=second,
+ byxxx=self._bysecond,
+ base=60)
+ else:
+ nminutes, second = divmod(second+interval, 60)
+
+ div, minute = divmod(minute+nminutes, 60)
+ if div:
+ hour += div
+ div, hour = divmod(hour, 24)
+ if div:
+ day += div
+ fixday = True
+
+ if ((not byhour or hour in byhour) and
+ (not byminute or minute in byminute) and
+ (not bysecond or second in bysecond)):
+ valid = True
+ break
+
+ if not valid:
+ raise ValueError('Invalid combination of interval, ' +
+ 'byhour and byminute resulting in empty' +
+ ' rule.')
+
+ timeset = gettimeset(hour, minute, second)
+
+ if fixday and day > 28:
+ daysinmonth = calendar.monthrange(year, month)[1]
+ if day > daysinmonth:
+ while day > daysinmonth:
+ day -= daysinmonth
+ month += 1
+ if month == 13:
+ month = 1
+ year += 1
+ if year > datetime.MAXYEAR:
+ self._len = total
+ return
+ daysinmonth = calendar.monthrange(year, month)[1]
+ ii.rebuild(year, month)
+
+ def __construct_byset(self, start, byxxx, base):
+ """
+ If a `BYXXX` sequence is passed to the constructor at the same level as
+ `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
+ specifications which cannot be reached given some starting conditions.
+
+ This occurs whenever the interval is not coprime with the base of a
+ given unit and the difference between the starting position and the
+ ending position is not coprime with the greatest common denominator
+ between the interval and the base. For example, with a FREQ of hourly
+ starting at 17:00 and an interval of 4, the only valid values for
+ BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
+ coprime.
+
+ :param start:
+ Specifies the starting position.
+ :param byxxx:
+ An iterable containing the list of allowed values.
+ :param base:
+ The largest allowable value for the specified frequency (e.g.
+ 24 hours, 60 minutes).
+
+ This does not preserve the type of the iterable, returning a set, since
+ the values should be unique and the order is irrelevant, this will
+ speed up later lookups.
+
+ In the event of an empty set, raises a :exception:`ValueError`, as this
+ results in an empty rrule.
+ """
+
+ cset = set()
+
+ # Support a single byxxx value.
+ if isinstance(byxxx, integer_types):
+ byxxx = (byxxx, )
+
+ for num in byxxx:
+ i_gcd = gcd(self._interval, base)
+ # Use divmod rather than % because we need to wrap negative nums.
+ if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
+ cset.add(num)
+
+ if len(cset) == 0:
+ raise ValueError("Invalid rrule byxxx generates an empty set.")
+
+ return cset
+
+ def __mod_distance(self, value, byxxx, base):
+ """
+ Calculates the next value in a sequence where the `FREQ` parameter is
+ specified along with a `BYXXX` parameter at the same "level"
+ (e.g. `HOURLY` specified with `BYHOUR`).
+
+ :param value:
+ The old value of the component.
+ :param byxxx:
+ The `BYXXX` set, which should have been generated by
+ `rrule._construct_byset`, or something else which checks that a
+ valid rule is present.
+ :param base:
+ The largest allowable value for the specified frequency (e.g.
+ 24 hours, 60 minutes).
+
+ If a valid value is not found after `base` iterations (the maximum
+ number before the sequence would start to repeat), this raises a
+ :exception:`ValueError`, as no valid values were found.
+
+ This returns a tuple of `divmod(n*interval, base)`, where `n` is the
+ smallest number of `interval` repetitions until the next specified
+ value in `byxxx` is found.
+ """
+ accumulator = 0
+ for ii in range(1, base + 1):
+ # Using divmod() over % to account for negative intervals
+ div, value = divmod(value + self._interval, base)
+ accumulator += div
+ if value in byxxx:
+ return (accumulator, value)
+
+
+class _iterinfo(object):
+ __slots__ = ["rrule", "lastyear", "lastmonth",
+ "yearlen", "nextyearlen", "yearordinal", "yearweekday",
+ "mmask", "mrange", "mdaymask", "nmdaymask",
+ "wdaymask", "wnomask", "nwdaymask", "eastermask"]
+
+ def __init__(self, rrule):
+ for attr in self.__slots__:
+ setattr(self, attr, None)
+ self.rrule = rrule
+
+ def rebuild(self, year, month):
+ # Every mask is 7 days longer to handle cross-year weekly periods.
+ rr = self.rrule
+ if year != self.lastyear:
+ self.yearlen = 365 + calendar.isleap(year)
+ self.nextyearlen = 365 + calendar.isleap(year + 1)
+ firstyday = datetime.date(year, 1, 1)
+ self.yearordinal = firstyday.toordinal()
+ self.yearweekday = firstyday.weekday()
+
+ wday = datetime.date(year, 1, 1).weekday()
+ if self.yearlen == 365:
+ self.mmask = M365MASK
+ self.mdaymask = MDAY365MASK
+ self.nmdaymask = NMDAY365MASK
+ self.wdaymask = WDAYMASK[wday:]
+ self.mrange = M365RANGE
+ else:
+ self.mmask = M366MASK
+ self.mdaymask = MDAY366MASK
+ self.nmdaymask = NMDAY366MASK
+ self.wdaymask = WDAYMASK[wday:]
+ self.mrange = M366RANGE
+
+ if not rr._byweekno:
+ self.wnomask = None
+ else:
+ self.wnomask = [0]*(self.yearlen+7)
+ # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
+ no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
+ if no1wkst >= 4:
+ no1wkst = 0
+ # Number of days in the year, plus the days we got
+ # from last year.
+ wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
+ else:
+ # Number of days in the year, minus the days we
+ # left in last year.
+ wyearlen = self.yearlen-no1wkst
+ div, mod = divmod(wyearlen, 7)
+ numweeks = div+mod//4
+ for n in rr._byweekno:
+ if n < 0:
+ n += numweeks+1
+ if not (0 < n <= numweeks):
+ continue
+ if n > 1:
+ i = no1wkst+(n-1)*7
+ if no1wkst != firstwkst:
+ i -= 7-firstwkst
+ else:
+ i = no1wkst
+ for j in range(7):
+ self.wnomask[i] = 1
+ i += 1
+ if self.wdaymask[i] == rr._wkst:
+ break
+ if 1 in rr._byweekno:
+ # Check week number 1 of next year as well
+ # TODO: Check -numweeks for next year.
+ i = no1wkst+numweeks*7
+ if no1wkst != firstwkst:
+ i -= 7-firstwkst
+ if i < self.yearlen:
+ # If week starts in next year, we
+ # don't care about it.
+ for j in range(7):
+ self.wnomask[i] = 1
+ i += 1
+ if self.wdaymask[i] == rr._wkst:
+ break
+ if no1wkst:
+ # Check last week number of last year as
+ # well. If no1wkst is 0, either the year
+ # started on week start, or week number 1
+ # got days from last year, so there are no
+ # days from last year's last week number in
+ # this year.
+ if -1 not in rr._byweekno:
+ lyearweekday = datetime.date(year-1, 1, 1).weekday()
+ lno1wkst = (7-lyearweekday+rr._wkst) % 7
+ lyearlen = 365+calendar.isleap(year-1)
+ if lno1wkst >= 4:
+ lno1wkst = 0
+ lnumweeks = 52+(lyearlen +
+ (lyearweekday-rr._wkst) % 7) % 7//4
+ else:
+ lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
+ else:
+ lnumweeks = -1
+ if lnumweeks in rr._byweekno:
+ for i in range(no1wkst):
+ self.wnomask[i] = 1
+
+ if (rr._bynweekday and (month != self.lastmonth or
+ year != self.lastyear)):
+ ranges = []
+ if rr._freq == YEARLY:
+ if rr._bymonth:
+ for month in rr._bymonth:
+ ranges.append(self.mrange[month-1:month+1])
+ else:
+ ranges = [(0, self.yearlen)]
+ elif rr._freq == MONTHLY:
+ ranges = [self.mrange[month-1:month+1]]
+ if ranges:
+ # Weekly frequency won't get here, so we may not
+ # care about cross-year weekly periods.
+ self.nwdaymask = [0]*self.yearlen
+ for first, last in ranges:
+ last -= 1
+ for wday, n in rr._bynweekday:
+ if n < 0:
+ i = last+(n+1)*7
+ i -= (self.wdaymask[i]-wday) % 7
+ else:
+ i = first+(n-1)*7
+ i += (7-self.wdaymask[i]+wday) % 7
+ if first <= i <= last:
+ self.nwdaymask[i] = 1
+
+ if rr._byeaster:
+ self.eastermask = [0]*(self.yearlen+7)
+ eyday = easter.easter(year).toordinal()-self.yearordinal
+ for offset in rr._byeaster:
+ self.eastermask[eyday+offset] = 1
+
+ self.lastyear = year
+ self.lastmonth = month
+
+ def ydayset(self, year, month, day):
+ return list(range(self.yearlen)), 0, self.yearlen
+
+ def mdayset(self, year, month, day):
+ dset = [None]*self.yearlen
+ start, end = self.mrange[month-1:month+1]
+ for i in range(start, end):
+ dset[i] = i
+ return dset, start, end
+
+ def wdayset(self, year, month, day):
+ # We need to handle cross-year weeks here.
+ dset = [None]*(self.yearlen+7)
+ i = datetime.date(year, month, day).toordinal()-self.yearordinal
+ start = i
+ for j in range(7):
+ dset[i] = i
+ i += 1
+ # if (not (0 <= i < self.yearlen) or
+ # self.wdaymask[i] == self.rrule._wkst):
+ # This will cross the year boundary, if necessary.
+ if self.wdaymask[i] == self.rrule._wkst:
+ break
+ return dset, start, i
+
+ def ddayset(self, year, month, day):
+ dset = [None] * self.yearlen
+ i = datetime.date(year, month, day).toordinal() - self.yearordinal
+ dset[i] = i
+ return dset, i, i + 1
+
+ def htimeset(self, hour, minute, second):
+ tset = []
+ rr = self.rrule
+ for minute in rr._byminute:
+ for second in rr._bysecond:
+ tset.append(datetime.time(hour, minute, second,
+ tzinfo=rr._tzinfo))
+ tset.sort()
+ return tset
+
+ def mtimeset(self, hour, minute, second):
+ tset = []
+ rr = self.rrule
+ for second in rr._bysecond:
+ tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
+ tset.sort()
+ return tset
+
+ def stimeset(self, hour, minute, second):
+ return (datetime.time(hour, minute, second,
+ tzinfo=self.rrule._tzinfo),)
+
+
+class rruleset(rrulebase):
+ """ The rruleset type allows more complex recurrence setups, mixing
+ multiple rules, dates, exclusion rules, and exclusion dates. The type
+ constructor takes the following keyword arguments:
+
+ :param cache: If True, caching of results will be enabled, improving
+ performance of multiple queries considerably. """
+
+ class _genitem(object):
+ def __init__(self, genlist, gen):
+ try:
+ self.dt = advance_iterator(gen)
+ genlist.append(self)
+ except StopIteration:
+ pass
+ self.genlist = genlist
+ self.gen = gen
+
+ def __next__(self):
+ try:
+ self.dt = advance_iterator(self.gen)
+ except StopIteration:
+ if self.genlist[0] is self:
+ heapq.heappop(self.genlist)
+ else:
+ self.genlist.remove(self)
+ heapq.heapify(self.genlist)
+
+ next = __next__
+
+ def __lt__(self, other):
+ return self.dt < other.dt
+
+ def __gt__(self, other):
+ return self.dt > other.dt
+
+ def __eq__(self, other):
+ return self.dt == other.dt
+
+ def __ne__(self, other):
+ return self.dt != other.dt
+
+ def __init__(self, cache=False):
+ super(rruleset, self).__init__(cache)
+ self._rrule = []
+ self._rdate = []
+ self._exrule = []
+ self._exdate = []
+
+ @_invalidates_cache
+ def rrule(self, rrule):
+ """ Include the given :py:class:`rrule` instance in the recurrence set
+ generation. """
+ self._rrule.append(rrule)
+
+ @_invalidates_cache
+ def rdate(self, rdate):
+ """ Include the given :py:class:`datetime` instance in the recurrence
+ set generation. """
+ self._rdate.append(rdate)
+
+ @_invalidates_cache
+ def exrule(self, exrule):
+ """ Include the given rrule instance in the recurrence set exclusion
+ list. Dates which are part of the given recurrence rules will not
+ be generated, even if some inclusive rrule or rdate matches them.
+ """
+ self._exrule.append(exrule)
+
+ @_invalidates_cache
+ def exdate(self, exdate):
+ """ Include the given datetime instance in the recurrence set
+ exclusion list. Dates included that way will not be generated,
+ even if some inclusive rrule or rdate matches them. """
+ self._exdate.append(exdate)
+
+ def _iter(self):
+ rlist = []
+ self._rdate.sort()
+ self._genitem(rlist, iter(self._rdate))
+ for gen in [iter(x) for x in self._rrule]:
+ self._genitem(rlist, gen)
+ exlist = []
+ self._exdate.sort()
+ self._genitem(exlist, iter(self._exdate))
+ for gen in [iter(x) for x in self._exrule]:
+ self._genitem(exlist, gen)
+ lastdt = None
+ total = 0
+ heapq.heapify(rlist)
+ heapq.heapify(exlist)
+ while rlist:
+ ritem = rlist[0]
+ if not lastdt or lastdt != ritem.dt:
+ while exlist and exlist[0] < ritem:
+ exitem = exlist[0]
+ advance_iterator(exitem)
+ if exlist and exlist[0] is exitem:
+ heapq.heapreplace(exlist, exitem)
+ if not exlist or ritem != exlist[0]:
+ total += 1
+ yield ritem.dt
+ lastdt = ritem.dt
+ advance_iterator(ritem)
+ if rlist and rlist[0] is ritem:
+ heapq.heapreplace(rlist, ritem)
+ self._len = total
+
+
+
+
+class _rrulestr(object):
+ """ Parses a string representation of a recurrence rule or set of
+ recurrence rules.
+
+ :param s:
+ Required, a string defining one or more recurrence rules.
+
+ :param dtstart:
+ If given, used as the default recurrence start if not specified in the
+ rule string.
+
+ :param cache:
+ If set ``True`` caching of results will be enabled, improving
+ performance of multiple queries considerably.
+
+ :param unfold:
+ If set ``True`` indicates that a rule string is split over more
+ than one line and should be joined before processing.
+
+ :param forceset:
+ If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
+ be returned.
+
+ :param compatible:
+ If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
+
+ :param ignoretz:
+ If set ``True``, time zones in parsed strings are ignored and a naive
+ :class:`datetime.datetime` object is returned.
+
+ :param tzids:
+ If given, a callable or mapping used to retrieve a
+ :class:`datetime.tzinfo` from a string representation.
+ Defaults to :func:`dateutil.tz.gettz`.
+
+ :param tzinfos:
+ Additional time zone names / aliases which may be present in a string
+ representation. See :func:`dateutil.parser.parse` for more
+ information.
+
+ :return:
+ Returns a :class:`dateutil.rrule.rruleset` or
+ :class:`dateutil.rrule.rrule`
+ """
+
+ _freq_map = {"YEARLY": YEARLY,
+ "MONTHLY": MONTHLY,
+ "WEEKLY": WEEKLY,
+ "DAILY": DAILY,
+ "HOURLY": HOURLY,
+ "MINUTELY": MINUTELY,
+ "SECONDLY": SECONDLY}
+
+ _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
+ "FR": 4, "SA": 5, "SU": 6}
+
+ def _handle_int(self, rrkwargs, name, value, **kwargs):
+ rrkwargs[name.lower()] = int(value)
+
+ def _handle_int_list(self, rrkwargs, name, value, **kwargs):
+ rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
+
+ _handle_INTERVAL = _handle_int
+ _handle_COUNT = _handle_int
+ _handle_BYSETPOS = _handle_int_list
+ _handle_BYMONTH = _handle_int_list
+ _handle_BYMONTHDAY = _handle_int_list
+ _handle_BYYEARDAY = _handle_int_list
+ _handle_BYEASTER = _handle_int_list
+ _handle_BYWEEKNO = _handle_int_list
+ _handle_BYHOUR = _handle_int_list
+ _handle_BYMINUTE = _handle_int_list
+ _handle_BYSECOND = _handle_int_list
+
+ def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
+ rrkwargs["freq"] = self._freq_map[value]
+
+ def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
+ global parser
+ if not parser:
+ from dateutil import parser
+ try:
+ rrkwargs["until"] = parser.parse(value,
+ ignoretz=kwargs.get("ignoretz"),
+ tzinfos=kwargs.get("tzinfos"))
+ except ValueError:
+ raise ValueError("invalid until date")
+
+ def _handle_WKST(self, rrkwargs, name, value, **kwargs):
+ rrkwargs["wkst"] = self._weekday_map[value]
+
+ def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
+ """
+ Two ways to specify this: +1MO or MO(+1)
+ """
+ l = []
+ for wday in value.split(','):
+ if '(' in wday:
+ # If it's of the form TH(+1), etc.
+ splt = wday.split('(')
+ w = splt[0]
+ n = int(splt[1][:-1])
+ elif len(wday):
+ # If it's of the form +1MO
+ for i in range(len(wday)):
+ if wday[i] not in '+-0123456789':
+ break
+ n = wday[:i] or None
+ w = wday[i:]
+ if n:
+ n = int(n)
+ else:
+ raise ValueError("Invalid (empty) BYDAY specification.")
+
+ l.append(weekdays[self._weekday_map[w]](n))
+ rrkwargs["byweekday"] = l
+
+ _handle_BYDAY = _handle_BYWEEKDAY
+
+ def _parse_rfc_rrule(self, line,
+ dtstart=None,
+ cache=False,
+ ignoretz=False,
+ tzinfos=None):
+ if line.find(':') != -1:
+ name, value = line.split(':')
+ if name != "RRULE":
+ raise ValueError("unknown parameter name")
+ else:
+ value = line
+ rrkwargs = {}
+ for pair in value.split(';'):
+ name, value = pair.split('=')
+ name = name.upper()
+ value = value.upper()
+ try:
+ getattr(self, "_handle_"+name)(rrkwargs, name, value,
+ ignoretz=ignoretz,
+ tzinfos=tzinfos)
+ except AttributeError:
+ raise ValueError("unknown parameter '%s'" % name)
+ except (KeyError, ValueError):
+ raise ValueError("invalid '%s': %s" % (name, value))
+ return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+
+ def _parse_date_value(self, date_value, parms, rule_tzids,
+ ignoretz, tzids, tzinfos):
+ global parser
+ if not parser:
+ from dateutil import parser
+
+ datevals = []
+ value_found = False
+ TZID = None
+
+ for parm in parms:
+ if parm.startswith("TZID="):
+ try:
+ tzkey = rule_tzids[parm.split('TZID=')[-1]]
+ except KeyError:
+ continue
+ if tzids is None:
+ from . import tz
+ tzlookup = tz.gettz
+ elif callable(tzids):
+ tzlookup = tzids
+ else:
+ tzlookup = getattr(tzids, 'get', None)
+ if tzlookup is None:
+ msg = ('tzids must be a callable, mapping, or None, '
+ 'not %s' % tzids)
+ raise ValueError(msg)
+
+ TZID = tzlookup(tzkey)
+ continue
+
+ # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+ # only once.
+ if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+ raise ValueError("unsupported parm: " + parm)
+ else:
+ if value_found:
+ msg = ("Duplicate value parameter found in: " + parm)
+ raise ValueError(msg)
+ value_found = True
+
+ for datestr in date_value.split(','):
+ date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+ if TZID is not None:
+ if date.tzinfo is None:
+ date = date.replace(tzinfo=TZID)
+ else:
+ raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+ datevals.append(date)
+
+ return datevals
+
+ def _parse_rfc(self, s,
+ dtstart=None,
+ cache=False,
+ unfold=False,
+ forceset=False,
+ compatible=False,
+ ignoretz=False,
+ tzids=None,
+ tzinfos=None):
+ global parser
+ if compatible:
+ forceset = True
+ unfold = True
+
+ TZID_NAMES = dict(map(
+ lambda x: (x.upper(), x),
+ re.findall('TZID=(?P<name>[^:]+):', s)
+ ))
+ s = s.upper()
+ if not s.strip():
+ raise ValueError("empty string")
+ if unfold:
+ lines = s.splitlines()
+ i = 0
+ while i < len(lines):
+ line = lines[i].rstrip()
+ if not line:
+ del lines[i]
+ elif i > 0 and line[0] == " ":
+ lines[i-1] += line[1:]
+ del lines[i]
+ else:
+ i += 1
+ else:
+ lines = s.split()
+ if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
+ s.startswith('RRULE:'))):
+ return self._parse_rfc_rrule(lines[0], cache=cache,
+ dtstart=dtstart, ignoretz=ignoretz,
+ tzinfos=tzinfos)
+ else:
+ rrulevals = []
+ rdatevals = []
+ exrulevals = []
+ exdatevals = []
+ for line in lines:
+ if not line:
+ continue
+ if line.find(':') == -1:
+ name = "RRULE"
+ value = line
+ else:
+ name, value = line.split(':', 1)
+ parms = name.split(';')
+ if not parms:
+ raise ValueError("empty property name")
+ name = parms[0]
+ parms = parms[1:]
+ if name == "RRULE":
+ for parm in parms:
+ raise ValueError("unsupported RRULE parm: "+parm)
+ rrulevals.append(value)
+ elif name == "RDATE":
+ for parm in parms:
+ if parm != "VALUE=DATE-TIME":
+ raise ValueError("unsupported RDATE parm: "+parm)
+ rdatevals.append(value)
+ elif name == "EXRULE":
+ for parm in parms:
+ raise ValueError("unsupported EXRULE parm: "+parm)
+ exrulevals.append(value)
+ elif name == "EXDATE":
+ exdatevals.extend(
+ self._parse_date_value(value, parms,
+ TZID_NAMES, ignoretz,
+ tzids, tzinfos)
+ )
+ elif name == "DTSTART":
+ dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+ ignoretz, tzids, tzinfos)
+ if len(dtvals) != 1:
+ raise ValueError("Multiple DTSTART values specified:" +
+ value)
+ dtstart = dtvals[0]
+ else:
+ raise ValueError("unsupported property: "+name)
+ if (forceset or len(rrulevals) > 1 or rdatevals
+ or exrulevals or exdatevals):
+ if not parser and (rdatevals or exdatevals):
+ from dateutil import parser
+ rset = rruleset(cache=cache)
+ for value in rrulevals:
+ rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+ ignoretz=ignoretz,
+ tzinfos=tzinfos))
+ for value in rdatevals:
+ for datestr in value.split(','):
+ rset.rdate(parser.parse(datestr,
+ ignoretz=ignoretz,
+ tzinfos=tzinfos))
+ for value in exrulevals:
+ rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+ ignoretz=ignoretz,
+ tzinfos=tzinfos))
+ for value in exdatevals:
+ rset.exdate(value)
+ if compatible and dtstart:
+ rset.rdate(dtstart)
+ return rset
+ else:
+ return self._parse_rfc_rrule(rrulevals[0],
+ dtstart=dtstart,
+ cache=cache,
+ ignoretz=ignoretz,
+ tzinfos=tzinfos)
+
+ def __call__(self, s, **kwargs):
+ return self._parse_rfc(s, **kwargs)
+
+
+rrulestr = _rrulestr()
+
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/test/__init__.py b/src/dateutil/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/dateutil/test/__init__.py
diff --git a/src/dateutil/test/_common.py b/src/dateutil/test/_common.py
new file mode 100644
index 0000000..b8d2047
--- /dev/null
+++ b/src/dateutil/test/_common.py
@@ -0,0 +1,233 @@
+from __future__ import unicode_literals
+import os
+import time
+import subprocess
+import warnings
+import tempfile
+import pickle
+
+import pytest
+
+
+class PicklableMixin(object):
+ def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs):
+ """
+ Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads``
+ """
+ pkl = pickle.dumps(obj, **dump_kwargs)
+ return pickle.loads(pkl, **load_kwargs)
+
+ def _get_nobj_file(self, obj, dump_kwargs, load_kwargs):
+ """
+ Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on
+ a temporary file.
+ """
+ with tempfile.TemporaryFile('w+b') as pkl:
+ pickle.dump(obj, pkl, **dump_kwargs)
+ pkl.seek(0) # Reset the file to the beginning to read it
+ nobj = pickle.load(pkl, **load_kwargs)
+
+ return nobj
+
+ def assertPicklable(self, obj, singleton=False, asfile=False,
+ dump_kwargs=None, load_kwargs=None):
+ """
+ Assert that an object can be pickled and unpickled. This assertion
+ assumes that the desired behavior is that the unpickled object compares
+ equal to the original object, but is not the same object.
+ """
+ get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes
+ dump_kwargs = dump_kwargs or {}
+ load_kwargs = load_kwargs or {}
+
+ nobj = get_nobj(obj, dump_kwargs, load_kwargs)
+ if not singleton:
+ self.assertIsNot(obj, nobj)
+ self.assertEqual(obj, nobj)
+
+
+class TZContextBase(object):
+ """
+ Base class for a context manager which allows changing of time zones.
+
+ Subclasses may define a guard variable to either block or or allow time
+ zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``.
+ The default is that the guard variable must be affirmatively set.
+
+ Subclasses must define ``get_current_tz`` and ``set_current_tz``.
+ """
+ _guard_var_name = "DATEUTIL_MAY_CHANGE_TZ"
+ _guard_allows_change = True
+
+ def __init__(self, tzval):
+ self.tzval = tzval
+ self._old_tz = None
+
+ @classmethod
+ def tz_change_allowed(cls):
+ """
+ Class method used to query whether or not this class allows time zone
+ changes.
+ """
+ guard = bool(os.environ.get(cls._guard_var_name, False))
+
+ # _guard_allows_change gives the "default" behavior - if True, the
+ # guard is overcoming a block. If false, the guard is causing a block.
+ # Whether tz_change is allowed is therefore the XNOR of the two.
+ return guard == cls._guard_allows_change
+
+ @classmethod
+ def tz_change_disallowed_message(cls):
+ """ Generate instructions on how to allow tz changes """
+ msg = ('Changing time zone not allowed. Set {envar} to {gval} '
+ 'if you would like to allow this behavior')
+
+ return msg.format(envar=cls._guard_var_name,
+ gval=cls._guard_allows_change)
+
+ def __enter__(self):
+ if not self.tz_change_allowed():
+ msg = self.tz_change_disallowed_message()
+ pytest.skip(msg)
+
+ # If this is used outside of a test suite, we still want an error.
+ raise ValueError(msg) # pragma: no cover
+
+ self._old_tz = self.get_current_tz()
+ self.set_current_tz(self.tzval)
+
+ def __exit__(self, type, value, traceback):
+ if self._old_tz is not None:
+ self.set_current_tz(self._old_tz)
+
+ self._old_tz = None
+
+ def get_current_tz(self):
+ raise NotImplementedError
+
+ def set_current_tz(self):
+ raise NotImplementedError
+
+
+class TZEnvContext(TZContextBase):
+ """
+ Context manager that temporarily sets the `TZ` variable (for use on
+ *nix-like systems). Because the effect is local to the shell anyway, this
+ will apply *unless* a guard is set.
+
+ If you do not want the TZ environment variable set, you may set the
+ ``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value.
+ """
+ _guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR"
+ _guard_allows_change = False
+
+ def get_current_tz(self):
+ return os.environ.get('TZ', UnsetTz)
+
+ def set_current_tz(self, tzval):
+ if tzval is UnsetTz and 'TZ' in os.environ:
+ del os.environ['TZ']
+ else:
+ os.environ['TZ'] = tzval
+
+ time.tzset()
+
+
+class TZWinContext(TZContextBase):
+ """
+ Context manager for changing local time zone on Windows.
+
+ Because the effect of this is system-wide and global, it may have
+ unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment
+ variable to a truthy value before using this context manager.
+ """
+ def get_current_tz(self):
+ p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE)
+
+ ctzname, err = p.communicate()
+ ctzname = ctzname.decode() # Popen returns
+
+ if p.returncode:
+ raise OSError('Failed to get current time zone: ' + err)
+
+ return ctzname
+
+ def set_current_tz(self, tzname):
+ p = subprocess.Popen('tzutil /s "' + tzname + '"')
+
+ out, err = p.communicate()
+
+ if p.returncode:
+ raise OSError('Failed to set current time zone: ' +
+ (err or 'Unknown error.'))
+
+
+###
+# Utility classes
+class NotAValueClass(object):
+ """
+ A class analogous to NaN that has operations defined for any type.
+ """
+ def _op(self, other):
+ return self # Operation with NotAValue returns NotAValue
+
+ def _cmp(self, other):
+ return False
+
+ __add__ = __radd__ = _op
+ __sub__ = __rsub__ = _op
+ __mul__ = __rmul__ = _op
+ __div__ = __rdiv__ = _op
+ __truediv__ = __rtruediv__ = _op
+ __floordiv__ = __rfloordiv__ = _op
+
+ __lt__ = __rlt__ = _op
+ __gt__ = __rgt__ = _op
+ __eq__ = __req__ = _op
+ __le__ = __rle__ = _op
+ __ge__ = __rge__ = _op
+
+
+NotAValue = NotAValueClass()
+
+
+class ComparesEqualClass(object):
+ """
+ A class that is always equal to whatever you compare it to.
+ """
+
+ def __eq__(self, other):
+ return True
+
+ def __ne__(self, other):
+ return False
+
+ def __le__(self, other):
+ return True
+
+ def __ge__(self, other):
+ return True
+
+ def __lt__(self, other):
+ return False
+
+ def __gt__(self, other):
+ return False
+
+ __req__ = __eq__
+ __rne__ = __ne__
+ __rle__ = __le__
+ __rge__ = __ge__
+ __rlt__ = __lt__
+ __rgt__ = __gt__
+
+
+ComparesEqual = ComparesEqualClass()
+
+
+class UnsetTzClass(object):
+ """ Sentinel class for unset time zone variable """
+ pass
+
+
+UnsetTz = UnsetTzClass()
diff --git a/src/dateutil/test/conftest.py b/src/dateutil/test/conftest.py
new file mode 100644
index 0000000..78ed70a
--- /dev/null
+++ b/src/dateutil/test/conftest.py
@@ -0,0 +1,41 @@
+import os
+import pytest
+
+
+# Configure pytest to ignore xfailing tests
+# See: https://stackoverflow.com/a/53198349/467366
+def pytest_collection_modifyitems(items):
+ for item in items:
+ marker_getter = getattr(item, 'get_closest_marker', None)
+
+ # Python 3.3 support
+ if marker_getter is None:
+ marker_getter = item.get_marker
+
+ marker = marker_getter('xfail')
+
+ # Need to query the args because conditional xfail tests still have
+ # the xfail mark even if they are not expected to fail
+ if marker and (not marker.args or marker.args[0]):
+ item.add_marker(pytest.mark.no_cover)
+
+
+def set_tzpath():
+ """
+ Sets the TZPATH variable if it's specified in an environment variable.
+ """
+ tzpath = os.environ.get('DATEUTIL_TZPATH', None)
+
+ if tzpath is None:
+ return
+
+ path_components = tzpath.split(':')
+
+ print("Setting TZPATH to {}".format(path_components))
+
+ from dateutil import tz
+ tz.TZPATHS.clear()
+ tz.TZPATHS.extend(path_components)
+
+
+set_tzpath()
diff --git a/src/dateutil/test/property/test_isoparse_prop.py b/src/dateutil/test/property/test_isoparse_prop.py
new file mode 100644
index 0000000..f8e288f
--- /dev/null
+++ b/src/dateutil/test/property/test_isoparse_prop.py
@@ -0,0 +1,27 @@
+from hypothesis import given, assume
+from hypothesis import strategies as st
+
+from dateutil import tz
+from dateutil.parser import isoparse
+
+import pytest
+
+# Strategies
+TIME_ZONE_STRATEGY = st.sampled_from([None, tz.UTC] +
+ [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific',
+ 'Australia/Sydney', 'Europe/London')])
+ASCII_STRATEGY = st.characters(max_codepoint=127)
+
+
+@pytest.mark.isoparser
+@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY)
+def test_timespec_auto(dt, sep):
+ if dt.tzinfo is not None:
+ # Assume offset has no sub-second components
+ assume(dt.utcoffset().total_seconds() % 60 == 0)
+
+ sep = str(sep) # Python 2.7 requires bytes
+ dtstr = dt.isoformat(sep=sep)
+ dt_rt = isoparse(dtstr)
+
+ assert dt_rt == dt
diff --git a/src/dateutil/test/property/test_parser_prop.py b/src/dateutil/test/property/test_parser_prop.py
new file mode 100644
index 0000000..fdfd171
--- /dev/null
+++ b/src/dateutil/test/property/test_parser_prop.py
@@ -0,0 +1,22 @@
+from hypothesis.strategies import integers
+from hypothesis import given
+
+import pytest
+
+from dateutil.parser import parserinfo
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=100, max_value=9999))
+def test_convertyear(n):
+ assert n == parserinfo().convertyear(n)
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=-50,
+ max_value=49))
+def test_convertyear_no_specified_century(n):
+ p = parserinfo()
+ new_year = p._year + n
+ result = p.convertyear(new_year % 100, century_specified=False)
+ assert result == new_year
diff --git a/src/dateutil/test/property/test_tz_prop.py b/src/dateutil/test/property/test_tz_prop.py
new file mode 100644
index 0000000..ec6d271
--- /dev/null
+++ b/src/dateutil/test/property/test_tz_prop.py
@@ -0,0 +1,35 @@
+from datetime import datetime, timedelta
+
+import pytest
+import six
+from hypothesis import assume, given
+from hypothesis import strategies as st
+
+from dateutil import tz as tz
+
+EPOCHALYPSE = datetime.fromtimestamp(2147483647)
+NEGATIVE_EPOCHALYPSE = datetime.fromtimestamp(0) - timedelta(seconds=2147483648)
+
+
+@pytest.mark.gettz
+@pytest.mark.parametrize("gettz_arg", [None, ""])
+# TODO: Remove bounds when GH #590 is resolved
+@given(
+ dt=st.datetimes(
+ min_value=NEGATIVE_EPOCHALYPSE, max_value=EPOCHALYPSE, timezones=st.just(tz.UTC),
+ )
+)
+def test_gettz_returns_local(gettz_arg, dt):
+ act_tz = tz.gettz(gettz_arg)
+ if isinstance(act_tz, tz.tzlocal):
+ return
+
+ dt_act = dt.astimezone(tz.gettz(gettz_arg))
+ if six.PY2:
+ dt_exp = dt.astimezone(tz.tzlocal())
+ else:
+ dt_exp = dt.astimezone()
+
+ assert dt_act == dt_exp
+ assert dt_act.tzname() == dt_exp.tzname()
+ assert dt_act.utcoffset() == dt_exp.utcoffset()
diff --git a/src/dateutil/test/test_easter.py b/src/dateutil/test/test_easter.py
new file mode 100644
index 0000000..cf2ec7f
--- /dev/null
+++ b/src/dateutil/test/test_easter.py
@@ -0,0 +1,93 @@
+from dateutil.easter import easter
+from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN
+
+from datetime import date
+import pytest
+
+# List of easters between 1990 and 2050
+western_easter_dates = [
+ date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11),
+ date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30),
+ date(1998, 4, 12), date(1999, 4, 4),
+
+ date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20),
+ date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8),
+ date(2008, 3, 23), date(2009, 4, 12),
+
+ date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31),
+ date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16),
+ date(2018, 4, 1), date(2019, 4, 21),
+
+ date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9),
+ date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28),
+ date(2028, 4, 16), date(2029, 4, 1),
+
+ date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17),
+ date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5),
+ date(2038, 4, 25), date(2039, 4, 10),
+
+ date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29),
+ date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14),
+ date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10)
+ ]
+
+orthodox_easter_dates = [
+ date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18),
+ date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27),
+ date(1998, 4, 19), date(1999, 4, 11),
+
+ date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27),
+ date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8),
+ date(2008, 4, 27), date(2009, 4, 19),
+
+ date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5),
+ date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16),
+ date(2018, 4, 8), date(2019, 4, 28),
+
+ date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16),
+ date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2),
+ date(2028, 4, 16), date(2029, 4, 8),
+
+ date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24),
+ date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5),
+ date(2038, 4, 25), date(2039, 4, 17),
+
+ date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3),
+ date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21),
+ date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17)
+]
+
+# A random smattering of Julian dates.
+# Pulled values from http://www.kevinlaughery.com/east4099.html
+julian_easter_dates = [
+ date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31),
+ date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19),
+ date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8),
+ date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14),
+ date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6),
+ date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30),
+ date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24),
+ date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17),
+ date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3),
+ date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14)
+]
+
+
+@pytest.mark.parametrize("easter_date", western_easter_dates)
+def test_easter_western(easter_date):
+ assert easter_date == easter(easter_date.year, EASTER_WESTERN)
+
+
+@pytest.mark.parametrize("easter_date", orthodox_easter_dates)
+def test_easter_orthodox(easter_date):
+ assert easter_date == easter(easter_date.year, EASTER_ORTHODOX)
+
+
+@pytest.mark.parametrize("easter_date", julian_easter_dates)
+def test_easter_julian(easter_date):
+ assert easter_date == easter(easter_date.year, EASTER_JULIAN)
+
+
+def test_easter_bad_method():
+ with pytest.raises(ValueError):
+ easter(1975, 4)
diff --git a/src/dateutil/test/test_import_star.py b/src/dateutil/test/test_import_star.py
new file mode 100644
index 0000000..2fb7098
--- /dev/null
+++ b/src/dateutil/test/test_import_star.py
@@ -0,0 +1,33 @@
+"""Test for the "import *" functionality.
+
+As import * can be only done at module level, it has been added in a separate file
+"""
+import pytest
+
+prev_locals = list(locals())
+from dateutil import *
+new_locals = {name:value for name,value in locals().items()
+ if name not in prev_locals}
+new_locals.pop('prev_locals')
+
+
+@pytest.mark.import_star
+def test_imported_modules():
+ """ Test that `from dateutil import *` adds modules in __all__ locally """
+ import dateutil.easter
+ import dateutil.parser
+ import dateutil.relativedelta
+ import dateutil.rrule
+ import dateutil.tz
+ import dateutil.utils
+ import dateutil.zoneinfo
+
+ assert dateutil.easter == new_locals.pop("easter")
+ assert dateutil.parser == new_locals.pop("parser")
+ assert dateutil.relativedelta == new_locals.pop("relativedelta")
+ assert dateutil.rrule == new_locals.pop("rrule")
+ assert dateutil.tz == new_locals.pop("tz")
+ assert dateutil.utils == new_locals.pop("utils")
+ assert dateutil.zoneinfo == new_locals.pop("zoneinfo")
+
+ assert not new_locals
diff --git a/src/dateutil/test/test_imports.py b/src/dateutil/test/test_imports.py
new file mode 100644
index 0000000..7d0749e
--- /dev/null
+++ b/src/dateutil/test/test_imports.py
@@ -0,0 +1,240 @@
+import sys
+import unittest
+import pytest
+import six
+
+MODULE_TYPE = type(sys)
+
+
+# Tests live in datetutil/test which cause a RuntimeWarning for Python2 builds.
+# But since we expect lazy imports tests to fail for Python < 3.7 we'll ignore those
+# warnings with this filter.
+
+if six.PY2:
+ filter_import_warning = pytest.mark.filterwarnings("ignore::RuntimeWarning")
+else:
+
+ def filter_import_warning(f):
+ return f
+
+
+@pytest.fixture(scope="function")
+def clean_import():
+ """Create a somewhat clean import base for lazy import tests"""
+ du_modules = {
+ mod_name: mod
+ for mod_name, mod in sys.modules.items()
+ if mod_name.startswith("dateutil")
+ }
+
+ other_modules = {
+ mod_name for mod_name in sys.modules if mod_name not in du_modules
+ }
+
+ for mod_name in du_modules:
+ del sys.modules[mod_name]
+
+ yield
+
+ # Delete anything that wasn't in the origin sys.modules list
+ for mod_name in list(sys.modules):
+ if mod_name not in other_modules:
+ del sys.modules[mod_name]
+
+ # Restore original modules
+ for mod_name, mod in du_modules.items():
+ sys.modules[mod_name] = mod
+
+
+@filter_import_warning
+@pytest.mark.parametrize(
+ "module",
+ ["easter", "parser", "relativedelta", "rrule", "tz", "utils", "zoneinfo"],
+)
+def test_lazy_import(clean_import, module):
+ """Test that dateutil.[submodule] works for py version > 3.7"""
+
+ import dateutil, importlib
+
+ if sys.version_info < (3, 7):
+ pytest.xfail("Lazy loading does not work for Python < 3.7")
+
+ mod_obj = getattr(dateutil, module, None)
+ assert isinstance(mod_obj, MODULE_TYPE)
+
+ mod_imported = importlib.import_module("dateutil.%s" % module)
+ assert mod_obj is mod_imported
+
+
+HOST_IS_WINDOWS = sys.platform.startswith('win')
+
+
+def test_import_version_str():
+ """ Test that dateutil.__version__ can be imported"""
+ from dateutil import __version__
+
+
+def test_import_version_root():
+ import dateutil
+ assert hasattr(dateutil, '__version__')
+
+
+# Test that dateutil.easter-related imports work properly
+def test_import_easter_direct():
+ import dateutil.easter
+
+
+def test_import_easter_from():
+ from dateutil import easter
+
+
+def test_import_easter_start():
+ from dateutil.easter import easter
+
+
+# Test that dateutil.parser-related imports work properly
+def test_import_parser_direct():
+ import dateutil.parser
+
+
+def test_import_parser_from():
+ from dateutil import parser
+
+
+def test_import_parser_all():
+ # All interface
+ from dateutil.parser import parse
+ from dateutil.parser import parserinfo
+
+ # Other public classes
+ from dateutil.parser import parser
+
+ for var in (parse, parserinfo, parser):
+ assert var is not None
+
+
+# Test that dateutil.relativedelta-related imports work properly
+def test_import_relative_delta_direct():
+ import dateutil.relativedelta
+
+
+def test_import_relative_delta_from():
+ from dateutil import relativedelta
+
+def test_import_relative_delta_all():
+ from dateutil.relativedelta import relativedelta
+ from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
+
+ for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU):
+ assert var is not None
+
+ # In the public interface but not in all
+ from dateutil.relativedelta import weekday
+ assert weekday is not None
+
+
+# Test that dateutil.rrule related imports work properly
+def test_import_rrule_direct():
+ import dateutil.rrule
+
+
+def test_import_rrule_from():
+ from dateutil import rrule
+
+
+def test_import_rrule_all():
+ from dateutil.rrule import rrule
+ from dateutil.rrule import rruleset
+ from dateutil.rrule import rrulestr
+ from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY
+ from dateutil.rrule import HOURLY, MINUTELY, SECONDLY
+ from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU
+
+ rr_all = (rrule, rruleset, rrulestr,
+ YEARLY, MONTHLY, WEEKLY, DAILY,
+ HOURLY, MINUTELY, SECONDLY,
+ MO, TU, WE, TH, FR, SA, SU)
+
+ for var in rr_all:
+ assert var is not None
+
+ # In the public interface but not in all
+ from dateutil.rrule import weekday
+ assert weekday is not None
+
+
+# Test that dateutil.tz related imports work properly
+def test_import_tztest_direct():
+ import dateutil.tz
+
+
+def test_import_tz_from():
+ from dateutil import tz
+
+
+def test_import_tz_all():
+ from dateutil.tz import tzutc
+ from dateutil.tz import tzoffset
+ from dateutil.tz import tzlocal
+ from dateutil.tz import tzfile
+ from dateutil.tz import tzrange
+ from dateutil.tz import tzstr
+ from dateutil.tz import tzical
+ from dateutil.tz import gettz
+ from dateutil.tz import tzwin
+ from dateutil.tz import tzwinlocal
+ from dateutil.tz import UTC
+ from dateutil.tz import datetime_ambiguous
+ from dateutil.tz import datetime_exists
+ from dateutil.tz import resolve_imaginary
+
+ tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+ "tzstr", "tzical", "gettz", "datetime_ambiguous",
+ "datetime_exists", "resolve_imaginary", "UTC"]
+
+ tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else []
+ lvars = locals()
+
+ for var in tz_all:
+ assert lvars[var] is not None
+
+# Test that dateutil.tzwin related imports work properly
+@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
+def test_import_tz_windows_direct():
+ import dateutil.tzwin
+
+
+@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
+def test_import_tz_windows_from():
+ from dateutil import tzwin
+
+
+@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
+def test_import_tz_windows_star():
+ from dateutil.tzwin import tzwin
+ from dateutil.tzwin import tzwinlocal
+
+ tzwin_all = [tzwin, tzwinlocal]
+
+ for var in tzwin_all:
+ assert var is not None
+
+
+# Test imports of Zone Info
+def test_import_zone_info_direct():
+ import dateutil.zoneinfo
+
+
+def test_import_zone_info_from():
+ from dateutil import zoneinfo
+
+
+def test_import_zone_info_star():
+ from dateutil.zoneinfo import gettz
+ from dateutil.zoneinfo import gettz_db_metadata
+ from dateutil.zoneinfo import rebuild
+
+ zi_all = (gettz, gettz_db_metadata, rebuild)
+
+ for var in zi_all:
+ assert var is not None
diff --git a/src/dateutil/test/test_internals.py b/src/dateutil/test/test_internals.py
new file mode 100644
index 0000000..5308131
--- /dev/null
+++ b/src/dateutil/test/test_internals.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for implementation details, not necessarily part of the user-facing
+API.
+
+The motivating case for these tests is #483, where we want to smoke-test
+code that may be difficult to reach through the standard API calls.
+"""
+
+import sys
+import pytest
+
+from dateutil.parser._parser import _ymd
+from dateutil import tz
+
+IS_PY32 = sys.version_info[0:2] == (3, 2)
+
+
+@pytest.mark.smoke
+def test_YMD_could_be_day():
+ ymd = _ymd('foo bar 124 baz')
+
+ ymd.append(2, 'M')
+ assert ymd.has_month
+ assert not ymd.has_year
+ assert ymd.could_be_day(4)
+ assert not ymd.could_be_day(-6)
+ assert not ymd.could_be_day(32)
+
+ # Assumes leap year
+ assert ymd.could_be_day(29)
+
+ ymd.append(1999)
+ assert ymd.has_year
+ assert not ymd.could_be_day(29)
+
+ ymd.append(16, 'D')
+ assert ymd.has_day
+ assert not ymd.could_be_day(1)
+
+ ymd = _ymd('foo bar 124 baz')
+ ymd.append(1999)
+ assert ymd.could_be_day(31)
+
+
+###
+# Test that private interfaces in _parser are deprecated properly
+@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
+def test_parser_private_warns():
+ from dateutil.parser import _timelex, _tzparser
+ from dateutil.parser import _parsetz
+
+ with pytest.warns(DeprecationWarning):
+ _tzparser()
+
+ with pytest.warns(DeprecationWarning):
+ _timelex('2014-03-03')
+
+ with pytest.warns(DeprecationWarning):
+ _parsetz('+05:00')
+
+
+@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
+def test_parser_parser_private_not_warns():
+ from dateutil.parser._parser import _timelex, _tzparser
+ from dateutil.parser._parser import _parsetz
+
+ with pytest.warns(None) as recorder:
+ _tzparser()
+ assert len(recorder) == 0
+
+ with pytest.warns(None) as recorder:
+ _timelex('2014-03-03')
+
+ assert len(recorder) == 0
+
+ with pytest.warns(None) as recorder:
+ _parsetz('+05:00')
+ assert len(recorder) == 0
+
+
+@pytest.mark.tzstr
+def test_tzstr_internal_timedeltas():
+ with pytest.warns(tz.DeprecatedTzFormatWarning):
+ tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200")
+
+ with pytest.warns(tz.DeprecatedTzFormatWarning):
+ tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200")
+
+ assert tz1._start_delta != tz2._start_delta
+ assert tz1._end_delta != tz2._end_delta
diff --git a/src/dateutil/test/test_isoparser.py b/src/dateutil/test/test_isoparser.py
new file mode 100644
index 0000000..35899ab
--- /dev/null
+++ b/src/dateutil/test/test_isoparser.py
@@ -0,0 +1,509 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime, timedelta, date, time
+import itertools as it
+
+from dateutil import tz
+from dateutil.tz import UTC
+from dateutil.parser import isoparser, isoparse
+
+import pytest
+import six
+
+
+def _generate_tzoffsets(limited):
+ def _mkoffset(hmtuple, fmt):
+ h, m = hmtuple
+ m_td = (-1 if h < 0 else 1) * m
+
+ tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
+ return tzo, fmt.format(h, m)
+
+ out = []
+ if not limited:
+ # The subset that's just hours
+ hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
+ out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])
+
+ # Ones that have hours and minutes
+ hm_out = [] + hm_out_h
+ hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
+ else:
+ hm_out = [(-5, -0)]
+
+ fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
+ out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]
+
+ # Also add in UTC and naive
+ out.append((UTC, 'Z'))
+ out.append((None, ''))
+
+ return out
+
+FULL_TZOFFSETS = _generate_tzoffsets(False)
+FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
+TZOFFSETS = _generate_tzoffsets(True)
+
+DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
+@pytest.mark.parametrize('dt', tuple(DATES))
+def test_year_only(dt):
+ dtstr = dt.strftime('%Y')
+
+ assert isoparse(dtstr) == dt
+
+DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
+@pytest.mark.parametrize('dt', tuple(DATES))
+def test_year_month(dt):
+ fmt = '%Y-%m'
+ dtstr = dt.strftime(fmt)
+
+ assert isoparse(dtstr) == dt
+
+DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
+YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
+@pytest.mark.parametrize('dt', tuple(DATES))
+@pytest.mark.parametrize('fmt', YMD_FMTS)
+def test_year_month_day(dt, fmt):
+ dtstr = dt.strftime(fmt)
+
+ assert isoparse(dtstr) == dt
+
+def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
+ microsecond_precision=None):
+ tzi, offset_str = tzoffset
+ fmt = date_fmt + 'T' + time_fmt
+ dt = dt.replace(tzinfo=tzi)
+ dtstr = dt.strftime(fmt)
+
+ if microsecond_precision is not None:
+ if not fmt.endswith('%f'): # pragma: nocover
+ raise ValueError('Time format has no microseconds!')
+
+ if microsecond_precision != 6:
+ dtstr = dtstr[:-(6 - microsecond_precision)]
+ elif microsecond_precision > 6: # pragma: nocover
+ raise ValueError('Precision must be 1-6')
+
+ dtstr += offset_str
+
+ assert isoparse(dtstr) == dt
+
+DATETIMES = [datetime(1998, 4, 16, 12),
+ datetime(2019, 11, 18, 23),
+ datetime(2014, 12, 16, 4)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_h(dt, date_fmt, tzoffset):
+ _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)
+
+DATETIMES = [datetime(2012, 1, 6, 9, 37)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
+ _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
+ datetime(2003, 8, 8, 14, 9, 14),
+ datetime(2003, 4, 7, 6, 14, 59)]
+HMS_FMTS = ('%H%M%S', '%H:%M:%S')
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', HMS_FMTS)
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
+ _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS
+ for sep in '.,'))
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+@pytest.mark.parametrize('precision', list(range(3, 7)))
+def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
+ # Truncate the microseconds to the desired precision for the representation
+ dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))
+
+ _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
+
+###
+# Truncation of extra digits beyond microsecond precision
+@pytest.mark.parametrize('dt_str', [
+ '2018-07-03T14:07:00.123456000001',
+ '2018-07-03T14:07:00.123456999999',
+])
+def test_extra_subsecond_digits(dt_str):
+ assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456)
+
+@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
+def test_full_tzoffsets(tzoffset):
+ dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
+ date_fmt = '%Y-%m-%d'
+ time_fmt = '%H:%M:%S.%f'
+
+ _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+@pytest.mark.parametrize('dt_str', [
+ '2014-04-11T00',
+ '2014-04-10T24',
+ '2014-04-11T00:00',
+ '2014-04-10T24:00',
+ '2014-04-11T00:00:00',
+ '2014-04-10T24:00:00',
+ '2014-04-11T00:00:00.000',
+ '2014-04-10T24:00:00.000',
+ '2014-04-11T00:00:00.000000',
+ '2014-04-10T24:00:00.000000']
+)
+def test_datetime_midnight(dt_str):
+ assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
+
+@pytest.mark.parametrize('datestr', [
+ '2014-01-01',
+ '20140101',
+])
+@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
+def test_isoparse_sep_none(datestr, sep):
+ isostr = datestr + sep + '14:33:09'
+ assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)
+
+##
+# Uncommon date formats
+TIME_ARGS = ('time_args',
+ ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
+ for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
+ TZOFFSETS)))
+
+@pytest.mark.parametrize('isocal,dt_expected',[
+ ((2017, 10), datetime(2017, 3, 6)),
+ ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year
+ ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014
+])
+def test_isoweek(isocal, dt_expected):
+ # TODO: Figure out how to parametrize this on formats, too
+ for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
+ dtstr = fmt.format(*isocal)
+ assert isoparse(dtstr) == dt_expected
+
+@pytest.mark.parametrize('isocal,dt_expected',[
+ ((2016, 13, 7), datetime(2016, 4, 3)),
+ ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year
+ ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year
+ ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year
+])
+def test_isoweek_day(isocal, dt_expected):
+ # TODO: Figure out how to parametrize this on formats, too
+ for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
+ dtstr = fmt.format(*isocal)
+ assert isoparse(dtstr) == dt_expected
+
+@pytest.mark.parametrize('isoord,dt_expected', [
+ ((2004, 1), datetime(2004, 1, 1)),
+ ((2016, 60), datetime(2016, 2, 29)),
+ ((2017, 60), datetime(2017, 3, 1)),
+ ((2016, 366), datetime(2016, 12, 31)),
+ ((2017, 365), datetime(2017, 12, 31))
+])
+def test_iso_ordinal(isoord, dt_expected):
+ for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
+ dtstr = fmt.format(*isoord)
+
+ assert isoparse(dtstr) == dt_expected
+
+
+###
+# Acceptance of bytes
+@pytest.mark.parametrize('isostr,dt', [
+ (b'2014', datetime(2014, 1, 1)),
+ (b'20140204', datetime(2014, 2, 4)),
+ (b'2014-02-04', datetime(2014, 2, 4)),
+ (b'2014-02-04T12', datetime(2014, 2, 4, 12)),
+ (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
+ (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
+ (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
+ (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
+ (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
+ UTC)),
+ (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000,
+ UTC)),
+ (b'2014-02-04T12:30:15.224+05:00',
+ datetime(2014, 2, 4, 12, 30, 15, 224000,
+ tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
+def test_bytes(isostr, dt):
+ assert isoparse(isostr) == dt
+
+
+###
+# Invalid ISO strings
+@pytest.mark.parametrize('isostr,exception', [
+ ('201', ValueError), # ISO string too short
+ ('2012-0425', ValueError), # Inconsistent date separators
+ ('201204-25', ValueError), # Inconsistent date separators
+ ('20120425T0120:00', ValueError), # Inconsistent time separators
+ ('20120425T01:2000', ValueError), # Inconsistent time separators
+ ('14:3015', ValueError), # Inconsistent time separator
+ ('20120425T012500-334', ValueError), # Wrong microsecond separator
+ ('2001-1', ValueError), # YYYY-M not valid
+ ('2012-04-9', ValueError), # YYYY-MM-D not valid
+ ('201204', ValueError), # YYYYMM not valid
+ ('20120411T03:30+', ValueError), # Time zone too short
+ ('20120411T03:30+1234567', ValueError), # Time zone too long
+ ('20120411T03:30-25:40', ValueError), # Time zone invalid
+ ('2012-1a', ValueError), # Invalid month
+ ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes
+ ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes
+ ('20120411T033030.123456012:00', # No sign in time zone
+ ValueError),
+ ('2012-W00', ValueError), # Invalid ISO week
+ ('2012-W55', ValueError), # Invalid ISO week
+ ('2012-W01-0', ValueError), # Invalid ISO week day
+ ('2012-W01-8', ValueError), # Invalid ISO week day
+ ('2013-000', ValueError), # Invalid ordinal day
+ ('2013-366', ValueError), # Invalid ordinal day
+ ('2013366', ValueError), # Invalid ordinal day
+ ('2014-03-12Т12:30:14', ValueError), # Cyrillic T
+ ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight
+ ('2014_W01-1', ValueError), # Invalid separator
+ ('2014W01-1', ValueError), # Inconsistent use of dashes
+ ('2014-W011', ValueError), # Inconsistent use of dashes
+
+])
+def test_iso_raises(isostr, exception):
+ with pytest.raises(exception):
+ isoparse(isostr)
+
+
+@pytest.mark.parametrize('sep_act, valid_sep, exception', [
+ ('T', 'C', ValueError),
+ ('C', 'T', ValueError),
+])
+def test_iso_with_sep_raises(sep_act, valid_sep, exception):
+ parser = isoparser(sep=valid_sep)
+ isostr = '2012-04-25' + sep_act + '01:25:00'
+ with pytest.raises(exception):
+ parser.isoparse(isostr)
+
+
+###
+# Test ISOParser constructor
+@pytest.mark.parametrize('sep', [' ', '9', '🍛'])
+def test_isoparser_invalid_sep(sep):
+ with pytest.raises(ValueError):
+ isoparser(sep=sep)
+
+
+# This only fails on Python 3
+@pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only")
+def test_isoparser_byte_sep():
+ dt = datetime(2017, 12, 6, 12, 30, 45)
+ dt_str = dt.isoformat(sep=str('T'))
+
+ dt_rt = isoparser(sep=b'T').isoparse(dt_str)
+
+ assert dt == dt_rt
+
+
+###
+# Test parse_tzstr
+@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
+def test_parse_tzstr(tzoffset):
+ dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
+ date_fmt = '%Y-%m-%d'
+ time_fmt = '%H:%M:%S.%f'
+
+ _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+
+@pytest.mark.parametrize('tzstr', [
+ '-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
+])
+@pytest.mark.parametrize('zero_as_utc', [True, False])
+def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
+ tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
+ assert tzi == UTC
+ assert (type(tzi) == tz.tzutc) == zero_as_utc
+
+
+@pytest.mark.parametrize('tzstr,exception', [
+ ('00:00', ValueError), # No sign
+ ('05:00', ValueError), # No sign
+ ('_00:00', ValueError), # Invalid sign
+ ('+25:00', ValueError), # Offset too large
+ ('00:0000', ValueError), # String too long
+])
+def test_parse_tzstr_fails(tzstr, exception):
+ with pytest.raises(exception):
+ isoparser().parse_tzstr(tzstr)
+
+###
+# Test parse_isodate
+def __make_date_examples():
+ dates_no_day = [
+ date(1999, 12, 1),
+ date(2016, 2, 1)
+ ]
+
+ if not six.PY2:
+ # strftime does not support dates before 1900 in Python 2
+ dates_no_day.append(date(1000, 11, 1))
+
+ # Only one supported format for dates with no day
+ o = zip(dates_no_day, it.repeat('%Y-%m'))
+
+ dates_w_day = [
+ date(1969, 12, 31),
+ date(1900, 1, 1),
+ date(2016, 2, 29),
+ date(2017, 11, 14)
+ ]
+
+ dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
+ o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))
+
+ return list(o)
+
+
+@pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
+@pytest.mark.parametrize('as_bytes', [True, False])
+def test_parse_isodate(d, dt_fmt, as_bytes):
+ d_str = d.strftime(dt_fmt)
+ if isinstance(d_str, six.text_type) and as_bytes:
+ d_str = d_str.encode('ascii')
+ elif isinstance(d_str, bytes) and not as_bytes:
+ d_str = d_str.decode('ascii')
+
+ iparser = isoparser()
+ assert iparser.parse_isodate(d_str) == d
+
+
+@pytest.mark.parametrize('isostr,exception', [
+ ('243', ValueError), # ISO string too short
+ ('2014-0423', ValueError), # Inconsistent date separators
+ ('201404-23', ValueError), # Inconsistent date separators
+ ('2014日03月14', ValueError), # Not ASCII
+ ('2013-02-29', ValueError), # Not a leap year
+ ('2014/12/03', ValueError), # Wrong separators
+ ('2014-04-19T', ValueError), # Unknown components
+ ('201202', ValueError), # Invalid format
+])
+def test_isodate_raises(isostr, exception):
+ with pytest.raises(exception):
+ isoparser().parse_isodate(isostr)
+
+
+def test_parse_isodate_error_text():
+ with pytest.raises(ValueError) as excinfo:
+ isoparser().parse_isodate('2014-0423')
+
+ # ensure the error message does not contain b' prefixes
+ if six.PY2:
+ expected_error = "String contains unknown ISO components: u'2014-0423'"
+ else:
+ expected_error = "String contains unknown ISO components: '2014-0423'"
+ assert expected_error == str(excinfo.value)
+
+
+###
+# Test parse_isotime
+def __make_time_examples():
+ outputs = []
+
+ # HH
+ time_h = [time(0), time(8), time(22)]
+ time_h_fmts = ['%H']
+
+ outputs.append(it.product(time_h, time_h_fmts))
+
+ # HHMM / HH:MM
+ time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
+ time_hm_fmts = ['%H%M', '%H:%M']
+
+ outputs.append(it.product(time_hm, time_hm_fmts))
+
+ # HHMMSS / HH:MM:SS
+ time_hms = [time(0, 0, 0), time(0, 15, 30),
+ time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]
+
+ time_hms_fmts = ['%H%M%S', '%H:%M:%S']
+
+ outputs.append(it.product(time_hms, time_hms_fmts))
+
+ # HHMMSS.ffffff / HH:MM:SS.ffffff
+ time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
+ time(14, 21, 59, 948730),
+ time(23, 59, 59, 999999)]
+
+ time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']
+
+ outputs.append(it.product(time_hmsu, time_hmsu_fmts))
+
+ outputs = list(map(list, outputs))
+
+ # Time zones
+ ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
+ o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr))
+ o = ((t.replace(tzinfo=tzi), fmt + off_str)
+ for (t, fmt), (tzi, off_str) in o)
+
+ outputs.append(o)
+
+ return list(it.chain.from_iterable(outputs))
+
+
+@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
+@pytest.mark.parametrize('as_bytes', [True, False])
+def test_isotime(time_val, time_fmt, as_bytes):
+ tstr = time_val.strftime(time_fmt)
+ if isinstance(tstr, six.text_type) and as_bytes:
+ tstr = tstr.encode('ascii')
+ elif isinstance(tstr, bytes) and not as_bytes:
+ tstr = tstr.decode('ascii')
+
+ iparser = isoparser()
+
+ assert iparser.parse_isotime(tstr) == time_val
+
+
+@pytest.mark.parametrize('isostr', [
+ '24:00',
+ '2400',
+ '24:00:00',
+ '240000',
+ '24:00:00.000',
+ '24:00:00,000',
+ '24:00:00.000000',
+ '24:00:00,000000',
+])
+def test_isotime_midnight(isostr):
+ iparser = isoparser()
+ assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0)
+
+
+@pytest.mark.parametrize('isostr,exception', [
+ ('3', ValueError), # ISO string too short
+ ('14時30分15秒', ValueError), # Not ASCII
+ ('14_30_15', ValueError), # Invalid separators
+ ('1430:15', ValueError), # Inconsistent separator use
+ ('25', ValueError), # Invalid hours
+ ('25:15', ValueError), # Invalid hours
+ ('14:60', ValueError), # Invalid minutes
+ ('14:59:61', ValueError), # Invalid seconds
+ ('14:30:15.34468305:00', ValueError), # No sign in time zone
+ ('14:30:15+', ValueError), # Time zone too short
+ ('14:30:15+1234567', ValueError), # Time zone invalid
+ ('14:59:59+25:00', ValueError), # Invalid tz hours
+ ('14:59:59+12:62', ValueError), # Invalid tz minutes
+ ('14:59:30_344583', ValueError), # Invalid microsecond separator
+ ('24:01', ValueError), # 24 used for non-midnight time
+ ('24:00:01', ValueError), # 24 used for non-midnight time
+ ('24:00:00.001', ValueError), # 24 used for non-midnight time
+ ('24:00:00.000001', ValueError), # 24 used for non-midnight time
+])
+def test_isotime_raises(isostr, exception):
+ iparser = isoparser()
+ with pytest.raises(exception):
+ iparser.parse_isotime(isostr)
diff --git a/src/dateutil/test/test_parser.py b/src/dateutil/test/test_parser.py
new file mode 100644
index 0000000..08a34da
--- /dev/null
+++ b/src/dateutil/test/test_parser.py
@@ -0,0 +1,964 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import itertools
+from datetime import datetime, timedelta
+import unittest
+import sys
+
+from dateutil import tz
+from dateutil.tz import tzoffset
+from dateutil.parser import parse, parserinfo
+from dateutil.parser import ParserError
+from dateutil.parser import UnknownTimezoneWarning
+
+from ._common import TZEnvContext
+
+from six import assertRaisesRegex, PY2
+from io import StringIO
+
+import pytest
+
+# Platform info
+IS_WIN = sys.platform.startswith('win')
+
+PLATFORM_HAS_DASH_D = False
+try:
+ if datetime.now().strftime('%-d'):
+ PLATFORM_HAS_DASH_D = True
+except ValueError:
+ pass
+
+
+@pytest.fixture(params=[True, False])
+def fuzzy(request):
+ """Fixture to pass fuzzy=True or fuzzy=False to parse"""
+ return request.param
+
+
+# Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message)
+PARSER_TEST_CASES = [
+ ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"),
+ ("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"),
+ ("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"),
+ ("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"),
+ ("2003-09-25", datetime(2003, 9, 25), "iso format strip"),
+ ("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"),
+ ("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"),
+ ("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"),
+ ("20030925", datetime(2003, 9, 25), "iso stripped format strip"),
+ ("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"),
+ ("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"),
+ ("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"),
+ ("09-25-2003", datetime(2003, 9, 25), "date with dash"),
+ ("25-09-2003", datetime(2003, 9, 25), "date with dash"),
+ ("10-09-2003", datetime(2003, 10, 9), "date with dash"),
+ ("10-09-03", datetime(2003, 10, 9), "date with dash"),
+ ("2003.09.25", datetime(2003, 9, 25), "date with dot"),
+ ("09.25.2003", datetime(2003, 9, 25), "date with dot"),
+ ("25.09.2003", datetime(2003, 9, 25), "date with dot"),
+ ("10.09.2003", datetime(2003, 10, 9), "date with dot"),
+ ("10.09.03", datetime(2003, 10, 9), "date with dot"),
+ ("2003/09/25", datetime(2003, 9, 25), "date with slash"),
+ ("09/25/2003", datetime(2003, 9, 25), "date with slash"),
+ ("25/09/2003", datetime(2003, 9, 25), "date with slash"),
+ ("10/09/2003", datetime(2003, 10, 9), "date with slash"),
+ ("10/09/03", datetime(2003, 10, 9), "date with slash"),
+ ("2003 09 25", datetime(2003, 9, 25), "date with space"),
+ ("09 25 2003", datetime(2003, 9, 25), "date with space"),
+ ("25 09 2003", datetime(2003, 9, 25), "date with space"),
+ ("10 09 2003", datetime(2003, 10, 9), "date with space"),
+ ("10 09 03", datetime(2003, 10, 9), "date with space"),
+ ("25 09 03", datetime(2003, 9, 25), "date with space"),
+ ("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"),
+ ("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"),
+ (" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"),
+ ("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"),
+ ("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"),
+ ("July 4, 1976", datetime(1976, 7, 4), "random format"),
+ ("7 4 1976", datetime(1976, 7, 4), "random format"),
+ ("4 jul 1976", datetime(1976, 7, 4), "random format"),
+ ("4 Jul 1976", datetime(1976, 7, 4), "'%-d %b %Y' format"),
+ ("7-4-76", datetime(1976, 7, 4), "random format"),
+ ("19760704", datetime(1976, 7, 4), "random format"),
+ ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"),
+ ("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"),
+ ("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"),
+ ("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"),
+ ("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"),
+ ("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"),
+ ("3rd of May 2001", datetime(2001, 5, 3), "random format"),
+ ("5th of March 2001", datetime(2001, 3, 5), "random format"),
+ ("1st of May 2003", datetime(2003, 5, 1), "random format"),
+ ('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"),
+ ('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"),
+ ("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"),
+ ('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"),
+ ('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"),
+ ('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)"),
+
+ # Cases with legacy h/m/s format, candidates for deprecation (GH#886)
+ ("2016-12-21 04.2h", datetime(2016, 12, 21, 4, 12), "Fractional Hours"),
+]
+# Check that we don't have any duplicates
+assert len(set([x[0] for x in PARSER_TEST_CASES])) == len(PARSER_TEST_CASES)
+
+
+@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES)
+def test_parser(parsable_text, expected_datetime, assertion_message):
+ assert parse(parsable_text) == expected_datetime, assertion_message
+
+
+# Parser test cases using datetime(2003, 9, 25) as a default.
+# Format: (parsable_text, expected_datetime, assertion_message)
+PARSER_DEFAULT_TEST_CASES = [
+ ("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
+ ("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"),
+ ("Sep 2003", datetime(2003, 9, 25), "date command format strip"),
+ ("Sep", datetime(2003, 9, 25), "date command format strip"),
+ ("2003", datetime(2003, 9, 25), "date command format strip"),
+ ("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"),
+ ("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"),
+ ("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"),
+ ("10h", datetime(2003, 9, 25, 10), "hour with letters strip"),
+ ("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"),
+ ("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"),
+ ("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"),
+ ("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"),
+ ("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"),
+ ("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"),
+ ("10h am", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10h pm", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("10am", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10pm", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("10:00am", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"),
+ ("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"),
+ ("Wed", datetime(2003, 10, 1), "weekday alone"),
+ ("Wednesday", datetime(2003, 10, 1), "long weekday"),
+ ("October", datetime(2003, 10, 25), "long month"),
+ ("31-Dec-00", datetime(2000, 12, 31), "zero year"),
+ ("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"),
+ ("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"),
+ ("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"),
+ ("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"),
+ ("01h02", datetime(2003, 9, 25, 1, 2), "random format"),
+ ("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"),
+ ("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"),
+ ("01m02h", datetime(2003, 9, 25, 2, 1), "random format"),
+ ("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format")
+]
+# Check that we don't have any duplicates
+assert len(set([x[0] for x in PARSER_DEFAULT_TEST_CASES])) == len(PARSER_DEFAULT_TEST_CASES)
+
+
+@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES)
+def test_parser_default(parsable_text, expected_datetime, assertion_message):
+ assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message
+
+
+@pytest.mark.parametrize('sep', ['-', '.', '/', ' '])
+def test_parse_dayfirst(sep):
+ expected = datetime(2003, 9, 10)
+ fmt = sep.join(['%d', '%m', '%Y'])
+ dstr = expected.strftime(fmt)
+ result = parse(dstr, dayfirst=True)
+ assert result == expected
+
+
+@pytest.mark.parametrize('sep', ['-', '.', '/', ' '])
+def test_parse_yearfirst(sep):
+ expected = datetime(2010, 9, 3)
+ fmt = sep.join(['%Y', '%m', '%d'])
+ dstr = expected.strftime(fmt)
+ result = parse(dstr, yearfirst=True)
+ assert result == expected
+
+
+@pytest.mark.parametrize('dstr,expected', [
+ ("Thu Sep 25 10:36:28 BRST 2003", datetime(2003, 9, 25, 10, 36, 28)),
+ ("1996.07.10 AD at 15:08:56 PDT", datetime(1996, 7, 10, 15, 8, 56)),
+ ("Tuesday, April 12, 1952 AD 3:30:42pm PST",
+ datetime(1952, 4, 12, 15, 30, 42)),
+ ("November 5, 1994, 8:15:30 am EST", datetime(1994, 11, 5, 8, 15, 30)),
+ ("1994-11-05T08:15:30-05:00", datetime(1994, 11, 5, 8, 15, 30)),
+ ("1994-11-05T08:15:30Z", datetime(1994, 11, 5, 8, 15, 30)),
+ ("1976-07-04T00:01:02Z", datetime(1976, 7, 4, 0, 1, 2)),
+ ("1986-07-05T08:15:30z", datetime(1986, 7, 5, 8, 15, 30)),
+ ("Tue Apr 4 00:22:12 PDT 1995", datetime(1995, 4, 4, 0, 22, 12)),
+])
+def test_parse_ignoretz(dstr, expected):
+ result = parse(dstr, ignoretz=True)
+ assert result == expected
+
+
+_brsttz = tzoffset("BRST", -10800)
+
+
+@pytest.mark.parametrize('dstr,expected', [
+ ("20030925T104941-0300",
+ datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
+ ("Thu, 25 Sep 2003 10:49:41 -0300",
+ datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
+ ("2003-09-25T10:49:41.5-03:00",
+ datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)),
+ ("2003-09-25T10:49:41-03:00",
+ datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
+ ("20030925T104941.5-0300",
+ datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)),
+])
+def test_parse_with_tzoffset(dstr, expected):
+ # In these cases, we are _not_ passing a tzinfos arg
+ result = parse(dstr)
+ assert result == expected
+
+
+class TestFormat(object):
+
+ def test_ybd(self):
+ # If we have a 4-digit year, a non-numeric month (abbreviated or not),
+ # and a day (1 or 2 digits), then there is no ambiguity as to which
+ # token is a year/month/day. This holds regardless of what order the
+ # terms are in and for each of the separators below.
+
+ seps = ['-', ' ', '/', '.']
+
+ year_tokens = ['%Y']
+ month_tokens = ['%b', '%B']
+ day_tokens = ['%d']
+ if PLATFORM_HAS_DASH_D:
+ day_tokens.append('%-d')
+
+ prods = itertools.product(year_tokens, month_tokens, day_tokens)
+ perms = [y for x in prods for y in itertools.permutations(x)]
+ unambig_fmts = [sep.join(perm) for sep in seps for perm in perms]
+
+ actual = datetime(2003, 9, 25)
+
+ for fmt in unambig_fmts:
+ dstr = actual.strftime(fmt)
+ res = parse(dstr)
+ assert res == actual
+
+ # TODO: some redundancy with PARSER_TEST_CASES cases
+ @pytest.mark.parametrize("fmt,dstr", [
+ ("%a %b %d %Y", "Thu Sep 25 2003"),
+ ("%b %d %Y", "Sep 25 2003"),
+ ("%Y-%m-%d", "2003-09-25"),
+ ("%Y%m%d", "20030925"),
+ ("%Y-%b-%d", "2003-Sep-25"),
+ ("%d-%b-%Y", "25-Sep-2003"),
+ ("%b-%d-%Y", "Sep-25-2003"),
+ ("%m-%d-%Y", "09-25-2003"),
+ ("%d-%m-%Y", "25-09-2003"),
+ ("%Y.%m.%d", "2003.09.25"),
+ ("%Y.%b.%d", "2003.Sep.25"),
+ ("%d.%b.%Y", "25.Sep.2003"),
+ ("%b.%d.%Y", "Sep.25.2003"),
+ ("%m.%d.%Y", "09.25.2003"),
+ ("%d.%m.%Y", "25.09.2003"),
+ ("%Y/%m/%d", "2003/09/25"),
+ ("%Y/%b/%d", "2003/Sep/25"),
+ ("%d/%b/%Y", "25/Sep/2003"),
+ ("%b/%d/%Y", "Sep/25/2003"),
+ ("%m/%d/%Y", "09/25/2003"),
+ ("%d/%m/%Y", "25/09/2003"),
+ ("%Y %m %d", "2003 09 25"),
+ ("%Y %b %d", "2003 Sep 25"),
+ ("%d %b %Y", "25 Sep 2003"),
+ ("%m %d %Y", "09 25 2003"),
+ ("%d %m %Y", "25 09 2003"),
+ ("%y %d %b", "03 25 Sep",),
+ ])
+ def test_strftime_formats_2003Sep25(self, fmt, dstr):
+ expected = datetime(2003, 9, 25)
+
+ # First check that the format strings behave as expected
+ # (not strictly necessary, but nice to have)
+ assert expected.strftime(fmt) == dstr
+
+ res = parse(dstr)
+ assert res == expected
+
+
+class TestInputTypes(object):
+ def test_empty_string_invalid(self):
+ with pytest.raises(ParserError):
+ parse('')
+
+ def test_none_invalid(self):
+ with pytest.raises(TypeError):
+ parse(None)
+
+ def test_int_invalid(self):
+ with pytest.raises(TypeError):
+ parse(13)
+
+ def test_duck_typing(self):
+ # We want to support arbitrary classes that implement the stream
+ # interface.
+
+ class StringPassThrough(object):
+ def __init__(self, stream):
+ self.stream = stream
+
+ def read(self, *args, **kwargs):
+ return self.stream.read(*args, **kwargs)
+
+ dstr = StringPassThrough(StringIO('2014 January 19'))
+
+ res = parse(dstr)
+ expected = datetime(2014, 1, 19)
+ assert res == expected
+
+ def test_parse_stream(self):
+ dstr = StringIO('2014 January 19')
+
+ res = parse(dstr)
+ expected = datetime(2014, 1, 19)
+ assert res == expected
+
+ def test_parse_str(self):
+ # Parser should be able to handle bytestring and unicode
+ uni_str = '2014-05-01 08:00:00'
+ bytes_str = uni_str.encode()
+
+ res = parse(bytes_str)
+ expected = parse(uni_str)
+ assert res == expected
+
+ def test_parse_bytes(self):
+ res = parse(b'2014 January 19')
+ expected = datetime(2014, 1, 19)
+ assert res == expected
+
+ def test_parse_bytearray(self):
+ # GH#417
+ res = parse(bytearray(b'2014 January 19'))
+ expected = datetime(2014, 1, 19)
+ assert res == expected
+
+
+class TestTzinfoInputTypes(object):
+ def assert_equal_same_tz(self, dt1, dt2):
+ assert dt1 == dt2
+ assert dt1.tzinfo is dt2.tzinfo
+
+ def test_tzinfo_dict_could_return_none(self):
+ dstr = "2017-02-03 12:40 BRST"
+ result = parse(dstr, tzinfos={"BRST": None})
+ expected = datetime(2017, 2, 3, 12, 40)
+ self.assert_equal_same_tz(result, expected)
+
+ def test_tzinfos_callable_could_return_none(self):
+ dstr = "2017-02-03 12:40 BRST"
+ result = parse(dstr, tzinfos=lambda *args: None)
+ expected = datetime(2017, 2, 3, 12, 40)
+ self.assert_equal_same_tz(result, expected)
+
+ def test_invalid_tzinfo_input(self):
+ dstr = "2014 January 19 09:00 UTC"
+ # Pass an absurd tzinfos object
+ tzinfos = {"UTC": ValueError}
+ with pytest.raises(TypeError):
+ parse(dstr, tzinfos=tzinfos)
+
+ def test_valid_tzinfo_tzinfo_input(self):
+ dstr = "2014 January 19 09:00 UTC"
+ tzinfos = {"UTC": tz.UTC}
+ expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC)
+ res = parse(dstr, tzinfos=tzinfos)
+ self.assert_equal_same_tz(res, expected)
+
+ def test_valid_tzinfo_unicode_input(self):
+ dstr = "2014 January 19 09:00 UTC"
+ tzinfos = {u"UTC": u"UTC+0"}
+ expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0"))
+ res = parse(dstr, tzinfos=tzinfos)
+ self.assert_equal_same_tz(res, expected)
+
+ def test_valid_tzinfo_callable_input(self):
+ dstr = "2014 January 19 09:00 UTC"
+
+ def tzinfos(*args, **kwargs):
+ return u"UTC+0"
+
+ expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0"))
+ res = parse(dstr, tzinfos=tzinfos)
+ self.assert_equal_same_tz(res, expected)
+
+ def test_valid_tzinfo_int_input(self):
+ dstr = "2014 January 19 09:00 UTC"
+ tzinfos = {u"UTC": -28800}
+ expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800))
+ res = parse(dstr, tzinfos=tzinfos)
+ self.assert_equal_same_tz(res, expected)
+
+
+class ParserTest(unittest.TestCase):
+
+ @classmethod
+ def setup_class(cls):
+ cls.tzinfos = {"BRST": -10800}
+ cls.brsttz = tzoffset("BRST", -10800)
+ cls.default = datetime(2003, 9, 25)
+
+ # Parser should be able to handle bytestring and unicode
+ cls.uni_str = '2014-05-01 08:00:00'
+ cls.str_str = cls.uni_str.encode()
+
+ def testParserParseStr(self):
+ from dateutil.parser import parser
+
+ assert parser().parse(self.str_str) == parser().parse(self.uni_str)
+
+ def testParseUnicodeWords(self):
+
+ class rus_parserinfo(parserinfo):
+ MONTHS = [("янв", "Январь"),
+ ("фев", "Февраль"),
+ ("мар", "Март"),
+ ("апр", "Апрель"),
+ ("май", "Май"),
+ ("июн", "Июнь"),
+ ("июл", "Июль"),
+ ("авг", "Август"),
+ ("сен", "Сентябрь"),
+ ("окт", "Октябрь"),
+ ("ноя", "Ноябрь"),
+ ("дек", "Декабрь")]
+
+ expected = datetime(2015, 9, 10, 10, 20)
+ res = parse('10 Сентябрь 2015 10:20', parserinfo=rus_parserinfo())
+ assert res == expected
+
+ def testParseWithNulls(self):
+ # This relies on the from __future__ import unicode_literals, because
+ # explicitly specifying a unicode literal is a syntax error in Py 3.2
+ # May want to switch to u'...' if we ever drop Python 3.2 support.
+ pstring = '\x00\x00August 29, 1924'
+
+ assert parse(pstring) == datetime(1924, 8, 29)
+
+ def testDateCommandFormat(self):
+ self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+ tzinfos=self.tzinfos),
+ datetime(2003, 9, 25, 10, 36, 28,
+ tzinfo=self.brsttz))
+
+ def testDateCommandFormatReversed(self):
+ self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu",
+ tzinfos=self.tzinfos),
+ datetime(2003, 9, 25, 10, 36, 28,
+ tzinfo=self.brsttz))
+
+ def testDateCommandFormatWithLong(self):
+ if PY2:
+ self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+ tzinfos={"BRST": long(-10800)}),
+ datetime(2003, 9, 25, 10, 36, 28,
+ tzinfo=self.brsttz))
+
+ def testISOFormatStrip2(self):
+ self.assertEqual(parse("2003-09-25T10:49:41+03:00"),
+ datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=tzoffset(None, 10800)))
+
+ def testISOStrippedFormatStrip2(self):
+ self.assertEqual(parse("20030925T104941+0300"),
+ datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=tzoffset(None, 10800)))
+
+ def testAMPMNoHour(self):
+ with pytest.raises(ParserError):
+ parse("AM")
+
+ with pytest.raises(ParserError):
+ parse("Jan 20, 2015 PM")
+
+ def testAMPMRange(self):
+ with pytest.raises(ParserError):
+ parse("13:44 AM")
+
+ with pytest.raises(ParserError):
+ parse("January 25, 1921 23:13 PM")
+
+ def testPertain(self):
+ self.assertEqual(parse("Sep 03", default=self.default),
+ datetime(2003, 9, 3))
+ self.assertEqual(parse("Sep of 03", default=self.default),
+ datetime(2003, 9, 25))
+
+ def testFuzzy(self):
+ s = "Today is 25 of September of 2003, exactly " \
+ "at 10:49:41 with timezone -03:00."
+ self.assertEqual(parse(s, fuzzy=True),
+ datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=self.brsttz))
+
+ def testFuzzyWithTokens(self):
+ s1 = "Today is 25 of September of 2003, exactly " \
+ "at 10:49:41 with timezone -03:00."
+ self.assertEqual(parse(s1, fuzzy_with_tokens=True),
+ (datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=self.brsttz),
+ ('Today is ', 'of ', ', exactly at ',
+ ' with timezone ', '.')))
+
+ s2 = "http://biz.yahoo.com/ipo/p/600221.html"
+ self.assertEqual(parse(s2, fuzzy_with_tokens=True),
+ (datetime(2060, 2, 21, 0, 0, 0),
+ ('http://biz.yahoo.com/ipo/p/', '.html')))
+
+ def testFuzzyAMPMProblem(self):
+ # Sometimes fuzzy parsing results in AM/PM flag being set without
+ # hours - if it's fuzzy it should ignore that.
+ s1 = "I have a meeting on March 1, 1974."
+ s2 = "On June 8th, 2020, I am going to be the first man on Mars"
+
+ # Also don't want any erroneous AM or PMs changing the parsed time
+ s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003"
+ s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset"
+
+ self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1))
+ self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8))
+ self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3))
+ self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3))
+
+ def testFuzzyIgnoreAMPM(self):
+ s1 = "Jan 29, 1945 14:45 AM I going to see you there?"
+ with pytest.warns(UnknownTimezoneWarning):
+ res = parse(s1, fuzzy=True)
+ self.assertEqual(res, datetime(1945, 1, 29, 14, 45))
+
+ def testRandomFormat24(self):
+ self.assertEqual(parse("0:00 PM, PST", default=self.default,
+ ignoretz=True),
+ datetime(2003, 9, 25, 12, 0))
+
+ def testRandomFormat26(self):
+ with pytest.warns(UnknownTimezoneWarning):
+ res = parse("5:50 A.M. on June 13, 1990")
+
+ self.assertEqual(res, datetime(1990, 6, 13, 5, 50))
+
+ def testUnspecifiedDayFallback(self):
+ # Test that for an unspecified day, the fallback behavior is correct.
+ self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)),
+ datetime(2009, 4, 30))
+
+ def testUnspecifiedDayFallbackFebNoLeapYear(self):
+ self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)),
+ datetime(2007, 2, 28))
+
+ def testUnspecifiedDayFallbackFebLeapYear(self):
+ self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)),
+ datetime(2008, 2, 29))
+
+ def testErrorType01(self):
+ with pytest.raises(ParserError):
+ parse('shouldfail')
+
+ def testCorrectErrorOnFuzzyWithTokens(self):
+ assertRaisesRegex(self, ParserError, 'Unknown string format',
+ parse, '04/04/32/423', fuzzy_with_tokens=True)
+ assertRaisesRegex(self, ParserError, 'Unknown string format',
+ parse, '04/04/04 +32423', fuzzy_with_tokens=True)
+ assertRaisesRegex(self, ParserError, 'Unknown string format',
+ parse, '04/04/0d4', fuzzy_with_tokens=True)
+
+ def testIncreasingCTime(self):
+ # This test will check 200 different years, every month, every day,
+ # every hour, every minute, every second, and every weekday, using
+ # a delta of more or less 1 year, 1 month, 1 day, 1 minute and
+ # 1 second.
+ delta = timedelta(days=365+31+1, seconds=1+60+60*60)
+ dt = datetime(1900, 1, 1, 0, 0, 0, 0)
+ for i in range(200):
+ assert parse(dt.ctime()) == dt
+ dt += delta
+
+ def testIncreasingISOFormat(self):
+ delta = timedelta(days=365+31+1, seconds=1+60+60*60)
+ dt = datetime(1900, 1, 1, 0, 0, 0, 0)
+ for i in range(200):
+ assert parse(dt.isoformat()) == dt
+ dt += delta
+
+ def testMicrosecondsPrecisionError(self):
+ # Skip found out that sad precision problem. :-(
+ dt1 = parse("00:11:25.01")
+ dt2 = parse("00:12:10.01")
+ assert dt1.microsecond == 10000
+ assert dt2.microsecond == 10000
+
+ def testMicrosecondPrecisionErrorReturns(self):
+ # One more precision issue, discovered by Eric Brown. This should
+ # be the last one, as we're no longer using floating points.
+ for ms in [100001, 100000, 99999, 99998,
+ 10001, 10000, 9999, 9998,
+ 1001, 1000, 999, 998,
+ 101, 100, 99, 98]:
+ dt = datetime(2008, 2, 27, 21, 26, 1, ms)
+ assert parse(dt.isoformat()) == dt
+
+ def testCustomParserInfo(self):
+ # Custom parser info wasn't working, as Michael Elsdörfer discovered.
+ from dateutil.parser import parserinfo, parser
+
+ class myparserinfo(parserinfo):
+ MONTHS = parserinfo.MONTHS[:]
+ MONTHS[0] = ("Foo", "Foo")
+ myparser = parser(myparserinfo())
+ dt = myparser.parse("01/Foo/2007")
+ assert dt == datetime(2007, 1, 1)
+
+ def testCustomParserShortDaynames(self):
+ # Horacio Hoyos discovered that day names shorter than 3 characters,
+ # for example two letter German day name abbreviations, don't work:
+ # https://github.com/dateutil/dateutil/issues/343
+ from dateutil.parser import parserinfo, parser
+
+ class GermanParserInfo(parserinfo):
+ WEEKDAYS = [("Mo", "Montag"),
+ ("Di", "Dienstag"),
+ ("Mi", "Mittwoch"),
+ ("Do", "Donnerstag"),
+ ("Fr", "Freitag"),
+ ("Sa", "Samstag"),
+ ("So", "Sonntag")]
+
+ myparser = parser(GermanParserInfo())
+ dt = myparser.parse("Sa 21. Jan 2017")
+ self.assertEqual(dt, datetime(2017, 1, 21))
+
+ def testNoYearFirstNoDayFirst(self):
+ dtstr = '090107'
+
+ # Should be MMDDYY
+ self.assertEqual(parse(dtstr),
+ datetime(2007, 9, 1))
+
+ self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False),
+ datetime(2007, 9, 1))
+
+ def testYearFirst(self):
+ dtstr = '090107'
+
+ # Should be MMDDYY
+ self.assertEqual(parse(dtstr, yearfirst=True),
+ datetime(2009, 1, 7))
+
+ self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False),
+ datetime(2009, 1, 7))
+
+ def testDayFirst(self):
+ dtstr = '090107'
+
+ # Should be DDMMYY
+ self.assertEqual(parse(dtstr, dayfirst=True),
+ datetime(2007, 1, 9))
+
+ self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True),
+ datetime(2007, 1, 9))
+
+ def testDayFirstYearFirst(self):
+ dtstr = '090107'
+ # Should be YYDDMM
+ self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True),
+ datetime(2009, 7, 1))
+
+ def testUnambiguousYearFirst(self):
+ dtstr = '2015 09 25'
+ self.assertEqual(parse(dtstr, yearfirst=True),
+ datetime(2015, 9, 25))
+
+ def testUnambiguousDayFirst(self):
+ dtstr = '2015 09 25'
+ self.assertEqual(parse(dtstr, dayfirst=True),
+ datetime(2015, 9, 25))
+
+ def testUnambiguousDayFirstYearFirst(self):
+ dtstr = '2015 09 25'
+ self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True),
+ datetime(2015, 9, 25))
+
+ def test_mstridx(self):
+ # See GH408
+ dtstr = '2015-15-May'
+ self.assertEqual(parse(dtstr),
+ datetime(2015, 5, 15))
+
+ def test_idx_check(self):
+ dtstr = '2017-07-17 06:15:'
+ # Pre-PR, the trailing colon will cause an IndexError at 824-825
+ # when checking `i < len_l` and then accessing `l[i+1]`
+ res = parse(dtstr, fuzzy=True)
+ assert res == datetime(2017, 7, 17, 6, 15)
+
+ def test_hmBY(self):
+ # See GH#483
+ dtstr = '02:17NOV2017'
+ res = parse(dtstr, default=self.default)
+ assert res == datetime(2017, 11, self.default.day, 2, 17)
+
+ def test_validate_hour(self):
+ # See GH353
+ invalid = "201A-01-01T23:58:39.239769+03:00"
+ with pytest.raises(ParserError):
+ parse(invalid)
+
+ def test_era_trailing_year(self):
+ dstr = 'AD2001'
+ res = parse(dstr)
+ assert res.year == 2001, res
+
+ def test_includes_timestr(self):
+ timestr = "2020-13-97T44:61:83"
+
+ try:
+ parse(timestr)
+ except ParserError as e:
+ assert e.args[1] == timestr
+ else:
+ pytest.fail("Failed to raise ParserError")
+
+
+class TestOutOfBounds(object):
+
+ def test_no_year_zero(self):
+ with pytest.raises(ParserError):
+ parse("0000 Jun 20")
+
+ def test_out_of_bound_day(self):
+ with pytest.raises(ParserError):
+ parse("Feb 30, 2007")
+
+ def test_illegal_month_error(self):
+ with pytest.raises(ParserError):
+ parse("0-100")
+
+ def test_day_sanity(self, fuzzy):
+ dstr = "2014-15-25"
+ with pytest.raises(ParserError):
+ parse(dstr, fuzzy=fuzzy)
+
+ def test_minute_sanity(self, fuzzy):
+ dstr = "2014-02-28 22:64"
+ with pytest.raises(ParserError):
+ parse(dstr, fuzzy=fuzzy)
+
+ def test_hour_sanity(self, fuzzy):
+ dstr = "2014-02-28 25:16 PM"
+ with pytest.raises(ParserError):
+ parse(dstr, fuzzy=fuzzy)
+
+ def test_second_sanity(self, fuzzy):
+ dstr = "2014-02-28 22:14:64"
+ with pytest.raises(ParserError):
+ parse(dstr, fuzzy=fuzzy)
+
+
+class TestParseUnimplementedCases(object):
+ @pytest.mark.xfail
+ def test_somewhat_ambiguous_string(self):
+ # Ref: github issue #487
+ # The parser is choosing the wrong part for hour
+ # causing datetime to raise an exception.
+ dtstr = '1237 PM BRST Mon Oct 30 2017'
+ res = parse(dtstr, tzinfo=self.tzinfos)
+ assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos)
+
+ @pytest.mark.xfail
+ def test_YmdH_M_S(self):
+ # found in nasdaq's ftp data
+ dstr = '1991041310:19:24'
+ expected = datetime(1991, 4, 13, 10, 19, 24)
+ res = parse(dstr)
+ assert res == expected, (res, expected)
+
+ @pytest.mark.xfail
+ def test_first_century(self):
+ dstr = '0031 Nov 03'
+ expected = datetime(31, 11, 3)
+ res = parse(dstr)
+ assert res == expected, res
+
+ @pytest.mark.xfail
+ def test_era_trailing_year_with_dots(self):
+ dstr = 'A.D.2001'
+ res = parse(dstr)
+ assert res.year == 2001, res
+
+ @pytest.mark.xfail
+ def test_ad_nospace(self):
+ expected = datetime(6, 5, 19)
+ for dstr in [' 6AD May 19', ' 06AD May 19',
+ ' 006AD May 19', ' 0006AD May 19']:
+ res = parse(dstr)
+ assert res == expected, (dstr, res)
+
+ @pytest.mark.xfail
+ def test_four_letter_day(self):
+ dstr = 'Frid Dec 30, 2016'
+ expected = datetime(2016, 12, 30)
+ res = parse(dstr)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_non_date_number(self):
+ dstr = '1,700'
+ with pytest.raises(ParserError):
+ parse(dstr)
+
+ @pytest.mark.xfail
+ def test_on_era(self):
+ # This could be classified as an "eras" test, but the relevant part
+ # about this is the ` on `
+ dstr = '2:15 PM on January 2nd 1973 A.D.'
+ expected = datetime(1973, 1, 2, 14, 15)
+ res = parse(dstr)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_extraneous_year(self):
+ # This was found in the wild at insidertrading.org
+ dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
+ res = parse(dstr, fuzzy_with_tokens=True)
+ expected = datetime(2012, 11, 7)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_extraneous_year_tokens(self):
+ # This was found in the wild at insidertrading.org
+ # Unlike in the case above, identifying the first "2012" as the year
+ # would not be a problem, but inferring that the latter 2012 is hhmm
+ # is a problem.
+ dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
+ expected = datetime(2012, 11, 7)
+ (res, tokens) = parse(dstr, fuzzy_with_tokens=True)
+ assert res == expected
+ assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",)
+
+ @pytest.mark.xfail
+ def test_extraneous_year2(self):
+ # This was found in the wild at insidertrading.org
+ dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust "
+ "u/d/t November 2, 1998 f/b/o Jennifer L Berylson")
+ res = parse(dstr, fuzzy_with_tokens=True)
+ expected = datetime(1998, 11, 2)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_extraneous_year3(self):
+ # This was found in the wild at insidertrading.org
+ dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994"
+ res = parse(dstr, fuzzy_with_tokens=True)
+ expected = datetime(1994, 12, 1)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_unambiguous_YYYYMM(self):
+ # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed
+ # as instance of YYMMDD and parser could fallback to YYYYMM format.
+ dstr = "201712"
+ res = parse(dstr)
+ expected = datetime(2017, 12, 1)
+ assert res == expected
+
+ @pytest.mark.xfail
+ def test_extraneous_numerical_content(self):
+ # ref: https://github.com/dateutil/dateutil/issues/1029
+ # parser interprets price and percentage as parts of the date
+ dstr = "£14.99 (25% off, until April 20)"
+ res = parse(dstr, fuzzy=True, default=datetime(2000, 1, 1))
+ expected = datetime(2000, 4, 20)
+ assert res == expected
+
+
+@pytest.mark.skipif(IS_WIN, reason="Windows does not use TZ var")
+class TestTZVar(object):
+ def test_parse_unambiguous_nonexistent_local(self):
+ # When dates are specified "EST" even when they should be "EDT" in the
+ # local time zone, we should still assign the local time zone
+ with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+ dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal())
+ dt = parse('2011-08-01T12:30 EST')
+
+ assert dt.tzname() == 'EDT'
+ assert dt == dt_exp
+
+ def test_tzlocal_in_gmt(self):
+ # GH #318
+ with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'):
+ # This is an imaginary datetime in tz.tzlocal() but should still
+ # parse using the GMT-as-alias-for-UTC rule
+ dt = parse('2004-05-01T12:00 GMT')
+ dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.UTC)
+
+ assert dt == dt_exp
+
+ def test_tzlocal_parse_fold(self):
+ # One manifestion of GH #318
+ with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+ dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal())
+ dt_exp = tz.enfold(dt_exp, fold=1)
+ dt = parse('2011-11-06T01:30 EST')
+
+ # Because this is ambiguous, until `tz.tzlocal() is tz.tzlocal()`
+ # we'll just check the attributes we care about rather than
+ # dt == dt_exp
+ assert dt.tzname() == dt_exp.tzname()
+ assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None)
+ assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+ assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC)
+
+
+def test_parse_tzinfos_fold():
+ NYC = tz.gettz('America/New_York')
+ tzinfos = {'EST': NYC, 'EDT': NYC}
+
+ dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1)
+ dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos)
+
+ assert dt == dt_exp
+ assert dt.tzinfo is dt_exp.tzinfo
+ assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+ assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC)
+
+
+@pytest.mark.parametrize('dtstr,dt', [
+ ('5.6h', datetime(2003, 9, 25, 5, 36)),
+ ('5.6m', datetime(2003, 9, 25, 0, 5, 36)),
+ # '5.6s' never had a rounding problem, test added for completeness
+ ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000))
+])
+def test_rounding_floatlike_strings(dtstr, dt):
+ assert parse(dtstr, default=datetime(2003, 9, 25)) == dt
+
+
+@pytest.mark.parametrize('value', ['1: test', 'Nan'])
+def test_decimal_error(value):
+ # GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception
+ # when constructed with an invalid value
+ with pytest.raises(ParserError):
+ parse(value)
+
+def test_parsererror_repr():
+ # GH 991 — the __repr__ was not properly indented and so was never defined.
+ # This tests the current behavior of the ParserError __repr__, but the
+ # precise format is not guaranteed to be stable and may change even in
+ # minor versions. This test exists to avoid regressions.
+ s = repr(ParserError("Problem with string: %s", "2019-01-01"))
+
+ assert s == "ParserError('Problem with string: %s', '2019-01-01')"
diff --git a/src/dateutil/test/test_relativedelta.py b/src/dateutil/test/test_relativedelta.py
new file mode 100644
index 0000000..1e5d170
--- /dev/null
+++ b/src/dateutil/test/test_relativedelta.py
@@ -0,0 +1,706 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import NotAValue
+
+import calendar
+from datetime import datetime, date, timedelta
+import unittest
+
+import pytest
+
+from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU
+
+
+class RelativeDeltaTest(unittest.TestCase):
+ now = datetime(2003, 9, 17, 20, 54, 47, 282310)
+ today = date(2003, 9, 17)
+
+ def testInheritance(self):
+ # Ensure that relativedelta is inheritance-friendly.
+ class rdChildClass(relativedelta):
+ pass
+
+ ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1,
+ hours=1, minutes=1, seconds=1, microseconds=1)
+
+ rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1,
+ hours=1, minutes=1, seconds=1, microseconds=1)
+
+ self.assertEqual(type(ccRD + rd), type(ccRD),
+ msg='Addition does not inherit type.')
+
+ self.assertEqual(type(ccRD - rd), type(ccRD),
+ msg='Subtraction does not inherit type.')
+
+ self.assertEqual(type(-ccRD), type(ccRD),
+ msg='Negation does not inherit type.')
+
+ self.assertEqual(type(ccRD * 5.0), type(ccRD),
+ msg='Multiplication does not inherit type.')
+
+ self.assertEqual(type(ccRD / 5.0), type(ccRD),
+ msg='Division does not inherit type.')
+
+ def testMonthEndMonthBeginning(self):
+ self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59),
+ datetime(2003, 3, 1, 0, 0, 0)),
+ relativedelta(months=-1, seconds=-1))
+
+ self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
+ datetime(2003, 1, 31, 23, 59, 59)),
+ relativedelta(months=1, seconds=1))
+
+ def testMonthEndMonthBeginningLeapYear(self):
+ self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59),
+ datetime(2012, 3, 1, 0, 0, 0)),
+ relativedelta(months=-1, seconds=-1))
+
+ self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
+ datetime(2003, 1, 31, 23, 59, 59)),
+ relativedelta(months=1, seconds=1))
+
+ def testNextMonth(self):
+ self.assertEqual(self.now+relativedelta(months=+1),
+ datetime(2003, 10, 17, 20, 54, 47, 282310))
+
+ def testNextMonthPlusOneWeek(self):
+ self.assertEqual(self.now+relativedelta(months=+1, weeks=+1),
+ datetime(2003, 10, 24, 20, 54, 47, 282310))
+
+ def testNextMonthPlusOneWeek10am(self):
+ self.assertEqual(self.today +
+ relativedelta(months=+1, weeks=+1, hour=10),
+ datetime(2003, 10, 24, 10, 0))
+
+ def testNextMonthPlusOneWeek10amDiff(self):
+ self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0),
+ self.today),
+ relativedelta(months=+1, days=+7, hours=+10))
+
+ def testOneMonthBeforeOneYear(self):
+ self.assertEqual(self.now+relativedelta(years=+1, months=-1),
+ datetime(2004, 8, 17, 20, 54, 47, 282310))
+
+ def testMonthsOfDiffNumOfDays(self):
+ self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1),
+ date(2003, 2, 27))
+ self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1),
+ date(2003, 2, 28))
+ self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2),
+ date(2003, 3, 31))
+
+ def testMonthsOfDiffNumOfDaysWithYears(self):
+ self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1),
+ date(2001, 2, 28))
+ self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1),
+ date(2001, 2, 28))
+
+ self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1),
+ date(2000, 2, 28))
+ self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
+ date(2000, 3, 1))
+ self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
+ date(2000, 3, 1))
+
+ self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1),
+ date(2000, 2, 28))
+ self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1),
+ date(2000, 3, 1))
+
+ def testNextFriday(self):
+ self.assertEqual(self.today+relativedelta(weekday=FR),
+ date(2003, 9, 19))
+
+ def testNextFridayInt(self):
+ self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY),
+ date(2003, 9, 19))
+
+ def testLastFridayInThisMonth(self):
+ self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)),
+ date(2003, 9, 26))
+
+ def testLastDayOfFebruary(self):
+ self.assertEqual(date(2021, 2, 1) + relativedelta(day=31),
+ date(2021, 2, 28))
+
+ def testLastDayOfFebruaryLeapYear(self):
+ self.assertEqual(date(2020, 2, 1) + relativedelta(day=31),
+ date(2020, 2, 29))
+
+ def testNextWednesdayIsToday(self):
+ self.assertEqual(self.today+relativedelta(weekday=WE),
+ date(2003, 9, 17))
+
+ def testNextWednesdayNotToday(self):
+ self.assertEqual(self.today+relativedelta(days=+1, weekday=WE),
+ date(2003, 9, 24))
+
+ def testAddMoreThan12Months(self):
+ self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13),
+ date(2005, 1, 1))
+
+ def testAddNegativeMonths(self):
+ self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2),
+ date(2002, 11, 1))
+
+ def test15thISOYearWeek(self):
+ self.assertEqual(date(2003, 1, 1) +
+ relativedelta(day=4, weeks=+14, weekday=MO(-1)),
+ date(2003, 4, 7))
+
+ def testMillenniumAge(self):
+ self.assertEqual(relativedelta(self.now, date(2001, 1, 1)),
+ relativedelta(years=+2, months=+8, days=+16,
+ hours=+20, minutes=+54, seconds=+47,
+ microseconds=+282310))
+
+ def testJohnAge(self):
+ self.assertEqual(relativedelta(self.now,
+ datetime(1978, 4, 5, 12, 0)),
+ relativedelta(years=+25, months=+5, days=+12,
+ hours=+8, minutes=+54, seconds=+47,
+ microseconds=+282310))
+
+ def testJohnAgeWithDate(self):
+ self.assertEqual(relativedelta(self.today,
+ datetime(1978, 4, 5, 12, 0)),
+ relativedelta(years=+25, months=+5, days=+11,
+ hours=+12))
+
+ def testYearDay(self):
+ self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260),
+ date(2003, 9, 17))
+ self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260),
+ date(2002, 9, 17))
+ self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260),
+ date(2000, 9, 16))
+ self.assertEqual(self.today+relativedelta(yearday=261),
+ date(2003, 9, 18))
+
+ def testYearDayBug(self):
+ # Tests a problem reported by Adam Ryan.
+ self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15),
+ date(2010, 1, 15))
+
+ def testNonLeapYearDay(self):
+ self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260),
+ date(2003, 9, 17))
+ self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260),
+ date(2002, 9, 17))
+ self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260),
+ date(2000, 9, 17))
+ self.assertEqual(self.today+relativedelta(yearday=261),
+ date(2003, 9, 18))
+
+ def testAddition(self):
+ self.assertEqual(relativedelta(days=10) +
+ relativedelta(years=1, months=2, days=3, hours=4,
+ minutes=5, microseconds=6),
+ relativedelta(years=1, months=2, days=13, hours=4,
+ minutes=5, microseconds=6))
+
+ def testAbsoluteAddition(self):
+ self.assertEqual(relativedelta() + relativedelta(day=0, hour=0),
+ relativedelta(day=0, hour=0))
+ self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(),
+ relativedelta(day=0, hour=0))
+
+ def testAdditionToDatetime(self):
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1),
+ datetime(2000, 1, 2))
+
+ def testRightAdditionToDatetime(self):
+ self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1),
+ datetime(2000, 1, 2))
+
+ def testAdditionInvalidType(self):
+ with self.assertRaises(TypeError):
+ relativedelta(days=3) + 9
+
+ def testAdditionUnsupportedType(self):
+ # For unsupported types that define their own comparators, etc.
+ self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
+
+ def testAdditionFloatValue(self):
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)),
+ datetime(2000, 1, 2))
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)),
+ datetime(2000, 2, 1))
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)),
+ datetime(2001, 1, 1))
+
+ def testAdditionFloatFractionals(self):
+ self.assertEqual(datetime(2000, 1, 1, 0) +
+ relativedelta(days=float(0.5)),
+ datetime(2000, 1, 1, 12))
+ self.assertEqual(datetime(2000, 1, 1, 0, 0) +
+ relativedelta(hours=float(0.5)),
+ datetime(2000, 1, 1, 0, 30))
+ self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) +
+ relativedelta(minutes=float(0.5)),
+ datetime(2000, 1, 1, 0, 0, 30))
+ self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
+ relativedelta(seconds=float(0.5)),
+ datetime(2000, 1, 1, 0, 0, 0, 500000))
+ self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
+ relativedelta(microseconds=float(500000.25)),
+ datetime(2000, 1, 1, 0, 0, 0, 500000))
+
+ def testSubtraction(self):
+ self.assertEqual(relativedelta(days=10) -
+ relativedelta(years=1, months=2, days=3, hours=4,
+ minutes=5, microseconds=6),
+ relativedelta(years=-1, months=-2, days=7, hours=-4,
+ minutes=-5, microseconds=-6))
+
+ def testRightSubtractionFromDatetime(self):
+ self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1),
+ datetime(2000, 1, 1))
+
+ def testSubractionWithDatetime(self):
+ self.assertRaises(TypeError, lambda x, y: x - y,
+ (relativedelta(days=1), datetime(2000, 1, 1)))
+
+ def testSubtractionInvalidType(self):
+ with self.assertRaises(TypeError):
+ relativedelta(hours=12) - 14
+
+ def testSubtractionUnsupportedType(self):
+ self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
+
+ def testMultiplication(self):
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28,
+ datetime(2000, 1, 29))
+ self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1),
+ datetime(2000, 1, 29))
+
+ def testMultiplicationUnsupportedType(self):
+ self.assertIs(relativedelta(days=1) * NotAValue, NotAValue)
+
+ def testDivision(self):
+ self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28,
+ datetime(2000, 1, 2))
+
+ def testDivisionUnsupportedType(self):
+ self.assertIs(relativedelta(days=1) / NotAValue, NotAValue)
+
+ def testBoolean(self):
+ self.assertFalse(relativedelta(days=0))
+ self.assertTrue(relativedelta(days=1))
+
+ def testAbsoluteValueNegative(self):
+ rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3,
+ minutes=-5, seconds=-2, microseconds=-12)
+ rd_expected = relativedelta(years=1, months=5, days=2, hours=3,
+ minutes=5, seconds=2, microseconds=12)
+ self.assertEqual(abs(rd_base), rd_expected)
+
+ def testAbsoluteValuePositive(self):
+ rd_base = relativedelta(years=1, months=5, days=2, hours=3,
+ minutes=5, seconds=2, microseconds=12)
+ rd_expected = rd_base
+
+ self.assertEqual(abs(rd_base), rd_expected)
+
+ def testComparison(self):
+ d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+ minutes=1, seconds=1, microseconds=1)
+ d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+ minutes=1, seconds=1, microseconds=1)
+ d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+ minutes=1, seconds=1, microseconds=2)
+
+ self.assertEqual(d1, d2)
+ self.assertNotEqual(d1, d3)
+
+ def testInequalityTypeMismatch(self):
+ # Different type
+ self.assertFalse(relativedelta(year=1) == 19)
+
+ def testInequalityUnsupportedType(self):
+ self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue)
+
+ def testInequalityWeekdays(self):
+ # Different weekdays
+ no_wday = relativedelta(year=1997, month=4)
+ wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1))
+ wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2))
+ wday_tu = relativedelta(year=1997, month=4, weekday=TU)
+
+ self.assertTrue(wday_mo_1 == wday_mo_1)
+
+ self.assertFalse(no_wday == wday_mo_1)
+ self.assertFalse(wday_mo_1 == no_wday)
+
+ self.assertFalse(wday_mo_1 == wday_mo_2)
+ self.assertFalse(wday_mo_2 == wday_mo_1)
+
+ self.assertFalse(wday_mo_1 == wday_tu)
+ self.assertFalse(wday_tu == wday_mo_1)
+
+ def testMonthOverflow(self):
+ self.assertEqual(relativedelta(months=273),
+ relativedelta(years=22, months=9))
+
+ def testWeeks(self):
+ # Test that the weeks property is working properly.
+ rd = relativedelta(years=4, months=2, weeks=8, days=6)
+ self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6))
+
+ rd.weeks = 3
+ self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6))
+
+ def testRelativeDeltaRepr(self):
+ self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)),
+ 'relativedelta(years=+1, months=-1, days=+15)')
+
+ self.assertEqual(repr(relativedelta(months=14, seconds=-25)),
+ 'relativedelta(years=+1, months=+2, seconds=-25)')
+
+ self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))),
+ 'relativedelta(month=3, weekday=SU(+3), hour=3)')
+
+ def testRelativeDeltaFractionalYear(self):
+ with self.assertRaises(ValueError):
+ relativedelta(years=1.5)
+
+ def testRelativeDeltaFractionalMonth(self):
+ with self.assertRaises(ValueError):
+ relativedelta(months=1.5)
+
+ def testRelativeDeltaInvalidDatetimeObject(self):
+ with self.assertRaises(TypeError):
+ relativedelta(dt1='2018-01-01', dt2='2018-01-02')
+
+ with self.assertRaises(TypeError):
+ relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02')
+
+ with self.assertRaises(TypeError):
+ relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2))
+
+ def testRelativeDeltaFractionalAbsolutes(self):
+ # Fractional absolute values will soon be unsupported,
+ # check for the deprecation warning.
+ with pytest.warns(DeprecationWarning):
+ relativedelta(year=2.86)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(month=1.29)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(day=0.44)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(hour=23.98)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(minute=45.21)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(second=13.2)
+
+ with pytest.warns(DeprecationWarning):
+ relativedelta(microsecond=157221.93)
+
+ def testRelativeDeltaFractionalRepr(self):
+ rd = relativedelta(years=3, months=-2, days=1.25)
+
+ self.assertEqual(repr(rd),
+ 'relativedelta(years=+3, months=-2, days=+1.25)')
+
+ rd = relativedelta(hours=0.5, seconds=9.22)
+ self.assertEqual(repr(rd),
+ 'relativedelta(hours=+0.5, seconds=+9.22)')
+
+ def testRelativeDeltaFractionalWeeks(self):
+ # Equivalent to days=8, hours=18
+ rd = relativedelta(weeks=1.25)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd,
+ datetime(2009, 9, 11, 18))
+
+ def testRelativeDeltaFractionalDays(self):
+ rd1 = relativedelta(days=1.48)
+
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd1,
+ datetime(2009, 9, 4, 11, 31, 12))
+
+ rd2 = relativedelta(days=1.5)
+ self.assertEqual(d1 + rd2,
+ datetime(2009, 9, 4, 12, 0, 0))
+
+ def testRelativeDeltaFractionalHours(self):
+ rd = relativedelta(days=1, hours=12.5)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd,
+ datetime(2009, 9, 4, 12, 30, 0))
+
+ def testRelativeDeltaFractionalMinutes(self):
+ rd = relativedelta(hours=1, minutes=30.5)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd,
+ datetime(2009, 9, 3, 1, 30, 30))
+
+ def testRelativeDeltaFractionalSeconds(self):
+ rd = relativedelta(hours=5, minutes=30, seconds=30.5)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd,
+ datetime(2009, 9, 3, 5, 30, 30, 500000))
+
+ def testRelativeDeltaFractionalPositiveOverflow(self):
+ # Equivalent to (days=1, hours=14)
+ rd1 = relativedelta(days=1.5, hours=2)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd1,
+ datetime(2009, 9, 4, 14, 0, 0))
+
+ # Equivalent to (days=1, hours=14, minutes=45)
+ rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
+ d1 = datetime(2009, 9, 3, 0, 0)
+ self.assertEqual(d1 + rd2,
+ datetime(2009, 9, 4, 14, 45))
+
+ # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1)
+ rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31)
+ self.assertEqual(d1 + rd3,
+ datetime(2009, 9, 5, 2, 0, 1))
+
+ def testRelativeDeltaFractionalNegativeDays(self):
+ # Equivalent to (days=-1, hours=-1)
+ rd1 = relativedelta(days=-1.5, hours=11)
+ d1 = datetime(2009, 9, 3, 12, 0)
+ self.assertEqual(d1 + rd1,
+ datetime(2009, 9, 2, 11, 0, 0))
+
+ # Equivalent to (days=-1, hours=-9)
+ rd2 = relativedelta(days=-1.25, hours=-3)
+ self.assertEqual(d1 + rd2,
+ datetime(2009, 9, 2, 3))
+
+ def testRelativeDeltaNormalizeFractionalDays(self):
+ # Equivalent to (days=2, hours=18)
+ rd1 = relativedelta(days=2.75)
+
+ self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18))
+
+ # Equivalent to (days=1, hours=11, minutes=31, seconds=12)
+ rd2 = relativedelta(days=1.48)
+
+ self.assertEqual(rd2.normalized(),
+ relativedelta(days=1, hours=11, minutes=31, seconds=12))
+
+ def testRelativeDeltaNormalizeFractionalDays2(self):
+ # Equivalent to (hours=1, minutes=30)
+ rd1 = relativedelta(hours=1.5)
+
+ self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30))
+
+ # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100)
+ rd2 = relativedelta(hours=3.28472225)
+
+ self.assertEqual(rd2.normalized(),
+ relativedelta(hours=3, minutes=17, seconds=5, microseconds=100))
+
+ def testRelativeDeltaNormalizeFractionalMinutes(self):
+ # Equivalent to (minutes=15, seconds=36)
+ rd1 = relativedelta(minutes=15.6)
+
+ self.assertEqual(rd1.normalized(),
+ relativedelta(minutes=15, seconds=36))
+
+ # Equivalent to (minutes=25, seconds=20, microseconds=25000)
+ rd2 = relativedelta(minutes=25.33375)
+
+ self.assertEqual(rd2.normalized(),
+ relativedelta(minutes=25, seconds=20, microseconds=25000))
+
+ def testRelativeDeltaNormalizeFractionalSeconds(self):
+ # Equivalent to (seconds=45, microseconds=25000)
+ rd1 = relativedelta(seconds=45.025)
+ self.assertEqual(rd1.normalized(),
+ relativedelta(seconds=45, microseconds=25000))
+
+ def testRelativeDeltaFractionalPositiveOverflow2(self):
+ # Equivalent to (days=1, hours=14)
+ rd1 = relativedelta(days=1.5, hours=2)
+ self.assertEqual(rd1.normalized(),
+ relativedelta(days=1, hours=14))
+
+ # Equivalent to (days=1, hours=14, minutes=45)
+ rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
+ self.assertEqual(rd2.normalized(),
+ relativedelta(days=1, hours=14, minutes=45))
+
+ # Carry back up - equivalent to:
+ # (days=2, hours=2, minutes=0, seconds=2, microseconds=3)
+ rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045,
+ seconds=31.473, microseconds=500003)
+ self.assertEqual(rd3.normalized(),
+ relativedelta(days=2, hours=2, minutes=0,
+ seconds=2, microseconds=3))
+
+ def testRelativeDeltaFractionalNegativeOverflow(self):
+ # Equivalent to (days=-1)
+ rd1 = relativedelta(days=-0.5, hours=-12)
+ self.assertEqual(rd1.normalized(),
+ relativedelta(days=-1))
+
+ # Equivalent to (days=-1)
+ rd2 = relativedelta(days=-1.5, hours=12)
+ self.assertEqual(rd2.normalized(),
+ relativedelta(days=-1))
+
+ # Equivalent to (days=-1, hours=-14, minutes=-45)
+ rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15)
+ self.assertEqual(rd3.normalized(),
+ relativedelta(days=-1, hours=-14, minutes=-45))
+
+ # Equivalent to (days=-1, hours=-14, minutes=+15)
+ rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45)
+ self.assertEqual(rd4.normalized(),
+ relativedelta(days=-1, hours=-14, minutes=+15))
+
+ # Carry back up - equivalent to:
+ # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3)
+ rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045,
+ seconds=-31.473, microseconds=-500003)
+ self.assertEqual(rd3.normalized(),
+ relativedelta(days=-2, hours=-2, minutes=0,
+ seconds=-2, microseconds=-3))
+
+ def testInvalidYearDay(self):
+ with self.assertRaises(ValueError):
+ relativedelta(yearday=367)
+
+ def testAddTimedeltaToUnpopulatedRelativedelta(self):
+ td = timedelta(
+ days=1,
+ seconds=1,
+ microseconds=1,
+ milliseconds=1,
+ minutes=1,
+ hours=1,
+ weeks=1
+ )
+
+ expected = relativedelta(
+ weeks=1,
+ days=1,
+ hours=1,
+ minutes=1,
+ seconds=1,
+ microseconds=1001
+ )
+
+ self.assertEqual(expected, relativedelta() + td)
+
+ def testAddTimedeltaToPopulatedRelativeDelta(self):
+ td = timedelta(
+ days=1,
+ seconds=1,
+ microseconds=1,
+ milliseconds=1,
+ minutes=1,
+ hours=1,
+ weeks=1
+ )
+
+ rd = relativedelta(
+ year=1,
+ month=1,
+ day=1,
+ hour=1,
+ minute=1,
+ second=1,
+ microsecond=1,
+ years=1,
+ months=1,
+ days=1,
+ weeks=1,
+ hours=1,
+ minutes=1,
+ seconds=1,
+ microseconds=1
+ )
+
+ expected = relativedelta(
+ year=1,
+ month=1,
+ day=1,
+ hour=1,
+ minute=1,
+ second=1,
+ microsecond=1,
+ years=1,
+ months=1,
+ weeks=2,
+ days=2,
+ hours=2,
+ minutes=2,
+ seconds=2,
+ microseconds=1002,
+ )
+
+ self.assertEqual(expected, rd + td)
+
+ def testHashable(self):
+ try:
+ {relativedelta(minute=1): 'test'}
+ except:
+ self.fail("relativedelta() failed to hash!")
+
+
+class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase):
+ """Test the weeks property getter"""
+
+ def test_one_day(self):
+ rd = relativedelta(days=1)
+ self.assertEqual(rd.days, 1)
+ self.assertEqual(rd.weeks, 0)
+
+ def test_minus_one_day(self):
+ rd = relativedelta(days=-1)
+ self.assertEqual(rd.days, -1)
+ self.assertEqual(rd.weeks, 0)
+
+ def test_height_days(self):
+ rd = relativedelta(days=8)
+ self.assertEqual(rd.days, 8)
+ self.assertEqual(rd.weeks, 1)
+
+ def test_minus_height_days(self):
+ rd = relativedelta(days=-8)
+ self.assertEqual(rd.days, -8)
+ self.assertEqual(rd.weeks, -1)
+
+
+class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase):
+ """Test the weeks setter which makes a "smart" update of the days attribute"""
+
+ def test_one_day_set_one_week(self):
+ rd = relativedelta(days=1)
+ rd.weeks = 1 # add 7 days
+ self.assertEqual(rd.days, 8)
+ self.assertEqual(rd.weeks, 1)
+
+ def test_minus_one_day_set_one_week(self):
+ rd = relativedelta(days=-1)
+ rd.weeks = 1 # add 7 days
+ self.assertEqual(rd.days, 6)
+ self.assertEqual(rd.weeks, 0)
+
+ def test_height_days_set_minus_one_week(self):
+ rd = relativedelta(days=8)
+ rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day
+ self.assertEqual(rd.days, -6)
+ self.assertEqual(rd.weeks, 0)
+
+ def test_minus_height_days_set_minus_one_week(self):
+ rd = relativedelta(days=-8)
+ rd.weeks = -1 # does not change anything
+ self.assertEqual(rd.days, -8)
+ self.assertEqual(rd.weeks, -1)
+
+
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/test/test_rrule.py b/src/dateutil/test/test_rrule.py
new file mode 100644
index 0000000..52673ec
--- /dev/null
+++ b/src/dateutil/test/test_rrule.py
@@ -0,0 +1,4914 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime, date
+import unittest
+from six import PY2
+
+from dateutil import tz
+from dateutil.rrule import (
+ rrule, rruleset, rrulestr,
+ YEARLY, MONTHLY, WEEKLY, DAILY,
+ HOURLY, MINUTELY, SECONDLY,
+ MO, TU, WE, TH, FR, SA, SU
+)
+
+from freezegun import freeze_time
+
+import pytest
+
+
+@pytest.mark.rrule
+class RRuleTest(unittest.TestCase):
+ def _rrulestr_reverse_test(self, rule):
+ """
+ Call with an `rrule` and it will test that `str(rrule)` generates a
+ string which generates the same `rrule` as the input when passed to
+ `rrulestr()`
+ """
+ rr_str = str(rule)
+ rrulestr_rrule = rrulestr(rr_str)
+
+ self.assertEqual(list(rule), list(rrulestr_rrule))
+
+ def testStrAppendRRULEToken(self):
+ # `_rrulestr_reverse_test` does not check if the "RRULE:" prefix
+ # property is appended properly, so give it a dedicated test
+ self.assertEqual(str(rrule(YEARLY,
+ count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=5")
+
+ rr_str = (
+ 'DTSTART:19970105T083000\nRRULE:FREQ=YEARLY;INTERVAL=2'
+ )
+ self.assertEqual(str(rrulestr(rr_str)), rr_str)
+
+ def testYearly(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testYearlyInterval(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0),
+ datetime(2001, 9, 2, 9, 0)])
+
+ def testYearlyIntervalLarge(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ interval=100,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(2097, 9, 2, 9, 0),
+ datetime(2197, 9, 2, 9, 0)])
+
+ def testYearlyByMonth(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 2, 9, 0),
+ datetime(1998, 3, 2, 9, 0),
+ datetime(1999, 1, 2, 9, 0)])
+
+ def testYearlyByMonthDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 10, 1, 9, 0),
+ datetime(1997, 10, 3, 9, 0)])
+
+ def testYearlyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 9, 0),
+ datetime(1998, 1, 7, 9, 0),
+ datetime(1998, 3, 5, 9, 0)])
+
+ def testYearlyByWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testYearlyByNWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 25, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 12, 31, 9, 0)])
+
+ def testYearlyByNWeekDayLarge(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 11, 9, 0),
+ datetime(1998, 1, 20, 9, 0),
+ datetime(1998, 12, 17, 9, 0)])
+
+ def testYearlyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testYearlyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 29, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testYearlyByMonthAndNWeekDayLarge(self):
+ # This is interesting because the TH(-3) ends up before
+ # the TU(3).
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 15, 9, 0),
+ datetime(1998, 1, 20, 9, 0),
+ datetime(1998, 3, 12, 9, 0)])
+
+ def testYearlyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 2, 3, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testYearlyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 3, 3, 9, 0),
+ datetime(2001, 3, 1, 9, 0)])
+
+ def testYearlyByYearDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testYearlyByYearDayNeg(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testYearlyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 4, 10, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testYearlyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 4, 10, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testYearlyByWeekNo(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 9, 0),
+ datetime(1998, 5, 12, 9, 0),
+ datetime(1998, 5, 13, 9, 0)])
+
+ def testYearlyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 9, 0),
+ datetime(1999, 1, 4, 9, 0),
+ datetime(2000, 1, 3, 9, 0)])
+
+ def testYearlyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1998, 12, 27, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testYearlyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1999, 1, 3, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testYearlyByEaster(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 9, 0),
+ datetime(1999, 4, 4, 9, 0),
+ datetime(2000, 4, 23, 9, 0)])
+
+ def testYearlyByEasterPos(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 9, 0),
+ datetime(1999, 4, 5, 9, 0),
+ datetime(2000, 4, 24, 9, 0)])
+
+ def testYearlyByEasterNeg(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 9, 0),
+ datetime(1999, 4, 3, 9, 0),
+ datetime(2000, 4, 22, 9, 0)])
+
+ def testYearlyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 9, 0),
+ datetime(2004, 12, 27, 9, 0),
+ datetime(2009, 12, 28, 9, 0)])
+
+ def testYearlyByHour(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1998, 9, 2, 6, 0),
+ datetime(1998, 9, 2, 18, 0)])
+
+ def testYearlyByMinute(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1998, 9, 2, 9, 6)])
+
+ def testYearlyBySecond(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1998, 9, 2, 9, 0, 6)])
+
+ def testYearlyByHourAndMinute(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1998, 9, 2, 6, 6)])
+
+ def testYearlyByHourAndSecond(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1998, 9, 2, 6, 0, 6)])
+
+ def testYearlyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testYearlyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testYearlyBySetPos(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonthday=15,
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 11, 15, 18, 0),
+ datetime(1998, 2, 15, 6, 0),
+ datetime(1998, 11, 15, 18, 0)])
+
+ def testMonthly(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 10, 2, 9, 0),
+ datetime(1997, 11, 2, 9, 0)])
+
+ def testMonthlyInterval(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 11, 2, 9, 0),
+ datetime(1998, 1, 2, 9, 0)])
+
+ def testMonthlyIntervalLarge(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ interval=18,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1999, 3, 2, 9, 0),
+ datetime(2000, 9, 2, 9, 0)])
+
+ def testMonthlyByMonth(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 2, 9, 0),
+ datetime(1998, 3, 2, 9, 0),
+ datetime(1999, 1, 2, 9, 0)])
+
+ def testMonthlyByMonthDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 10, 1, 9, 0),
+ datetime(1997, 10, 3, 9, 0)])
+
+ def testMonthlyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 9, 0),
+ datetime(1998, 1, 7, 9, 0),
+ datetime(1998, 3, 5, 9, 0)])
+
+ def testMonthlyByWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ # Third Monday of the month
+ self.assertEqual(rrule(MONTHLY,
+ byweekday=(MO(+3)),
+ dtstart=datetime(1997, 9, 1)).between(datetime(1997, 9, 1),
+ datetime(1997, 12, 1)),
+ [datetime(1997, 9, 15, 0, 0),
+ datetime(1997, 10, 20, 0, 0),
+ datetime(1997, 11, 17, 0, 0)])
+
+ def testMonthlyByNWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 25, 9, 0),
+ datetime(1997, 10, 7, 9, 0)])
+
+ def testMonthlyByNWeekDayLarge(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 16, 9, 0),
+ datetime(1997, 10, 16, 9, 0)])
+
+ def testMonthlyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testMonthlyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 29, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testMonthlyByMonthAndNWeekDayLarge(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 15, 9, 0),
+ datetime(1998, 1, 20, 9, 0),
+ datetime(1998, 3, 12, 9, 0)])
+
+ def testMonthlyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 2, 3, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testMonthlyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 3, 3, 9, 0),
+ datetime(2001, 3, 1, 9, 0)])
+
+ def testMonthlyByYearDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testMonthlyByYearDayNeg(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testMonthlyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 4, 10, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testMonthlyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 4, 10, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testMonthlyByWeekNo(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 9, 0),
+ datetime(1998, 5, 12, 9, 0),
+ datetime(1998, 5, 13, 9, 0)])
+
+ def testMonthlyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 9, 0),
+ datetime(1999, 1, 4, 9, 0),
+ datetime(2000, 1, 3, 9, 0)])
+
+ def testMonthlyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1998, 12, 27, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testMonthlyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1999, 1, 3, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testMonthlyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 9, 0),
+ datetime(2004, 12, 27, 9, 0),
+ datetime(2009, 12, 28, 9, 0)])
+
+ def testMonthlyByEaster(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 9, 0),
+ datetime(1999, 4, 4, 9, 0),
+ datetime(2000, 4, 23, 9, 0)])
+
+ def testMonthlyByEasterPos(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 9, 0),
+ datetime(1999, 4, 5, 9, 0),
+ datetime(2000, 4, 24, 9, 0)])
+
+ def testMonthlyByEasterNeg(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 9, 0),
+ datetime(1999, 4, 3, 9, 0),
+ datetime(2000, 4, 22, 9, 0)])
+
+ def testMonthlyByHour(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 10, 2, 6, 0),
+ datetime(1997, 10, 2, 18, 0)])
+
+ def testMonthlyByMinute(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1997, 10, 2, 9, 6)])
+
+ def testMonthlyBySecond(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 10, 2, 9, 0, 6)])
+
+ def testMonthlyByHourAndMinute(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1997, 10, 2, 6, 6)])
+
+ def testMonthlyByHourAndSecond(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 10, 2, 6, 0, 6)])
+
+ def testMonthlyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testMonthlyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testMonthlyBySetPos(self):
+ self.assertEqual(list(rrule(MONTHLY,
+ count=3,
+ bymonthday=(13, 17),
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 13, 18, 0),
+ datetime(1997, 9, 17, 6, 0),
+ datetime(1997, 10, 13, 18, 0)])
+
+ def testWeekly(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testWeeklyInterval(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 16, 9, 0),
+ datetime(1997, 9, 30, 9, 0)])
+
+ def testWeeklyIntervalLarge(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ interval=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 1, 20, 9, 0),
+ datetime(1998, 6, 9, 9, 0)])
+
+ def testWeeklyByMonth(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 13, 9, 0),
+ datetime(1998, 1, 20, 9, 0)])
+
+ def testWeeklyByMonthDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 10, 1, 9, 0),
+ datetime(1997, 10, 3, 9, 0)])
+
+ def testWeeklyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 9, 0),
+ datetime(1998, 1, 7, 9, 0),
+ datetime(1998, 3, 5, 9, 0)])
+
+ def testWeeklyByWeekDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testWeeklyByNWeekDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testWeeklyByMonthAndWeekDay(self):
+ # This test is interesting, because it crosses the year
+ # boundary in a weekly period to find day '1' as a
+ # valid recurrence.
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testWeeklyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testWeeklyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 2, 3, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testWeeklyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 3, 3, 9, 0),
+ datetime(2001, 3, 1, 9, 0)])
+
+ def testWeeklyByYearDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testWeeklyByYearDayNeg(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testWeeklyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 1, 1, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testWeeklyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 1, 1, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testWeeklyByWeekNo(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 9, 0),
+ datetime(1998, 5, 12, 9, 0),
+ datetime(1998, 5, 13, 9, 0)])
+
+ def testWeeklyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 9, 0),
+ datetime(1999, 1, 4, 9, 0),
+ datetime(2000, 1, 3, 9, 0)])
+
+ def testWeeklyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1998, 12, 27, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testWeeklyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1999, 1, 3, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testWeeklyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 9, 0),
+ datetime(2004, 12, 27, 9, 0),
+ datetime(2009, 12, 28, 9, 0)])
+
+ def testWeeklyByEaster(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 9, 0),
+ datetime(1999, 4, 4, 9, 0),
+ datetime(2000, 4, 23, 9, 0)])
+
+ def testWeeklyByEasterPos(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 9, 0),
+ datetime(1999, 4, 5, 9, 0),
+ datetime(2000, 4, 24, 9, 0)])
+
+ def testWeeklyByEasterNeg(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 9, 0),
+ datetime(1999, 4, 3, 9, 0),
+ datetime(2000, 4, 22, 9, 0)])
+
+ def testWeeklyByHour(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 9, 9, 6, 0),
+ datetime(1997, 9, 9, 18, 0)])
+
+ def testWeeklyByMinute(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1997, 9, 9, 9, 6)])
+
+ def testWeeklyBySecond(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 9, 9, 9, 0, 6)])
+
+ def testWeeklyByHourAndMinute(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1997, 9, 9, 6, 6)])
+
+ def testWeeklyByHourAndSecond(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 9, 9, 6, 0, 6)])
+
+ def testWeeklyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testWeeklyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testWeeklyBySetPos(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU, TH),
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 9, 4, 6, 0),
+ datetime(1997, 9, 9, 18, 0)])
+
+ def testDaily(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testDailyInterval(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 6, 9, 0)])
+
+ def testDailyIntervalLarge(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ interval=92,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 12, 3, 9, 0),
+ datetime(1998, 3, 5, 9, 0)])
+
+ def testDailyByMonth(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 2, 9, 0),
+ datetime(1998, 1, 3, 9, 0)])
+
+ def testDailyByMonthDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 10, 1, 9, 0),
+ datetime(1997, 10, 3, 9, 0)])
+
+ def testDailyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 9, 0),
+ datetime(1998, 1, 7, 9, 0),
+ datetime(1998, 3, 5, 9, 0)])
+
+ def testDailyByWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testDailyByNWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testDailyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testDailyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 1, 8, 9, 0)])
+
+ def testDailyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 2, 3, 9, 0),
+ datetime(1998, 3, 3, 9, 0)])
+
+ def testDailyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 3, 3, 9, 0),
+ datetime(2001, 3, 1, 9, 0)])
+
+ def testDailyByYearDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testDailyByYearDayNeg(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 9, 0),
+ datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 4, 10, 9, 0),
+ datetime(1998, 7, 19, 9, 0)])
+
+ def testDailyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 1, 1, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testDailyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 9, 0),
+ datetime(1998, 7, 19, 9, 0),
+ datetime(1999, 1, 1, 9, 0),
+ datetime(1999, 7, 19, 9, 0)])
+
+ def testDailyByWeekNo(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 9, 0),
+ datetime(1998, 5, 12, 9, 0),
+ datetime(1998, 5, 13, 9, 0)])
+
+ def testDailyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 9, 0),
+ datetime(1999, 1, 4, 9, 0),
+ datetime(2000, 1, 3, 9, 0)])
+
+ def testDailyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1998, 12, 27, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testDailyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 9, 0),
+ datetime(1999, 1, 3, 9, 0),
+ datetime(2000, 1, 2, 9, 0)])
+
+ def testDailyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 9, 0),
+ datetime(2004, 12, 27, 9, 0),
+ datetime(2009, 12, 28, 9, 0)])
+
+ def testDailyByEaster(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 9, 0),
+ datetime(1999, 4, 4, 9, 0),
+ datetime(2000, 4, 23, 9, 0)])
+
+ def testDailyByEasterPos(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 9, 0),
+ datetime(1999, 4, 5, 9, 0),
+ datetime(2000, 4, 24, 9, 0)])
+
+ def testDailyByEasterNeg(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 9, 0),
+ datetime(1999, 4, 3, 9, 0),
+ datetime(2000, 4, 22, 9, 0)])
+
+ def testDailyByHour(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 9, 3, 6, 0),
+ datetime(1997, 9, 3, 18, 0)])
+
+ def testDailyByMinute(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1997, 9, 3, 9, 6)])
+
+ def testDailyBySecond(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 9, 3, 9, 0, 6)])
+
+ def testDailyByHourAndMinute(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1997, 9, 3, 6, 6)])
+
+ def testDailyByHourAndSecond(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 9, 3, 6, 0, 6)])
+
+ def testDailyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testDailyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testDailyBySetPos(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(15, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 15),
+ datetime(1997, 9, 3, 6, 45),
+ datetime(1997, 9, 3, 18, 15)])
+
+ def testHourly(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 10, 0),
+ datetime(1997, 9, 2, 11, 0)])
+
+ def testHourlyInterval(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 11, 0),
+ datetime(1997, 9, 2, 13, 0)])
+
+ def testHourlyIntervalLarge(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ interval=769,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 10, 4, 10, 0),
+ datetime(1997, 11, 5, 11, 0)])
+
+ def testHourlyByMonth(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 1, 0),
+ datetime(1998, 1, 1, 2, 0)])
+
+ def testHourlyByMonthDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 0, 0),
+ datetime(1997, 9, 3, 1, 0),
+ datetime(1997, 9, 3, 2, 0)])
+
+ def testHourlyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 0, 0),
+ datetime(1998, 1, 5, 1, 0),
+ datetime(1998, 1, 5, 2, 0)])
+
+ def testHourlyByWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 10, 0),
+ datetime(1997, 9, 2, 11, 0)])
+
+ def testHourlyByNWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 10, 0),
+ datetime(1997, 9, 2, 11, 0)])
+
+ def testHourlyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 1, 0),
+ datetime(1998, 1, 1, 2, 0)])
+
+ def testHourlyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 1, 0),
+ datetime(1998, 1, 1, 2, 0)])
+
+ def testHourlyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 1, 0),
+ datetime(1998, 1, 1, 2, 0)])
+
+ def testHourlyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 1, 0),
+ datetime(1998, 1, 1, 2, 0)])
+
+ def testHourlyByYearDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0),
+ datetime(1997, 12, 31, 1, 0),
+ datetime(1997, 12, 31, 2, 0),
+ datetime(1997, 12, 31, 3, 0)])
+
+ def testHourlyByYearDayNeg(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0),
+ datetime(1997, 12, 31, 1, 0),
+ datetime(1997, 12, 31, 2, 0),
+ datetime(1997, 12, 31, 3, 0)])
+
+ def testHourlyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0),
+ datetime(1998, 4, 10, 1, 0),
+ datetime(1998, 4, 10, 2, 0),
+ datetime(1998, 4, 10, 3, 0)])
+
+ def testHourlyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0),
+ datetime(1998, 4, 10, 1, 0),
+ datetime(1998, 4, 10, 2, 0),
+ datetime(1998, 4, 10, 3, 0)])
+
+ def testHourlyByWeekNo(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 0, 0),
+ datetime(1998, 5, 11, 1, 0),
+ datetime(1998, 5, 11, 2, 0)])
+
+ def testHourlyByWeekNoAndWeekDay(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 0, 0),
+ datetime(1997, 12, 29, 1, 0),
+ datetime(1997, 12, 29, 2, 0)])
+
+ def testHourlyByWeekNoAndWeekDayLarge(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0),
+ datetime(1997, 12, 28, 1, 0),
+ datetime(1997, 12, 28, 2, 0)])
+
+ def testHourlyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0),
+ datetime(1997, 12, 28, 1, 0),
+ datetime(1997, 12, 28, 2, 0)])
+
+ def testHourlyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 0, 0),
+ datetime(1998, 12, 28, 1, 0),
+ datetime(1998, 12, 28, 2, 0)])
+
+ def testHourlyByEaster(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 0, 0),
+ datetime(1998, 4, 12, 1, 0),
+ datetime(1998, 4, 12, 2, 0)])
+
+ def testHourlyByEasterPos(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 0, 0),
+ datetime(1998, 4, 13, 1, 0),
+ datetime(1998, 4, 13, 2, 0)])
+
+ def testHourlyByEasterNeg(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 0, 0),
+ datetime(1998, 4, 11, 1, 0),
+ datetime(1998, 4, 11, 2, 0)])
+
+ def testHourlyByHour(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 9, 3, 6, 0),
+ datetime(1997, 9, 3, 18, 0)])
+
+ def testHourlyByMinute(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1997, 9, 2, 10, 6)])
+
+ def testHourlyBySecond(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 9, 2, 10, 0, 6)])
+
+ def testHourlyByHourAndMinute(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1997, 9, 3, 6, 6)])
+
+ def testHourlyByHourAndSecond(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 9, 3, 6, 0, 6)])
+
+ def testHourlyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testHourlyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testHourlyBySetPos(self):
+ self.assertEqual(list(rrule(HOURLY,
+ count=3,
+ byminute=(15, 45),
+ bysecond=(15, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 15, 45),
+ datetime(1997, 9, 2, 9, 45, 15),
+ datetime(1997, 9, 2, 10, 15, 45)])
+
+ def testMinutely(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 9, 1),
+ datetime(1997, 9, 2, 9, 2)])
+
+ def testMinutelyInterval(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 9, 2),
+ datetime(1997, 9, 2, 9, 4)])
+
+ def testMinutelyIntervalLarge(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ interval=1501,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 10, 1),
+ datetime(1997, 9, 4, 11, 2)])
+
+ def testMinutelyByMonth(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 0, 1),
+ datetime(1998, 1, 1, 0, 2)])
+
+ def testMinutelyByMonthDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 0, 0),
+ datetime(1997, 9, 3, 0, 1),
+ datetime(1997, 9, 3, 0, 2)])
+
+ def testMinutelyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 0, 0),
+ datetime(1998, 1, 5, 0, 1),
+ datetime(1998, 1, 5, 0, 2)])
+
+ def testMinutelyByWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 9, 1),
+ datetime(1997, 9, 2, 9, 2)])
+
+ def testMinutelyByNWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 2, 9, 1),
+ datetime(1997, 9, 2, 9, 2)])
+
+ def testMinutelyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 0, 1),
+ datetime(1998, 1, 1, 0, 2)])
+
+ def testMinutelyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 0, 1),
+ datetime(1998, 1, 1, 0, 2)])
+
+ def testMinutelyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 0, 1),
+ datetime(1998, 1, 1, 0, 2)])
+
+ def testMinutelyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0),
+ datetime(1998, 1, 1, 0, 1),
+ datetime(1998, 1, 1, 0, 2)])
+
+ def testMinutelyByYearDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0),
+ datetime(1997, 12, 31, 0, 1),
+ datetime(1997, 12, 31, 0, 2),
+ datetime(1997, 12, 31, 0, 3)])
+
+ def testMinutelyByYearDayNeg(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0),
+ datetime(1997, 12, 31, 0, 1),
+ datetime(1997, 12, 31, 0, 2),
+ datetime(1997, 12, 31, 0, 3)])
+
+ def testMinutelyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0),
+ datetime(1998, 4, 10, 0, 1),
+ datetime(1998, 4, 10, 0, 2),
+ datetime(1998, 4, 10, 0, 3)])
+
+ def testMinutelyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0),
+ datetime(1998, 4, 10, 0, 1),
+ datetime(1998, 4, 10, 0, 2),
+ datetime(1998, 4, 10, 0, 3)])
+
+ def testMinutelyByWeekNo(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 0, 0),
+ datetime(1998, 5, 11, 0, 1),
+ datetime(1998, 5, 11, 0, 2)])
+
+ def testMinutelyByWeekNoAndWeekDay(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 0, 0),
+ datetime(1997, 12, 29, 0, 1),
+ datetime(1997, 12, 29, 0, 2)])
+
+ def testMinutelyByWeekNoAndWeekDayLarge(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0),
+ datetime(1997, 12, 28, 0, 1),
+ datetime(1997, 12, 28, 0, 2)])
+
+ def testMinutelyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0),
+ datetime(1997, 12, 28, 0, 1),
+ datetime(1997, 12, 28, 0, 2)])
+
+ def testMinutelyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 0, 0),
+ datetime(1998, 12, 28, 0, 1),
+ datetime(1998, 12, 28, 0, 2)])
+
+ def testMinutelyByEaster(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 0, 0),
+ datetime(1998, 4, 12, 0, 1),
+ datetime(1998, 4, 12, 0, 2)])
+
+ def testMinutelyByEasterPos(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 0, 0),
+ datetime(1998, 4, 13, 0, 1),
+ datetime(1998, 4, 13, 0, 2)])
+
+ def testMinutelyByEasterNeg(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 0, 0),
+ datetime(1998, 4, 11, 0, 1),
+ datetime(1998, 4, 11, 0, 2)])
+
+ def testMinutelyByHour(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0),
+ datetime(1997, 9, 2, 18, 1),
+ datetime(1997, 9, 2, 18, 2)])
+
+ def testMinutelyByMinute(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6),
+ datetime(1997, 9, 2, 9, 18),
+ datetime(1997, 9, 2, 10, 6)])
+
+ def testMinutelyBySecond(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 9, 2, 9, 1, 6)])
+
+ def testMinutelyByHourAndMinute(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6),
+ datetime(1997, 9, 2, 18, 18),
+ datetime(1997, 9, 3, 6, 6)])
+
+ def testMinutelyByHourAndSecond(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 9, 2, 18, 1, 6)])
+
+ def testMinutelyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testMinutelyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testMinutelyBySetPos(self):
+ self.assertEqual(list(rrule(MINUTELY,
+ count=3,
+ bysecond=(15, 30, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 15),
+ datetime(1997, 9, 2, 9, 0, 45),
+ datetime(1997, 9, 2, 9, 1, 15)])
+
+ def testSecondly(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1997, 9, 2, 9, 0, 1),
+ datetime(1997, 9, 2, 9, 0, 2)])
+
+ def testSecondlyInterval(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1997, 9, 2, 9, 0, 2),
+ datetime(1997, 9, 2, 9, 0, 4)])
+
+ def testSecondlyIntervalLarge(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ interval=90061,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1997, 9, 3, 10, 1, 1),
+ datetime(1997, 9, 4, 11, 2, 2)])
+
+ def testSecondlyByMonth(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 1),
+ datetime(1998, 1, 1, 0, 0, 2)])
+
+ def testSecondlyByMonthDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 3, 0, 0, 0),
+ datetime(1997, 9, 3, 0, 0, 1),
+ datetime(1997, 9, 3, 0, 0, 2)])
+
+ def testSecondlyByMonthAndMonthDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 0, 0, 0),
+ datetime(1998, 1, 5, 0, 0, 1),
+ datetime(1998, 1, 5, 0, 0, 2)])
+
+ def testSecondlyByWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1997, 9, 2, 9, 0, 1),
+ datetime(1997, 9, 2, 9, 0, 2)])
+
+ def testSecondlyByNWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1997, 9, 2, 9, 0, 1),
+ datetime(1997, 9, 2, 9, 0, 2)])
+
+ def testSecondlyByMonthAndWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 1),
+ datetime(1998, 1, 1, 0, 0, 2)])
+
+ def testSecondlyByMonthAndNWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 1),
+ datetime(1998, 1, 1, 0, 0, 2)])
+
+ def testSecondlyByMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 1),
+ datetime(1998, 1, 1, 0, 0, 2)])
+
+ def testSecondlyByMonthAndMonthDayAndWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 1, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 1),
+ datetime(1998, 1, 1, 0, 0, 2)])
+
+ def testSecondlyByYearDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0, 0),
+ datetime(1997, 12, 31, 0, 0, 1),
+ datetime(1997, 12, 31, 0, 0, 2),
+ datetime(1997, 12, 31, 0, 0, 3)])
+
+ def testSecondlyByYearDayNeg(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 31, 0, 0, 0),
+ datetime(1997, 12, 31, 0, 0, 1),
+ datetime(1997, 12, 31, 0, 0, 2),
+ datetime(1997, 12, 31, 0, 0, 3)])
+
+ def testSecondlyByMonthAndYearDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0, 0),
+ datetime(1998, 4, 10, 0, 0, 1),
+ datetime(1998, 4, 10, 0, 0, 2),
+ datetime(1998, 4, 10, 0, 0, 3)])
+
+ def testSecondlyByMonthAndYearDayNeg(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 10, 0, 0, 0),
+ datetime(1998, 4, 10, 0, 0, 1),
+ datetime(1998, 4, 10, 0, 0, 2),
+ datetime(1998, 4, 10, 0, 0, 3)])
+
+ def testSecondlyByWeekNo(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 5, 11, 0, 0, 0),
+ datetime(1998, 5, 11, 0, 0, 1),
+ datetime(1998, 5, 11, 0, 0, 2)])
+
+ def testSecondlyByWeekNoAndWeekDay(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 29, 0, 0, 0),
+ datetime(1997, 12, 29, 0, 0, 1),
+ datetime(1997, 12, 29, 0, 0, 2)])
+
+ def testSecondlyByWeekNoAndWeekDayLarge(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0, 0),
+ datetime(1997, 12, 28, 0, 0, 1),
+ datetime(1997, 12, 28, 0, 0, 2)])
+
+ def testSecondlyByWeekNoAndWeekDayLast(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 12, 28, 0, 0, 0),
+ datetime(1997, 12, 28, 0, 0, 1),
+ datetime(1997, 12, 28, 0, 0, 2)])
+
+ def testSecondlyByWeekNoAndWeekDay53(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 12, 28, 0, 0, 0),
+ datetime(1998, 12, 28, 0, 0, 1),
+ datetime(1998, 12, 28, 0, 0, 2)])
+
+ def testSecondlyByEaster(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 12, 0, 0, 0),
+ datetime(1998, 4, 12, 0, 0, 1),
+ datetime(1998, 4, 12, 0, 0, 2)])
+
+ def testSecondlyByEasterPos(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 13, 0, 0, 0),
+ datetime(1998, 4, 13, 0, 0, 1),
+ datetime(1998, 4, 13, 0, 0, 2)])
+
+ def testSecondlyByEasterNeg(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 4, 11, 0, 0, 0),
+ datetime(1998, 4, 11, 0, 0, 1),
+ datetime(1998, 4, 11, 0, 0, 2)])
+
+ def testSecondlyByHour(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 0),
+ datetime(1997, 9, 2, 18, 0, 1),
+ datetime(1997, 9, 2, 18, 0, 2)])
+
+ def testSecondlyByMinute(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 0),
+ datetime(1997, 9, 2, 9, 6, 1),
+ datetime(1997, 9, 2, 9, 6, 2)])
+
+ def testSecondlyBySecond(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0, 6),
+ datetime(1997, 9, 2, 9, 0, 18),
+ datetime(1997, 9, 2, 9, 1, 6)])
+
+ def testSecondlyByHourAndMinute(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 0),
+ datetime(1997, 9, 2, 18, 6, 1),
+ datetime(1997, 9, 2, 18, 6, 2)])
+
+ def testSecondlyByHourAndSecond(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 0, 6),
+ datetime(1997, 9, 2, 18, 0, 18),
+ datetime(1997, 9, 2, 18, 1, 6)])
+
+ def testSecondlyByMinuteAndSecond(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 6, 6),
+ datetime(1997, 9, 2, 9, 6, 18),
+ datetime(1997, 9, 2, 9, 18, 6)])
+
+ def testSecondlyByHourAndMinuteAndSecond(self):
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 18, 6, 6),
+ datetime(1997, 9, 2, 18, 6, 18),
+ datetime(1997, 9, 2, 18, 18, 6)])
+
+ def testSecondlyByHourAndMinuteAndSecondBug(self):
+ # This explores a bug found by Mathieu Bridon.
+ self.assertEqual(list(rrule(SECONDLY,
+ count=3,
+ bysecond=(0,),
+ byminute=(1,),
+ dtstart=datetime(2010, 3, 22, 12, 1))),
+ [datetime(2010, 3, 22, 12, 1),
+ datetime(2010, 3, 22, 13, 1),
+ datetime(2010, 3, 22, 14, 1)])
+
+ def testLongIntegers(self):
+ if PY2: # There are no longs in python3
+ self.assertEqual(list(rrule(MINUTELY,
+ count=long(2),
+ interval=long(2),
+ bymonth=long(2),
+ byweekday=long(3),
+ byhour=long(6),
+ byminute=long(6),
+ bysecond=long(6),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 2, 5, 6, 6, 6),
+ datetime(1998, 2, 12, 6, 6, 6)])
+ self.assertEqual(list(rrule(YEARLY,
+ count=long(2),
+ bymonthday=long(5),
+ byweekno=long(2),
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1998, 1, 5, 9, 0),
+ datetime(2004, 1, 5, 9, 0)])
+
+ def testHourlyBadRRule(self):
+ """
+ When `byhour` is specified with `freq=HOURLY`, there are certain
+ combinations of `dtstart` and `byhour` which result in an rrule with no
+ valid values.
+
+ See https://github.com/dateutil/dateutil/issues/4
+ """
+
+ self.assertRaises(ValueError, rrule, HOURLY,
+ **dict(interval=4, byhour=(7, 11, 15, 19),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testMinutelyBadRRule(self):
+ """
+ See :func:`testHourlyBadRRule` for details.
+ """
+
+ self.assertRaises(ValueError, rrule, MINUTELY,
+ **dict(interval=12, byminute=(10, 11, 25, 39, 50),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testSecondlyBadRRule(self):
+ """
+ See :func:`testHourlyBadRRule` for details.
+ """
+
+ self.assertRaises(ValueError, rrule, SECONDLY,
+ **dict(interval=10, bysecond=(2, 15, 37, 42, 59),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testMinutelyBadComboRRule(self):
+ """
+ Certain values of :param:`interval` in :class:`rrule`, when combined
+ with certain values of :param:`byhour` create rules which apply to no
+ valid dates. The library should detect this case in the iterator and
+ raise a :exception:`ValueError`.
+ """
+
+ # In Python 2.7 you can use a context manager for this.
+ def make_bad_rrule():
+ list(rrule(MINUTELY, interval=120, byhour=(10, 12, 14, 16),
+ count=2, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ self.assertRaises(ValueError, make_bad_rrule)
+
+ def testSecondlyBadComboRRule(self):
+ """
+ See :func:`testMinutelyBadComboRRule' for details.
+ """
+
+ # In Python 2.7 you can use a context manager for this.
+ def make_bad_minute_rrule():
+ list(rrule(SECONDLY, interval=360, byminute=(10, 28, 49),
+ count=4, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def make_bad_hour_rrule():
+ list(rrule(SECONDLY, interval=43200, byhour=(2, 10, 18, 23),
+ count=4, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ self.assertRaises(ValueError, make_bad_minute_rrule)
+ self.assertRaises(ValueError, make_bad_hour_rrule)
+
+ def testBadUntilCountRRule(self):
+ """
+ See rfc-5545 3.3.10 - This checks for the deprecation warning, and will
+ eventually check for an error.
+ """
+ with pytest.warns(DeprecationWarning):
+ rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0),
+ count=3, until=datetime(1997, 9, 4, 9, 0))
+
+ def testUntilNotMatching(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0),
+ until=datetime(1997, 9, 5, 8, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testUntilMatching(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0),
+ until=datetime(1997, 9, 4, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testUntilSingle(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0),
+ until=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0)])
+
+ def testUntilEmpty(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0),
+ until=datetime(1997, 9, 1, 9, 0))),
+ [])
+
+ def testUntilWithDate(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0),
+ until=date(1997, 9, 5))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testWkStIntervalMO(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ interval=2,
+ byweekday=(TU, SU),
+ wkst=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 7, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testWkStIntervalSU(self):
+ self.assertEqual(list(rrule(WEEKLY,
+ count=3,
+ interval=2,
+ byweekday=(TU, SU),
+ wkst=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testDTStartIsDate(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ dtstart=date(1997, 9, 2))),
+ [datetime(1997, 9, 2, 0, 0),
+ datetime(1997, 9, 3, 0, 0),
+ datetime(1997, 9, 4, 0, 0)])
+
+ def testDTStartWithMicroseconds(self):
+ self.assertEqual(list(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0, 0, 500000))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testMaxYear(self):
+ self.assertEqual(list(rrule(YEARLY,
+ count=3,
+ bymonth=2,
+ bymonthday=31,
+ dtstart=datetime(9997, 9, 2, 9, 0, 0))),
+ [])
+
+ def testGetItem(self):
+ self.assertEqual(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))[0],
+ datetime(1997, 9, 2, 9, 0))
+
+ def testGetItemNeg(self):
+ self.assertEqual(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))[-1],
+ datetime(1997, 9, 4, 9, 0))
+
+ def testGetItemSlice(self):
+ self.assertEqual(rrule(DAILY,
+ # count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))[1:2],
+ [datetime(1997, 9, 3, 9, 0)])
+
+ def testGetItemSliceEmpty(self):
+ self.assertEqual(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))[:],
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0)])
+
+ def testGetItemSliceStep(self):
+ self.assertEqual(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0))[::-2],
+ [datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 2, 9, 0)])
+
+ def testCount(self):
+ self.assertEqual(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)).count(),
+ 3)
+
+ def testCountZero(self):
+ self.assertEqual(rrule(YEARLY,
+ count=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)).count(),
+ 0)
+
+ def testContains(self):
+ rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+ self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+ def testContainsNot(self):
+ rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+ self.assertEqual(datetime(1997, 9, 3, 9, 0) not in rr, False)
+
+ def testBefore(self):
+ self.assertEqual(rrule(DAILY, # count=5
+ dtstart=datetime(1997, 9, 2, 9, 0)).before(datetime(1997, 9, 5, 9, 0)),
+ datetime(1997, 9, 4, 9, 0))
+
+ def testBeforeInc(self):
+ self.assertEqual(rrule(DAILY,
+ #count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .before(datetime(1997, 9, 5, 9, 0), inc=True),
+ datetime(1997, 9, 5, 9, 0))
+
+ def testAfter(self):
+ self.assertEqual(rrule(DAILY,
+ #count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .after(datetime(1997, 9, 4, 9, 0)),
+ datetime(1997, 9, 5, 9, 0))
+
+ def testAfterInc(self):
+ self.assertEqual(rrule(DAILY,
+ #count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .after(datetime(1997, 9, 4, 9, 0), inc=True),
+ datetime(1997, 9, 4, 9, 0))
+
+ def testXAfter(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .xafter(datetime(1997, 9, 8, 9, 0), count=12)),
+ [datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 10, 9, 0),
+ datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 12, 9, 0),
+ datetime(1997, 9, 13, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 15, 9, 0),
+ datetime(1997, 9, 16, 9, 0),
+ datetime(1997, 9, 17, 9, 0),
+ datetime(1997, 9, 18, 9, 0),
+ datetime(1997, 9, 19, 9, 0),
+ datetime(1997, 9, 20, 9, 0)])
+
+ def testXAfterInc(self):
+ self.assertEqual(list(rrule(DAILY,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .xafter(datetime(1997, 9, 8, 9, 0), count=12, inc=True)),
+ [datetime(1997, 9, 8, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 10, 9, 0),
+ datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 12, 9, 0),
+ datetime(1997, 9, 13, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 15, 9, 0),
+ datetime(1997, 9, 16, 9, 0),
+ datetime(1997, 9, 17, 9, 0),
+ datetime(1997, 9, 18, 9, 0),
+ datetime(1997, 9, 19, 9, 0)])
+
+ def testBetween(self):
+ self.assertEqual(rrule(DAILY,
+ #count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .between(datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 6, 9, 0)),
+ [datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 5, 9, 0)])
+
+ def testBetweenInc(self):
+ self.assertEqual(rrule(DAILY,
+ #count=5,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ .between(datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 6, 9, 0), inc=True),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 5, 9, 0),
+ datetime(1997, 9, 6, 9, 0)])
+
+ def testCachePre(self):
+ rr = rrule(DAILY, count=15, cache=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ self.assertEqual(list(rr),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 5, 9, 0),
+ datetime(1997, 9, 6, 9, 0),
+ datetime(1997, 9, 7, 9, 0),
+ datetime(1997, 9, 8, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 10, 9, 0),
+ datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 12, 9, 0),
+ datetime(1997, 9, 13, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 15, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testCachePost(self):
+ rr = rrule(DAILY, count=15, cache=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ for x in rr: pass
+ self.assertEqual(list(rr),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 5, 9, 0),
+ datetime(1997, 9, 6, 9, 0),
+ datetime(1997, 9, 7, 9, 0),
+ datetime(1997, 9, 8, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 10, 9, 0),
+ datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 12, 9, 0),
+ datetime(1997, 9, 13, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 15, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testCachePostInternal(self):
+ rr = rrule(DAILY, count=15, cache=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ for x in rr: pass
+ self.assertEqual(rr._cache,
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 3, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 5, 9, 0),
+ datetime(1997, 9, 6, 9, 0),
+ datetime(1997, 9, 7, 9, 0),
+ datetime(1997, 9, 8, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 10, 9, 0),
+ datetime(1997, 9, 11, 9, 0),
+ datetime(1997, 9, 12, 9, 0),
+ datetime(1997, 9, 13, 9, 0),
+ datetime(1997, 9, 14, 9, 0),
+ datetime(1997, 9, 15, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testCachePreContains(self):
+ rr = rrule(DAILY, count=3, cache=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+ def testCachePostContains(self):
+ rr = rrule(DAILY, count=3, cache=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ for x in rr: pass
+ self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+ def testStr(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrWithTZID(self):
+ NYC = tz.gettz('America/New_York')
+ self.assertEqual(list(rrulestr(
+ "DTSTART;TZID=America/New_York:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0, tzinfo=NYC),
+ datetime(1998, 9, 2, 9, 0, tzinfo=NYC),
+ datetime(1999, 9, 2, 9, 0, tzinfo=NYC)])
+
+ def testStrWithTZIDMapping(self):
+ rrstr = ("DTSTART;TZID=Eastern:19970902T090000\n" +
+ "RRULE:FREQ=YEARLY;COUNT=3")
+
+ NYC = tz.gettz('America/New_York')
+ rr = rrulestr(rrstr, tzids={'Eastern': NYC})
+ exp = [datetime(1997, 9, 2, 9, 0, tzinfo=NYC),
+ datetime(1998, 9, 2, 9, 0, tzinfo=NYC),
+ datetime(1999, 9, 2, 9, 0, tzinfo=NYC)]
+
+ self.assertEqual(list(rr), exp)
+
+ def testStrWithTZIDCallable(self):
+ rrstr = ('DTSTART;TZID=UTC+04:19970902T090000\n' +
+ 'RRULE:FREQ=YEARLY;COUNT=3')
+
+ TZ = tz.tzstr('UTC+04')
+ def parse_tzstr(tzstr):
+ if tzstr is None:
+ raise ValueError('Invalid tzstr')
+
+ return tz.tzstr(tzstr)
+
+ rr = rrulestr(rrstr, tzids=parse_tzstr)
+
+ exp = [datetime(1997, 9, 2, 9, 0, tzinfo=TZ),
+ datetime(1998, 9, 2, 9, 0, tzinfo=TZ),
+ datetime(1999, 9, 2, 9, 0, tzinfo=TZ),]
+
+ self.assertEqual(list(rr), exp)
+
+ def testStrWithTZIDCallableFailure(self):
+ rrstr = ('DTSTART;TZID=America/New_York:19970902T090000\n' +
+ 'RRULE:FREQ=YEARLY;COUNT=3')
+
+ class TzInfoError(Exception):
+ pass
+
+ def tzinfos(tzstr):
+ if tzstr == 'America/New_York':
+ raise TzInfoError('Invalid!')
+ return None
+
+ with self.assertRaises(TzInfoError):
+ rrulestr(rrstr, tzids=tzinfos)
+
+ def testStrWithConflictingTZID(self):
+ # RFC 5545 Section 3.3.5, FORM #2: DATE WITH UTC TIME
+ # https://tools.ietf.org/html/rfc5545#section-3.3.5
+ # The "TZID" property parameter MUST NOT be applied to DATE-TIME
+ with self.assertRaises(ValueError):
+ rrulestr("DTSTART;TZID=America/New_York:19970902T090000Z\n"+
+ "RRULE:FREQ=YEARLY;COUNT=3\n")
+
+ def testStrType(self):
+ self.assertEqual(isinstance(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3\n"
+ ), rrule), True)
+
+ def testStrForceSetType(self):
+ self.assertEqual(isinstance(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3\n"
+ , forceset=True), rruleset), True)
+
+ def testStrSetType(self):
+ self.assertEqual(isinstance(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n"
+ "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n"
+ ), rruleset), True)
+
+ def testStrCase(self):
+ self.assertEqual(list(rrulestr(
+ "dtstart:19970902T090000\n"
+ "rrule:freq=yearly;count=3\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrSpaces(self):
+ self.assertEqual(list(rrulestr(
+ " DTSTART:19970902T090000 "
+ " RRULE:FREQ=YEARLY;COUNT=3 "
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrSpacesAndLines(self):
+ self.assertEqual(list(rrulestr(
+ " DTSTART:19970902T090000 \n"
+ " \n"
+ " RRULE:FREQ=YEARLY;COUNT=3 \n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrNoDTStart(self):
+ self.assertEqual(list(rrulestr(
+ "RRULE:FREQ=YEARLY;COUNT=3\n"
+ , dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrValueOnly(self):
+ self.assertEqual(list(rrulestr(
+ "FREQ=YEARLY;COUNT=3\n"
+ , dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrUnfold(self):
+ self.assertEqual(list(rrulestr(
+ "FREQ=YEA\n RLY;COUNT=3\n", unfold=True,
+ dtstart=datetime(1997, 9, 2, 9, 0))),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1998, 9, 2, 9, 0),
+ datetime(1999, 9, 2, 9, 0)])
+
+ def testStrSet(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n"
+ "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testStrSetDate(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n"
+ "RDATE:19970904T090000\n"
+ "RDATE:19970909T090000\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testStrSetExRule(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testStrSetExDate(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE:19970904T090000\n"
+ "EXDATE:19970911T090000\n"
+ "EXDATE:19970918T090000\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testStrSetExDateMultiple(self):
+ rrstr = ("DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE:19970904T090000,19970911T090000,19970918T090000\n")
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)]
+
+ def testStrSetExDateWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rr = rrulestr("DTSTART;TZID=Europe/Brussels:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE;TZID=Europe/Brussels:19970904T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970911T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970918T090000\n")
+
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 9, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 16, 9, 0, tzinfo=BXL)]
+
+ def testStrSetExDateValueDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueMixDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueDateTimeWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9, tzinfo=BXL),
+ datetime(1997, 9, 11, 9, tzinfo=BXL)]
+
+ def testStrSetExDateValueDate(self):
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE:19970902",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE:19970902",
+ "EXDATE;VALUE=DATE:19970909",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4), datetime(1997, 9, 11)]
+
+ def testStrSetDateAndExDate(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RDATE:19970902T090000\n"
+ "RDATE:19970904T090000\n"
+ "RDATE:19970909T090000\n"
+ "RDATE:19970911T090000\n"
+ "RDATE:19970916T090000\n"
+ "RDATE:19970918T090000\n"
+ "EXDATE:19970904T090000\n"
+ "EXDATE:19970911T090000\n"
+ "EXDATE:19970918T090000\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testStrSetDateAndExRule(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RDATE:19970902T090000\n"
+ "RDATE:19970904T090000\n"
+ "RDATE:19970909T090000\n"
+ "RDATE:19970911T090000\n"
+ "RDATE:19970916T090000\n"
+ "RDATE:19970918T090000\n"
+ "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n"
+ )),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testStrKeywords(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;"
+ "BYMONTH=3;BYWEEKDAY=TH;BYMONTHDAY=3;"
+ "BYHOUR=3;BYMINUTE=3;BYSECOND=3\n"
+ )),
+ [datetime(2033, 3, 3, 3, 3, 3),
+ datetime(2039, 3, 3, 3, 3, 3),
+ datetime(2072, 3, 3, 3, 3, 3)])
+
+ def testStrNWeekDay(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n"
+ )),
+ [datetime(1997, 12, 25, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 12, 31, 9, 0)])
+
+ def testStrUntil(self):
+ self.assertEqual(list(rrulestr(
+ "DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;"
+ "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n"
+ )),
+ [datetime(1997, 12, 25, 9, 0),
+ datetime(1998, 1, 6, 9, 0),
+ datetime(1998, 12, 31, 9, 0)])
+
+ def testStrValueDatetime(self):
+ rr = rrulestr("DTSTART;VALUE=DATE-TIME:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=2")
+
+ self.assertEqual(list(rr), [datetime(1997, 9, 2, 9, 0, 0),
+ datetime(1998, 9, 2, 9, 0, 0)])
+
+ def testStrValueDate(self):
+ rr = rrulestr("DTSTART;VALUE=DATE:19970902\n"
+ "RRULE:FREQ=YEARLY;COUNT=2")
+
+ self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0),
+ datetime(1998, 9, 2, 0, 0, 0)])
+
+ def testStrMultipleDTStartComma(self):
+ with pytest.raises(ValueError):
+ rr = rrulestr("DTSTART:19970101T000000,19970202T000000\n"
+ "RRULE:FREQ=YEARLY;COUNT=1")
+
+ def testStrInvalidUntil(self):
+ with self.assertRaises(ValueError):
+ list(rrulestr("DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;"
+ "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n"))
+
+ def testStrUntilMustBeUTC(self):
+ with self.assertRaises(ValueError):
+ list(rrulestr("DTSTART;TZID=America/New_York:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;"
+ "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n"))
+
+ def testStrUntilWithTZ(self):
+ NYC = tz.gettz('America/New_York')
+ rr = list(rrulestr("DTSTART;TZID=America/New_York:19970101T000000\n"
+ "RRULE:FREQ=YEARLY;"
+ "UNTIL=19990101T000000Z\n"))
+ self.assertEqual(list(rr), [datetime(1997, 1, 1, 0, 0, 0, tzinfo=NYC),
+ datetime(1998, 1, 1, 0, 0, 0, tzinfo=NYC)])
+
+ def testStrEmptyByDay(self):
+ with self.assertRaises(ValueError):
+ list(rrulestr("DTSTART:19970902T090000\n"
+ "FREQ=WEEKLY;"
+ "BYDAY=;" # This part is invalid
+ "WKST=SU"))
+
+ def testStrInvalidByDay(self):
+ with self.assertRaises(ValueError):
+ list(rrulestr("DTSTART:19970902T090000\n"
+ "FREQ=WEEKLY;"
+ "BYDAY=-1OK;" # This part is invalid
+ "WKST=SU"))
+
+ def testBadBySetPos(self):
+ self.assertRaises(ValueError,
+ rrule, MONTHLY,
+ count=1,
+ bysetpos=0,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+
+ def testBadBySetPosMany(self):
+ self.assertRaises(ValueError,
+ rrule, MONTHLY,
+ count=1,
+ bysetpos=(-1, 0, 1),
+ dtstart=datetime(1997, 9, 2, 9, 0))
+
+ # Tests to ensure that str(rrule) works
+ def testToStrYearly(self):
+ rule = rrule(YEARLY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+ self._rrulestr_reverse_test(rule)
+
+ def testToStrYearlyInterval(self):
+ rule = rrule(YEARLY, count=3, interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0))
+ self._rrulestr_reverse_test(rule)
+
+ def testToStrYearlyByMonth(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByNWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndNWeekDayLarge(self):
+ # This is interesting because the TH(-3) ends up before
+ # the TU(3).
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByEaster(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByHour(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMinute(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyBySecond(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrYearlyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=3,
+ bymonthday=15,
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthly(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyInterval(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ interval=18,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonth(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ # Third Monday of the month
+ self.assertEqual(rrule(MONTHLY,
+ byweekday=(MO(+3)),
+ dtstart=datetime(1997, 9, 1)).between(datetime(1997,
+ 9,
+ 1),
+ datetime(1997,
+ 12,
+ 1)),
+ [datetime(1997, 9, 15, 0, 0),
+ datetime(1997, 10, 20, 0, 0),
+ datetime(1997, 11, 17, 0, 0)])
+
+ def testToStrMonthlyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByNWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndNWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(3), TH(-3)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByEaster(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByHour(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMinute(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyBySecond(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMonthlyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(MONTHLY,
+ count=3,
+ bymonthday=(13, 17),
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeekly(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyInterval(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ interval=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonth(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndWeekDay(self):
+ # This test is interesting, because it crosses the year
+ # boundary in a weekly period to find day '1' as a
+ # valid recurrence.
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByEaster(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByHour(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMinute(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyBySecond(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrWeeklyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ byweekday=(TU, TH),
+ byhour=(6, 18),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDaily(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyInterval(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ interval=92,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonth(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=4,
+ bymonth=(1, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekNoAndWeekDay(self):
+ # That's a nice one. The first days of week number one
+ # may be in the last year.
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekNoAndWeekDayLarge(self):
+ # Another nice test. The last days of week number 52/53
+ # may be in the next year.
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByEaster(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByHour(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMinute(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyBySecond(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrDailyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(DAILY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(15, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourly(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyInterval(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ interval=769,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonth(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekNoAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekNoAndWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByEaster(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByHour(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMinute(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyBySecond(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrHourlyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(HOURLY,
+ count=3,
+ byminute=(15, 45),
+ bysecond=(15, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutely(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyInterval(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ interval=1501,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonth(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekNoAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekNoAndWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByEaster(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByHour(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMinute(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyBySecond(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrMinutelyBySetPos(self):
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=3,
+ bysecond=(15, 30, 45),
+ bysetpos=(3, -3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondly(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyInterval(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ interval=2,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyIntervalLarge(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ interval=90061,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonth(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonthday=(1, 3),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndMonthDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(5, 7),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndNWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ byweekday=(TU(1), TH(-1)),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndMonthDayAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bymonth=(1, 3),
+ bymonthday=(1, 3),
+ byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByYearDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=4,
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=4,
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndYearDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(1, 100, 200, 365),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMonthAndYearDayNeg(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=4,
+ bymonth=(4, 7),
+ byyearday=(-365, -266, -166, -1),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekNo(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekno=20,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekNoAndWeekDay(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekno=1,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekNoAndWeekDayLarge(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekno=52,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekNoAndWeekDayLast(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekno=-1,
+ byweekday=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByWeekNoAndWeekDay53(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byweekno=53,
+ byweekday=MO,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByEaster(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byeaster=0,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByEasterPos(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byeaster=1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByEasterNeg(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byeaster=-1,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByHour(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMinute(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyBySecond(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByHourAndMinute(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByHourAndSecond(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByHourAndMinuteAndSecond(self):
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ byhour=(6, 18),
+ byminute=(6, 18),
+ bysecond=(6, 18),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrSecondlyByHourAndMinuteAndSecondBug(self):
+ # This explores a bug found by Mathieu Bridon.
+ self._rrulestr_reverse_test(rrule(SECONDLY,
+ count=3,
+ bysecond=(0,),
+ byminute=(1,),
+ dtstart=datetime(2010, 3, 22, 12, 1)))
+
+ def testToStrWithWkSt(self):
+ self._rrulestr_reverse_test(rrule(WEEKLY,
+ count=3,
+ wkst=SU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testToStrLongIntegers(self):
+ if PY2: # There are no longs in python3
+ self._rrulestr_reverse_test(rrule(MINUTELY,
+ count=long(2),
+ interval=long(2),
+ bymonth=long(2),
+ byweekday=long(3),
+ byhour=long(6),
+ byminute=long(6),
+ bysecond=long(6),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ self._rrulestr_reverse_test(rrule(YEARLY,
+ count=long(2),
+ bymonthday=long(5),
+ byweekno=long(2),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+
+ def testReplaceIfSet(self):
+ rr = rrule(YEARLY,
+ count=1,
+ bymonthday=5,
+ dtstart=datetime(1997, 1, 1))
+ newrr = rr.replace(bymonthday=6)
+ self.assertEqual(list(rr), [datetime(1997, 1, 5)])
+ self.assertEqual(list(newrr),
+ [datetime(1997, 1, 6)])
+
+ def testReplaceIfNotSet(self):
+ rr = rrule(YEARLY,
+ count=1,
+ dtstart=datetime(1997, 1, 1))
+ newrr = rr.replace(bymonthday=6)
+ self.assertEqual(list(rr), [datetime(1997, 1, 1)])
+ self.assertEqual(list(newrr),
+ [datetime(1997, 1, 6)])
+
+
+@pytest.mark.rrule
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart():
+ dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)
+ UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC)
+
+ rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL)
+ rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL)
+ assert list(rule_without_dtstart) == list(rule_with_dtstart)
+
+
+@pytest.mark.rrule
+@pytest.mark.rrulestr
+@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637")
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart_rrulestr():
+ rrule_without_dtstart = rrule(freq=HOURLY,
+ until=datetime(2018, 3, 6, 8, 0,
+ tzinfo=tz.UTC))
+ rrule_r = rrulestr(str(rrule_without_dtstart))
+
+ assert list(rrule_r) == list(rrule_without_dtstart)
+
+
+@pytest.mark.rruleset
+class RRuleSetTest(unittest.TestCase):
+ def testSet(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testSetDate(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=1, byweekday=TU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.rdate(datetime(1997, 9, 4, 9))
+ rrset.rdate(datetime(1997, 9, 9, 9))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testSetExRule(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testSetExDate(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.exdate(datetime(1997, 9, 4, 9))
+ rrset.exdate(datetime(1997, 9, 11, 9))
+ rrset.exdate(datetime(1997, 9, 18, 9))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testSetExDateRevOrder(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(MONTHLY, count=5, bymonthday=10,
+ dtstart=datetime(2004, 1, 1, 9, 0)))
+ rrset.exdate(datetime(2004, 4, 10, 9, 0))
+ rrset.exdate(datetime(2004, 2, 10, 9, 0))
+ self.assertEqual(list(rrset),
+ [datetime(2004, 1, 10, 9, 0),
+ datetime(2004, 3, 10, 9, 0),
+ datetime(2004, 5, 10, 9, 0)])
+
+ def testSetDateAndExDate(self):
+ rrset = rruleset()
+ rrset.rdate(datetime(1997, 9, 2, 9))
+ rrset.rdate(datetime(1997, 9, 4, 9))
+ rrset.rdate(datetime(1997, 9, 9, 9))
+ rrset.rdate(datetime(1997, 9, 11, 9))
+ rrset.rdate(datetime(1997, 9, 16, 9))
+ rrset.rdate(datetime(1997, 9, 18, 9))
+ rrset.exdate(datetime(1997, 9, 4, 9))
+ rrset.exdate(datetime(1997, 9, 11, 9))
+ rrset.exdate(datetime(1997, 9, 18, 9))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testSetDateAndExRule(self):
+ rrset = rruleset()
+ rrset.rdate(datetime(1997, 9, 2, 9))
+ rrset.rdate(datetime(1997, 9, 4, 9))
+ rrset.rdate(datetime(1997, 9, 9, 9))
+ rrset.rdate(datetime(1997, 9, 11, 9))
+ rrset.rdate(datetime(1997, 9, 16, 9))
+ rrset.rdate(datetime(1997, 9, 18, 9))
+ rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)])
+
+ def testSetCount(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ self.assertEqual(rrset.count(), 3)
+
+ def testSetCachePre(self):
+ rrset = rruleset()
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testSetCachePost(self):
+ rrset = rruleset(cache=True)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ for x in rrset: pass
+ self.assertEqual(list(rrset),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testSetCachePostInternal(self):
+ rrset = rruleset(cache=True)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+ dtstart=datetime(1997, 9, 2, 9, 0)))
+ for x in rrset: pass
+ self.assertEqual(list(rrset._cache),
+ [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 4, 9, 0),
+ datetime(1997, 9, 9, 9, 0)])
+
+ def testSetRRuleCount(self):
+ # Test that the count is updated when an rrule is added
+ rrset = rruleset(cache=False)
+ for cache in (True, False):
+ rrset = rruleset(cache=cache)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+ dtstart=datetime(1983, 4, 1)))
+ rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+ dtstart=datetime(1991, 6, 3)))
+
+ # Check the length twice - first one sets a cache, second reads it
+ self.assertEqual(rrset.count(), 6)
+ self.assertEqual(rrset.count(), 6)
+
+ # This should invalidate the cache and force an update
+ rrset.rrule(rrule(MONTHLY, count=3, dtstart=datetime(1994, 1, 3)))
+
+ self.assertEqual(rrset.count(), 9)
+ self.assertEqual(rrset.count(), 9)
+
+ def testSetRDateCount(self):
+ # Test that the count is updated when an rdate is added
+ rrset = rruleset(cache=False)
+ for cache in (True, False):
+ rrset = rruleset(cache=cache)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+ dtstart=datetime(1983, 4, 1)))
+ rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+ dtstart=datetime(1991, 6, 3)))
+
+ # Check the length twice - first one sets a cache, second reads it
+ self.assertEqual(rrset.count(), 6)
+ self.assertEqual(rrset.count(), 6)
+
+ # This should invalidate the cache and force an update
+ rrset.rdate(datetime(1993, 2, 14))
+
+ self.assertEqual(rrset.count(), 7)
+ self.assertEqual(rrset.count(), 7)
+
+ def testSetExRuleCount(self):
+ # Test that the count is updated when an exrule is added
+ rrset = rruleset(cache=False)
+ for cache in (True, False):
+ rrset = rruleset(cache=cache)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+ dtstart=datetime(1983, 4, 1)))
+ rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+ dtstart=datetime(1991, 6, 3)))
+
+ # Check the length twice - first one sets a cache, second reads it
+ self.assertEqual(rrset.count(), 6)
+ self.assertEqual(rrset.count(), 6)
+
+ # This should invalidate the cache and force an update
+ rrset.exrule(rrule(WEEKLY, count=2, interval=2,
+ dtstart=datetime(1991, 6, 14)))
+
+ self.assertEqual(rrset.count(), 4)
+ self.assertEqual(rrset.count(), 4)
+
+ def testSetExDateCount(self):
+ # Test that the count is updated when an rdate is added
+ for cache in (True, False):
+ rrset = rruleset(cache=cache)
+ rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+ dtstart=datetime(1983, 4, 1)))
+ rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+ dtstart=datetime(1991, 6, 3)))
+
+ # Check the length twice - first one sets a cache, second reads it
+ self.assertEqual(rrset.count(), 6)
+ self.assertEqual(rrset.count(), 6)
+
+ # This should invalidate the cache and force an update
+ rrset.exdate(datetime(1991, 6, 28))
+
+ self.assertEqual(rrset.count(), 5)
+ self.assertEqual(rrset.count(), 5)
+
+
+class WeekdayTest(unittest.TestCase):
+ def testInvalidNthWeekday(self):
+ with self.assertRaises(ValueError):
+ FR(0)
+
+ def testWeekdayCallable(self):
+ # Calling a weekday instance generates a new weekday instance with the
+ # value of n changed.
+ from dateutil.rrule import weekday
+ self.assertEqual(MO(1), weekday(0, 1))
+
+ # Calling a weekday instance with the identical n returns the original
+ # object
+ FR_3 = weekday(4, 3)
+ self.assertIs(FR_3(3), FR_3)
+
+ def testWeekdayEquality(self):
+ # Two weekday objects are not equal if they have different values for n
+ self.assertNotEqual(TH, TH(-1))
+ self.assertNotEqual(SA(3), SA(2))
+
+ def testWeekdayEqualitySubclass(self):
+ # Two weekday objects equal if their "weekday" and "n" attributes are
+ # available and the same
+ class BasicWeekday(object):
+ def __init__(self, weekday):
+ self.weekday = weekday
+
+ class BasicNWeekday(BasicWeekday):
+ def __init__(self, weekday, n=None):
+ super(BasicNWeekday, self).__init__(weekday)
+ self.n = n
+
+ MO_Basic = BasicWeekday(0)
+
+ self.assertNotEqual(MO, MO_Basic)
+ self.assertNotEqual(MO(1), MO_Basic)
+
+ TU_BasicN = BasicNWeekday(1)
+
+ self.assertEqual(TU, TU_BasicN)
+ self.assertNotEqual(TU(3), TU_BasicN)
+
+ WE_Basic3 = BasicNWeekday(2, 3)
+ self.assertEqual(WE(3), WE_Basic3)
+ self.assertNotEqual(WE(2), WE_Basic3)
+
+ def testWeekdayReprNoN(self):
+ no_n_reprs = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU')
+ no_n_wdays = (MO, TU, WE, TH, FR, SA, SU)
+
+ for repstr, wday in zip(no_n_reprs, no_n_wdays):
+ self.assertEqual(repr(wday), repstr)
+
+ def testWeekdayReprWithN(self):
+ with_n_reprs = ('WE(+1)', 'TH(-2)', 'SU(+3)')
+ with_n_wdays = (WE(1), TH(-2), SU(+3))
+
+ for repstr, wday in zip(with_n_reprs, with_n_wdays):
+ self.assertEqual(repr(wday), repstr)
diff --git a/src/dateutil/test/test_tz.py b/src/dateutil/test/test_tz.py
new file mode 100644
index 0000000..e5e4772
--- /dev/null
+++ b/src/dateutil/test/test_tz.py
@@ -0,0 +1,2811 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import PicklableMixin
+from ._common import TZEnvContext, TZWinContext
+from ._common import ComparesEqual
+
+from datetime import datetime, timedelta
+from datetime import time as dt_time
+from datetime import tzinfo
+from six import PY2
+from io import BytesIO, StringIO
+import unittest
+
+import sys
+import base64
+import copy
+import gc
+import weakref
+
+from functools import partial
+
+IS_WIN = sys.platform.startswith('win')
+
+import pytest
+
+# dateutil imports
+from dateutil.relativedelta import relativedelta, SU, TH
+from dateutil.parser import parse
+from dateutil import tz as tz
+from dateutil import zoneinfo
+
+try:
+ from dateutil import tzwin
+except ImportError as e:
+ if IS_WIN:
+ raise e
+ else:
+ pass
+
+MISSING_TARBALL = ("This test fails if you don't have the dateutil "
+ "timezone file installed. Please read the README")
+
+TZFILE_EST5EDT = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
+ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
+S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
+YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
+yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
+wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
+8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
+YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
+BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR
+iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x
+znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H
+cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w
+Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH
+JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM
+jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w
+4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg
+b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8
+o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
+AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
+AEVQVAAAAAABAAAAAQ==
+"""
+
+EUROPE_HELSINKI = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV
+I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM
+VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8
+kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q
+Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK
+46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV
+RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u
+kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ
+czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC
+AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD
+BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME
+AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ==
+"""
+
+NEW_YORK = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
+ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
+S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
+YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
+yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
+wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
+8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
+YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
+BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR
+iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x
+zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H
+gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG
+Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH
+LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV
+yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr
+d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3
+b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8
+fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
+AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
+AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G
+AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A
+AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA
+ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE=
+"""
+
+TZICAL_EST5EDT = """
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:19870101T000000Z
+TZURL:http://zones.stds_r_us.net/tz/US-Eastern
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"""
+
+TZICAL_PST8PDT = """
+BEGIN:VTIMEZONE
+TZID:US-Pacific
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+"""
+
+EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
+EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))
+
+SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6)
+
+
+###
+# Helper functions
+def get_timezone_tuple(dt):
+ """Retrieve a (tzname, utcoffset, dst) tuple for a given DST"""
+ return dt.tzname(), dt.utcoffset(), dt.dst()
+
+
+###
+# Mix-ins
+class context_passthrough(object):
+ def __init__(*args, **kwargs):
+ pass
+
+ def __enter__(*args, **kwargs):
+ pass
+
+ def __exit__(*args, **kwargs):
+ pass
+
+
+class TzFoldMixin(object):
+ """ Mix-in class for testing ambiguous times """
+ def gettz(self, tzname):
+ raise NotImplementedError
+
+ def _get_tzname(self, tzname):
+ return tzname
+
+ def _gettz_context(self, tzname):
+ return context_passthrough()
+
+ def testFoldPositiveUTCOffset(self):
+ # Test that we can resolve ambiguous times
+ tzname = self._get_tzname('Australia/Sydney')
+
+ with self._gettz_context(tzname):
+ SYD = self.gettz(tzname)
+
+ t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.UTC) # AEST
+ t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.UTC) # AEDT
+
+ t0_syd0 = t0_u.astimezone(SYD)
+ t1_syd1 = t1_u.astimezone(SYD)
+
+ self.assertEqual(t0_syd0.replace(tzinfo=None),
+ datetime(2012, 4, 1, 2, 30))
+
+ self.assertEqual(t1_syd1.replace(tzinfo=None),
+ datetime(2012, 4, 1, 2, 30))
+
+ self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11))
+ self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10))
+
+ def testGapPositiveUTCOffset(self):
+ # Test that we don't have a problem around gaps.
+ tzname = self._get_tzname('Australia/Sydney')
+
+ with self._gettz_context(tzname):
+ SYD = self.gettz(tzname)
+
+ t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.UTC) # AEST
+ t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.UTC) # AEDT
+
+ t0 = t0_u.astimezone(SYD)
+ t1 = t1_u.astimezone(SYD)
+
+ self.assertEqual(t0.replace(tzinfo=None),
+ datetime(2012, 10, 7, 1, 30))
+
+ self.assertEqual(t1.replace(tzinfo=None),
+ datetime(2012, 10, 7, 3, 30))
+
+ self.assertEqual(t0.utcoffset(), timedelta(hours=10))
+ self.assertEqual(t1.utcoffset(), timedelta(hours=11))
+
+ def testFoldNegativeUTCOffset(self):
+ # Test that we can resolve ambiguous times
+ tzname = self._get_tzname('America/Toronto')
+
+ with self._gettz_context(tzname):
+ TOR = self.gettz(tzname)
+
+ t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.UTC)
+ t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.UTC)
+
+ t0_tor = t0_u.astimezone(TOR)
+ t1_tor = t1_u.astimezone(TOR)
+
+ self.assertEqual(t0_tor.replace(tzinfo=None),
+ datetime(2011, 11, 6, 1, 30))
+
+ self.assertEqual(t1_tor.replace(tzinfo=None),
+ datetime(2011, 11, 6, 1, 30))
+
+ self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
+ self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
+ self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
+
+ def testGapNegativeUTCOffset(self):
+ # Test that we don't have a problem around gaps.
+ tzname = self._get_tzname('America/Toronto')
+
+ with self._gettz_context(tzname):
+ TOR = self.gettz(tzname)
+
+ t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.UTC)
+ t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.UTC)
+
+ t0 = t0_u.astimezone(TOR)
+ t1 = t1_u.astimezone(TOR)
+
+ self.assertEqual(t0.replace(tzinfo=None),
+ datetime(2011, 3, 13, 1, 30))
+
+ self.assertEqual(t1.replace(tzinfo=None),
+ datetime(2011, 3, 13, 3, 30))
+
+ self.assertNotEqual(t0, t1)
+ self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
+ self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
+
+ def testFoldLondon(self):
+ tzname = self._get_tzname('Europe/London')
+
+ with self._gettz_context(tzname):
+ LON = self.gettz(tzname)
+ UTC = tz.UTC
+
+ t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC) # BST
+ t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC) # GMT
+
+ t0 = t0_u.astimezone(LON)
+ t1 = t1_u.astimezone(LON)
+
+ self.assertEqual(t0.replace(tzinfo=None),
+ datetime(2013, 10, 27, 1, 30))
+
+ self.assertEqual(t1.replace(tzinfo=None),
+ datetime(2013, 10, 27, 1, 30))
+
+ self.assertEqual(t0.utcoffset(), timedelta(hours=1))
+ self.assertEqual(t1.utcoffset(), timedelta(hours=0))
+
+ def testFoldIndependence(self):
+ tzname = self._get_tzname('America/New_York')
+
+ with self._gettz_context(tzname):
+ NYC = self.gettz(tzname)
+ UTC = tz.UTC
+ hour = timedelta(hours=1)
+
+ # Firmly 2015-11-01 0:30 EDT-4
+ pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC)
+
+ # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
+ in_dst = pre_dst + hour
+ in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT
+
+ # Doing the arithmetic in UTC creates a date that is unambiguously
+ # 2015-11-01 1:30 EDT-5
+ in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
+
+ # Make sure the dates are actually ambiguous
+ self.assertEqual(in_dst, in_dst_via_utc)
+
+ # Make sure we got the right folding behavior
+ self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
+
+ # Now check to make sure in_dst's tzname hasn't changed
+ self.assertEqual(in_dst_tzname_0, in_dst.tzname())
+
+ def testInZoneFoldEquality(self):
+ # Two datetimes in the same zone are considered to be equal if their
+ # wall times are equal, even if they have different absolute times.
+
+ tzname = self._get_tzname('America/New_York')
+
+ with self._gettz_context(tzname):
+ NYC = self.gettz(tzname)
+ UTC = tz.UTC
+
+ dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC)
+ dt1 = tz.enfold(dt0, fold=1)
+
+ # Make sure these actually represent different times
+ self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
+
+ # Test that they compare equal
+ self.assertEqual(dt0, dt1)
+
+ def _test_ambiguous_time(self, dt, tzid, ambiguous):
+ # This is a test to check that the individual is_ambiguous values
+ # on the _tzinfo subclasses work.
+ tzname = self._get_tzname(tzid)
+
+ with self._gettz_context(tzname):
+ tzi = self.gettz(tzname)
+
+ self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous)
+
+ def testAmbiguousNegativeUTCOffset(self):
+ self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30),
+ 'America/New_York', True)
+
+ def testAmbiguousPositiveUTCOffset(self):
+ self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30),
+ 'Australia/Sydney', True)
+
+ def testUnambiguousNegativeUTCOffset(self):
+ self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30),
+ 'America/New_York', False)
+
+ def testUnambiguousPositiveUTCOffset(self):
+ self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30),
+ 'Australia/Sydney', False)
+
+ def testUnambiguousGapNegativeUTCOffset(self):
+ # Imaginary time
+ self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30),
+ 'America/New_York', False)
+
+ def testUnambiguousGapPositiveUTCOffset(self):
+ # Imaginary time
+ self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30),
+ 'Australia/Sydney', False)
+
+ def _test_imaginary_time(self, dt, tzid, exists):
+ tzname = self._get_tzname(tzid)
+ with self._gettz_context(tzname):
+ tzi = self.gettz(tzname)
+
+ self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists)
+
+ def testImaginaryNegativeUTCOffset(self):
+ self._test_imaginary_time(datetime(2011, 3, 13, 2, 30),
+ 'America/New_York', False)
+
+ def testNotImaginaryNegativeUTCOffset(self):
+ self._test_imaginary_time(datetime(2011, 3, 13, 1, 30),
+ 'America/New_York', True)
+
+ def testImaginaryPositiveUTCOffset(self):
+ self._test_imaginary_time(datetime(2012, 10, 7, 2, 30),
+ 'Australia/Sydney', False)
+
+ def testNotImaginaryPositiveUTCOffset(self):
+ self._test_imaginary_time(datetime(2012, 10, 7, 1, 30),
+ 'Australia/Sydney', True)
+
+ def testNotImaginaryFoldNegativeUTCOffset(self):
+ self._test_imaginary_time(datetime(2015, 11, 1, 1, 30),
+ 'America/New_York', True)
+
+ def testNotImaginaryFoldPositiveUTCOffset(self):
+ self._test_imaginary_time(datetime(2012, 4, 1, 3, 30),
+ 'Australia/Sydney', True)
+
+ @unittest.skip("Known failure in Python 3.6.")
+ def testEqualAmbiguousComparison(self):
+ tzname = self._get_tzname('Australia/Sydney')
+
+ with self._gettz_context(tzname):
+ SYD0 = self.gettz(tzname)
+ SYD1 = self.gettz(tzname)
+
+ t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.UTC) # AEST
+
+ t0_syd0 = t0_u.astimezone(SYD0)
+ t0_syd1 = t0_u.astimezone(SYD1)
+
+ # This is considered an "inter-zone comparison" because it's an
+ # ambiguous datetime.
+ self.assertEqual(t0_syd0, t0_syd1)
+
+
+class TzWinFoldMixin(object):
+ def get_args(self, tzname):
+ return (tzname, )
+
+ class context(object):
+ def __init__(*args, **kwargs):
+ pass
+
+ def __enter__(*args, **kwargs):
+ pass
+
+ def __exit__(*args, **kwargs):
+ pass
+
+ def get_utc_transitions(self, tzi, year, gap):
+ dston, dstoff = tzi.transitions(year)
+ if gap:
+ t_n = dston - timedelta(minutes=30)
+
+ t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC)
+ t1_u = t0_u + timedelta(hours=1)
+ else:
+ # Get 1 hour before the first ambiguous date
+ t_n = dstoff - timedelta(minutes=30)
+
+ t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC)
+ t_n += timedelta(hours=1) # Naive ambiguous date
+ t0_u = t0_u + timedelta(hours=1) # First ambiguous date
+ t1_u = t0_u + timedelta(hours=1) # Second ambiguous date
+
+ return t_n, t0_u, t1_u
+
+ def testFoldPositiveUTCOffset(self):
+ # Test that we can resolve ambiguous times
+ tzname = 'AUS Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ # Calling fromutc() alters the tzfile object
+ SYD = self.tzclass(*args)
+
+ # Get the transition time in UTC from the object, because
+ # Windows doesn't store historical info
+ t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False)
+
+ # Using fresh tzfiles
+ t0_syd = t0_u.astimezone(SYD)
+ t1_syd = t1_u.astimezone(SYD)
+
+ self.assertEqual(t0_syd.replace(tzinfo=None), t_n)
+
+ self.assertEqual(t1_syd.replace(tzinfo=None), t_n)
+
+ self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11))
+ self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10))
+ self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname())
+
+ def testGapPositiveUTCOffset(self):
+ # Test that we don't have a problem around gaps.
+ tzname = 'AUS Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ SYD = self.tzclass(*args)
+
+ t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True)
+
+ t0 = t0_u.astimezone(SYD)
+ t1 = t1_u.astimezone(SYD)
+
+ self.assertEqual(t0.replace(tzinfo=None), t_n)
+
+ self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2))
+
+ self.assertEqual(t0.utcoffset(), timedelta(hours=10))
+ self.assertEqual(t1.utcoffset(), timedelta(hours=11))
+
+ def testFoldNegativeUTCOffset(self):
+ # Test that we can resolve ambiguous times
+ tzname = 'Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ TOR = self.tzclass(*args)
+
+ t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False)
+
+ t0_tor = t0_u.astimezone(TOR)
+ t1_tor = t1_u.astimezone(TOR)
+
+ self.assertEqual(t0_tor.replace(tzinfo=None), t_n)
+ self.assertEqual(t1_tor.replace(tzinfo=None), t_n)
+
+ self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
+ self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
+ self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
+
+ def testGapNegativeUTCOffset(self):
+ # Test that we don't have a problem around gaps.
+ tzname = 'Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ TOR = self.tzclass(*args)
+
+ t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True)
+
+ t0 = t0_u.astimezone(TOR)
+ t1 = t1_u.astimezone(TOR)
+
+ self.assertEqual(t0.replace(tzinfo=None),
+ t_n)
+
+ self.assertEqual(t1.replace(tzinfo=None),
+ t_n + timedelta(hours=2))
+
+ self.assertNotEqual(t0.tzname(), t1.tzname())
+ self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
+ self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
+
+ def testFoldIndependence(self):
+ tzname = 'Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ NYC = self.tzclass(*args)
+ UTC = tz.UTC
+ hour = timedelta(hours=1)
+
+ # Firmly 2015-11-01 0:30 EDT-4
+ t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False)
+
+ pre_dst = (t_n - hour).replace(tzinfo=NYC)
+
+ # Currently, there's no way around the fact that this resolves to an
+ # ambiguous date, which defaults to EST. I'm not hard-coding in the
+ # answer, though, because the preferred behavior would be that this
+ # results in a time on the EDT side.
+
+ # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
+ in_dst = pre_dst + hour
+ in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT
+
+ # Doing the arithmetic in UTC creates a date that is unambiguously
+ # 2015-11-01 1:30 EDT-5
+ in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
+
+ # Make sure we got the right folding behavior
+ self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
+
+ # Now check to make sure in_dst's tzname hasn't changed
+ self.assertEqual(in_dst_tzname_0, in_dst.tzname())
+
+ def testInZoneFoldEquality(self):
+ # Two datetimes in the same zone are considered to be equal if their
+ # wall times are equal, even if they have different absolute times.
+ tzname = 'Eastern Standard Time'
+ args = self.get_args(tzname)
+
+ with self.context(tzname):
+ NYC = self.tzclass(*args)
+ UTC = tz.UTC
+
+ t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False)
+
+ dt0 = t_n.replace(tzinfo=NYC)
+ dt1 = tz.enfold(dt0, fold=1)
+
+ # Make sure these actually represent different times
+ self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
+
+ # Test that they compare equal
+ self.assertEqual(dt0, dt1)
+
+###
+# Test Cases
+class TzUTCTest(unittest.TestCase):
+ def testSingleton(self):
+ UTC_0 = tz.tzutc()
+ UTC_1 = tz.tzutc()
+
+ self.assertIs(UTC_0, UTC_1)
+
+ def testOffset(self):
+ ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+
+ self.assertEqual(ct.utcoffset(), timedelta(seconds=0))
+
+ def testDst(self):
+ ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+
+ self.assertEqual(ct.dst(), timedelta(seconds=0))
+
+ def testTzName(self):
+ ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+ self.assertEqual(ct.tzname(), 'UTC')
+
+ def testEquality(self):
+ UTC0 = tz.tzutc()
+ UTC1 = tz.tzutc()
+
+ self.assertEqual(UTC0, UTC1)
+
+ def testInequality(self):
+ UTC = tz.tzutc()
+ UTCp4 = tz.tzoffset('UTC+4', 14400)
+
+ self.assertNotEqual(UTC, UTCp4)
+
+ def testInequalityInteger(self):
+ self.assertFalse(tz.tzutc() == 7)
+ self.assertNotEqual(tz.tzutc(), 7)
+
+ def testInequalityUnsupported(self):
+ self.assertEqual(tz.tzutc(), ComparesEqual)
+
+ def testRepr(self):
+ UTC = tz.tzutc()
+ self.assertEqual(repr(UTC), 'tzutc()')
+
+ def testTimeOnlyUTC(self):
+ # https://github.com/dateutil/dateutil/issues/132
+ # tzutc doesn't care
+ tz_utc = tz.tzutc()
+ self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(),
+ timedelta(0))
+
+ def testAmbiguity(self):
+ # Pick an arbitrary datetime, this should always return False.
+ dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc())
+
+ self.assertFalse(tz.datetime_ambiguous(dt))
+
+
+@pytest.mark.tzoffset
+class TzOffsetTest(unittest.TestCase):
+ def testTimedeltaOffset(self):
+ est = tz.tzoffset('EST', timedelta(hours=-5))
+ est_s = tz.tzoffset('EST', -18000)
+
+ self.assertEqual(est, est_s)
+
+ def testTzNameNone(self):
+ gmt5 = tz.tzoffset(None, -18000) # -5:00
+ self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(),
+ None)
+
+ def testTimeOnlyOffset(self):
+ # tzoffset doesn't care
+ tz_offset = tz.tzoffset('+3', 3600)
+ self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(),
+ timedelta(seconds=3600))
+
+ def testTzOffsetRepr(self):
+ tname = 'EST'
+ tzo = tz.tzoffset(tname, -5 * 3600)
+ self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)")
+
+ def testEquality(self):
+ utc = tz.tzoffset('UTC', 0)
+ gmt = tz.tzoffset('GMT', 0)
+
+ self.assertEqual(utc, gmt)
+
+ def testUTCEquality(self):
+ utc = tz.UTC
+ o_utc = tz.tzoffset('UTC', 0)
+
+ self.assertEqual(utc, o_utc)
+ self.assertEqual(o_utc, utc)
+
+ def testInequalityInvalid(self):
+ tzo = tz.tzoffset('-3', -3 * 3600)
+ self.assertFalse(tzo == -3)
+ self.assertNotEqual(tzo, -3)
+
+ def testInequalityUnsupported(self):
+ tzo = tz.tzoffset('-5', -5 * 3600)
+
+ self.assertTrue(tzo == ComparesEqual)
+ self.assertFalse(tzo != ComparesEqual)
+ self.assertEqual(tzo, ComparesEqual)
+
+ def testAmbiguity(self):
+ # Pick an arbitrary datetime, this should always return False.
+ dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600))
+
+ self.assertFalse(tz.datetime_ambiguous(dt))
+
+ def testTzOffsetInstance(self):
+ tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5))
+ tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5))
+
+ assert tz1 is not tz2
+
+ def testTzOffsetSingletonDifferent(self):
+ tz1 = tz.tzoffset('EST', timedelta(hours=-5))
+ tz2 = tz.tzoffset('EST', -18000)
+
+ assert tz1 is tz2
+
+
+@pytest.mark.smoke
+@pytest.mark.tzoffset
+def test_tzoffset_weakref():
+ UTC1 = tz.tzoffset('UTC', 0)
+ UTC_ref = weakref.ref(tz.tzoffset('UTC', 0))
+ UTC1 is UTC_ref()
+ del UTC1
+ gc.collect()
+
+ assert UTC_ref() is not None # Should be in the strong cache
+ assert UTC_ref() is tz.tzoffset('UTC', 0)
+
+ # Fill the strong cache with other items
+ for offset in range(5,15):
+ tz.tzoffset('RandomZone', offset)
+
+ gc.collect()
+ assert UTC_ref() is None
+ assert UTC_ref() is not tz.tzoffset('UTC', 0)
+
+
+@pytest.mark.tzoffset
+@pytest.mark.parametrize('args', [
+ ('UTC', 0),
+ ('EST', -18000),
+ ('EST', timedelta(hours=-5)),
+ (None, timedelta(hours=3)),
+])
+def test_tzoffset_singleton(args):
+ tz1 = tz.tzoffset(*args)
+ tz2 = tz.tzoffset(*args)
+
+ assert tz1 is tz2
+
+
+@pytest.mark.tzoffset
+@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets not supported')
+def test_tzoffset_sub_minute():
+ delta = timedelta(hours=12, seconds=30)
+ test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
+ assert test_datetime.utcoffset() == delta
+
+
+@pytest.mark.tzoffset
+@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets supported')
+def test_tzoffset_sub_minute_rounding():
+ delta = timedelta(hours=12, seconds=30)
+ test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
+ assert test_date.utcoffset() == timedelta(hours=12, minutes=1)
+
+
+@pytest.mark.tzlocal
+class TzLocalTest(unittest.TestCase):
+ def testEquality(self):
+ tz1 = tz.tzlocal()
+ tz2 = tz.tzlocal()
+
+ # Explicitly calling == and != here to ensure the operators work
+ self.assertTrue(tz1 == tz2)
+ self.assertFalse(tz1 != tz2)
+
+ def testInequalityFixedOffset(self):
+ tzl = tz.tzlocal()
+ tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds())
+ tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds())
+
+ self.assertFalse(tzl == tzos)
+ self.assertFalse(tzl == tzod)
+ self.assertTrue(tzl != tzos)
+ self.assertTrue(tzl != tzod)
+
+ def testInequalityInvalid(self):
+ tzl = tz.tzlocal()
+
+ self.assertTrue(tzl != 1)
+ self.assertFalse(tzl == 1)
+
+ # TODO: Use some sort of universal local mocking so that it's clear
+ # that we're expecting tzlocal to *not* be Pacific/Kiritimati
+ LINT = tz.gettz('Pacific/Kiritimati')
+ self.assertTrue(tzl != LINT)
+ self.assertFalse(tzl == LINT)
+
+ def testInequalityUnsupported(self):
+ tzl = tz.tzlocal()
+
+ self.assertTrue(tzl == ComparesEqual)
+ self.assertFalse(tzl != ComparesEqual)
+
+ def testRepr(self):
+ tzl = tz.tzlocal()
+
+ self.assertEqual(repr(tzl), 'tzlocal()')
+
+
+@pytest.mark.parametrize('args,kwargs', [
+ (('EST', -18000), {}),
+ (('EST', timedelta(hours=-5)), {}),
+ (('EST',), {'offset': -18000}),
+ (('EST',), {'offset': timedelta(hours=-5)}),
+ (tuple(), {'name': 'EST', 'offset': -18000})
+])
+def test_tzoffset_is(args, kwargs):
+ tz_ref = tz.tzoffset('EST', -18000)
+ assert tz.tzoffset(*args, **kwargs) is tz_ref
+
+
+def test_tzoffset_is_not():
+ assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000)
+
+
+@pytest.mark.tzlocal
+@unittest.skipIf(IS_WIN, "requires Unix")
+class TzLocalNixTest(unittest.TestCase, TzFoldMixin):
+ # This is a set of tests for `tzlocal()` on *nix systems
+
+ # POSIX string indicating change to summer time on the 2nd Sunday in March
+ # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
+ TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
+
+ # POSIX string for AEST/AEDT (valid >= 2008)
+ TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
+
+ # POSIX string for BST/GMT
+ TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
+ # POSIX string for UTC
+ UTC = 'UTC'
+
+ def gettz(self, tzname):
+ # Actual time zone changes are handled by the _gettz_context function
+ return tz.tzlocal()
+
+ def _gettz_context(self, tzname):
+ tzname_map = {'Australia/Sydney': self.TZ_AEST,
+ 'America/Toronto': self.TZ_EST,
+ 'America/New_York': self.TZ_EST,
+ 'Europe/London': self.TZ_LON}
+
+ return TZEnvContext(tzname_map.get(tzname, tzname))
+
+ def _testTzFunc(self, tzval, func, std_val, dst_val):
+ """
+ This generates tests about how the behavior of a function ``func``
+ changes between STD and DST (e.g. utcoffset, tzname, dst).
+
+ It assume that DST starts the 2nd Sunday in March and ends the 1st
+ Sunday in November
+ """
+ with TZEnvContext(tzval):
+ dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal()) # STD
+ dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal()) # DST
+
+ self.assertEqual(func(dt1), std_val)
+ self.assertEqual(func(dt2), dst_val)
+
+ def _testTzName(self, tzval, std_name, dst_name):
+ func = datetime.tzname
+
+ self._testTzFunc(tzval, func, std_name, dst_name)
+
+ def testTzNameDST(self):
+ # Test tzname in a zone with DST
+ self._testTzName(self.TZ_EST, 'EST', 'EDT')
+
+ def testTzNameUTC(self):
+ # Test tzname in a zone without DST
+ self._testTzName(self.UTC, 'UTC', 'UTC')
+
+ def _testOffset(self, tzval, std_off, dst_off):
+ func = datetime.utcoffset
+
+ self._testTzFunc(tzval, func, std_off, dst_off)
+
+ def testOffsetDST(self):
+ self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4))
+
+ def testOffsetUTC(self):
+ self._testOffset(self.UTC, timedelta(0), timedelta(0))
+
+ def _testDST(self, tzval, dst_dst):
+ func = datetime.dst
+ std_dst = timedelta(0)
+
+ self._testTzFunc(tzval, func, std_dst, dst_dst)
+
+ def testDSTDST(self):
+ self._testDST(self.TZ_EST, timedelta(hours=1))
+
+ def testDSTUTC(self):
+ self._testDST(self.UTC, timedelta(0))
+
+ def testTimeOnlyOffsetLocalUTC(self):
+ with TZEnvContext(self.UTC):
+ self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
+ timedelta(0))
+
+ def testTimeOnlyOffsetLocalDST(self):
+ with TZEnvContext(self.TZ_EST):
+ self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
+ None)
+
+ def testTimeOnlyDSTLocalUTC(self):
+ with TZEnvContext(self.UTC):
+ self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
+ timedelta(0))
+
+ def testTimeOnlyDSTLocalDST(self):
+ with TZEnvContext(self.TZ_EST):
+ self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
+ None)
+
+ def testUTCEquality(self):
+ with TZEnvContext(self.UTC):
+ assert tz.tzlocal() == tz.UTC
+
+
+# TODO: Maybe a better hack than this?
+def mark_tzlocal_nix(f):
+ marks = [
+ pytest.mark.tzlocal,
+ pytest.mark.skipif(IS_WIN, reason='requires Unix'),
+ ]
+
+ for mark in reversed(marks):
+ f = mark(f)
+
+ return f
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0'])
+def test_tzlocal_utc_equal(tzvar):
+ with TZEnvContext(tzvar):
+ assert tz.tzlocal() == tz.UTC
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar', [
+ 'Europe/London', 'America/New_York',
+ 'GMT0BST', 'EST5EDT'])
+def test_tzlocal_utc_unequal(tzvar):
+ with TZEnvContext(tzvar):
+ assert tz.tzlocal() != tz.UTC
+
+
+@mark_tzlocal_nix
+def test_tzlocal_local_time_trim_colon():
+ with TZEnvContext(':/etc/localtime'):
+ assert tz.gettz() is not None
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar, tzoff', [
+ ('EST5', tz.tzoffset('EST', -18000)),
+ ('GMT0', tz.tzoffset('GMT', 0)),
+ ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))),
+ ('JST-9', tz.tzoffset('JST', timedelta(hours=9))),
+])
+def test_tzlocal_offset_equal(tzvar, tzoff):
+ with TZEnvContext(tzvar):
+ # Including both to test both __eq__ and __ne__
+ assert tz.tzlocal() == tzoff
+ assert not (tz.tzlocal() != tzoff)
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar, tzoff', [
+ ('EST5EDT', tz.tzoffset('EST', -18000)),
+ ('GMT0BST', tz.tzoffset('GMT', 0)),
+ ('EST5', tz.tzoffset('EST', -14400)),
+ ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))),
+ ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))),
+])
+def test_tzlocal_offset_unequal(tzvar, tzoff):
+ with TZEnvContext(tzvar):
+ # Including both to test both __eq__ and __ne__
+ assert tz.tzlocal() != tzoff
+ assert not (tz.tzlocal() == tzoff)
+
+
+@pytest.mark.gettz
+class GettzTest(unittest.TestCase, TzFoldMixin):
+ gettz = staticmethod(tz.gettz)
+
+ def testGettz(self):
+ # bug 892569
+ str(self.gettz('UTC'))
+
+ def testGetTzEquality(self):
+ self.assertEqual(self.gettz('UTC'), self.gettz('UTC'))
+
+ def testTimeOnlyGettz(self):
+ # gettz returns None
+ tz_get = self.gettz('Europe/Minsk')
+ self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None)
+
+ def testTimeOnlyGettzDST(self):
+ # gettz returns None
+ tz_get = self.gettz('Europe/Minsk')
+ self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None)
+
+ def testTimeOnlyGettzTzName(self):
+ tz_get = self.gettz('Europe/Minsk')
+ self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None)
+
+ def testTimeOnlyFormatZ(self):
+ tz_get = self.gettz('Europe/Minsk')
+ t = dt_time(13, 20, tzinfo=tz_get)
+
+ self.assertEqual(t.strftime('%H%M%Z'), '1320')
+
+ def testPortugalDST(self):
+ # In 1996, Portugal changed from CET to WET
+ PORTUGAL = self.gettz('Portugal')
+
+ t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL)
+
+ self.assertEqual(t_cet.tzname(), 'CET')
+ self.assertEqual(t_cet.utcoffset(), timedelta(hours=1))
+ self.assertEqual(t_cet.dst(), timedelta(0))
+
+ t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL)
+
+ self.assertEqual(t_west.tzname(), 'WEST')
+ self.assertEqual(t_west.utcoffset(), timedelta(hours=1))
+ self.assertEqual(t_west.dst(), timedelta(hours=1))
+
+ def testGettzCacheTzFile(self):
+ NYC1 = tz.gettz('America/New_York')
+ NYC2 = tz.gettz('America/New_York')
+
+ assert NYC1 is NYC2
+
+ def testGettzCacheTzLocal(self):
+ local1 = tz.gettz()
+ local2 = tz.gettz()
+
+ assert local1 is not local2
+
+
+@pytest.mark.gettz
+def test_gettz_same_result_for_none_and_empty_string():
+ local_from_none = tz.gettz()
+ local_from_empty_string = tz.gettz("")
+ assert local_from_none is not None
+ assert local_from_empty_string is not None
+ assert local_from_none == local_from_empty_string
+
+
+@pytest.mark.gettz
+@pytest.mark.parametrize('badzone', [
+ 'Fake.Region/Abcdefghijklmnop', # Violates several tz project name rules
+])
+def test_gettz_badzone(badzone):
+ # Make sure passing a bad TZ string to gettz returns None (GH #800)
+ tzi = tz.gettz(badzone)
+ assert tzi is None
+
+
+@pytest.mark.gettz
+def test_gettz_badzone_unicode():
+ # Make sure a unicode string can be passed to TZ (GH #802)
+ # When fixed, combine this with test_gettz_badzone
+ tzi = tz.gettz('🐼')
+ assert tzi is None
+
+
+@pytest.mark.gettz
+@pytest.mark.parametrize(
+ "badzone,exc_reason",
+ [
+ pytest.param(
+ b"America/New_York",
+ ".*should be str, not bytes.*",
+ id="bytes on Python 3",
+ marks=[
+ pytest.mark.skipif(
+ PY2, reason="bytes arguments accepted in Python 2"
+ )
+ ],
+ ),
+ pytest.param(
+ object(),
+ None,
+ id="no startswith()",
+ marks=[
+ pytest.mark.xfail(reason="AttributeError instead of TypeError",
+ raises=AttributeError),
+ ],
+ ),
+ ],
+)
+def test_gettz_zone_wrong_type(badzone, exc_reason):
+ with pytest.raises(TypeError, match=exc_reason):
+ tz.gettz(badzone)
+
+
+@pytest.mark.gettz
+@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
+def test_gettz_cache_clear():
+ NYC1 = tz.gettz('America/New_York')
+ tz.gettz.cache_clear()
+
+ NYC2 = tz.gettz('America/New_York')
+
+ assert NYC1 is not NYC2
+
+@pytest.mark.gettz
+@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
+def test_gettz_set_cache_size():
+ tz.gettz.cache_clear()
+ tz.gettz.set_cache_size(3)
+
+ MONACO_ref = weakref.ref(tz.gettz('Europe/Monaco'))
+ EASTER_ref = weakref.ref(tz.gettz('Pacific/Easter'))
+ CURRIE_ref = weakref.ref(tz.gettz('Australia/Currie'))
+
+ gc.collect()
+
+ assert MONACO_ref() is not None
+ assert EASTER_ref() is not None
+ assert CURRIE_ref() is not None
+
+ tz.gettz.set_cache_size(2)
+ gc.collect()
+
+ assert MONACO_ref() is None
+
+@pytest.mark.xfail(IS_WIN, reason="Windows does not use system zoneinfo")
+@pytest.mark.smoke
+@pytest.mark.gettz
+def test_gettz_weakref():
+ tz.gettz.cache_clear()
+ tz.gettz.set_cache_size(2)
+ NYC1 = tz.gettz('America/New_York')
+ NYC_ref = weakref.ref(tz.gettz('America/New_York'))
+
+ assert NYC1 is NYC_ref()
+
+ del NYC1
+ gc.collect()
+
+ assert NYC_ref() is not None # Should still be in the strong cache
+ assert tz.gettz('America/New_York') is NYC_ref()
+
+ # Populate strong cache with other timezones
+ tz.gettz('Europe/Monaco')
+ tz.gettz('Pacific/Easter')
+ tz.gettz('Australia/Currie')
+
+ gc.collect()
+ assert NYC_ref() is None # Should have been pushed out
+ assert tz.gettz('America/New_York') is not NYC_ref()
+
+class ZoneInfoGettzTest(GettzTest):
+ def gettz(self, name):
+ zoneinfo_file = zoneinfo.get_zonefile_instance()
+ return zoneinfo_file.get(name)
+
+ def testZoneInfoFileStart1(self):
+ tz = self.gettz("EST5EDT")
+ self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST",
+ MISSING_TARBALL)
+ self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT")
+
+ def testZoneInfoFileEnd1(self):
+ tzc = self.gettz("EST5EDT")
+ self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
+ "EDT", MISSING_TARBALL)
+
+ end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1)
+ self.assertEqual(end_est.tzname(), "EST")
+
+ def testZoneInfoOffsetSignal(self):
+ utc = self.gettz("UTC")
+ nyc = self.gettz("America/New_York")
+ self.assertNotEqual(utc, None, MISSING_TARBALL)
+ self.assertNotEqual(nyc, None)
+ t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc)
+ t1 = t0.astimezone(utc)
+ t2 = t1.astimezone(nyc)
+ self.assertEqual(t0, t2)
+ self.assertEqual(nyc.dst(t0), timedelta(hours=1))
+
+ def testZoneInfoCopy(self):
+ # copy.copy() called on a ZoneInfo file was returning the same instance
+ CHI = self.gettz('America/Chicago')
+ CHI_COPY = copy.copy(CHI)
+
+ self.assertIsNot(CHI, CHI_COPY)
+ self.assertEqual(CHI, CHI_COPY)
+
+ def testZoneInfoDeepCopy(self):
+ CHI = self.gettz('America/Chicago')
+ CHI_COPY = copy.deepcopy(CHI)
+
+ self.assertIsNot(CHI, CHI_COPY)
+ self.assertEqual(CHI, CHI_COPY)
+
+ def testZoneInfoInstanceCaching(self):
+ zif_0 = zoneinfo.get_zonefile_instance()
+ zif_1 = zoneinfo.get_zonefile_instance()
+
+ self.assertIs(zif_0, zif_1)
+
+ def testZoneInfoNewInstance(self):
+ zif_0 = zoneinfo.get_zonefile_instance()
+ zif_1 = zoneinfo.get_zonefile_instance(new_instance=True)
+ zif_2 = zoneinfo.get_zonefile_instance()
+
+ self.assertIsNot(zif_0, zif_1)
+ self.assertIs(zif_1, zif_2)
+
+ def testZoneInfoDeprecated(self):
+ with pytest.warns(DeprecationWarning):
+ zoneinfo.gettz('US/Eastern')
+
+ def testZoneInfoMetadataDeprecated(self):
+ with pytest.warns(DeprecationWarning):
+ zoneinfo.gettz_db_metadata()
+
+
+class TZRangeTest(unittest.TestCase, TzFoldMixin):
+ TZ_EST = tz.tzrange('EST', timedelta(hours=-5),
+ 'EDT', timedelta(hours=-4),
+ start=relativedelta(month=3, day=1, hour=2,
+ weekday=SU(+2)),
+ end=relativedelta(month=11, day=1, hour=1,
+ weekday=SU(+1)))
+
+ TZ_AEST = tz.tzrange('AEST', timedelta(hours=10),
+ 'AEDT', timedelta(hours=11),
+ start=relativedelta(month=10, day=1, hour=2,
+ weekday=SU(+1)),
+ end=relativedelta(month=4, day=1, hour=2,
+ weekday=SU(+1)))
+
+ TZ_LON = tz.tzrange('GMT', timedelta(hours=0),
+ 'BST', timedelta(hours=1),
+ start=relativedelta(month=3, day=31, weekday=SU(-1),
+ hours=2),
+ end=relativedelta(month=10, day=31, weekday=SU(-1),
+ hours=1))
+ # POSIX string for UTC
+ UTC = 'UTC'
+
+ def gettz(self, tzname):
+ tzname_map = {'Australia/Sydney': self.TZ_AEST,
+ 'America/Toronto': self.TZ_EST,
+ 'America/New_York': self.TZ_EST,
+ 'Europe/London': self.TZ_LON}
+
+ return tzname_map[tzname]
+
+ def testRangeCmp1(self):
+ self.assertEqual(tz.tzstr("EST5EDT"),
+ tz.tzrange("EST", -18000, "EDT", -14400,
+ relativedelta(hours=+2,
+ month=4, day=1,
+ weekday=SU(+1)),
+ relativedelta(hours=+1,
+ month=10, day=31,
+ weekday=SU(-1))))
+
+ def testRangeCmp2(self):
+ self.assertEqual(tz.tzstr("EST5EDT"),
+ tz.tzrange("EST", -18000, "EDT"))
+
+ def testRangeOffsets(self):
+ TZR = tz.tzrange('EST', -18000, 'EDT', -14400,
+ start=relativedelta(hours=2, month=4, day=1,
+ weekday=SU(+2)),
+ end=relativedelta(hours=1, month=10, day=31,
+ weekday=SU(-1)))
+
+ dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR) # STD
+ dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR) # DST
+
+ dst_zero = timedelta(0)
+ dst_hour = timedelta(hours=1)
+
+ std_offset = timedelta(hours=-5)
+ dst_offset = timedelta(hours=-4)
+
+ # Check dst()
+ self.assertEqual(dt_std.dst(), dst_zero)
+ self.assertEqual(dt_dst.dst(), dst_hour)
+
+ # Check utcoffset()
+ self.assertEqual(dt_std.utcoffset(), std_offset)
+ self.assertEqual(dt_dst.utcoffset(), dst_offset)
+
+ # Check tzname
+ self.assertEqual(dt_std.tzname(), 'EST')
+ self.assertEqual(dt_dst.tzname(), 'EDT')
+
+ def testTimeOnlyRangeFixed(self):
+ # This is a fixed-offset zone, so tzrange allows this
+ tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3))
+ self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(),
+ timedelta(hours=-3))
+
+ def testTimeOnlyRange(self):
+ # tzrange returns None because this zone has DST
+ tz_range = tz.tzrange('EST', timedelta(hours=-5),
+ 'EDT', timedelta(hours=-4))
+ self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None)
+
+ def testBrokenIsDstHandling(self):
+ # tzrange._isdst() was using a date() rather than a datetime().
+ # Issue reported by Lennart Regebro.
+ dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.UTC)
+ self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")),
+ datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
+
+ def testRangeTimeDelta(self):
+ # Test that tzrange can be specified with a timedelta instead of an int.
+ EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5),
+ 'EDT', timedelta(hours=-4))
+
+ EST5EDT_sec = tz.tzrange('EST', -18000,
+ 'EDT', -14400)
+
+ self.assertEqual(EST5EDT_td, EST5EDT_sec)
+
+ def testRangeEquality(self):
+ TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400)
+
+ # Standard abbreviation different
+ TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400)
+ self.assertNotEqual(TZR1, TZR2)
+
+ # DST abbreviation different
+ TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400)
+ self.assertNotEqual(TZR1, TZR3)
+
+ # STD offset different
+ TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400)
+ self.assertNotEqual(TZR1, TZR4)
+
+ # DST offset different
+ TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000)
+ self.assertNotEqual(TZR1, TZR5)
+
+ # Start delta different
+ TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400,
+ start=relativedelta(hours=+1, month=3,
+ day=1, weekday=SU(+2)))
+ self.assertNotEqual(TZR1, TZR6)
+
+ # End delta different
+ TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400,
+ end=relativedelta(hours=+1, month=11,
+ day=1, weekday=SU(+2)))
+ self.assertNotEqual(TZR1, TZR7)
+
+ def testRangeInequalityUnsupported(self):
+ TZR = tz.tzrange('EST', -18000, 'EDT', -14400)
+
+ self.assertFalse(TZR == 4)
+ self.assertTrue(TZR == ComparesEqual)
+ self.assertFalse(TZR != ComparesEqual)
+
+
+@pytest.mark.tzstr
+class TZStrTest(unittest.TestCase, TzFoldMixin):
+ # POSIX string indicating change to summer time on the 2nd Sunday in March
+ # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
+ TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
+
+ # POSIX string for AEST/AEDT (valid >= 2008)
+ TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
+
+ # POSIX string for GMT/BST
+ TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
+ def gettz(self, tzname):
+ # Actual time zone changes are handled by the _gettz_context function
+ tzname_map = {'Australia/Sydney': self.TZ_AEST,
+ 'America/Toronto': self.TZ_EST,
+ 'America/New_York': self.TZ_EST,
+ 'Europe/London': self.TZ_LON}
+
+ return tz.tzstr(tzname_map[tzname])
+
+ def testStrStr(self):
+ # Test that tz.tzstr() won't throw an error if given a str instead
+ # of a unicode literal.
+ self.assertEqual(datetime(2003, 4, 6, 1, 59,
+ tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST")
+ self.assertEqual(datetime(2003, 4, 6, 2, 00,
+ tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT")
+
+ def testStrInequality(self):
+ TZS1 = tz.tzstr('EST5EDT4')
+
+ # Standard abbreviation different
+ TZS2 = tz.tzstr('ET5EDT4')
+ self.assertNotEqual(TZS1, TZS2)
+
+ # DST abbreviation different
+ TZS3 = tz.tzstr('EST5EMT')
+ self.assertNotEqual(TZS1, TZS3)
+
+ # STD offset different
+ TZS4 = tz.tzstr('EST4EDT4')
+ self.assertNotEqual(TZS1, TZS4)
+
+ # DST offset different
+ TZS5 = tz.tzstr('EST5EDT3')
+ self.assertNotEqual(TZS1, TZS5)
+
+ def testStrInequalityStartEnd(self):
+ TZS1 = tz.tzstr('EST5EDT4')
+
+ # Start delta different
+ TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00')
+ self.assertNotEqual(TZS1, TZS2)
+
+ # End delta different
+ TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00')
+ self.assertNotEqual(TZS1, TZS3)
+
+ def testPosixOffset(self):
+ TZ1 = tz.tzstr('UTC-3')
+ self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(),
+ timedelta(hours=-3))
+
+ TZ2 = tz.tzstr('UTC-3', posix_offset=True)
+ self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(),
+ timedelta(hours=+3))
+
+ def testStrInequalityUnsupported(self):
+ TZS = tz.tzstr('EST5EDT')
+
+ self.assertFalse(TZS == 4)
+ self.assertTrue(TZS == ComparesEqual)
+ self.assertFalse(TZS != ComparesEqual)
+
+ def testTzStrRepr(self):
+ TZS1 = tz.tzstr('EST5EDT4')
+ TZS2 = tz.tzstr('EST')
+
+ self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")")
+ self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")")
+
+ def testTzStrFailure(self):
+ with self.assertRaises(ValueError):
+ tz.tzstr('InvalidString;439999')
+
+ def testTzStrSingleton(self):
+ tz1 = tz.tzstr('EST5EDT')
+ tz2 = tz.tzstr('CST4CST')
+ tz3 = tz.tzstr('EST5EDT')
+
+ self.assertIsNot(tz1, tz2)
+ self.assertIs(tz1, tz3)
+
+ def testTzStrSingletonPosix(self):
+ tz_t1 = tz.tzstr('GMT+3', posix_offset=True)
+ tz_f1 = tz.tzstr('GMT+3', posix_offset=False)
+
+ tz_t2 = tz.tzstr('GMT+3', posix_offset=True)
+ tz_f2 = tz.tzstr('GMT+3', posix_offset=False)
+
+ self.assertIs(tz_t1, tz_t2)
+ self.assertIsNot(tz_t1, tz_f1)
+
+ self.assertIs(tz_f1, tz_f2)
+
+ def testTzStrInstance(self):
+ tz1 = tz.tzstr('EST5EDT')
+ tz2 = tz.tzstr.instance('EST5EDT')
+ tz3 = tz.tzstr.instance('EST5EDT')
+
+ assert tz1 is not tz2
+ assert tz2 is not tz3
+
+ # Ensure that these still are all the same zone
+ assert tz1 == tz2 == tz3
+
+
+@pytest.mark.smoke
+@pytest.mark.tzstr
+def test_tzstr_weakref():
+ tz_t1 = tz.tzstr('EST5EDT')
+ tz_t2_ref = weakref.ref(tz.tzstr('EST5EDT'))
+ assert tz_t1 is tz_t2_ref()
+
+ del tz_t1
+ gc.collect()
+
+ assert tz_t2_ref() is not None
+ assert tz.tzstr('EST5EDT') is tz_t2_ref()
+
+ for offset in range(5,15):
+ tz.tzstr('GMT+{}'.format(offset))
+ gc.collect()
+
+ assert tz_t2_ref() is None
+ assert tz.tzstr('EST5EDT') is not tz_t2_ref()
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str,expected', [
+ # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+ ('', tz.tzrange(None)), # TODO: Should change this so tz.tzrange('') works
+ ('EST+5EDT,M3.2.0/2,M11.1.0/12',
+ tz.tzrange('EST', -18000, 'EDT', -14400,
+ start=relativedelta(month=3, day=1, weekday=SU(2), hours=2),
+ end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))),
+ ('WART4WARST,J1/0,J365/25', # This is DST all year, Western Argentina Summer Time
+ tz.tzrange('WART', timedelta(hours=-4), 'WARST',
+ start=relativedelta(month=1, day=1, hours=0),
+ end=relativedelta(month=12, day=31, days=1))),
+ ('IST-2IDT,M3.4.4/26,M10.5.0', # Israel Standard / Daylight Time
+ tz.tzrange('IST', timedelta(hours=2), 'IDT',
+ start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2),
+ end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))),
+ ('WGT3WGST,M3.5.0/2,M10.5.0/1',
+ tz.tzrange('WGT', timedelta(hours=-3), 'WGST',
+ start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2),
+ end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))),
+
+ # Different offset specifications
+ ('WGT0300WGST',
+ tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
+ ('WGT03:00WGST',
+ tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
+ ('AEST-1100AEDT',
+ tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
+ ('AEST-11:00AEDT',
+ tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
+
+ # Different time formats
+ ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+ end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+ ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+ end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+ ('EST5EDT,M3.2.0/0400,M11.1.0/0300',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+ end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+])
+def test_valid_GNU_tzstr(tz_str, expected):
+ tzi = tz.tzstr(tz_str)
+
+ assert tzi == expected
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str, expected', [
+ ('EST5EDT,5,4,0,7200,11,3,0,7200',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2),
+ end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))),
+ ('EST5EDT,5,-4,0,7200,11,3,0,7200',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)),
+ end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6),
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3),
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))),
+ ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600',
+ tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+ start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+ end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+])
+def test_valid_dateutil_format(tz_str, expected):
+ # This tests the dateutil-specific format that is used widely in the tests
+ # and examples. It is unclear where this format originated from.
+ with pytest.warns(tz.DeprecatedTzFormatWarning):
+ tzi = tz.tzstr.instance(tz_str)
+
+ assert tzi == expected
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', [
+ 'hdfiughdfuig,dfughdfuigpu87ñ::',
+ ',dfughdfuigpu87ñ::',
+ '-1:WART4WARST,J1,J365/25',
+ 'WART4WARST,J1,J365/-25',
+ 'IST-2IDT,M3.4.-1/26,M10.5.0',
+ 'IST-2IDT,M3,2000,1/26,M10,5,0'
+])
+def test_invalid_GNU_tzstr(tz_str):
+ with pytest.raises(ValueError):
+ tz.tzstr(tz_str)
+
+
+# Different representations of the same default rule set
+DEFAULT_TZSTR_RULES_EQUIV_2003 = [
+ 'EST5EDT',
+ 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00',
+ 'EST5EDT4,95/02:00:00,298/02:00',
+ 'EST5EDT4,J96/02:00:00,J299/02:00',
+ 'EST5EDT4,J96/02:00:00,J299/02'
+]
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
+def test_tzstr_default_start(tz_str):
+ tzi = tz.tzstr(tz_str)
+ dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi)
+ dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi)
+
+ assert get_timezone_tuple(dt_std) == EST_TUPLE
+ assert get_timezone_tuple(dt_dst) == EDT_TUPLE
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
+def test_tzstr_default_end(tz_str):
+ tzi = tz.tzstr(tz_str)
+ dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi)
+ dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi)
+ dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1)
+ dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi)
+
+ assert get_timezone_tuple(dt_dst) == EDT_TUPLE
+ assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE
+ assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE
+ assert get_timezone_tuple(dt_std) == EST_TUPLE
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tzstr_1', ['EST5EDT',
+ 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
+@pytest.mark.parametrize('tzstr_2', ['EST5EDT',
+ 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
+def test_tzstr_default_cmp(tzstr_1, tzstr_2):
+ tz1 = tz.tzstr(tzstr_1)
+ tz2 = tz.tzstr(tzstr_2)
+
+ assert tz1 == tz2
+
+class TZICalTest(unittest.TestCase, TzFoldMixin):
+ def _gettz_str_tuple(self, tzname):
+ TZ_EST = (
+ 'BEGIN:VTIMEZONE',
+ 'TZID:US-Eastern',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19971029T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
+ 'TZOFFSETFROM:-0400',
+ 'TZOFFSETTO:-0500',
+ 'TZNAME:EST',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19980301T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
+ 'TZOFFSETFROM:-0500',
+ 'TZOFFSETTO:-0400',
+ 'TZNAME:EDT',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE'
+ )
+
+ TZ_PST = (
+ 'BEGIN:VTIMEZONE',
+ 'TZID:US-Pacific',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19971029T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
+ 'TZOFFSETFROM:-0700',
+ 'TZOFFSETTO:-0800',
+ 'TZNAME:PST',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19980301T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
+ 'TZOFFSETFROM:-0800',
+ 'TZOFFSETTO:-0700',
+ 'TZNAME:PDT',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE'
+ )
+
+ TZ_AEST = (
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Australia-Sydney',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19980301T030000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04',
+ 'TZOFFSETFROM:+1100',
+ 'TZOFFSETTO:+1000',
+ 'TZNAME:AEST',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19971029T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10',
+ 'TZOFFSETFROM:+1000',
+ 'TZOFFSETTO:+1100',
+ 'TZNAME:AEDT',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE'
+ )
+
+ TZ_LON = (
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Europe-London',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19810301T030000',
+ 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'TZNAME:GMT',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19961001T030000',
+ 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01',
+ 'TZOFFSETFROM:+0000',
+ 'TZOFFSETTO:+0100',
+ 'TZNAME:BST',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE'
+ )
+
+ tzname_map = {'Australia/Sydney': TZ_AEST,
+ 'America/Toronto': TZ_EST,
+ 'America/New_York': TZ_EST,
+ 'America/Los_Angeles': TZ_PST,
+ 'Europe/London': TZ_LON}
+
+ return tzname_map[tzname]
+
+ def _gettz_str(self, tzname):
+ return '\n'.join(self._gettz_str_tuple(tzname))
+
+ def _tzstr_dtstart_with_params(self, tzname, param_str):
+ # Adds parameters to the DTSTART values of a given tzstr
+ tz_str_tuple = self._gettz_str_tuple(tzname)
+
+ out_tz = []
+ for line in tz_str_tuple:
+ if line.startswith('DTSTART'):
+ name, value = line.split(':', 1)
+ line = name + ';' + param_str + ':' + value
+
+ out_tz.append(line)
+
+ return '\n'.join(out_tz)
+
+ def gettz(self, tzname):
+ tz_str = self._gettz_str(tzname)
+
+ tzc = tz.tzical(StringIO(tz_str)).get()
+
+ return tzc
+
+ def testRepr(self):
+ instr = StringIO(TZICAL_PST8PDT)
+ instr.name = 'StringIO(PST8PDT)'
+ tzc = tz.tzical(instr)
+
+ self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")")
+
+ # Test performance
+ def _test_us_zone(self, tzc, func, values, start):
+ if start:
+ dt1 = datetime(2003, 3, 9, 1, 59)
+ dt2 = datetime(2003, 3, 9, 2, 00)
+ fold = [0, 0]
+ else:
+ dt1 = datetime(2003, 11, 2, 0, 59)
+ dt2 = datetime(2003, 11, 2, 1, 00)
+ fold = [0, 1]
+
+ dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f)
+ for dt, f in zip((dt1, dt2), fold))
+
+ for value, dt in zip(values, dts):
+ self.assertEqual(func(dt), value)
+
+ def _test_multi_zones(self, tzstrs, tzids, func, values, start):
+ tzic = tz.tzical(StringIO('\n'.join(tzstrs)))
+ for tzid, vals in zip(tzids, values):
+ tzc = tzic.get(tzid)
+
+ self._test_us_zone(tzc, func, vals, start)
+
+ def _prepare_EST(self):
+ tz_str = self._gettz_str('America/New_York')
+ return tz.tzical(StringIO(tz_str)).get()
+
+ def _testEST(self, start, test_type, tzc=None):
+ if tzc is None:
+ tzc = self._prepare_EST()
+
+ argdict = {
+ 'name': (datetime.tzname, ('EST', 'EDT')),
+ 'offset': (datetime.utcoffset, (timedelta(hours=-5),
+ timedelta(hours=-4))),
+ 'dst': (datetime.dst, (timedelta(hours=0),
+ timedelta(hours=1)))
+ }
+
+ func, values = argdict[test_type]
+
+ if not start:
+ values = reversed(values)
+
+ self._test_us_zone(tzc, func, values, start=start)
+
+ def testESTStartName(self):
+ self._testEST(start=True, test_type='name')
+
+ def testESTEndName(self):
+ self._testEST(start=False, test_type='name')
+
+ def testESTStartOffset(self):
+ self._testEST(start=True, test_type='offset')
+
+ def testESTEndOffset(self):
+ self._testEST(start=False, test_type='offset')
+
+ def testESTStartDST(self):
+ self._testEST(start=True, test_type='dst')
+
+ def testESTEndDST(self):
+ self._testEST(start=False, test_type='dst')
+
+ def testESTValueDatetime(self):
+ # Violating one-test-per-test rule because we're not set up to do
+ # parameterized tests and the manual proliferation is getting a bit
+ # out of hand.
+ tz_str = self._tzstr_dtstart_with_params('America/New_York',
+ 'VALUE=DATE-TIME')
+
+ tzc = tz.tzical(StringIO(tz_str)).get()
+
+ for start in (True, False):
+ for test_type in ('name', 'offset', 'dst'):
+ self._testEST(start=start, test_type=test_type, tzc=tzc)
+
+ def _testMultizone(self, start, test_type):
+ tzstrs = (self._gettz_str('America/New_York'),
+ self._gettz_str('America/Los_Angeles'))
+ tzids = ('US-Eastern', 'US-Pacific')
+
+ argdict = {
+ 'name': (datetime.tzname, (('EST', 'EDT'),
+ ('PST', 'PDT'))),
+ 'offset': (datetime.utcoffset, ((timedelta(hours=-5),
+ timedelta(hours=-4)),
+ (timedelta(hours=-8),
+ timedelta(hours=-7)))),
+ 'dst': (datetime.dst, ((timedelta(hours=0),
+ timedelta(hours=1)),
+ (timedelta(hours=0),
+ timedelta(hours=1))))
+ }
+
+ func, values = argdict[test_type]
+
+ if not start:
+ values = map(reversed, values)
+
+ self._test_multi_zones(tzstrs, tzids, func, values, start)
+
+ def testMultiZoneStartName(self):
+ self._testMultizone(start=True, test_type='name')
+
+ def testMultiZoneEndName(self):
+ self._testMultizone(start=False, test_type='name')
+
+ def testMultiZoneStartOffset(self):
+ self._testMultizone(start=True, test_type='offset')
+
+ def testMultiZoneEndOffset(self):
+ self._testMultizone(start=False, test_type='offset')
+
+ def testMultiZoneStartDST(self):
+ self._testMultizone(start=True, test_type='dst')
+
+ def testMultiZoneEndDST(self):
+ self._testMultizone(start=False, test_type='dst')
+
+ def testMultiZoneKeys(self):
+ est_str = self._gettz_str('America/New_York')
+ pst_str = self._gettz_str('America/Los_Angeles')
+ tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str))))
+
+ # Sort keys because they are in a random order, being dictionary keys
+ keys = sorted(tzic.keys())
+
+ self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
+
+ # Test error conditions
+ def testEmptyString(self):
+ with self.assertRaises(ValueError):
+ tz.tzical(StringIO(""))
+
+ def testMultiZoneGet(self):
+ tzic = tz.tzical(StringIO(TZICAL_EST5EDT + TZICAL_PST8PDT))
+
+ with self.assertRaises(ValueError):
+ tzic.get()
+
+ def testDtstartDate(self):
+ tz_str = self._tzstr_dtstart_with_params('America/New_York',
+ 'VALUE=DATE')
+ with self.assertRaises(ValueError):
+ tz.tzical(StringIO(tz_str))
+
+ def testDtstartTzid(self):
+ tz_str = self._tzstr_dtstart_with_params('America/New_York',
+ 'TZID=UTC')
+ with self.assertRaises(ValueError):
+ tz.tzical(StringIO(tz_str))
+
+ def testDtstartBadParam(self):
+ tz_str = self._tzstr_dtstart_with_params('America/New_York',
+ 'FOO=BAR')
+ with self.assertRaises(ValueError):
+ tz.tzical(StringIO(tz_str))
+
+ # Test Parsing
+ def testGap(self):
+ tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT))))
+
+ keys = sorted(tzic.keys())
+ self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
+
+
+class TZTest(unittest.TestCase):
+ def testFileStart1(self):
+ tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+ self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST")
+ self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT")
+
+ def testFileEnd1(self):
+ tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+ self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
+ "EDT")
+ end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc))
+ self.assertEqual(end_est.tzname(), "EST")
+
+ def testFileLastTransition(self):
+ # After the last transition, it goes to standard time in perpetuity
+ tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+ self.assertEqual(datetime(2037, 10, 25, 0, 59, tzinfo=tzc).tzname(),
+ "EDT")
+
+ last_date = tz.enfold(datetime(2037, 10, 25, 1, 00, tzinfo=tzc), fold=1)
+ self.assertEqual(last_date.tzname(),
+ "EST")
+
+ self.assertEqual(datetime(2038, 5, 25, 12, 0, tzinfo=tzc).tzname(),
+ "EST")
+
+ def testInvalidFile(self):
+ # Should throw a ValueError if an invalid file is passed
+ with self.assertRaises(ValueError):
+ tz.tzfile(BytesIO(b'BadFile'))
+
+ def testFilestreamWithNameRepr(self):
+ # If fileobj is a filestream with a "name" attribute this name should
+ # be reflected in the tz object's repr
+ fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT))
+ fileobj.name = 'foo'
+ tzc = tz.tzfile(fileobj)
+ self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')')
+
+ def testLeapCountDecodesProperly(self):
+ # This timezone has leapcnt, and failed to decode until
+ # Eugene Oden notified about the issue.
+
+ # As leap information is currently unused (and unstored) by tzfile() we
+ # can only indirectly test this: Take advantage of tzfile() not closing
+ # the input file if handed in as an opened file and assert that the
+ # full file content has been read by tzfile(). Note: For this test to
+ # work NEW_YORK must be in TZif version 1 format i.e. no more data
+ # after TZif v1 header + data has been read
+ fileobj = BytesIO(base64.b64decode(NEW_YORK))
+ tz.tzfile(fileobj)
+ # we expect no remaining file content now, i.e. zero-length; if there's
+ # still data we haven't read the file format correctly
+ remaining_tzfile_content = fileobj.read()
+ self.assertEqual(len(remaining_tzfile_content), 0)
+
+ def testIsStd(self):
+ # NEW_YORK tzfile contains this isstd information:
+ isstd_expected = (0, 0, 0, 1)
+ tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
+ # gather the actual information as parsed by the tzfile class
+ isstd = []
+ for ttinfo in tzc._ttinfo_list:
+ # ttinfo objects contain boolean values
+ isstd.append(int(ttinfo.isstd))
+ # ttinfo list may contain more entries than isstd file content
+ isstd = tuple(isstd[:len(isstd_expected)])
+ self.assertEqual(
+ isstd_expected, isstd,
+ "isstd UTC/local indicators parsed: %s != tzfile contents: %s"
+ % (isstd, isstd_expected))
+
+ def testGMTHasNoDaylight(self):
+ # tz.tzstr("GMT+2") improperly considered daylight saving time.
+ # Issue reported by Lennart Regebro.
+ dt = datetime(2007, 8, 6, 4, 10)
+ self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0))
+
+ def testGMTOffset(self):
+ # GMT and UTC offsets have inverted signal when compared to the
+ # usual TZ variable handling.
+ dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.UTC)
+ self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")),
+ datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
+ self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")),
+ datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2")))
+
+ @unittest.skipIf(IS_WIN, "requires Unix")
+ def testTZSetDoesntCorrupt(self):
+ # if we start in non-UTC then tzset UTC make sure parse doesn't get
+ # confused
+ with TZEnvContext('UTC'):
+ # this should parse to UTC timezone not the original timezone
+ dt = parse('2014-07-20T12:34:56+00:00')
+ self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00')
+
+
+@pytest.mark.tzfile
+@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets not supported')
+def test_tzfile_sub_minute_offset():
+ # If user running python 3.6 or newer, exact offset is used
+ tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+ offset = timedelta(hours=1, minutes=39, seconds=52)
+ assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
+
+
+@pytest.mark.tzfile
+@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets supported.')
+def test_sub_minute_rounding_tzfile():
+ # This timezone has an offset of 5992 seconds in 1900-01-01.
+ # For python version pre-3.6, this will be rounded
+ tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+ offset = timedelta(hours=1, minutes=40)
+ assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
+
+
+@pytest.mark.tzfile
+def test_samoa_transition():
+ # utcoffset() was erroneously returning +14:00 an hour early (GH #812)
+ APIA = tz.gettz('Pacific/Apia')
+ dt = datetime(2011, 12, 29, 23, 59, tzinfo=APIA)
+ assert dt.utcoffset() == timedelta(hours=-10)
+
+ # Make sure the transition actually works, too
+ dt_after = (dt.astimezone(tz.UTC) + timedelta(minutes=1)).astimezone(APIA)
+ assert dt_after == datetime(2011, 12, 31, tzinfo=APIA)
+ assert dt_after.utcoffset() == timedelta(hours=14)
+
+
+@unittest.skipUnless(IS_WIN, "Requires Windows")
+class TzWinTest(unittest.TestCase, TzWinFoldMixin):
+ def setUp(self):
+ self.tzclass = tzwin.tzwin
+
+ def testTzResLoadName(self):
+ # This may not work right on non-US locales.
+ tzr = tzwin.tzres()
+ self.assertEqual(tzr.load_name(112), "Eastern Standard Time")
+
+ def testTzResNameFromString(self):
+ tzr = tzwin.tzres()
+ self.assertEqual(tzr.name_from_string('@tzres.dll,-221'),
+ 'Alaskan Daylight Time')
+
+ self.assertEqual(tzr.name_from_string('Samoa Daylight Time'),
+ 'Samoa Daylight Time')
+
+ with self.assertRaises(ValueError):
+ tzr.name_from_string('@tzres.dll,100')
+
+ def testIsdstZoneWithNoDaylightSaving(self):
+ tz = tzwin.tzwin("UTC")
+ dt = parse("2013-03-06 19:08:15")
+ self.assertFalse(tz._isdst(dt))
+
+ def testOffset(self):
+ tz = tzwin.tzwin("Cape Verde Standard Time")
+ self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)),
+ timedelta(-1, 82800))
+
+ def testTzwinName(self):
+ # https://github.com/dateutil/dateutil/issues/143
+ tw = tz.tzwin('Eastern Standard Time')
+
+ # Cover the transitions for at least two years.
+ ESTs = 'Eastern Standard Time'
+ EDTs = 'Eastern Daylight Time'
+ transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
+ (datetime(2015, 3, 8, 3, 1), EDTs),
+ (datetime(2015, 11, 1, 0, 59), EDTs),
+ (datetime(2015, 11, 1, 3, 1), ESTs),
+ (datetime(2016, 3, 13, 0, 59), ESTs),
+ (datetime(2016, 3, 13, 3, 1), EDTs),
+ (datetime(2016, 11, 6, 0, 59), EDTs),
+ (datetime(2016, 11, 6, 3, 1), ESTs)]
+
+ for t_date, expected in transition_dates:
+ self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
+
+ def testTzwinRepr(self):
+ tw = tz.tzwin('Yakutsk Standard Time')
+ self.assertEqual(repr(tw), 'tzwin(' +
+ repr('Yakutsk Standard Time') + ')')
+
+ def testTzWinEquality(self):
+ # https://github.com/dateutil/dateutil/issues/151
+ tzwin_names = ('Eastern Standard Time',
+ 'West Pacific Standard Time',
+ 'Yakutsk Standard Time',
+ 'Iran Standard Time',
+ 'UTC')
+
+ for tzwin_name in tzwin_names:
+ # Get two different instances to compare
+ tw1 = tz.tzwin(tzwin_name)
+ tw2 = tz.tzwin(tzwin_name)
+
+ self.assertEqual(tw1, tw2)
+
+ def testTzWinInequality(self):
+ # https://github.com/dateutil/dateutil/issues/151
+ # Note these last two currently differ only in their name.
+ tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'),
+ ('Greenwich Standard Time', 'GMT Standard Time'),
+ ('GMT Standard Time', 'UTC'),
+ ('E. South America Standard Time',
+ 'Argentina Standard Time'))
+
+ for tzwn1, tzwn2 in tzwin_names:
+ # Get two different instances to compare
+ tw1 = tz.tzwin(tzwn1)
+ tw2 = tz.tzwin(tzwn2)
+
+ self.assertNotEqual(tw1, tw2)
+
+ def testTzWinEqualityInvalid(self):
+ # Compare to objects that do not implement comparison with this
+ # (should default to False)
+ UTC = tz.UTC
+ EST = tz.tzwin('Eastern Standard Time')
+
+ self.assertFalse(EST == UTC)
+ self.assertFalse(EST == 1)
+ self.assertFalse(UTC == EST)
+
+ self.assertTrue(EST != UTC)
+ self.assertTrue(EST != 1)
+
+ def testTzWinInequalityUnsupported(self):
+ # Compare it to an object that is promiscuous about equality, but for
+ # which tzwin does not implement an equality operator.
+ EST = tz.tzwin('Eastern Standard Time')
+ self.assertTrue(EST == ComparesEqual)
+ self.assertFalse(EST != ComparesEqual)
+
+ def testTzwinTimeOnlyDST(self):
+ # For zones with DST, .dst() should return None
+ tw_est = tz.tzwin('Eastern Standard Time')
+ self.assertIs(dt_time(14, 10, tzinfo=tw_est).dst(), None)
+
+ # This zone has no DST, so .dst() can return 0
+ tw_sast = tz.tzwin('South Africa Standard Time')
+ self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).dst(),
+ timedelta(0))
+
+ def testTzwinTimeOnlyUTCOffset(self):
+ # For zones with DST, .utcoffset() should return None
+ tw_est = tz.tzwin('Eastern Standard Time')
+ self.assertIs(dt_time(14, 10, tzinfo=tw_est).utcoffset(), None)
+
+ # This zone has no DST, so .utcoffset() returns standard offset
+ tw_sast = tz.tzwin('South Africa Standard Time')
+ self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).utcoffset(),
+ timedelta(hours=2))
+
+ def testTzwinTimeOnlyTZName(self):
+ # For zones with DST, the name defaults to standard time
+ tw_est = tz.tzwin('Eastern Standard Time')
+ self.assertEqual(dt_time(14, 10, tzinfo=tw_est).tzname(),
+ 'Eastern Standard Time')
+
+ # For zones with no DST, this should work normally.
+ tw_sast = tz.tzwin('South Africa Standard Time')
+ self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).tzname(),
+ 'South Africa Standard Time')
+
+
+@unittest.skipUnless(IS_WIN, "Requires Windows")
+class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin):
+
+ def setUp(self):
+ self.tzclass = tzwin.tzwinlocal
+ self.context = TZWinContext
+
+ def get_args(self, tzname):
+ return ()
+
+ def testLocal(self):
+ # Not sure how to pin a local time zone, so for now we're just going
+ # to run this and make sure it doesn't raise an error
+ # See GitHub Issue #135: https://github.com/dateutil/dateutil/issues/135
+ datetime.now(tzwin.tzwinlocal())
+
+ def testTzwinLocalUTCOffset(self):
+ with TZWinContext('Eastern Standard Time'):
+ tzwl = tzwin.tzwinlocal()
+ self.assertEqual(datetime(2014, 3, 11, tzinfo=tzwl).utcoffset(),
+ timedelta(hours=-4))
+
+ def testTzwinLocalName(self):
+ # https://github.com/dateutil/dateutil/issues/143
+ ESTs = 'Eastern Standard Time'
+ EDTs = 'Eastern Daylight Time'
+ transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
+ (datetime(2015, 3, 8, 3, 1), EDTs),
+ (datetime(2015, 11, 1, 0, 59), EDTs),
+ (datetime(2015, 11, 1, 3, 1), ESTs),
+ (datetime(2016, 3, 13, 0, 59), ESTs),
+ (datetime(2016, 3, 13, 3, 1), EDTs),
+ (datetime(2016, 11, 6, 0, 59), EDTs),
+ (datetime(2016, 11, 6, 3, 1), ESTs)]
+
+ with TZWinContext('Eastern Standard Time'):
+ tw = tz.tzwinlocal()
+
+ for t_date, expected in transition_dates:
+ self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
+
+ def testTzWinLocalRepr(self):
+ tw = tz.tzwinlocal()
+ self.assertEqual(repr(tw), 'tzwinlocal()')
+
+ def testTzwinLocalRepr(self):
+ # https://github.com/dateutil/dateutil/issues/143
+ with TZWinContext('Eastern Standard Time'):
+ tw = tz.tzwinlocal()
+
+ self.assertEqual(str(tw), 'tzwinlocal(' +
+ repr('Eastern Standard Time') + ')')
+
+ with TZWinContext('Pacific Standard Time'):
+ tw = tz.tzwinlocal()
+
+ self.assertEqual(str(tw), 'tzwinlocal(' +
+ repr('Pacific Standard Time') + ')')
+
+ def testTzwinLocalEquality(self):
+ tw_est = tz.tzwin('Eastern Standard Time')
+ tw_pst = tz.tzwin('Pacific Standard Time')
+
+ with TZWinContext('Eastern Standard Time'):
+ twl1 = tz.tzwinlocal()
+ twl2 = tz.tzwinlocal()
+
+ self.assertEqual(twl1, twl2)
+ self.assertEqual(twl1, tw_est)
+ self.assertNotEqual(twl1, tw_pst)
+
+ with TZWinContext('Pacific Standard Time'):
+ twl1 = tz.tzwinlocal()
+ twl2 = tz.tzwinlocal()
+ tw = tz.tzwin('Pacific Standard Time')
+
+ self.assertEqual(twl1, twl2)
+ self.assertEqual(twl1, tw)
+ self.assertEqual(twl1, tw_pst)
+ self.assertNotEqual(twl1, tw_est)
+
+ def testTzwinLocalTimeOnlyDST(self):
+ # For zones with DST, .dst() should return None
+ with TZWinContext('Eastern Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertIs(dt_time(14, 10, tzinfo=twl).dst(), None)
+
+ # This zone has no DST, so .dst() can return 0
+ with TZWinContext('South Africa Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertEqual(dt_time(14, 10, tzinfo=twl).dst(), timedelta(0))
+
+ def testTzwinLocalTimeOnlyUTCOffset(self):
+ # For zones with DST, .utcoffset() should return None
+ with TZWinContext('Eastern Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertIs(dt_time(14, 10, tzinfo=twl).utcoffset(), None)
+
+ # This zone has no DST, so .utcoffset() returns standard offset
+ with TZWinContext('South Africa Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertEqual(dt_time(14, 10, tzinfo=twl).utcoffset(),
+ timedelta(hours=2))
+
+ def testTzwinLocalTimeOnlyTZName(self):
+ # For zones with DST, the name defaults to standard time
+ with TZWinContext('Eastern Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
+ 'Eastern Standard Time')
+
+ # For zones with no DST, this should work normally.
+ with TZWinContext('South Africa Standard Time'):
+ twl = tz.tzwinlocal()
+ self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
+ 'South Africa Standard Time')
+
+
+class TzPickleTest(PicklableMixin, unittest.TestCase):
+ _asfile = False
+
+ def setUp(self):
+ self.assertPicklable = partial(self.assertPicklable,
+ asfile=self._asfile)
+
+ def testPickleTzUTC(self):
+ self.assertPicklable(tz.tzutc(), singleton=True)
+
+ def testPickleTzOffsetZero(self):
+ self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True)
+
+ def testPickleTzOffsetPos(self):
+ self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True)
+
+ def testPickleTzOffsetNeg(self):
+ self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True)
+
+ @pytest.mark.tzlocal
+ def testPickleTzLocal(self):
+ self.assertPicklable(tz.tzlocal())
+
+ def testPickleTzFileEST5EDT(self):
+ tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+ self.assertPicklable(tzc)
+
+ def testPickleTzFileEurope_Helsinki(self):
+ tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+ self.assertPicklable(tzc)
+
+ def testPickleTzFileNew_York(self):
+ tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
+ self.assertPicklable(tzc)
+
+ @unittest.skip("Known failure")
+ def testPickleTzICal(self):
+ tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get()
+ self.assertPicklable(tzc)
+
+ def testPickleTzGettz(self):
+ self.assertPicklable(tz.gettz('America/New_York'))
+
+ def testPickleZoneFileGettz(self):
+ zoneinfo_file = zoneinfo.get_zonefile_instance()
+ tzi = zoneinfo_file.get('America/New_York')
+ self.assertIsNot(tzi, None)
+ self.assertPicklable(tzi)
+
+
+class TzPickleFileTest(TzPickleTest):
+ """ Run all the TzPickleTest tests, using a temporary file """
+ _asfile = True
+
+
+class DatetimeAmbiguousTest(unittest.TestCase):
+ """ Test the datetime_exists / datetime_ambiguous functions """
+
+ def testNoTzSpecified(self):
+ with self.assertRaises(ValueError):
+ tz.datetime_ambiguous(datetime(2016, 4, 1, 2, 9))
+
+ def _get_no_support_tzinfo_class(self, dt_start, dt_end, dst_only=False):
+ # Generates a class of tzinfo with no support for is_ambiguous
+ # where dates between dt_start and dt_end are ambiguous.
+
+ class FoldingTzInfo(tzinfo):
+ def utcoffset(self, dt):
+ if not dst_only:
+ dt_n = dt.replace(tzinfo=None)
+
+ if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
+ return timedelta(hours=-1)
+
+ return timedelta(hours=0)
+
+ def dst(self, dt):
+ dt_n = dt.replace(tzinfo=None)
+
+ if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
+ return timedelta(hours=1)
+ else:
+ return timedelta(0)
+
+ return FoldingTzInfo
+
+ def _get_no_support_tzinfo(self, dt_start, dt_end, dst_only=False):
+ return self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)()
+
+ def testNoSupportAmbiguityFoldNaive(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+ tz=tzi))
+
+ def testNoSupportAmbiguityFoldAware(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
+ tzinfo=tzi)))
+
+ def testNoSupportAmbiguityUnambiguousNaive(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+ tz=tzi))
+
+ def testNoSupportAmbiguityUnambiguousAware(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
+ tzinfo=tzi)))
+
+ def testNoSupportAmbiguityFoldDSTOnly(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+ tz=tzi))
+
+ def testNoSupportAmbiguityUnambiguousDSTOnly(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+ tz=tzi))
+
+ def testSupportAmbiguityFoldNaive(self):
+ tzi = tz.gettz('US/Eastern')
+
+ dt = datetime(2011, 11, 6, 1, 30)
+
+ self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
+
+ def testSupportAmbiguityFoldAware(self):
+ tzi = tz.gettz('US/Eastern')
+
+ dt = datetime(2011, 11, 6, 1, 30, tzinfo=tzi)
+
+ self.assertTrue(tz.datetime_ambiguous(dt))
+
+ def testSupportAmbiguityUnambiguousAware(self):
+ tzi = tz.gettz('US/Eastern')
+
+ dt = datetime(2011, 11, 6, 4, 30)
+
+ self.assertFalse(tz.datetime_ambiguous(dt, tz=tzi))
+
+ def testSupportAmbiguityUnambiguousNaive(self):
+ tzi = tz.gettz('US/Eastern')
+
+ dt = datetime(2011, 11, 6, 4, 30, tzinfo=tzi)
+
+ self.assertFalse(tz.datetime_ambiguous(dt))
+
+ def _get_ambig_error_tzinfo(self, dt_start, dt_end, dst_only=False):
+ cTzInfo = self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)
+
+ # Takes the wrong number of arguments and raises an error anyway.
+ class FoldTzInfoRaises(cTzInfo):
+ def is_ambiguous(self, dt, other_arg):
+ raise NotImplementedError('This is not implemented')
+
+ return FoldTzInfoRaises()
+
+ def testIncompatibleAmbiguityFoldNaive(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+ tz=tzi))
+
+ def testIncompatibleAmbiguityFoldAware(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
+ tzinfo=tzi)))
+
+ def testIncompatibleAmbiguityUnambiguousNaive(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+ tz=tzi))
+
+ def testIncompatibleAmbiguityUnambiguousAware(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
+ tzinfo=tzi)))
+
+ def testIncompatibleAmbiguityFoldDSTOnly(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
+
+ self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+ tz=tzi))
+
+ def testIncompatibleAmbiguityUnambiguousDSTOnly(self):
+ dt_start = datetime(2018, 9, 1, 1, 0)
+ dt_end = datetime(2018, 9, 1, 2, 0)
+
+ tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
+
+ self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+ tz=tzi))
+
+ def testSpecifiedTzOverridesAttached(self):
+ # If a tz is specified, the datetime will be treated as naive.
+
+ # This is not ambiguous in the local zone
+ dt = datetime(2011, 11, 6, 1, 30, tzinfo=tz.gettz('Australia/Sydney'))
+
+ self.assertFalse(tz.datetime_ambiguous(dt))
+
+ tzi = tz.gettz('US/Eastern')
+ self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
+
+
+class DatetimeExistsTest(unittest.TestCase):
+ def testNoTzSpecified(self):
+ with self.assertRaises(ValueError):
+ tz.datetime_exists(datetime(2016, 4, 1, 2, 9))
+
+ def testInGapNaive(self):
+ tzi = tz.gettz('Australia/Sydney')
+
+ dt = datetime(2012, 10, 7, 2, 30)
+
+ self.assertFalse(tz.datetime_exists(dt, tz=tzi))
+
+ def testInGapAware(self):
+ tzi = tz.gettz('Australia/Sydney')
+
+ dt = datetime(2012, 10, 7, 2, 30, tzinfo=tzi)
+
+ self.assertFalse(tz.datetime_exists(dt))
+
+ def testExistsNaive(self):
+ tzi = tz.gettz('Australia/Sydney')
+
+ dt = datetime(2012, 10, 7, 10, 30)
+
+ self.assertTrue(tz.datetime_exists(dt, tz=tzi))
+
+ def testExistsAware(self):
+ tzi = tz.gettz('Australia/Sydney')
+
+ dt = datetime(2012, 10, 7, 10, 30, tzinfo=tzi)
+
+ self.assertTrue(tz.datetime_exists(dt))
+
+ def testSpecifiedTzOverridesAttached(self):
+ EST = tz.gettz('US/Eastern')
+ AEST = tz.gettz('Australia/Sydney')
+
+ dt = datetime(2012, 10, 7, 2, 30, tzinfo=EST) # This time exists
+
+ self.assertFalse(tz.datetime_exists(dt, tz=AEST))
+
+
+class TestEnfold:
+ def test_enter_fold_default(self):
+ dt = tz.enfold(datetime(2020, 1, 19, 3, 32))
+
+ assert dt.fold == 1
+
+ def test_enter_fold(self):
+ dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
+
+ assert dt.fold == 1
+
+ def test_exit_fold(self):
+ dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0)
+
+ # Before Python 3.6, dt.fold won't exist if fold is 0.
+ assert getattr(dt, 'fold', 0) == 0
+
+ def test_defold(self):
+ dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
+
+ dt2 = tz.enfold(dt, fold=0)
+
+ assert getattr(dt2, 'fold', 0) == 0
+
+ def test_fold_replace_args(self):
+ # This test can be dropped when Python < 3.6 is dropped, since it
+ # is mainly to cover the `replace` method on _DatetimeWithFold
+ dt = tz.enfold(datetime(1950, 1, 2, 12, 30, 15, 8), fold=1)
+
+ dt2 = dt.replace(1952, 2, 3, 13, 31, 16, 9)
+ assert dt2 == tz.enfold(datetime(1952, 2, 3, 13, 31, 16, 9), fold=1)
+ assert dt2.fold == 1
+
+ def test_fold_replace_exception_duplicate_args(self):
+ dt = tz.enfold(datetime(1999, 1, 3), fold=1)
+
+ with pytest.raises(TypeError):
+ dt.replace(1950, year=2000)
+
+
+@pytest.mark.tz_resolve_imaginary
+class ImaginaryDateTest(unittest.TestCase):
+ def testCanberraForward(self):
+ tzi = tz.gettz('Australia/Canberra')
+ dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi)
+ dt_act = tz.resolve_imaginary(dt)
+ dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi)
+ self.assertEqual(dt_act, dt_exp)
+
+ def testLondonForward(self):
+ tzi = tz.gettz('Europe/London')
+ dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi)
+ dt_act = tz.resolve_imaginary(dt)
+ dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi)
+ self.assertEqual(dt_act, dt_exp)
+
+ def testKeivForward(self):
+ tzi = tz.gettz('Europe/Kiev')
+ dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi)
+ dt_act = tz.resolve_imaginary(dt)
+ dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi)
+ self.assertEqual(dt_act, dt_exp)
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('dt', [
+ datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')),
+ datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')),
+ datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')),
+])
+def test_resolve_imaginary_ambiguous(dt):
+ assert tz.resolve_imaginary(dt) is dt
+
+ dt_f = tz.enfold(dt)
+ assert dt is not dt_f
+ assert tz.resolve_imaginary(dt_f) is dt_f
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('dt', [
+ datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
+ datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
+ datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
+ datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
+ datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
+ datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
+ datetime(2025, 9, 25, 1, 17, tzinfo=tz.UTC),
+ datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)),
+ datetime(2019, 3, 4, tzinfo=None)
+])
+def test_resolve_imaginary_existing(dt):
+ assert tz.resolve_imaginary(dt) is dt
+
+
+def __get_kiritimati_resolve_imaginary_test():
+ # In the 2018d release of the IANA database, the Kiritimati "imaginary day"
+ # data was corrected, so if the system zoneinfo is older than 2018d, the
+ # Kiritimati test will fail.
+
+ tzi = tz.gettz('Pacific/Kiritimati')
+ new_version = False
+ if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi):
+ zif = zoneinfo.get_zonefile_instance()
+ if zif.metadata is not None:
+ new_version = zif.metadata['tzversion'] >= '2018d'
+
+ if new_version:
+ tzi = zif.get('Pacific/Kiritimati')
+ else:
+ new_version = True
+
+ if new_version:
+ dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30))
+ else:
+ dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30))
+
+ return (tzi, ) + dates
+
+
+resolve_imaginary_tests = [
+ (tz.gettz('Europe/London'),
+ datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)),
+ (tz.gettz('America/New_York'),
+ datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)),
+ (tz.gettz('Australia/Sydney'),
+ datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)),
+ __get_kiritimati_resolve_imaginary_test(),
+]
+
+
+if SUPPORTS_SUB_MINUTE_OFFSETS:
+ resolve_imaginary_tests.append(
+ (tz.gettz('Africa/Monrovia'),
+ datetime(1972, 1, 7, 0, 30), datetime(1972, 1, 7, 1, 14, 30)))
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('tzi, dt, dt_exp', resolve_imaginary_tests)
+def test_resolve_imaginary(tzi, dt, dt_exp):
+ dt = dt.replace(tzinfo=tzi)
+ dt_exp = dt_exp.replace(tzinfo=tzi)
+
+ dt_r = tz.resolve_imaginary(dt)
+ assert dt_r == dt_exp
+ assert dt_r.tzname() == dt_exp.tzname()
+ assert dt_r.utcoffset() == dt_exp.utcoffset()
diff --git a/src/dateutil/test/test_utils.py b/src/dateutil/test/test_utils.py
new file mode 100644
index 0000000..fe1bfdc
--- /dev/null
+++ b/src/dateutil/test/test_utils.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from datetime import timedelta, datetime
+
+from dateutil import tz
+from dateutil import utils
+from dateutil.tz import UTC
+from dateutil.utils import within_delta
+
+from freezegun import freeze_time
+
+NYC = tz.gettz("America/New_York")
+
+
+@freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003))
+def test_utils_today():
+ assert utils.today() == datetime(2014, 12, 15, 0, 0, 0)
+
+
+@freeze_time(datetime(2014, 12, 15, 12), tz_offset=5)
+def test_utils_today_tz_info():
+ assert utils.today(NYC) == datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC)
+
+
+@freeze_time(datetime(2014, 12, 15, 23), tz_offset=5)
+def test_utils_today_tz_info_different_day():
+ assert utils.today(UTC) == datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC)
+
+
+def test_utils_default_tz_info_naive():
+ dt = datetime(2014, 9, 14, 9, 30)
+ assert utils.default_tzinfo(dt, NYC).tzinfo is NYC
+
+
+def test_utils_default_tz_info_aware():
+ dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC)
+ assert utils.default_tzinfo(dt, NYC).tzinfo is UTC
+
+
+def test_utils_within_delta():
+ d1 = datetime(2016, 1, 1, 12, 14, 1, 9)
+ d2 = d1.replace(microsecond=15)
+
+ assert within_delta(d1, d2, timedelta(seconds=1))
+ assert not within_delta(d1, d2, timedelta(microseconds=1))
+
+
+def test_utils_within_delta_with_negative_delta():
+ d1 = datetime(2016, 1, 1)
+ d2 = datetime(2015, 12, 31)
+
+ assert within_delta(d2, d1, timedelta(days=-1))
diff --git a/src/dateutil/tz/__init__.py b/src/dateutil/tz/__init__.py
new file mode 100644
index 0000000..af1352c
--- /dev/null
+++ b/src/dateutil/tz/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+from .tz import *
+from .tz import __doc__
+
+__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+ "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
+ "enfold", "datetime_ambiguous", "datetime_exists",
+ "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
+
+
+class DeprecatedTzFormatWarning(Warning):
+ """Warning raised when time zones are parsed from deprecated formats."""
diff --git a/src/dateutil/tz/_common.py b/src/dateutil/tz/_common.py
new file mode 100644
index 0000000..e6ac118
--- /dev/null
+++ b/src/dateutil/tz/_common.py
@@ -0,0 +1,419 @@
+from six import PY2
+
+from functools import wraps
+
+from datetime import datetime, timedelta, tzinfo
+
+
+ZERO = timedelta(0)
+
+__all__ = ['tzname_in_python2', 'enfold']
+
+
+def tzname_in_python2(namefunc):
+ """Change unicode output into bytestrings in Python 2
+
+ tzname() API changed in Python 3. It used to return bytes, but was changed
+ to unicode strings
+ """
+ if PY2:
+ @wraps(namefunc)
+ def adjust_encoding(*args, **kwargs):
+ name = namefunc(*args, **kwargs)
+ if name is not None:
+ name = name.encode()
+
+ return name
+
+ return adjust_encoding
+ else:
+ return namefunc
+
+
+# The following is adapted from Alexander Belopolsky's tz library
+# https://github.com/abalkin/tz
+if hasattr(datetime, 'fold'):
+ # This is the pre-python 3.6 fold situation
+ def enfold(dt, fold=1):
+ """
+ Provides a unified interface for assigning the ``fold`` attribute to
+ datetimes both before and after the implementation of PEP-495.
+
+ :param fold:
+ The value for the ``fold`` attribute in the returned datetime. This
+ should be either 0 or 1.
+
+ :return:
+ Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+ ``fold`` for all versions of Python. In versions prior to
+ Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+ subclass of :py:class:`datetime.datetime` with the ``fold``
+ attribute added, if ``fold`` is 1.
+
+ .. versionadded:: 2.6.0
+ """
+ return dt.replace(fold=fold)
+
+else:
+ class _DatetimeWithFold(datetime):
+ """
+ This is a class designed to provide a PEP 495-compliant interface for
+ Python versions before 3.6. It is used only for dates in a fold, so
+ the ``fold`` attribute is fixed at ``1``.
+
+ .. versionadded:: 2.6.0
+ """
+ __slots__ = ()
+
+ def replace(self, *args, **kwargs):
+ """
+ Return a datetime with the same attributes, except for those
+ attributes given new values by whichever keyword arguments are
+ specified. Note that tzinfo=None can be specified to create a naive
+ datetime from an aware datetime with no conversion of date and time
+ data.
+
+ This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
+ return a ``datetime.datetime`` even if ``fold`` is unchanged.
+ """
+ argnames = (
+ 'year', 'month', 'day', 'hour', 'minute', 'second',
+ 'microsecond', 'tzinfo'
+ )
+
+ for arg, argname in zip(args, argnames):
+ if argname in kwargs:
+ raise TypeError('Duplicate argument: {}'.format(argname))
+
+ kwargs[argname] = arg
+
+ for argname in argnames:
+ if argname not in kwargs:
+ kwargs[argname] = getattr(self, argname)
+
+ dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
+
+ return dt_class(**kwargs)
+
+ @property
+ def fold(self):
+ return 1
+
+ def enfold(dt, fold=1):
+ """
+ Provides a unified interface for assigning the ``fold`` attribute to
+ datetimes both before and after the implementation of PEP-495.
+
+ :param fold:
+ The value for the ``fold`` attribute in the returned datetime. This
+ should be either 0 or 1.
+
+ :return:
+ Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+ ``fold`` for all versions of Python. In versions prior to
+ Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+ subclass of :py:class:`datetime.datetime` with the ``fold``
+ attribute added, if ``fold`` is 1.
+
+ .. versionadded:: 2.6.0
+ """
+ if getattr(dt, 'fold', 0) == fold:
+ return dt
+
+ args = dt.timetuple()[:6]
+ args += (dt.microsecond, dt.tzinfo)
+
+ if fold:
+ return _DatetimeWithFold(*args)
+ else:
+ return datetime(*args)
+
+
+def _validate_fromutc_inputs(f):
+ """
+ The CPython version of ``fromutc`` checks that the input is a ``datetime``
+ object and that ``self`` is attached as its ``tzinfo``.
+ """
+ @wraps(f)
+ def fromutc(self, dt):
+ if not isinstance(dt, datetime):
+ raise TypeError("fromutc() requires a datetime argument")
+ if dt.tzinfo is not self:
+ raise ValueError("dt.tzinfo is not self")
+
+ return f(self, dt)
+
+ return fromutc
+
+
+class _tzinfo(tzinfo):
+ """
+ Base class for all ``dateutil`` ``tzinfo`` objects.
+ """
+
+ def is_ambiguous(self, dt):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+
+ dt = dt.replace(tzinfo=self)
+
+ wall_0 = enfold(dt, fold=0)
+ wall_1 = enfold(dt, fold=1)
+
+ same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+ same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
+
+ return same_dt and not same_offset
+
+ def _fold_status(self, dt_utc, dt_wall):
+ """
+ Determine the fold status of a "wall" datetime, given a representation
+ of the same datetime as a (naive) UTC datetime. This is calculated based
+ on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
+ datetimes, and that this offset is the actual number of hours separating
+ ``dt_utc`` and ``dt_wall``.
+
+ :param dt_utc:
+ Representation of the datetime as UTC
+
+ :param dt_wall:
+ Representation of the datetime as "wall time". This parameter must
+ either have a `fold` attribute or have a fold-naive
+ :class:`datetime.tzinfo` attached, otherwise the calculation may
+ fail.
+ """
+ if self.is_ambiguous(dt_wall):
+ delta_wall = dt_wall - dt_utc
+ _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
+ else:
+ _fold = 0
+
+ return _fold
+
+ def _fold(self, dt):
+ return getattr(dt, 'fold', 0)
+
+ def _fromutc(self, dt):
+ """
+ Given a timezone-aware datetime in a given timezone, calculates a
+ timezone-aware datetime in a new timezone.
+
+ Since this is the one time that we *know* we have an unambiguous
+ datetime object, we take this opportunity to determine whether the
+ datetime is ambiguous and in a "fold" state (e.g. if it's the first
+ occurrence, chronologically, of the ambiguous datetime).
+
+ :param dt:
+ A timezone-aware :class:`datetime.datetime` object.
+ """
+
+ # Re-implement the algorithm from Python's datetime.py
+ dtoff = dt.utcoffset()
+ if dtoff is None:
+ raise ValueError("fromutc() requires a non-None utcoffset() "
+ "result")
+
+ # The original datetime.py code assumes that `dst()` defaults to
+ # zero during ambiguous times. PEP 495 inverts this presumption, so
+ # for pre-PEP 495 versions of python, we need to tweak the algorithm.
+ dtdst = dt.dst()
+ if dtdst is None:
+ raise ValueError("fromutc() requires a non-None dst() result")
+ delta = dtoff - dtdst
+
+ dt += delta
+ # Set fold=1 so we can default to being in the fold for
+ # ambiguous dates.
+ dtdst = enfold(dt, fold=1).dst()
+ if dtdst is None:
+ raise ValueError("fromutc(): dt.dst gave inconsistent "
+ "results; cannot convert")
+ return dt + dtdst
+
+ @_validate_fromutc_inputs
+ def fromutc(self, dt):
+ """
+ Given a timezone-aware datetime in a given timezone, calculates a
+ timezone-aware datetime in a new timezone.
+
+ Since this is the one time that we *know* we have an unambiguous
+ datetime object, we take this opportunity to determine whether the
+ datetime is ambiguous and in a "fold" state (e.g. if it's the first
+ occurrence, chronologically, of the ambiguous datetime).
+
+ :param dt:
+ A timezone-aware :class:`datetime.datetime` object.
+ """
+ dt_wall = self._fromutc(dt)
+
+ # Calculate the fold status given the two datetimes.
+ _fold = self._fold_status(dt, dt_wall)
+
+ # Set the default fold value for ambiguous dates
+ return enfold(dt_wall, fold=_fold)
+
+
+class tzrangebase(_tzinfo):
+ """
+ This is an abstract base class for time zones represented by an annual
+ transition into and out of DST. Child classes should implement the following
+ methods:
+
+ * ``__init__(self, *args, **kwargs)``
+ * ``transitions(self, year)`` - this is expected to return a tuple of
+ datetimes representing the DST on and off transitions in standard
+ time.
+
+ A fully initialized ``tzrangebase`` subclass should also provide the
+ following attributes:
+ * ``hasdst``: Boolean whether or not the zone uses DST.
+ * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
+ representing the respective UTC offsets.
+ * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
+ abbreviations in DST and STD, respectively.
+ * ``_hasdst``: Whether or not the zone has DST.
+
+ .. versionadded:: 2.6.0
+ """
+ def __init__(self):
+ raise NotImplementedError('tzrangebase is an abstract base class')
+
+ def utcoffset(self, dt):
+ isdst = self._isdst(dt)
+
+ if isdst is None:
+ return None
+ elif isdst:
+ return self._dst_offset
+ else:
+ return self._std_offset
+
+ def dst(self, dt):
+ isdst = self._isdst(dt)
+
+ if isdst is None:
+ return None
+ elif isdst:
+ return self._dst_base_offset
+ else:
+ return ZERO
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ if self._isdst(dt):
+ return self._dst_abbr
+ else:
+ return self._std_abbr
+
+ def fromutc(self, dt):
+ """ Given a datetime in UTC, return local time """
+ if not isinstance(dt, datetime):
+ raise TypeError("fromutc() requires a datetime argument")
+
+ if dt.tzinfo is not self:
+ raise ValueError("dt.tzinfo is not self")
+
+ # Get transitions - if there are none, fixed offset
+ transitions = self.transitions(dt.year)
+ if transitions is None:
+ return dt + self.utcoffset(dt)
+
+ # Get the transition times in UTC
+ dston, dstoff = transitions
+
+ dston -= self._std_offset
+ dstoff -= self._std_offset
+
+ utc_transitions = (dston, dstoff)
+ dt_utc = dt.replace(tzinfo=None)
+
+ isdst = self._naive_isdst(dt_utc, utc_transitions)
+
+ if isdst:
+ dt_wall = dt + self._dst_offset
+ else:
+ dt_wall = dt + self._std_offset
+
+ _fold = int(not isdst and self.is_ambiguous(dt_wall))
+
+ return enfold(dt_wall, fold=_fold)
+
+ def is_ambiguous(self, dt):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+ if not self.hasdst:
+ return False
+
+ start, end = self.transitions(dt.year)
+
+ dt = dt.replace(tzinfo=None)
+ return (end <= dt < end + self._dst_base_offset)
+
+ def _isdst(self, dt):
+ if not self.hasdst:
+ return False
+ elif dt is None:
+ return None
+
+ transitions = self.transitions(dt.year)
+
+ if transitions is None:
+ return False
+
+ dt = dt.replace(tzinfo=None)
+
+ isdst = self._naive_isdst(dt, transitions)
+
+ # Handle ambiguous dates
+ if not isdst and self.is_ambiguous(dt):
+ return not self._fold(dt)
+ else:
+ return isdst
+
+ def _naive_isdst(self, dt, transitions):
+ dston, dstoff = transitions
+
+ dt = dt.replace(tzinfo=None)
+
+ if dston < dstoff:
+ isdst = dston <= dt < dstoff
+ else:
+ isdst = not dstoff <= dt < dston
+
+ return isdst
+
+ @property
+ def _dst_base_offset(self):
+ return self._dst_offset - self._std_offset
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ return "%s(...)" % self.__class__.__name__
+
+ __reduce__ = object.__reduce__
diff --git a/src/dateutil/tz/_factories.py b/src/dateutil/tz/_factories.py
new file mode 100644
index 0000000..f8a6589
--- /dev/null
+++ b/src/dateutil/tz/_factories.py
@@ -0,0 +1,80 @@
+from datetime import timedelta
+import weakref
+from collections import OrderedDict
+
+from six.moves import _thread
+
+
+class _TzSingleton(type):
+ def __init__(cls, *args, **kwargs):
+ cls.__instance = None
+ super(_TzSingleton, cls).__init__(*args, **kwargs)
+
+ def __call__(cls):
+ if cls.__instance is None:
+ cls.__instance = super(_TzSingleton, cls).__call__()
+ return cls.__instance
+
+
+class _TzFactory(type):
+ def instance(cls, *args, **kwargs):
+ """Alternate constructor that returns a fresh instance"""
+ return type.__call__(cls, *args, **kwargs)
+
+
+class _TzOffsetFactory(_TzFactory):
+ def __init__(cls, *args, **kwargs):
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
+
+ cls._cache_lock = _thread.allocate_lock()
+
+ def __call__(cls, name, offset):
+ if isinstance(offset, timedelta):
+ key = (name, offset.total_seconds())
+ else:
+ key = (name, offset)
+
+ instance = cls.__instances.get(key, None)
+ if instance is None:
+ instance = cls.__instances.setdefault(key,
+ cls.instance(name, offset))
+
+ # This lock may not be necessary in Python 3. See GH issue #901
+ with cls._cache_lock:
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+ # Remove an item if the strong cache is overpopulated
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
+
+ return instance
+
+
+class _TzStrFactory(_TzFactory):
+ def __init__(cls, *args, **kwargs):
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
+
+ cls.__cache_lock = _thread.allocate_lock()
+
+ def __call__(cls, s, posix_offset=False):
+ key = (s, posix_offset)
+ instance = cls.__instances.get(key, None)
+
+ if instance is None:
+ instance = cls.__instances.setdefault(key,
+ cls.instance(s, posix_offset))
+
+ # This lock may not be necessary in Python 3. See GH issue #901
+ with cls.__cache_lock:
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+ # Remove an item if the strong cache is overpopulated
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
+
+ return instance
+
diff --git a/src/dateutil/tz/tz.py b/src/dateutil/tz/tz.py
new file mode 100644
index 0000000..c67f56d
--- /dev/null
+++ b/src/dateutil/tz/tz.py
@@ -0,0 +1,1849 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers timezone implementations subclassing the abstract
+:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
+files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
+etc), TZ environment string (in all known formats), given ranges (with help
+from relative deltas), local machine timezone, fixed offset timezone, and UTC
+timezone.
+"""
+import datetime
+import struct
+import time
+import sys
+import os
+import bisect
+import weakref
+from collections import OrderedDict
+
+import six
+from six import string_types
+from six.moves import _thread
+from ._common import tzname_in_python2, _tzinfo
+from ._common import tzrangebase, enfold
+from ._common import _validate_fromutc_inputs
+
+from ._factories import _TzSingleton, _TzOffsetFactory
+from ._factories import _TzStrFactory
+try:
+ from .win import tzwin, tzwinlocal
+except ImportError:
+ tzwin = tzwinlocal = None
+
+# For warning about rounding tzinfo
+from warnings import warn
+
+ZERO = datetime.timedelta(0)
+EPOCH = datetime.datetime.utcfromtimestamp(0)
+EPOCHORDINAL = EPOCH.toordinal()
+
+
+@six.add_metaclass(_TzSingleton)
+class tzutc(datetime.tzinfo):
+ """
+ This is a tzinfo object that represents the UTC time zone.
+
+ **Examples:**
+
+ .. doctest::
+
+ >>> from datetime import *
+ >>> from dateutil.tz import *
+
+ >>> datetime.now()
+ datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
+
+ >>> datetime.now(tzutc())
+ datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
+
+ >>> datetime.now(tzutc()).tzname()
+ 'UTC'
+
+ .. versionchanged:: 2.7.0
+ ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
+ always return the same object.
+
+ .. doctest::
+
+ >>> from dateutil.tz import tzutc, UTC
+ >>> tzutc() is tzutc()
+ True
+ >>> tzutc() is UTC
+ True
+ """
+ def utcoffset(self, dt):
+ return ZERO
+
+ def dst(self, dt):
+ return ZERO
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ return "UTC"
+
+ def is_ambiguous(self, dt):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+ return False
+
+ @_validate_fromutc_inputs
+ def fromutc(self, dt):
+ """
+ Fast track version of fromutc() returns the original ``dt`` object for
+ any valid :py:class:`datetime.datetime` object.
+ """
+ return dt
+
+ def __eq__(self, other):
+ if not isinstance(other, (tzutc, tzoffset)):
+ return NotImplemented
+
+ return (isinstance(other, tzutc) or
+ (isinstance(other, tzoffset) and other._offset == ZERO))
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ return "%s()" % self.__class__.__name__
+
+ __reduce__ = object.__reduce__
+
+
+#: Convenience constant providing a :class:`tzutc()` instance
+#:
+#: .. versionadded:: 2.7.0
+UTC = tzutc()
+
+
+@six.add_metaclass(_TzOffsetFactory)
+class tzoffset(datetime.tzinfo):
+ """
+ A simple class for representing a fixed offset from UTC.
+
+ :param name:
+ The timezone name, to be returned when ``tzname()`` is called.
+ :param offset:
+ The time zone offset in seconds, or (since version 2.6.0, represented
+ as a :py:class:`datetime.timedelta` object).
+ """
+ def __init__(self, name, offset):
+ self._name = name
+
+ try:
+ # Allow a timedelta
+ offset = offset.total_seconds()
+ except (TypeError, AttributeError):
+ pass
+
+ self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def dst(self, dt):
+ return ZERO
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ return self._name
+
+ @_validate_fromutc_inputs
+ def fromutc(self, dt):
+ return dt + self._offset
+
+ def is_ambiguous(self, dt):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+ return False
+
+ def __eq__(self, other):
+ if not isinstance(other, tzoffset):
+ return NotImplemented
+
+ return self._offset == other._offset
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ return "%s(%s, %s)" % (self.__class__.__name__,
+ repr(self._name),
+ int(self._offset.total_seconds()))
+
+ __reduce__ = object.__reduce__
+
+
+class tzlocal(_tzinfo):
+ """
+ A :class:`tzinfo` subclass built around the ``time`` timezone functions.
+ """
+ def __init__(self):
+ super(tzlocal, self).__init__()
+
+ self._std_offset = datetime.timedelta(seconds=-time.timezone)
+ if time.daylight:
+ self._dst_offset = datetime.timedelta(seconds=-time.altzone)
+ else:
+ self._dst_offset = self._std_offset
+
+ self._dst_saved = self._dst_offset - self._std_offset
+ self._hasdst = bool(self._dst_saved)
+ self._tznames = tuple(time.tzname)
+
+ def utcoffset(self, dt):
+ if dt is None and self._hasdst:
+ return None
+
+ if self._isdst(dt):
+ return self._dst_offset
+ else:
+ return self._std_offset
+
+ def dst(self, dt):
+ if dt is None and self._hasdst:
+ return None
+
+ if self._isdst(dt):
+ return self._dst_offset - self._std_offset
+ else:
+ return ZERO
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ return self._tznames[self._isdst(dt)]
+
+ def is_ambiguous(self, dt):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+ naive_dst = self._naive_is_dst(dt)
+ return (not naive_dst and
+ (naive_dst != self._naive_is_dst(dt - self._dst_saved)))
+
+ def _naive_is_dst(self, dt):
+ timestamp = _datetime_to_timestamp(dt)
+ return time.localtime(timestamp + time.timezone).tm_isdst
+
+ def _isdst(self, dt, fold_naive=True):
+ # We can't use mktime here. It is unstable when deciding if
+ # the hour near to a change is DST or not.
+ #
+ # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
+ # dt.minute, dt.second, dt.weekday(), 0, -1))
+ # return time.localtime(timestamp).tm_isdst
+ #
+ # The code above yields the following result:
+ #
+ # >>> import tz, datetime
+ # >>> t = tz.tzlocal()
+ # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+ # 'BRDT'
+ # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
+ # 'BRST'
+ # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+ # 'BRST'
+ # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
+ # 'BRDT'
+ # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+ # 'BRDT'
+ #
+ # Here is a more stable implementation:
+ #
+ if not self._hasdst:
+ return False
+
+ # Check for ambiguous times:
+ dstval = self._naive_is_dst(dt)
+ fold = getattr(dt, 'fold', None)
+
+ if self.is_ambiguous(dt):
+ if fold is not None:
+ return not self._fold(dt)
+ else:
+ return True
+
+ return dstval
+
+ def __eq__(self, other):
+ if isinstance(other, tzlocal):
+ return (self._std_offset == other._std_offset and
+ self._dst_offset == other._dst_offset)
+ elif isinstance(other, tzutc):
+ return (not self._hasdst and
+ self._tznames[0] in {'UTC', 'GMT'} and
+ self._std_offset == ZERO)
+ elif isinstance(other, tzoffset):
+ return (not self._hasdst and
+ self._tznames[0] == other._name and
+ self._std_offset == other._offset)
+ else:
+ return NotImplemented
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ return "%s()" % self.__class__.__name__
+
+ __reduce__ = object.__reduce__
+
+
+class _ttinfo(object):
+ __slots__ = ["offset", "delta", "isdst", "abbr",
+ "isstd", "isgmt", "dstoffset"]
+
+ def __init__(self):
+ for attr in self.__slots__:
+ setattr(self, attr, None)
+
+ def __repr__(self):
+ l = []
+ for attr in self.__slots__:
+ value = getattr(self, attr)
+ if value is not None:
+ l.append("%s=%s" % (attr, repr(value)))
+ return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
+
+ def __eq__(self, other):
+ if not isinstance(other, _ttinfo):
+ return NotImplemented
+
+ return (self.offset == other.offset and
+ self.delta == other.delta and
+ self.isdst == other.isdst and
+ self.abbr == other.abbr and
+ self.isstd == other.isstd and
+ self.isgmt == other.isgmt and
+ self.dstoffset == other.dstoffset)
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __getstate__(self):
+ state = {}
+ for name in self.__slots__:
+ state[name] = getattr(self, name, None)
+ return state
+
+ def __setstate__(self, state):
+ for name in self.__slots__:
+ if name in state:
+ setattr(self, name, state[name])
+
+
+class _tzfile(object):
+ """
+ Lightweight class for holding the relevant transition and time zone
+ information read from binary tzfiles.
+ """
+ attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
+ 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
+
+ def __init__(self, **kwargs):
+ for attr in self.attrs:
+ setattr(self, attr, kwargs.get(attr, None))
+
+
+class tzfile(_tzinfo):
+ """
+ This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
+ format timezone files to extract current and historical zone information.
+
+ :param fileobj:
+ This can be an opened file stream or a file name that the time zone
+ information can be read from.
+
+ :param filename:
+ This is an optional parameter specifying the source of the time zone
+ information in the event that ``fileobj`` is a file object. If omitted
+ and ``fileobj`` is a file stream, this parameter will be set either to
+ ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
+
+ See `Sources for Time Zone and Daylight Saving Time Data
+ <https://data.iana.org/time-zones/tz-link.html>`_ for more information.
+ Time zone files can be compiled from the `IANA Time Zone database files
+ <https://www.iana.org/time-zones>`_ with the `zic time zone compiler
+ <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
+
+ .. note::
+
+ Only construct a ``tzfile`` directly if you have a specific timezone
+ file on disk that you want to read into a Python ``tzinfo`` object.
+ If you want to get a ``tzfile`` representing a specific IANA zone,
+ (e.g. ``'America/New_York'``), you should call
+ :func:`dateutil.tz.gettz` with the zone identifier.
+
+
+ **Examples:**
+
+ Using the US Eastern time zone as an example, we can see that a ``tzfile``
+ provides time zone information for the standard Daylight Saving offsets:
+
+ .. testsetup:: tzfile
+
+ from dateutil.tz import gettz
+ from datetime import datetime
+
+ .. doctest:: tzfile
+
+ >>> NYC = gettz('America/New_York')
+ >>> NYC
+ tzfile('/usr/share/zoneinfo/America/New_York')
+
+ >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST
+ 2016-01-03 00:00:00-05:00
+
+ >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT
+ 2016-07-07 00:00:00-04:00
+
+
+ The ``tzfile`` structure contains a fully history of the time zone,
+ so historical dates will also have the right offsets. For example, before
+ the adoption of the UTC standards, New York used local solar mean time:
+
+ .. doctest:: tzfile
+
+ >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT
+ 1901-04-12 00:00:00-04:56
+
+ And during World War II, New York was on "Eastern War Time", which was a
+ state of permanent daylight saving time:
+
+ .. doctest:: tzfile
+
+ >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT
+ 1944-02-07 00:00:00-04:00
+
+ """
+
+ def __init__(self, fileobj, filename=None):
+ super(tzfile, self).__init__()
+
+ file_opened_here = False
+ if isinstance(fileobj, string_types):
+ self._filename = fileobj
+ fileobj = open(fileobj, 'rb')
+ file_opened_here = True
+ elif filename is not None:
+ self._filename = filename
+ elif hasattr(fileobj, "name"):
+ self._filename = fileobj.name
+ else:
+ self._filename = repr(fileobj)
+
+ if fileobj is not None:
+ if not file_opened_here:
+ fileobj = _nullcontext(fileobj)
+
+ with fileobj as file_stream:
+ tzobj = self._read_tzfile(file_stream)
+
+ self._set_tzdata(tzobj)
+
+ def _set_tzdata(self, tzobj):
+ """ Set the time zone data of this object from a _tzfile object """
+ # Copy the relevant attributes over as private attributes
+ for attr in _tzfile.attrs:
+ setattr(self, '_' + attr, getattr(tzobj, attr))
+
+ def _read_tzfile(self, fileobj):
+ out = _tzfile()
+
+ # From tzfile(5):
+ #
+ # The time zone information files used by tzset(3)
+ # begin with the magic characters "TZif" to identify
+ # them as time zone information files, followed by
+ # sixteen bytes reserved for future use, followed by
+ # six four-byte values of type long, written in a
+ # ``standard'' byte order (the high-order byte
+ # of the value is written first).
+ if fileobj.read(4).decode() != "TZif":
+ raise ValueError("magic not found")
+
+ fileobj.read(16)
+
+ (
+ # The number of UTC/local indicators stored in the file.
+ ttisgmtcnt,
+
+ # The number of standard/wall indicators stored in the file.
+ ttisstdcnt,
+
+ # The number of leap seconds for which data is
+ # stored in the file.
+ leapcnt,
+
+ # The number of "transition times" for which data
+ # is stored in the file.
+ timecnt,
+
+ # The number of "local time types" for which data
+ # is stored in the file (must not be zero).
+ typecnt,
+
+ # The number of characters of "time zone
+ # abbreviation strings" stored in the file.
+ charcnt,
+
+ ) = struct.unpack(">6l", fileobj.read(24))
+
+ # The above header is followed by tzh_timecnt four-byte
+ # values of type long, sorted in ascending order.
+ # These values are written in ``standard'' byte order.
+ # Each is used as a transition time (as returned by
+ # time(2)) at which the rules for computing local time
+ # change.
+
+ if timecnt:
+ out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
+ fileobj.read(timecnt*4)))
+ else:
+ out.trans_list_utc = []
+
+ # Next come tzh_timecnt one-byte values of type unsigned
+ # char; each one tells which of the different types of
+ # ``local time'' types described in the file is associated
+ # with the same-indexed transition time. These values
+ # serve as indices into an array of ttinfo structures that
+ # appears next in the file.
+
+ if timecnt:
+ out.trans_idx = struct.unpack(">%dB" % timecnt,
+ fileobj.read(timecnt))
+ else:
+ out.trans_idx = []
+
+ # Each ttinfo structure is written as a four-byte value
+ # for tt_gmtoff of type long, in a standard byte
+ # order, followed by a one-byte value for tt_isdst
+ # and a one-byte value for tt_abbrind. In each
+ # structure, tt_gmtoff gives the number of
+ # seconds to be added to UTC, tt_isdst tells whether
+ # tm_isdst should be set by localtime(3), and
+ # tt_abbrind serves as an index into the array of
+ # time zone abbreviation characters that follow the
+ # ttinfo structure(s) in the file.
+
+ ttinfo = []
+
+ for i in range(typecnt):
+ ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
+
+ abbr = fileobj.read(charcnt).decode()
+
+ # Then there are tzh_leapcnt pairs of four-byte
+ # values, written in standard byte order; the
+ # first value of each pair gives the time (as
+ # returned by time(2)) at which a leap second
+ # occurs; the second gives the total number of
+ # leap seconds to be applied after the given time.
+ # The pairs of values are sorted in ascending order
+ # by time.
+
+ # Not used, for now (but seek for correct file position)
+ if leapcnt:
+ fileobj.seek(leapcnt * 8, os.SEEK_CUR)
+
+ # Then there are tzh_ttisstdcnt standard/wall
+ # indicators, each stored as a one-byte value;
+ # they tell whether the transition times associated
+ # with local time types were specified as standard
+ # time or wall clock time, and are used when
+ # a time zone file is used in handling POSIX-style
+ # time zone environment variables.
+
+ if ttisstdcnt:
+ isstd = struct.unpack(">%db" % ttisstdcnt,
+ fileobj.read(ttisstdcnt))
+
+ # Finally, there are tzh_ttisgmtcnt UTC/local
+ # indicators, each stored as a one-byte value;
+ # they tell whether the transition times associated
+ # with local time types were specified as UTC or
+ # local time, and are used when a time zone file
+ # is used in handling POSIX-style time zone envi-
+ # ronment variables.
+
+ if ttisgmtcnt:
+ isgmt = struct.unpack(">%db" % ttisgmtcnt,
+ fileobj.read(ttisgmtcnt))
+
+ # Build ttinfo list
+ out.ttinfo_list = []
+ for i in range(typecnt):
+ gmtoff, isdst, abbrind = ttinfo[i]
+ gmtoff = _get_supported_offset(gmtoff)
+ tti = _ttinfo()
+ tti.offset = gmtoff
+ tti.dstoffset = datetime.timedelta(0)
+ tti.delta = datetime.timedelta(seconds=gmtoff)
+ tti.isdst = isdst
+ tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
+ tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
+ tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
+ out.ttinfo_list.append(tti)
+
+ # Replace ttinfo indexes for ttinfo objects.
+ out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx]
+
+ # Set standard, dst, and before ttinfos. before will be
+ # used when a given time is before any transitions,
+ # and will be set to the first non-dst ttinfo, or to
+ # the first dst, if all of them are dst.
+ out.ttinfo_std = None
+ out.ttinfo_dst = None
+ out.ttinfo_before = None
+ if out.ttinfo_list:
+ if not out.trans_list_utc:
+ out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
+ else:
+ for i in range(timecnt-1, -1, -1):
+ tti = out.trans_idx[i]
+ if not out.ttinfo_std and not tti.isdst:
+ out.ttinfo_std = tti
+ elif not out.ttinfo_dst and tti.isdst:
+ out.ttinfo_dst = tti
+
+ if out.ttinfo_std and out.ttinfo_dst:
+ break
+ else:
+ if out.ttinfo_dst and not out.ttinfo_std:
+ out.ttinfo_std = out.ttinfo_dst
+
+ for tti in out.ttinfo_list:
+ if not tti.isdst:
+ out.ttinfo_before = tti
+ break
+ else:
+ out.ttinfo_before = out.ttinfo_list[0]
+
+ # Now fix transition times to become relative to wall time.
+ #
+ # I'm not sure about this. In my tests, the tz source file
+ # is setup to wall time, and in the binary file isstd and
+ # isgmt are off, so it should be in wall time. OTOH, it's
+ # always in gmt time. Let me know if you have comments
+ # about this.
+ lastdst = None
+ lastoffset = None
+ lastdstoffset = None
+ lastbaseoffset = None
+ out.trans_list = []
+
+ for i, tti in enumerate(out.trans_idx):
+ offset = tti.offset
+ dstoffset = 0
+
+ if lastdst is not None:
+ if tti.isdst:
+ if not lastdst:
+ dstoffset = offset - lastoffset
+
+ if not dstoffset and lastdstoffset:
+ dstoffset = lastdstoffset
+
+ tti.dstoffset = datetime.timedelta(seconds=dstoffset)
+ lastdstoffset = dstoffset
+
+ # If a time zone changes its base offset during a DST transition,
+ # then you need to adjust by the previous base offset to get the
+ # transition time in local time. Otherwise you use the current
+ # base offset. Ideally, I would have some mathematical proof of
+ # why this is true, but I haven't really thought about it enough.
+ baseoffset = offset - dstoffset
+ adjustment = baseoffset
+ if (lastbaseoffset is not None and baseoffset != lastbaseoffset
+ and tti.isdst != lastdst):
+ # The base DST has changed
+ adjustment = lastbaseoffset
+
+ lastdst = tti.isdst
+ lastoffset = offset
+ lastbaseoffset = baseoffset
+
+ out.trans_list.append(out.trans_list_utc[i] + adjustment)
+
+ out.trans_idx = tuple(out.trans_idx)
+ out.trans_list = tuple(out.trans_list)
+ out.trans_list_utc = tuple(out.trans_list_utc)
+
+ return out
+
+ def _find_last_transition(self, dt, in_utc=False):
+ # If there's no list, there are no transitions to find
+ if not self._trans_list:
+ return None
+
+ timestamp = _datetime_to_timestamp(dt)
+
+ # Find where the timestamp fits in the transition list - if the
+ # timestamp is a transition time, it's part of the "after" period.
+ trans_list = self._trans_list_utc if in_utc else self._trans_list
+ idx = bisect.bisect_right(trans_list, timestamp)
+
+ # We want to know when the previous transition was, so subtract off 1
+ return idx - 1
+
+ def _get_ttinfo(self, idx):
+ # For no list or after the last transition, default to _ttinfo_std
+ if idx is None or (idx + 1) >= len(self._trans_list):
+ return self._ttinfo_std
+
+ # If there is a list and the time is before it, return _ttinfo_before
+ if idx < 0:
+ return self._ttinfo_before
+
+ return self._trans_idx[idx]
+
+ def _find_ttinfo(self, dt):
+ idx = self._resolve_ambiguous_time(dt)
+
+ return self._get_ttinfo(idx)
+
+ def fromutc(self, dt):
+ """
+ The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
+
+ :param dt:
+ A :py:class:`datetime.datetime` object.
+
+ :raises TypeError:
+ Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
+
+ :raises ValueError:
+ Raised if this is called with a ``dt`` which does not have this
+ ``tzinfo`` attached.
+
+ :return:
+ Returns a :py:class:`datetime.datetime` object representing the
+ wall time in ``self``'s time zone.
+ """
+ # These isinstance checks are in datetime.tzinfo, so we'll preserve
+ # them, even if we don't care about duck typing.
+ if not isinstance(dt, datetime.datetime):
+ raise TypeError("fromutc() requires a datetime argument")
+
+ if dt.tzinfo is not self:
+ raise ValueError("dt.tzinfo is not self")
+
+ # First treat UTC as wall time and get the transition we're in.
+ idx = self._find_last_transition(dt, in_utc=True)
+ tti = self._get_ttinfo(idx)
+
+ dt_out = dt + datetime.timedelta(seconds=tti.offset)
+
+ fold = self.is_ambiguous(dt_out, idx=idx)
+
+ return enfold(dt_out, fold=int(fold))
+
+ def is_ambiguous(self, dt, idx=None):
+ """
+ Whether or not the "wall time" of a given datetime is ambiguous in this
+ zone.
+
+ :param dt:
+ A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+ :return:
+ Returns ``True`` if ambiguous, ``False`` otherwise.
+
+ .. versionadded:: 2.6.0
+ """
+ if idx is None:
+ idx = self._find_last_transition(dt)
+
+ # Calculate the difference in offsets from current to previous
+ timestamp = _datetime_to_timestamp(dt)
+ tti = self._get_ttinfo(idx)
+
+ if idx is None or idx <= 0:
+ return False
+
+ od = self._get_ttinfo(idx - 1).offset - tti.offset
+ tt = self._trans_list[idx] # Transition time
+
+ return timestamp < tt + od
+
+ def _resolve_ambiguous_time(self, dt):
+ idx = self._find_last_transition(dt)
+
+ # If we have no transitions, return the index
+ _fold = self._fold(dt)
+ if idx is None or idx == 0:
+ return idx
+
+ # If it's ambiguous and we're in a fold, shift to a different index.
+ idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
+
+ return idx - idx_offset
+
+ def utcoffset(self, dt):
+ if dt is None:
+ return None
+
+ if not self._ttinfo_std:
+ return ZERO
+
+ return self._find_ttinfo(dt).delta
+
+ def dst(self, dt):
+ if dt is None:
+ return None
+
+ if not self._ttinfo_dst:
+ return ZERO
+
+ tti = self._find_ttinfo(dt)
+
+ if not tti.isdst:
+ return ZERO
+
+ # The documentation says that utcoffset()-dst() must
+ # be constant for every dt.
+ return tti.dstoffset
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ if not self._ttinfo_std or dt is None:
+ return None
+ return self._find_ttinfo(dt).abbr
+
+ def __eq__(self, other):
+ if not isinstance(other, tzfile):
+ return NotImplemented
+ return (self._trans_list == other._trans_list and
+ self._trans_idx == other._trans_idx and
+ self._ttinfo_list == other._ttinfo_list)
+
+ __hash__ = None
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
+
+ def __reduce__(self):
+ return self.__reduce_ex__(None)
+
+ def __reduce_ex__(self, protocol):
+ return (self.__class__, (None, self._filename), self.__dict__)
+
+
+class tzrange(tzrangebase):
+ """
+ The ``tzrange`` object is a time zone specified by a set of offsets and
+ abbreviations, equivalent to the way the ``TZ`` variable can be specified
+ in POSIX-like systems, but using Python delta objects to specify DST
+ start, end and offsets.
+
+ :param stdabbr:
+ The abbreviation for standard time (e.g. ``'EST'``).
+
+ :param stdoffset:
+ An integer or :class:`datetime.timedelta` object or equivalent
+ specifying the base offset from UTC.
+
+ If unspecified, +00:00 is used.
+
+ :param dstabbr:
+ The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
+
+ If specified, with no other DST information, DST is assumed to occur
+ and the default behavior or ``dstoffset``, ``start`` and ``end`` is
+ used. If unspecified and no other DST information is specified, it
+ is assumed that this zone has no DST.
+
+ If this is unspecified and other DST information is *is* specified,
+ DST occurs in the zone but the time zone abbreviation is left
+ unchanged.
+
+ :param dstoffset:
+ A an integer or :class:`datetime.timedelta` object or equivalent
+ specifying the UTC offset during DST. If unspecified and any other DST
+ information is specified, it is assumed to be the STD offset +1 hour.
+
+ :param start:
+ A :class:`relativedelta.relativedelta` object or equivalent specifying
+ the time and time of year that daylight savings time starts. To
+ specify, for example, that DST starts at 2AM on the 2nd Sunday in
+ March, pass:
+
+ ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
+
+ If unspecified and any other DST information is specified, the default
+ value is 2 AM on the first Sunday in April.
+
+ :param end:
+ A :class:`relativedelta.relativedelta` object or equivalent
+ representing the time and time of year that daylight savings time
+ ends, with the same specification method as in ``start``. One note is
+ that this should point to the first time in the *standard* zone, so if
+ a transition occurs at 2AM in the DST zone and the clocks are set back
+ 1 hour to 1AM, set the ``hours`` parameter to +1.
+
+
+ **Examples:**
+
+ .. testsetup:: tzrange
+
+ from dateutil.tz import tzrange, tzstr
+
+ .. doctest:: tzrange
+
+ >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
+ True
+
+ >>> from dateutil.relativedelta import *
+ >>> range1 = tzrange("EST", -18000, "EDT")
+ >>> range2 = tzrange("EST", -18000, "EDT", -14400,
+ ... relativedelta(hours=+2, month=4, day=1,
+ ... weekday=SU(+1)),
+ ... relativedelta(hours=+1, month=10, day=31,
+ ... weekday=SU(-1)))
+ >>> tzstr('EST5EDT') == range1 == range2
+ True
+
+ """
+ def __init__(self, stdabbr, stdoffset=None,
+ dstabbr=None, dstoffset=None,
+ start=None, end=None):
+
+ global relativedelta
+ from dateutil import relativedelta
+
+ self._std_abbr = stdabbr
+ self._dst_abbr = dstabbr
+
+ try:
+ stdoffset = stdoffset.total_seconds()
+ except (TypeError, AttributeError):
+ pass
+
+ try:
+ dstoffset = dstoffset.total_seconds()
+ except (TypeError, AttributeError):
+ pass
+
+ if stdoffset is not None:
+ self._std_offset = datetime.timedelta(seconds=stdoffset)
+ else:
+ self._std_offset = ZERO
+
+ if dstoffset is not None:
+ self._dst_offset = datetime.timedelta(seconds=dstoffset)
+ elif dstabbr and stdoffset is not None:
+ self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
+ else:
+ self._dst_offset = ZERO
+
+ if dstabbr and start is None:
+ self._start_delta = relativedelta.relativedelta(
+ hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
+ else:
+ self._start_delta = start
+
+ if dstabbr and end is None:
+ self._end_delta = relativedelta.relativedelta(
+ hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
+ else:
+ self._end_delta = end
+
+ self._dst_base_offset_ = self._dst_offset - self._std_offset
+ self.hasdst = bool(self._start_delta)
+
+ def transitions(self, year):
+ """
+ For a given year, get the DST on and off transition times, expressed
+ always on the standard time side. For zones with no transitions, this
+ function returns ``None``.
+
+ :param year:
+ The year whose transitions you would like to query.
+
+ :return:
+ Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+ ``(dston, dstoff)`` for zones with an annual DST transition, or
+ ``None`` for fixed offset zones.
+ """
+ if not self.hasdst:
+ return None
+
+ base_year = datetime.datetime(year, 1, 1)
+
+ start = base_year + self._start_delta
+ end = base_year + self._end_delta
+
+ return (start, end)
+
+ def __eq__(self, other):
+ if not isinstance(other, tzrange):
+ return NotImplemented
+
+ return (self._std_abbr == other._std_abbr and
+ self._dst_abbr == other._dst_abbr and
+ self._std_offset == other._std_offset and
+ self._dst_offset == other._dst_offset and
+ self._start_delta == other._start_delta and
+ self._end_delta == other._end_delta)
+
+ @property
+ def _dst_base_offset(self):
+ return self._dst_base_offset_
+
+
+@six.add_metaclass(_TzStrFactory)
+class tzstr(tzrange):
+ """
+ ``tzstr`` objects are time zone objects specified by a time-zone string as
+ it would be passed to a ``TZ`` variable on POSIX-style systems (see
+ the `GNU C Library: TZ Variable`_ for more details).
+
+ There is one notable exception, which is that POSIX-style time zones use an
+ inverted offset format, so normally ``GMT+3`` would be parsed as an offset
+ 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
+ offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
+ behavior, pass a ``True`` value to ``posix_offset``.
+
+ The :class:`tzrange` object provides the same functionality, but is
+ specified using :class:`relativedelta.relativedelta` objects. rather than
+ strings.
+
+ :param s:
+ A time zone string in ``TZ`` variable format. This can be a
+ :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
+ :class:`unicode`) or a stream emitting unicode characters
+ (e.g. :class:`StringIO`).
+
+ :param posix_offset:
+ Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
+ ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
+ POSIX standard.
+
+ .. caution::
+
+ Prior to version 2.7.0, this function also supported time zones
+ in the format:
+
+ * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
+ * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
+
+ This format is non-standard and has been deprecated; this function
+ will raise a :class:`DeprecatedTZFormatWarning` until
+ support is removed in a future version.
+
+ .. _`GNU C Library: TZ Variable`:
+ https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+ """
+ def __init__(self, s, posix_offset=False):
+ global parser
+ from dateutil.parser import _parser as parser
+
+ self._s = s
+
+ res = parser._parsetz(s)
+ if res is None or res.any_unused_tokens:
+ raise ValueError("unknown string format")
+
+ # Here we break the compatibility with the TZ variable handling.
+ # GMT-3 actually *means* the timezone -3.
+ if res.stdabbr in ("GMT", "UTC") and not posix_offset:
+ res.stdoffset *= -1
+
+ # We must initialize it first, since _delta() needs
+ # _std_offset and _dst_offset set. Use False in start/end
+ # to avoid building it two times.
+ tzrange.__init__(self, res.stdabbr, res.stdoffset,
+ res.dstabbr, res.dstoffset,
+ start=False, end=False)
+
+ if not res.dstabbr:
+ self._start_delta = None
+ self._end_delta = None
+ else:
+ self._start_delta = self._delta(res.start)
+ if self._start_delta:
+ self._end_delta = self._delta(res.end, isend=1)
+
+ self.hasdst = bool(self._start_delta)
+
+ def _delta(self, x, isend=0):
+ from dateutil import relativedelta
+ kwargs = {}
+ if x.month is not None:
+ kwargs["month"] = x.month
+ if x.weekday is not None:
+ kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
+ if x.week > 0:
+ kwargs["day"] = 1
+ else:
+ kwargs["day"] = 31
+ elif x.day:
+ kwargs["day"] = x.day
+ elif x.yday is not None:
+ kwargs["yearday"] = x.yday
+ elif x.jyday is not None:
+ kwargs["nlyearday"] = x.jyday
+ if not kwargs:
+ # Default is to start on first sunday of april, and end
+ # on last sunday of october.
+ if not isend:
+ kwargs["month"] = 4
+ kwargs["day"] = 1
+ kwargs["weekday"] = relativedelta.SU(+1)
+ else:
+ kwargs["month"] = 10
+ kwargs["day"] = 31
+ kwargs["weekday"] = relativedelta.SU(-1)
+ if x.time is not None:
+ kwargs["seconds"] = x.time
+ else:
+ # Default is 2AM.
+ kwargs["seconds"] = 7200
+ if isend:
+ # Convert to standard time, to follow the documented way
+ # of working with the extra hour. See the documentation
+ # of the tzinfo class.
+ delta = self._dst_offset - self._std_offset
+ kwargs["seconds"] -= delta.seconds + delta.days * 86400
+ return relativedelta.relativedelta(**kwargs)
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+class _tzicalvtzcomp(object):
+ def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
+ tzname=None, rrule=None):
+ self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
+ self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
+ self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
+ self.isdst = isdst
+ self.tzname = tzname
+ self.rrule = rrule
+
+
+class _tzicalvtz(_tzinfo):
+ def __init__(self, tzid, comps=[]):
+ super(_tzicalvtz, self).__init__()
+
+ self._tzid = tzid
+ self._comps = comps
+ self._cachedate = []
+ self._cachecomp = []
+ self._cache_lock = _thread.allocate_lock()
+
+ def _find_comp(self, dt):
+ if len(self._comps) == 1:
+ return self._comps[0]
+
+ dt = dt.replace(tzinfo=None)
+
+ try:
+ with self._cache_lock:
+ return self._cachecomp[self._cachedate.index(
+ (dt, self._fold(dt)))]
+ except ValueError:
+ pass
+
+ lastcompdt = None
+ lastcomp = None
+
+ for comp in self._comps:
+ compdt = self._find_compdt(comp, dt)
+
+ if compdt and (not lastcompdt or lastcompdt < compdt):
+ lastcompdt = compdt
+ lastcomp = comp
+
+ if not lastcomp:
+ # RFC says nothing about what to do when a given
+ # time is before the first onset date. We'll look for the
+ # first standard component, or the first component, if
+ # none is found.
+ for comp in self._comps:
+ if not comp.isdst:
+ lastcomp = comp
+ break
+ else:
+ lastcomp = comp[0]
+
+ with self._cache_lock:
+ self._cachedate.insert(0, (dt, self._fold(dt)))
+ self._cachecomp.insert(0, lastcomp)
+
+ if len(self._cachedate) > 10:
+ self._cachedate.pop()
+ self._cachecomp.pop()
+
+ return lastcomp
+
+ def _find_compdt(self, comp, dt):
+ if comp.tzoffsetdiff < ZERO and self._fold(dt):
+ dt -= comp.tzoffsetdiff
+
+ compdt = comp.rrule.before(dt, inc=True)
+
+ return compdt
+
+ def utcoffset(self, dt):
+ if dt is None:
+ return None
+
+ return self._find_comp(dt).tzoffsetto
+
+ def dst(self, dt):
+ comp = self._find_comp(dt)
+ if comp.isdst:
+ return comp.tzoffsetdiff
+ else:
+ return ZERO
+
+ @tzname_in_python2
+ def tzname(self, dt):
+ return self._find_comp(dt).tzname
+
+ def __repr__(self):
+ return "<tzicalvtz %s>" % repr(self._tzid)
+
+ __reduce__ = object.__reduce__
+
+
+class tzical(object):
+ """
+ This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
+ as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
+
+ :param `fileobj`:
+ A file or stream in iCalendar format, which should be UTF-8 encoded
+ with CRLF endings.
+
+ .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
+ """
+ def __init__(self, fileobj):
+ global rrule
+ from dateutil import rrule
+
+ if isinstance(fileobj, string_types):
+ self._s = fileobj
+ # ical should be encoded in UTF-8 with CRLF
+ fileobj = open(fileobj, 'r')
+ else:
+ self._s = getattr(fileobj, 'name', repr(fileobj))
+ fileobj = _nullcontext(fileobj)
+
+ self._vtz = {}
+
+ with fileobj as fobj:
+ self._parse_rfc(fobj.read())
+
+ def keys(self):
+ """
+ Retrieves the available time zones as a list.
+ """
+ return list(self._vtz.keys())
+
+ def get(self, tzid=None):
+ """
+ Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
+
+ :param tzid:
+ If there is exactly one time zone available, omitting ``tzid``
+ or passing :py:const:`None` value returns it. Otherwise a valid
+ key (which can be retrieved from :func:`keys`) is required.
+
+ :raises ValueError:
+ Raised if ``tzid`` is not specified but there are either more
+ or fewer than 1 zone defined.
+
+ :returns:
+ Returns either a :py:class:`datetime.tzinfo` object representing
+ the relevant time zone or :py:const:`None` if the ``tzid`` was
+ not found.
+ """
+ if tzid is None:
+ if len(self._vtz) == 0:
+ raise ValueError("no timezones defined")
+ elif len(self._vtz) > 1:
+ raise ValueError("more than one timezone available")
+ tzid = next(iter(self._vtz))
+
+ return self._vtz.get(tzid)
+
+ def _parse_offset(self, s):
+ s = s.strip()
+ if not s:
+ raise ValueError("empty offset")
+ if s[0] in ('+', '-'):
+ signal = (-1, +1)[s[0] == '+']
+ s = s[1:]
+ else:
+ signal = +1
+ if len(s) == 4:
+ return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
+ elif len(s) == 6:
+ return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
+ else:
+ raise ValueError("invalid offset: " + s)
+
+ def _parse_rfc(self, s):
+ lines = s.splitlines()
+ if not lines:
+ raise ValueError("empty string")
+
+ # Unfold
+ i = 0
+ while i < len(lines):
+ line = lines[i].rstrip()
+ if not line:
+ del lines[i]
+ elif i > 0 and line[0] == " ":
+ lines[i-1] += line[1:]
+ del lines[i]
+ else:
+ i += 1
+
+ tzid = None
+ comps = []
+ invtz = False
+ comptype = None
+ for line in lines:
+ if not line:
+ continue
+ name, value = line.split(':', 1)
+ parms = name.split(';')
+ if not parms:
+ raise ValueError("empty property name")
+ name = parms[0].upper()
+ parms = parms[1:]
+ if invtz:
+ if name == "BEGIN":
+ if value in ("STANDARD", "DAYLIGHT"):
+ # Process component
+ pass
+ else:
+ raise ValueError("unknown component: "+value)
+ comptype = value
+ founddtstart = False
+ tzoffsetfrom = None
+ tzoffsetto = None
+ rrulelines = []
+ tzname = None
+ elif name == "END":
+ if value == "VTIMEZONE":
+ if comptype:
+ raise ValueError("component not closed: "+comptype)
+ if not tzid:
+ raise ValueError("mandatory TZID not found")
+ if not comps:
+ raise ValueError(
+ "at least one component is needed")
+ # Process vtimezone
+ self._vtz[tzid] = _tzicalvtz(tzid, comps)
+ invtz = False
+ elif value == comptype:
+ if not founddtstart:
+ raise ValueError("mandatory DTSTART not found")
+ if tzoffsetfrom is None:
+ raise ValueError(
+ "mandatory TZOFFSETFROM not found")
+ if tzoffsetto is None:
+ raise ValueError(
+ "mandatory TZOFFSETFROM not found")
+ # Process component
+ rr = None
+ if rrulelines:
+ rr = rrule.rrulestr("\n".join(rrulelines),
+ compatible=True,
+ ignoretz=True,
+ cache=True)
+ comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
+ (comptype == "DAYLIGHT"),
+ tzname, rr)
+ comps.append(comp)
+ comptype = None
+ else:
+ raise ValueError("invalid component end: "+value)
+ elif comptype:
+ if name == "DTSTART":
+ # DTSTART in VTIMEZONE takes a subset of valid RRULE
+ # values under RFC 5545.
+ for parm in parms:
+ if parm != 'VALUE=DATE-TIME':
+ msg = ('Unsupported DTSTART param in ' +
+ 'VTIMEZONE: ' + parm)
+ raise ValueError(msg)
+ rrulelines.append(line)
+ founddtstart = True
+ elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
+ rrulelines.append(line)
+ elif name == "TZOFFSETFROM":
+ if parms:
+ raise ValueError(
+ "unsupported %s parm: %s " % (name, parms[0]))
+ tzoffsetfrom = self._parse_offset(value)
+ elif name == "TZOFFSETTO":
+ if parms:
+ raise ValueError(
+ "unsupported TZOFFSETTO parm: "+parms[0])
+ tzoffsetto = self._parse_offset(value)
+ elif name == "TZNAME":
+ if parms:
+ raise ValueError(
+ "unsupported TZNAME parm: "+parms[0])
+ tzname = value
+ elif name == "COMMENT":
+ pass
+ else:
+ raise ValueError("unsupported property: "+name)
+ else:
+ if name == "TZID":
+ if parms:
+ raise ValueError(
+ "unsupported TZID parm: "+parms[0])
+ tzid = value
+ elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
+ pass
+ else:
+ raise ValueError("unsupported property: "+name)
+ elif name == "BEGIN" and value == "VTIMEZONE":
+ tzid = None
+ comps = []
+ invtz = True
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+if sys.platform != "win32":
+ TZFILES = ["/etc/localtime", "localtime"]
+ TZPATHS = ["/usr/share/zoneinfo",
+ "/usr/lib/zoneinfo",
+ "/usr/share/lib/zoneinfo",
+ "/etc/zoneinfo"]
+else:
+ TZFILES = []
+ TZPATHS = []
+
+
+def __get_gettz():
+ tzlocal_classes = (tzlocal,)
+ if tzwinlocal is not None:
+ tzlocal_classes += (tzwinlocal,)
+
+ class GettzFunc(object):
+ """
+ Retrieve a time zone object from a string representation
+
+ This function is intended to retrieve the :py:class:`tzinfo` subclass
+ that best represents the time zone that would be used if a POSIX
+ `TZ variable`_ were set to the same value.
+
+ If no argument or an empty string is passed to ``gettz``, local time
+ is returned:
+
+ .. code-block:: python3
+
+ >>> gettz()
+ tzfile('/etc/localtime')
+
+ This function is also the preferred way to map IANA tz database keys
+ to :class:`tzfile` objects:
+
+ .. code-block:: python3
+
+ >>> gettz('Pacific/Kiritimati')
+ tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
+
+ On Windows, the standard is extended to include the Windows-specific
+ zone names provided by the operating system:
+
+ .. code-block:: python3
+
+ >>> gettz('Egypt Standard Time')
+ tzwin('Egypt Standard Time')
+
+ Passing a GNU ``TZ`` style string time zone specification returns a
+ :class:`tzstr` object:
+
+ .. code-block:: python3
+
+ >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+ tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+
+ :param name:
+ A time zone name (IANA, or, on Windows, Windows keys), location of
+ a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
+ specifier. An empty string, no argument or ``None`` is interpreted
+ as local time.
+
+ :return:
+ Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
+ subclasses.
+
+ .. versionchanged:: 2.7.0
+
+ After version 2.7.0, any two calls to ``gettz`` using the same
+ input strings will return the same object:
+
+ .. code-block:: python3
+
+ >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
+ True
+
+ In addition to improving performance, this ensures that
+ `"same zone" semantics`_ are used for datetimes in the same zone.
+
+
+ .. _`TZ variable`:
+ https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+
+ .. _`"same zone" semantics`:
+ https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
+ """
+ def __init__(self):
+
+ self.__instances = weakref.WeakValueDictionary()
+ self.__strong_cache_size = 8
+ self.__strong_cache = OrderedDict()
+ self._cache_lock = _thread.allocate_lock()
+
+ def __call__(self, name=None):
+ with self._cache_lock:
+ rv = self.__instances.get(name, None)
+
+ if rv is None:
+ rv = self.nocache(name=name)
+ if not (name is None
+ or isinstance(rv, tzlocal_classes)
+ or rv is None):
+ # tzlocal is slightly more complicated than the other
+ # time zone providers because it depends on environment
+ # at construction time, so don't cache that.
+ #
+ # We also cannot store weak references to None, so we
+ # will also not store that.
+ self.__instances[name] = rv
+ else:
+ # No need for strong caching, return immediately
+ return rv
+
+ self.__strong_cache[name] = self.__strong_cache.pop(name, rv)
+
+ if len(self.__strong_cache) > self.__strong_cache_size:
+ self.__strong_cache.popitem(last=False)
+
+ return rv
+
+ def set_cache_size(self, size):
+ with self._cache_lock:
+ self.__strong_cache_size = size
+ while len(self.__strong_cache) > size:
+ self.__strong_cache.popitem(last=False)
+
+ def cache_clear(self):
+ with self._cache_lock:
+ self.__instances = weakref.WeakValueDictionary()
+ self.__strong_cache.clear()
+
+ @staticmethod
+ def nocache(name=None):
+ """A non-cached version of gettz"""
+ tz = None
+ if not name:
+ try:
+ name = os.environ["TZ"]
+ except KeyError:
+ pass
+ if name is None or name in ("", ":"):
+ for filepath in TZFILES:
+ if not os.path.isabs(filepath):
+ filename = filepath
+ for path in TZPATHS:
+ filepath = os.path.join(path, filename)
+ if os.path.isfile(filepath):
+ break
+ else:
+ continue
+ if os.path.isfile(filepath):
+ try:
+ tz = tzfile(filepath)
+ break
+ except (IOError, OSError, ValueError):
+ pass
+ else:
+ tz = tzlocal()
+ else:
+ try:
+ if name.startswith(":"):
+ name = name[1:]
+ except TypeError as e:
+ if isinstance(name, bytes):
+ new_msg = "gettz argument should be str, not bytes"
+ six.raise_from(TypeError(new_msg), e)
+ else:
+ raise
+ if os.path.isabs(name):
+ if os.path.isfile(name):
+ tz = tzfile(name)
+ else:
+ tz = None
+ else:
+ for path in TZPATHS:
+ filepath = os.path.join(path, name)
+ if not os.path.isfile(filepath):
+ filepath = filepath.replace(' ', '_')
+ if not os.path.isfile(filepath):
+ continue
+ try:
+ tz = tzfile(filepath)
+ break
+ except (IOError, OSError, ValueError):
+ pass
+ else:
+ tz = None
+ if tzwin is not None:
+ try:
+ tz = tzwin(name)
+ except (WindowsError, UnicodeEncodeError):
+ # UnicodeEncodeError is for Python 2.7 compat
+ tz = None
+
+ if not tz:
+ from dateutil.zoneinfo import get_zonefile_instance
+ tz = get_zonefile_instance().get(name)
+
+ if not tz:
+ for c in name:
+ # name is not a tzstr unless it has at least
+ # one offset. For short values of "name", an
+ # explicit for loop seems to be the fastest way
+ # To determine if a string contains a digit
+ if c in "0123456789":
+ try:
+ tz = tzstr(name)
+ except ValueError:
+ pass
+ break
+ else:
+ if name in ("GMT", "UTC"):
+ tz = UTC
+ elif name in time.tzname:
+ tz = tzlocal()
+ return tz
+
+ return GettzFunc()
+
+
+gettz = __get_gettz()
+del __get_gettz
+
+
+def datetime_exists(dt, tz=None):
+ """
+ Given a datetime and a time zone, determine whether or not a given datetime
+ would fall in a gap.
+
+ :param dt:
+ A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+ is provided.)
+
+ :param tz:
+ A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+ ``None`` or not provided, the datetime's own time zone will be used.
+
+ :return:
+ Returns a boolean value whether or not the "wall time" exists in
+ ``tz``.
+
+ .. versionadded:: 2.7.0
+ """
+ if tz is None:
+ if dt.tzinfo is None:
+ raise ValueError('Datetime is naive and no time zone provided.')
+ tz = dt.tzinfo
+
+ dt = dt.replace(tzinfo=None)
+
+ # This is essentially a test of whether or not the datetime can survive
+ # a round trip to UTC.
+ dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz)
+ dt_rt = dt_rt.replace(tzinfo=None)
+
+ return dt == dt_rt
+
+
+def datetime_ambiguous(dt, tz=None):
+ """
+ Given a datetime and a time zone, determine whether or not a given datetime
+ is ambiguous (i.e if there are two times differentiated only by their DST
+ status).
+
+ :param dt:
+ A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+ is provided.)
+
+ :param tz:
+ A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+ ``None`` or not provided, the datetime's own time zone will be used.
+
+ :return:
+ Returns a boolean value whether or not the "wall time" is ambiguous in
+ ``tz``.
+
+ .. versionadded:: 2.6.0
+ """
+ if tz is None:
+ if dt.tzinfo is None:
+ raise ValueError('Datetime is naive and no time zone provided.')
+
+ tz = dt.tzinfo
+
+ # If a time zone defines its own "is_ambiguous" function, we'll use that.
+ is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
+ if is_ambiguous_fn is not None:
+ try:
+ return tz.is_ambiguous(dt)
+ except Exception:
+ pass
+
+ # If it doesn't come out and tell us it's ambiguous, we'll just check if
+ # the fold attribute has any effect on this particular date and time.
+ dt = dt.replace(tzinfo=tz)
+ wall_0 = enfold(dt, fold=0)
+ wall_1 = enfold(dt, fold=1)
+
+ same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+ same_dst = wall_0.dst() == wall_1.dst()
+
+ return not (same_offset and same_dst)
+
+
+def resolve_imaginary(dt):
+ """
+ Given a datetime that may be imaginary, return an existing datetime.
+
+ This function assumes that an imaginary datetime represents what the
+ wall time would be in a zone had the offset transition not occurred, so
+ it will always fall forward by the transition's change in offset.
+
+ .. doctest::
+
+ >>> from dateutil import tz
+ >>> from datetime import datetime
+ >>> NYC = tz.gettz('America/New_York')
+ >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
+ 2017-03-12 03:30:00-04:00
+
+ >>> KIR = tz.gettz('Pacific/Kiritimati')
+ >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
+ 1995-01-02 12:30:00+14:00
+
+ As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
+ existing datetime, so a round-trip to and from UTC is sufficient to get
+ an extant datetime, however, this generally "falls back" to an earlier time
+ rather than falling forward to the STD side (though no guarantees are made
+ about this behavior).
+
+ :param dt:
+ A :class:`datetime.datetime` which may or may not exist.
+
+ :return:
+ Returns an existing :class:`datetime.datetime`. If ``dt`` was not
+ imaginary, the datetime returned is guaranteed to be the same object
+ passed to the function.
+
+ .. versionadded:: 2.7.0
+ """
+ if dt.tzinfo is not None and not datetime_exists(dt):
+
+ curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
+ old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
+
+ dt += curr_offset - old_offset
+
+ return dt
+
+
+def _datetime_to_timestamp(dt):
+ """
+ Convert a :class:`datetime.datetime` object to an epoch timestamp in
+ seconds since January 1, 1970, ignoring the time zone.
+ """
+ return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
+
+
+if sys.version_info >= (3, 6):
+ def _get_supported_offset(second_offset):
+ return second_offset
+else:
+ def _get_supported_offset(second_offset):
+ # For python pre-3.6, round to full-minutes if that's not the case.
+ # Python's datetime doesn't accept sub-minute timezones. Check
+ # http://python.org/sf/1447945 or https://bugs.python.org/issue5288
+ # for some information.
+ old_offset = second_offset
+ calculated_offset = 60 * ((second_offset + 30) // 60)
+ return calculated_offset
+
+
+try:
+ # Python 3.7 feature
+ from contextlib import nullcontext as _nullcontext
+except ImportError:
+ class _nullcontext(object):
+ """
+ Class for wrapping contexts so that they are passed through in a
+ with statement.
+ """
+ def __init__(self, context):
+ self.context = context
+
+ def __enter__(self):
+ return self.context
+
+ def __exit__(*args, **kwargs):
+ pass
+
+# vim:ts=4:sw=4:et
diff --git a/src/dateutil/tz/win.py b/src/dateutil/tz/win.py
new file mode 100644
index 0000000..cde07ba
--- /dev/null
+++ b/src/dateutil/tz/win.py
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+"""
+This module provides an interface to the native time zone data on Windows,
+including :py:class:`datetime.tzinfo` implementations.
+
+Attempting to import this module on a non-Windows platform will raise an
+:py:obj:`ImportError`.
+"""
+# This code was originally contributed by Jeffrey Harris.
+import datetime
+import struct
+
+from six.moves import winreg
+from six import text_type
+
+try:
+ import ctypes
+ from ctypes import wintypes
+except ValueError:
+ # ValueError is raised on non-Windows systems for some horrible reason.
+ raise ImportError("Running tzwin on non-Windows system")
+
+from ._common import tzrangebase
+
+__all__ = ["tzwin", "tzwinlocal", "tzres"]
+
+ONEWEEK = datetime.timedelta(7)
+
+TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
+TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+
+
+def _settzkeyname():
+ handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+ try:
+ winreg.OpenKey(handle, TZKEYNAMENT).Close()
+ TZKEYNAME = TZKEYNAMENT
+ except WindowsError:
+ TZKEYNAME = TZKEYNAME9X
+ handle.Close()
+ return TZKEYNAME
+
+
+TZKEYNAME = _settzkeyname()
+
+
+class tzres(object):
+ """
+ Class for accessing ``tzres.dll``, which contains timezone name related
+ resources.
+
+ .. versionadded:: 2.5.0
+ """
+ p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
+
+ def __init__(self, tzres_loc='tzres.dll'):
+ # Load the user32 DLL so we can load strings from tzres
+ user32 = ctypes.WinDLL('user32')
+
+ # Specify the LoadStringW function
+ user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
+ wintypes.UINT,
+ wintypes.LPWSTR,
+ ctypes.c_int)
+
+ self.LoadStringW = user32.LoadStringW
+ self._tzres = ctypes.WinDLL(tzres_loc)
+ self.tzres_loc = tzres_loc
+
+ def load_name(self, offset):
+ """
+ Load a timezone name from a DLL offset (integer).
+
+ >>> from dateutil.tzwin import tzres
+ >>> tzr = tzres()
+ >>> print(tzr.load_name(112))
+ 'Eastern Standard Time'
+
+ :param offset:
+ A positive integer value referring to a string from the tzres dll.
+
+ .. note::
+
+ Offsets found in the registry are generally of the form
+ ``@tzres.dll,-114``. The offset in this case is 114, not -114.
+
+ """
+ resource = self.p_wchar()
+ lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
+ nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
+ return resource[:nchar]
+
+ def name_from_string(self, tzname_str):
+ """
+ Parse strings as returned from the Windows registry into the time zone
+ name as defined in the registry.
+
+ >>> from dateutil.tzwin import tzres
+ >>> tzr = tzres()
+ >>> print(tzr.name_from_string('@tzres.dll,-251'))
+ 'Dateline Daylight Time'
+ >>> print(tzr.name_from_string('Eastern Standard Time'))
+ 'Eastern Standard Time'
+
+ :param tzname_str:
+ A timezone name string as returned from a Windows registry key.
+
+ :return:
+ Returns the localized timezone string from tzres.dll if the string
+ is of the form `@tzres.dll,-offset`, else returns the input string.
+ """
+ if not tzname_str.startswith('@'):
+ return tzname_str
+
+ name_splt = tzname_str.split(',-')
+ try:
+ offset = int(name_splt[1])
+ except:
+ raise ValueError("Malformed timezone string.")
+
+ return self.load_name(offset)
+
+
+class tzwinbase(tzrangebase):
+ """tzinfo class based on win32's timezones available in the registry."""
+ def __init__(self):
+ raise NotImplementedError('tzwinbase is an abstract base class')
+
+ def __eq__(self, other):
+ # Compare on all relevant dimensions, including name.
+ if not isinstance(other, tzwinbase):
+ return NotImplemented
+
+ return (self._std_offset == other._std_offset and
+ self._dst_offset == other._dst_offset and
+ self._stddayofweek == other._stddayofweek and
+ self._dstdayofweek == other._dstdayofweek and
+ self._stdweeknumber == other._stdweeknumber and
+ self._dstweeknumber == other._dstweeknumber and
+ self._stdhour == other._stdhour and
+ self._dsthour == other._dsthour and
+ self._stdminute == other._stdminute and
+ self._dstminute == other._dstminute and
+ self._std_abbr == other._std_abbr and
+ self._dst_abbr == other._dst_abbr)
+
+ @staticmethod
+ def list():
+ """Return a list of all time zones known to the system."""
+ with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+ with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
+ result = [winreg.EnumKey(tzkey, i)
+ for i in range(winreg.QueryInfoKey(tzkey)[0])]
+ return result
+
+ def display(self):
+ """
+ Return the display name of the time zone.
+ """
+ return self._display
+
+ def transitions(self, year):
+ """
+ For a given year, get the DST on and off transition times, expressed
+ always on the standard time side. For zones with no transitions, this
+ function returns ``None``.
+
+ :param year:
+ The year whose transitions you would like to query.
+
+ :return:
+ Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+ ``(dston, dstoff)`` for zones with an annual DST transition, or
+ ``None`` for fixed offset zones.
+ """
+
+ if not self.hasdst:
+ return None
+
+ dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
+ self._dsthour, self._dstminute,
+ self._dstweeknumber)
+
+ dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
+ self._stdhour, self._stdminute,
+ self._stdweeknumber)
+
+ # Ambiguous dates default to the STD side
+ dstoff -= self._dst_base_offset
+
+ return dston, dstoff
+
+ def _get_hasdst(self):
+ return self._dstmonth != 0
+
+ @property
+ def _dst_base_offset(self):
+ return self._dst_base_offset_
+
+
+class tzwin(tzwinbase):
+ """
+ Time zone object created from the zone info in the Windows registry
+
+ These are similar to :py:class:`dateutil.tz.tzrange` objects in that
+ the time zone data is provided in the format of a single offset rule
+ for either 0 or 2 time zone transitions per year.
+
+ :param: name
+ The name of a Windows time zone key, e.g. "Eastern Standard Time".
+ The full list of keys can be retrieved with :func:`tzwin.list`.
+ """
+
+ def __init__(self, name):
+ self._name = name
+
+ with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+ tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
+ with winreg.OpenKey(handle, tzkeyname) as tzkey:
+ keydict = valuestodict(tzkey)
+
+ self._std_abbr = keydict["Std"]
+ self._dst_abbr = keydict["Dlt"]
+
+ self._display = keydict["Display"]
+
+ # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+ tup = struct.unpack("=3l16h", keydict["TZI"])
+ stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
+ dstoffset = stdoffset-tup[2] # + DaylightBias * -1
+ self._std_offset = datetime.timedelta(minutes=stdoffset)
+ self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+ # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
+ (self._stdmonth,
+ self._stddayofweek, # Sunday = 0
+ self._stdweeknumber, # Last = 5
+ self._stdhour,
+ self._stdminute) = tup[4:9]
+
+ (self._dstmonth,
+ self._dstdayofweek, # Sunday = 0
+ self._dstweeknumber, # Last = 5
+ self._dsthour,
+ self._dstminute) = tup[12:17]
+
+ self._dst_base_offset_ = self._dst_offset - self._std_offset
+ self.hasdst = self._get_hasdst()
+
+ def __repr__(self):
+ return "tzwin(%s)" % repr(self._name)
+
+ def __reduce__(self):
+ return (self.__class__, (self._name,))
+
+
+class tzwinlocal(tzwinbase):
+ """
+ Class representing the local time zone information in the Windows registry
+
+ While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
+ module) to retrieve time zone information, ``tzwinlocal`` retrieves the
+ rules directly from the Windows registry and creates an object like
+ :class:`dateutil.tz.tzwin`.
+
+ Because Windows does not have an equivalent of :func:`time.tzset`, on
+ Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
+ time zone settings *at the time that the process was started*, meaning
+ changes to the machine's time zone settings during the run of a program
+ on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
+ Because ``tzwinlocal`` reads the registry directly, it is unaffected by
+ this issue.
+ """
+ def __init__(self):
+ with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+ with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
+ keydict = valuestodict(tzlocalkey)
+
+ self._std_abbr = keydict["StandardName"]
+ self._dst_abbr = keydict["DaylightName"]
+
+ try:
+ tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
+ sn=self._std_abbr)
+ with winreg.OpenKey(handle, tzkeyname) as tzkey:
+ _keydict = valuestodict(tzkey)
+ self._display = _keydict["Display"]
+ except OSError:
+ self._display = None
+
+ stdoffset = -keydict["Bias"]-keydict["StandardBias"]
+ dstoffset = stdoffset-keydict["DaylightBias"]
+
+ self._std_offset = datetime.timedelta(minutes=stdoffset)
+ self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+ # For reasons unclear, in this particular key, the day of week has been
+ # moved to the END of the SYSTEMTIME structure.
+ tup = struct.unpack("=8h", keydict["StandardStart"])
+
+ (self._stdmonth,
+ self._stdweeknumber, # Last = 5
+ self._stdhour,
+ self._stdminute) = tup[1:5]
+
+ self._stddayofweek = tup[7]
+
+ tup = struct.unpack("=8h", keydict["DaylightStart"])
+
+ (self._dstmonth,
+ self._dstweeknumber, # Last = 5
+ self._dsthour,
+ self._dstminute) = tup[1:5]
+
+ self._dstdayofweek = tup[7]
+
+ self._dst_base_offset_ = self._dst_offset - self._std_offset
+ self.hasdst = self._get_hasdst()
+
+ def __repr__(self):
+ return "tzwinlocal()"
+
+ def __str__(self):
+ # str will return the standard name, not the daylight name.
+ return "tzwinlocal(%s)" % repr(self._std_abbr)
+
+ def __reduce__(self):
+ return (self.__class__, ())
+
+
+def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
+ """ dayofweek == 0 means Sunday, whichweek 5 means last instance """
+ first = datetime.datetime(year, month, 1, hour, minute)
+
+ # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
+ # Because 7 % 7 = 0
+ weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
+ wd = weekdayone + ((whichweek - 1) * ONEWEEK)
+ if (wd.month != month):
+ wd -= ONEWEEK
+
+ return wd
+
+
+def valuestodict(key):
+ """Convert a registry key's values to a dictionary."""
+ dout = {}
+ size = winreg.QueryInfoKey(key)[1]
+ tz_res = None
+
+ for i in range(size):
+ key_name, value, dtype = winreg.EnumValue(key, i)
+ if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
+ # If it's a DWORD (32-bit integer), it's stored as unsigned - convert
+ # that to a proper signed integer
+ if value & (1 << 31):
+ value = value - (1 << 32)
+ elif dtype == winreg.REG_SZ:
+ # If it's a reference to the tzres DLL, load the actual string
+ if value.startswith('@tzres'):
+ tz_res = tz_res or tzres()
+ value = tz_res.name_from_string(value)
+
+ value = value.rstrip('\x00') # Remove trailing nulls
+
+ dout[key_name] = value
+
+ return dout
diff --git a/src/dateutil/tzwin.py b/src/dateutil/tzwin.py
new file mode 100644
index 0000000..cebc673
--- /dev/null
+++ b/src/dateutil/tzwin.py
@@ -0,0 +1,2 @@
+# tzwin has moved to dateutil.tz.win
+from .tz.win import *
diff --git a/src/dateutil/utils.py b/src/dateutil/utils.py
new file mode 100644
index 0000000..dd2d245
--- /dev/null
+++ b/src/dateutil/utils.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers general convenience and utility functions for dealing with
+datetimes.
+
+.. versionadded:: 2.7.0
+"""
+from __future__ import unicode_literals
+
+from datetime import datetime, time
+
+
+def today(tzinfo=None):
+ """
+ Returns a :py:class:`datetime` representing the current day at midnight
+
+ :param tzinfo:
+ The time zone to attach (also used to determine the current day).
+
+ :return:
+ A :py:class:`datetime.datetime` object representing the current day
+ at midnight.
+ """
+
+ dt = datetime.now(tzinfo)
+ return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
+
+
+def default_tzinfo(dt, tzinfo):
+ """
+ Sets the ``tzinfo`` parameter on naive datetimes only
+
+ This is useful for example when you are provided a datetime that may have
+ either an implicit or explicit time zone, such as when parsing a time zone
+ string.
+
+ .. doctest::
+
+ >>> from dateutil.tz import tzoffset
+ >>> from dateutil.parser import parse
+ >>> from dateutil.utils import default_tzinfo
+ >>> dflt_tz = tzoffset("EST", -18000)
+ >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
+ 2014-01-01 12:30:00+00:00
+ >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
+ 2014-01-01 12:30:00-05:00
+
+ :param dt:
+ The datetime on which to replace the time zone
+
+ :param tzinfo:
+ The :py:class:`datetime.tzinfo` subclass instance to assign to
+ ``dt`` if (and only if) it is naive.
+
+ :return:
+ Returns an aware :py:class:`datetime.datetime`.
+ """
+ if dt.tzinfo is not None:
+ return dt
+ else:
+ return dt.replace(tzinfo=tzinfo)
+
+
+def within_delta(dt1, dt2, delta):
+ """
+ Useful for comparing two datetimes that may have a negligible difference
+ to be considered equal.
+ """
+ delta = abs(delta)
+ difference = dt1 - dt2
+ return -delta <= difference <= delta
diff --git a/src/dateutil/zoneinfo/__init__.py b/src/dateutil/zoneinfo/__init__.py
new file mode 100644
index 0000000..34f11ad
--- /dev/null
+++ b/src/dateutil/zoneinfo/__init__.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+import warnings
+import json
+
+from tarfile import TarFile
+from pkgutil import get_data
+from io import BytesIO
+
+from dateutil.tz import tzfile as _tzfile
+
+__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
+
+ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
+METADATA_FN = 'METADATA'
+
+
+class tzfile(_tzfile):
+ def __reduce__(self):
+ return (gettz, (self._filename,))
+
+
+def getzoneinfofile_stream():
+ try:
+ return BytesIO(get_data(__name__, ZONEFILENAME))
+ except IOError as e: # TODO switch to FileNotFoundError?
+ warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
+ return None
+
+
+class ZoneInfoFile(object):
+ def __init__(self, zonefile_stream=None):
+ if zonefile_stream is not None:
+ with TarFile.open(fileobj=zonefile_stream) as tf:
+ self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
+ for zf in tf.getmembers()
+ if zf.isfile() and zf.name != METADATA_FN}
+ # deal with links: They'll point to their parent object. Less
+ # waste of memory
+ links = {zl.name: self.zones[zl.linkname]
+ for zl in tf.getmembers() if
+ zl.islnk() or zl.issym()}
+ self.zones.update(links)
+ try:
+ metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
+ metadata_str = metadata_json.read().decode('UTF-8')
+ self.metadata = json.loads(metadata_str)
+ except KeyError:
+ # no metadata in tar file
+ self.metadata = None
+ else:
+ self.zones = {}
+ self.metadata = None
+
+ def get(self, name, default=None):
+ """
+ Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
+ for retrieving zones from the zone dictionary.
+
+ :param name:
+ The name of the zone to retrieve. (Generally IANA zone names)
+
+ :param default:
+ The value to return in the event of a missing key.
+
+ .. versionadded:: 2.6.0
+
+ """
+ return self.zones.get(name, default)
+
+
+# The current API has gettz as a module function, although in fact it taps into
+# a stateful class. So as a workaround for now, without changing the API, we
+# will create a new "global" class instance the first time a user requests a
+# timezone. Ugly, but adheres to the api.
+#
+# TODO: Remove after deprecation period.
+_CLASS_ZONE_INSTANCE = []
+
+
+def get_zonefile_instance(new_instance=False):
+ """
+ This is a convenience function which provides a :class:`ZoneInfoFile`
+ instance using the data provided by the ``dateutil`` package. By default, it
+ caches a single instance of the ZoneInfoFile object and returns that.
+
+ :param new_instance:
+ If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
+ used as the cached instance for the next call. Otherwise, new instances
+ are created only as necessary.
+
+ :return:
+ Returns a :class:`ZoneInfoFile` object.
+
+ .. versionadded:: 2.6
+ """
+ if new_instance:
+ zif = None
+ else:
+ zif = getattr(get_zonefile_instance, '_cached_instance', None)
+
+ if zif is None:
+ zif = ZoneInfoFile(getzoneinfofile_stream())
+
+ get_zonefile_instance._cached_instance = zif
+
+ return zif
+
+
+def gettz(name):
+ """
+ This retrieves a time zone from the local zoneinfo tarball that is packaged
+ with dateutil.
+
+ :param name:
+ An IANA-style time zone name, as found in the zoneinfo file.
+
+ :return:
+ Returns a :class:`dateutil.tz.tzfile` time zone object.
+
+ .. warning::
+ It is generally inadvisable to use this function, and it is only
+ provided for API compatibility with earlier versions. This is *not*
+ equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
+ time zone based on the inputs, favoring system zoneinfo. This is ONLY
+ for accessing the dateutil-specific zoneinfo (which may be out of
+ date compared to the system zoneinfo).
+
+ .. deprecated:: 2.6
+ If you need to use a specific zoneinfofile over the system zoneinfo,
+ instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
+ :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
+
+ Use :func:`get_zonefile_instance` to retrieve an instance of the
+ dateutil-provided zoneinfo.
+ """
+ warnings.warn("zoneinfo.gettz() will be removed in future versions, "
+ "to use the dateutil-provided zoneinfo files, instantiate a "
+ "ZoneInfoFile object and use ZoneInfoFile.zones.get() "
+ "instead. See the documentation for details.",
+ DeprecationWarning)
+
+ if len(_CLASS_ZONE_INSTANCE) == 0:
+ _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+ return _CLASS_ZONE_INSTANCE[0].zones.get(name)
+
+
+def gettz_db_metadata():
+ """ Get the zonefile metadata
+
+ See `zonefile_metadata`_
+
+ :returns:
+ A dictionary with the database metadata
+
+ .. deprecated:: 2.6
+ See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
+ query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
+ """
+ warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
+ "versions, to use the dateutil-provided zoneinfo files, "
+ "ZoneInfoFile object and query the 'metadata' attribute "
+ "instead. See the documentation for details.",
+ DeprecationWarning)
+
+ if len(_CLASS_ZONE_INSTANCE) == 0:
+ _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+ return _CLASS_ZONE_INSTANCE[0].metadata
diff --git a/src/dateutil/zoneinfo/rebuild.py b/src/dateutil/zoneinfo/rebuild.py
new file mode 100644
index 0000000..684c658
--- /dev/null
+++ b/src/dateutil/zoneinfo/rebuild.py
@@ -0,0 +1,75 @@
+import logging
+import os
+import tempfile
+import shutil
+import json
+from subprocess import check_call, check_output
+from tarfile import TarFile
+
+from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
+
+
+def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
+ """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
+
+ filename is the timezone tarball from ``ftp.iana.org/tz``.
+
+ """
+ tmpdir = tempfile.mkdtemp()
+ zonedir = os.path.join(tmpdir, "zoneinfo")
+ moduledir = os.path.dirname(__file__)
+ try:
+ with TarFile.open(filename) as tf:
+ for name in zonegroups:
+ tf.extract(name, tmpdir)
+ filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
+
+ _run_zic(zonedir, filepaths)
+
+ # write metadata file
+ with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
+ json.dump(metadata, f, indent=4, sort_keys=True)
+ target = os.path.join(moduledir, ZONEFILENAME)
+ with TarFile.open(target, "w:%s" % format) as tf:
+ for entry in os.listdir(zonedir):
+ entrypath = os.path.join(zonedir, entry)
+ tf.add(entrypath, entry)
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+def _run_zic(zonedir, filepaths):
+ """Calls the ``zic`` compiler in a compatible way to get a "fat" binary.
+
+ Recent versions of ``zic`` default to ``-b slim``, while older versions
+ don't even have the ``-b`` option (but default to "fat" binaries). The
+ current version of dateutil does not support Version 2+ TZif files, which
+ causes problems when used in conjunction with "slim" binaries, so this
+ function is used to ensure that we always get a "fat" binary.
+ """
+
+ try:
+ help_text = check_output(["zic", "--help"])
+ except OSError as e:
+ _print_on_nosuchfile(e)
+ raise
+
+ if b"-b " in help_text:
+ bloat_args = ["-b", "fat"]
+ else:
+ bloat_args = []
+
+ check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths)
+
+
+def _print_on_nosuchfile(e):
+ """Print helpful troubleshooting message
+
+ e is an exception raised by subprocess.check_call()
+
+ """
+ if e.errno == 2:
+ logging.error(
+ "Could not find zic. Perhaps you need to install "
+ "libc-bin or some other package that provides it, "
+ "or it's not in your PATH?")