├── dateutil ├── test │ └── __init__.py ├── __init__.py ├── parser.py ├── easter.py ├── zoneinfo │ └── __init__.py ├── tzwin.py ├── relativedelta.py ├── tz.py └── rrule.py ├── setup.cfg ├── NEWS ├── MANIFEST.in ├── docs ├── tz.rst ├── easter.rst ├── parser.rst ├── zoneinfo.rst ├── relativedelta.rst ├── rrule.rst ├── index.rst ├── Makefile ├── make.bat ├── conf.py └── examples.rst ├── tox.ini ├── .gitignore ├── .travis.yml ├── appveyor.yml ├── zonefile_metadata.json ├── updatezinfo.py ├── LICENSE ├── setup.py └── README.rst /dateutil/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/dateutil/master/NEWS -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE NEWS zonefile_metadata.json updatezinfo.py 2 | -------------------------------------------------------------------------------- /dateutil/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = "2.4.2" 3 | -------------------------------------------------------------------------------- /dateutil/parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/dateutil/master/dateutil/parser.py -------------------------------------------------------------------------------- /docs/tz.rst: -------------------------------------------------------------------------------- 1 | == 2 | tz 3 | == 4 | .. automodule:: dateutil.tz 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/easter.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | easter 3 | ====== 4 | .. automodule:: dateutil.easter 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/parser.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | parser 3 | ====== 4 | .. automodule:: dateutil.parser 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32, py33, py34 3 | 4 | [testenv] 5 | commands = python setup.py test -q 6 | deps = six 7 | -------------------------------------------------------------------------------- /docs/zoneinfo.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | zoneinfo 3 | ======== 4 | .. automodule:: dateutil.zoneinfo 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/relativedelta.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | relativedelta 3 | ============= 4 | .. automodule:: dateutil.relativedelta 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | build/ 6 | dist/ 7 | *.egg-info/ 8 | .tox/ 9 | 10 | # Sphinx documentation 11 | docs/_build/ 12 | -------------------------------------------------------------------------------- /docs/rrule.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | rrule 3 | ===== 4 | The rrule module offers a small, complete, and very fast, implementation of the recurrence rules documented in the iCalendar RFC, including support for caching of results. 5 | 6 | .. automodule:: dateutil.rrule 7 | :members: 8 | :undoc-members: 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.2" 6 | - "3.3" 7 | - "3.4" 8 | - "pypy" 9 | - "pypy3" 10 | install: 11 | - pip install six 12 | - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi 13 | - python updatezinfo.py 14 | script: 15 | - python setup.py test 16 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | environment: 3 | matrix: 4 | - PYTHON: "C:/Python27" 5 | - PYTHON: "C:/Python27-x64" 6 | - PYTHON: "C:/Python33" 7 | - PYTHON: "C:/Python33-x64" 8 | - PYTHON: "C:/Python34" 9 | - PYTHON: "C:/Python34-x64" 10 | install: 11 | - ps: Start-FileDownload 'https://raw.github.com/pypa/pip/master/contrib/get-pip.py' 12 | - "%PYTHON%/python.exe get-pip.py" 13 | - "%PYTHON%/Scripts/pip.exe install six" 14 | # use postgres' zic 15 | - set path=c:\Program Files\PostgreSQL\9.3\bin\;%PATH% 16 | - "%PYTHON%/python.exe updatezinfo.py" 17 | test_script: 18 | - "%PYTHON%/python.exe setup.py test" 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. dateutil documentation master file, created by 2 | sphinx-quickstart on Thu Nov 20 23:18:41 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | .. include:: ../README.rst 8 | 9 | Documentation 10 | ============= 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | self 17 | easter 18 | parser 19 | relativedelta 20 | rrule 21 | tz 22 | zoneinfo 23 | examples 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | 32 | -------------------------------------------------------------------------------- /zonefile_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata_version" : 0.1, 3 | "releases_url" : "ftp://ftp.iana.org/tz/releases/", 4 | "tzdata_file" : "tzdata2015b.tar.gz", 5 | "tzdata_file_sha512" : "767782b87e62a8f7a4dbcae595d16a54197c9e04ca974d7016d11f90ebaf2537b804d111f204af9052c68d4670afe0af0af9e5b150867a357fc199bb541368d0", 6 | "zonegroups" : [ 7 | "africa", 8 | "antarctica", 9 | "asia", 10 | "australasia", 11 | "europe", 12 | "northamerica", 13 | "southamerica", 14 | "pacificnew", 15 | "etcetera", 16 | "systemv", 17 | "factory", 18 | "backzone", 19 | "backward"] 20 | } 21 | 22 | -------------------------------------------------------------------------------- /updatezinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import hashlib 4 | import json 5 | import io 6 | 7 | from six.moves.urllib import request 8 | 9 | from dateutil.zoneinfo import rebuild 10 | 11 | METADATA_FILE = "zonefile_metadata.json" 12 | 13 | 14 | def main(): 15 | with io.open(METADATA_FILE, 'r') as f: 16 | metadata = json.load(f) 17 | 18 | if not os.path.isfile(metadata['tzdata_file']): 19 | print("Downloading tz file from iana") 20 | request.urlretrieve(os.path.join(metadata['releases_url'], 21 | metadata['tzdata_file']), 22 | metadata['tzdata_file']) 23 | with open(metadata['tzdata_file'], 'rb') as tzfile: 24 | sha_hasher = hashlib.sha512() 25 | sha_hasher.update(tzfile.read()) 26 | sha_512_file = sha_hasher.hexdigest() 27 | assert metadata['tzdata_file_sha512'] == sha_512_file, "SHA failed for" 28 | print("Updating timezone information...") 29 | rebuild(metadata['tzdata_file'], zonegroups=metadata['zonegroups']) 30 | print("Done.") 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | dateutil - Extensions to the standard Python datetime module. 2 | 3 | Copyright (c) 2003-2011 - Gustavo Niemeyer 4 | Copyright (c) 2012-2014 - Tomi Pieviläinen 5 | Copyright (c) 2014 - Yaron de Leeuw 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 25 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 26 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 27 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from os.path import isfile 3 | import codecs 4 | import os 5 | import re 6 | 7 | from setuptools import setup 8 | 9 | 10 | if isfile("MANIFEST"): 11 | os.unlink("MANIFEST") 12 | 13 | 14 | TOPDIR = os.path.dirname(__file__) or "." 15 | VERSION = re.search('__version__ = "([^"]+)"', 16 | codecs.open(TOPDIR + "/dateutil/__init__.py", 17 | encoding='utf-8').read()).group(1) 18 | 19 | 20 | setup(name="python-dateutil", 21 | version=VERSION, 22 | description="Extensions to the standard Python datetime module", 23 | author="Yaron de Leeuw", 24 | author_email="me@jarondl.net", 25 | url="https://dateutil.readthedocs.org", 26 | license="Simplified BSD", 27 | long_description=""" 28 | The dateutil module provides powerful extensions to the 29 | datetime module available in the Python standard library. 30 | """, 31 | packages=["dateutil", "dateutil.zoneinfo"], 32 | package_data={"dateutil.zoneinfo": ["dateutil-zoneinfo.tar.gz"]}, 33 | zip_safe=True, 34 | requires=["six"], 35 | install_requires=["six >=1.5"], # XXX fix when packaging is sane again 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Intended Audience :: Developers', 39 | 'License :: OSI Approved :: BSD License', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.2', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Topic :: Software Development :: Libraries', 49 | ], 50 | test_suite="dateutil.test.test" 51 | ) 52 | -------------------------------------------------------------------------------- /dateutil/easter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module offers a generic easter computing method for any given year, using 4 | Western, Orthodox or Julian algorithms. 5 | """ 6 | 7 | import datetime 8 | 9 | __all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] 10 | 11 | EASTER_JULIAN = 1 12 | EASTER_ORTHODOX = 2 13 | EASTER_WESTERN = 3 14 | 15 | 16 | def easter(year, method=EASTER_WESTERN): 17 | """ 18 | This method was ported from the work done by GM Arts, 19 | on top of the algorithm by Claus Tondering, which was 20 | based in part on the algorithm of Ouding (1940), as 21 | quoted in "Explanatory Supplement to the Astronomical 22 | Almanac", P. Kenneth Seidelmann, editor. 23 | 24 | This algorithm implements three different easter 25 | calculation methods: 26 | 27 | 1 - Original calculation in Julian calendar, valid in 28 | dates after 326 AD 29 | 2 - Original method, with date converted to Gregorian 30 | calendar, valid in years 1583 to 4099 31 | 3 - Revised method, in Gregorian calendar, valid in 32 | years 1583 to 4099 as well 33 | 34 | These methods are represented by the constants: 35 | 36 | EASTER_JULIAN = 1 37 | EASTER_ORTHODOX = 2 38 | EASTER_WESTERN = 3 39 | 40 | The default method is method 3. 41 | 42 | More about the algorithm may be found at: 43 | 44 | http://users.chariot.net.au/~gmarts/eastalg.htm 45 | 46 | and 47 | 48 | http://www.tondering.dk/claus/calendar.html 49 | 50 | """ 51 | 52 | if not (1 <= method <= 3): 53 | raise ValueError("invalid method") 54 | 55 | # g - Golden year - 1 56 | # c - Century 57 | # h - (23 - Epact) mod 30 58 | # i - Number of days from March 21 to Paschal Full Moon 59 | # j - Weekday for PFM (0=Sunday, etc) 60 | # p - Number of days from March 21 to Sunday on or before PFM 61 | # (-6 to 28 methods 1 & 3, to 56 for method 2) 62 | # e - Extra days to add for method 2 (converting Julian 63 | # date to Gregorian date) 64 | 65 | y = year 66 | g = y % 19 67 | e = 0 68 | if method < 3: 69 | # Old method 70 | i = (19*g + 15) % 30 71 | j = (y + y//4 + i) % 7 72 | if method == 2: 73 | # Extra dates to convert Julian to Gregorian date 74 | e = 10 75 | if y > 1600: 76 | e = e + y//100 - 16 - (y//100 - 16)//4 77 | else: 78 | # New method 79 | c = y//100 80 | h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 81 | i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) 82 | j = (y + y//4 + i + 2 - c + c//4) % 7 83 | 84 | # p can be from -6 to 56 corresponding to dates 22 March to 23 May 85 | # (later dates apply to method 2, although 23 May never actually occurs) 86 | p = i - j + e 87 | d = 1 + (p + 27 + (p + 6)//40) % 31 88 | m = 3 + (p + 26)//30 89 | return datetime.date(int(y), int(m), int(d)) 90 | -------------------------------------------------------------------------------- /dateutil/zoneinfo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import warnings 5 | import tempfile 6 | import shutil 7 | from subprocess import check_call 8 | from tarfile import TarFile 9 | from pkgutil import get_data 10 | from io import BytesIO 11 | from contextlib import closing 12 | 13 | from dateutil.tz import tzfile 14 | 15 | __all__ = ["gettz", "rebuild"] 16 | 17 | _ZONEFILENAME = "dateutil-zoneinfo.tar.gz" 18 | 19 | # python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but 20 | # it's close enough for python2.6 21 | _tar_open = TarFile.open 22 | if not hasattr(TarFile, '__exit__'): 23 | def _tar_open(*args, **kwargs): 24 | return closing(TarFile.open(*args, **kwargs)) 25 | 26 | 27 | class tzfile(tzfile): 28 | def __reduce__(self): 29 | return (gettz, (self._filename,)) 30 | 31 | 32 | def getzoneinfofile_stream(): 33 | try: 34 | return BytesIO(get_data(__name__, _ZONEFILENAME)) 35 | except IOError as e: # TODO switch to FileNotFoundError? 36 | warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) 37 | return None 38 | 39 | 40 | class ZoneInfoFile(object): 41 | def __init__(self, zonefile_stream=None): 42 | if zonefile_stream is not None: 43 | with _tar_open(fileobj=zonefile_stream, mode='r') as tf: 44 | # dict comprehension does not work on python2.6 45 | # TODO: get back to the nicer syntax when we ditch python2.6 46 | # self.zones = {zf.name: tzfile(tf.extractfile(zf), 47 | # filename = zf.name) 48 | # for zf in tf.getmembers() if zf.isfile()} 49 | self.zones = dict((zf.name, tzfile(tf.extractfile(zf), 50 | filename=zf.name)) 51 | for zf in tf.getmembers() if zf.isfile()) 52 | # deal with links: They'll point to their parent object. Less 53 | # waste of memory 54 | # links = {zl.name: self.zones[zl.linkname] 55 | # for zl in tf.getmembers() if zl.islnk() or zl.issym()} 56 | links = dict((zl.name, self.zones[zl.linkname]) 57 | for zl in tf.getmembers() if 58 | zl.islnk() or zl.issym()) 59 | self.zones.update(links) 60 | else: 61 | self.zones = dict() 62 | 63 | 64 | # The current API has gettz as a module function, although in fact it taps into 65 | # a stateful class. So as a workaround for now, without changing the API, we 66 | # will create a new "global" class instance the first time a user requests a 67 | # timezone. Ugly, but adheres to the api. 68 | # 69 | # TODO: deprecate this. 70 | _CLASS_ZONE_INSTANCE = list() 71 | 72 | 73 | def gettz(name): 74 | if len(_CLASS_ZONE_INSTANCE) == 0: 75 | _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) 76 | return _CLASS_ZONE_INSTANCE[0].zones.get(name) 77 | 78 | 79 | def rebuild(filename, tag=None, format="gz", zonegroups=[]): 80 | """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* 81 | 82 | filename is the timezone tarball from ftp.iana.org/tz. 83 | 84 | """ 85 | tmpdir = tempfile.mkdtemp() 86 | zonedir = os.path.join(tmpdir, "zoneinfo") 87 | moduledir = os.path.dirname(__file__) 88 | try: 89 | with _tar_open(filename) as tf: 90 | for name in zonegroups: 91 | tf.extract(name, tmpdir) 92 | filepaths = [os.path.join(tmpdir, n) for n in zonegroups] 93 | try: 94 | check_call(["zic", "-d", zonedir] + filepaths) 95 | except OSError as e: 96 | if e.errno == 2: 97 | logging.error( 98 | "Could not find zic. Perhaps you need to install " 99 | "libc-bin or some other package that provides it, " 100 | "or it's not in your PATH?") 101 | raise 102 | target = os.path.join(moduledir, _ZONEFILENAME) 103 | with _tar_open(target, "w:%s" % format) as tf: 104 | for entry in os.listdir(zonedir): 105 | entrypath = os.path.join(zonedir, entry) 106 | tf.add(entrypath, entry) 107 | finally: 108 | shutil.rmtree(tmpdir) 109 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | dateutil - powerful extensions to datetime 2 | ========================================== 3 | 4 | .. image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square 5 | :target: https://travis-ci.org/dateutil/dateutil 6 | :alt: travis build status 7 | 8 | .. image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square 9 | :target: https://ci.appveyor.com/project/dateutil/dateutil 10 | :alt: appveyor build status 11 | 12 | .. image:: https://img.shields.io/pypi/dd/python-dateutil.svg?style=flat-square 13 | :target: https://pypi.python.org/pypi/python-dateutil/ 14 | :alt: pypi downloads per day 15 | 16 | .. image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square 17 | :target: https://pypi.python.org/pypi/python-dateutil/ 18 | :alt: pypi version 19 | 20 | 21 | 22 | The `dateutil` module provides powerful extensions to 23 | the standard `datetime` module, available in Python. 24 | 25 | 26 | Download 27 | ======== 28 | dateutil is available on PyPI 29 | https://pypi.python.org/pypi/python-dateutil/ 30 | 31 | The documentation is hosted at: 32 | https://dateutil.readthedocs.org/ 33 | 34 | Code 35 | ==== 36 | https://github.com/dateutil/dateutil/ 37 | 38 | Features 39 | ======== 40 | 41 | * Computing of relative deltas (next month, next year, 42 | next monday, last week of month, etc); 43 | * Computing of relative deltas between two given 44 | date and/or datetime objects; 45 | * Computing of dates based on very flexible recurrence rules, 46 | using a superset of the `iCalendar `_ 47 | specification. Parsing of RFC strings is supported as well. 48 | * Generic parsing of dates in almost any string format; 49 | * Timezone (tzinfo) implementations for tzfile(5) format 50 | files (/etc/localtime, /usr/share/zoneinfo, etc), TZ 51 | environment string (in all known formats), iCalendar 52 | format files, given ranges (with help from relative deltas), 53 | local machine timezone, fixed offset timezone, UTC timezone, 54 | and Windows registry-based time zones. 55 | * Internal up-to-date world timezone information based on 56 | Olson's database. 57 | * Computing of Easter Sunday dates for any given year, 58 | using Western, Orthodox or Julian algorithms; 59 | * More than 400 test cases. 60 | 61 | Quick example 62 | ============= 63 | Here's a snapshot, just to give an idea about the power of the 64 | package. For more examples, look at the documentation. 65 | 66 | Suppose you want to know how much time is left, in 67 | years/months/days/etc, before the next easter happening on a 68 | year with a Friday 13th in August, and you want to get today's 69 | date out of the "date" unix system command. Here is the code: 70 | 71 | .. doctest:: readmeexample 72 | 73 | >>> from dateutil.relativedelta import * 74 | >>> from dateutil.easter import * 75 | >>> from dateutil.rrule import * 76 | >>> from dateutil.parser import * 77 | >>> from datetime import * 78 | >>> now = parse("Sat Oct 11 17:13:46 UTC 2003") 79 | >>> today = now.date() 80 | >>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year 81 | >>> rdelta = relativedelta(easter(year), today) 82 | >>> print("Today is: %s" % today) 83 | Today is: 2003-10-11 84 | >>> print("Year with next Aug 13th on a Friday is: %s" % year) 85 | Year with next Aug 13th on a Friday is: 2004 86 | >>> print("How far is the Easter of that year: %s" % rdelta) 87 | How far is the Easter of that year: relativedelta(months=+6) 88 | >>> print("And the Easter of that year is: %s" % (today+rdelta)) 89 | And the Easter of that year is: 2004-04-11 90 | 91 | Being exactly 6 months ahead was **really** a coincidence :) 92 | 93 | 94 | Author 95 | ====== 96 | The dateutil module was written by Gustavo Niemeyer 97 | in 2003 98 | 99 | It is maintained by: 100 | 101 | * Gustavo Niemeyer 2003-2011 102 | * Tomi Pieviläinen 2012-2014 103 | * Yaron de Leeuw 2014- 104 | 105 | Building and releasing 106 | ====================== 107 | When you get the source, it does not contain the internal zoneinfo 108 | database. To get (and update) the database, run the updatezinfo.py script. Make sure 109 | that the zic command is in your path, and that you have network connectivity 110 | to get the latest timezone information from IANA. If you have downloaded 111 | the timezone data earlier, you can give the tarball as a parameter to 112 | updatezinfo.py. 113 | 114 | 115 | Testing 116 | ======= 117 | dateutil has a comprehensive test suite, which can be run simply by running 118 | `python setup.py test [-q]` in the project root. Note that if you don't have the internal 119 | zoneinfo database, some tests will fail. Apart from that, all tests should pass. 120 | 121 | To easily test dateutil against all supported Python versions, you can use 122 | `tox `_. 123 | 124 | All github pull requests are automatically tested using travis. 125 | -------------------------------------------------------------------------------- /dateutil/tzwin.py: -------------------------------------------------------------------------------- 1 | # This code was originally contributed by Jeffrey Harris. 2 | import datetime 3 | import struct 4 | 5 | from six.moves import winreg 6 | 7 | __all__ = ["tzwin", "tzwinlocal"] 8 | 9 | ONEWEEK = datetime.timedelta(7) 10 | 11 | TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" 12 | TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" 13 | TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" 14 | 15 | 16 | def _settzkeyname(): 17 | handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 18 | try: 19 | winreg.OpenKey(handle, TZKEYNAMENT).Close() 20 | TZKEYNAME = TZKEYNAMENT 21 | except WindowsError: 22 | TZKEYNAME = TZKEYNAME9X 23 | handle.Close() 24 | return TZKEYNAME 25 | 26 | TZKEYNAME = _settzkeyname() 27 | 28 | 29 | class tzwinbase(datetime.tzinfo): 30 | """tzinfo class based on win32's timezones available in the registry.""" 31 | 32 | def utcoffset(self, dt): 33 | if self._isdst(dt): 34 | return datetime.timedelta(minutes=self._dstoffset) 35 | else: 36 | return datetime.timedelta(minutes=self._stdoffset) 37 | 38 | def dst(self, dt): 39 | if self._isdst(dt): 40 | minutes = self._dstoffset - self._stdoffset 41 | return datetime.timedelta(minutes=minutes) 42 | else: 43 | return datetime.timedelta(0) 44 | 45 | def tzname(self, dt): 46 | if self._isdst(dt): 47 | return self._dstname 48 | else: 49 | return self._stdname 50 | 51 | def list(): 52 | """Return a list of all time zones known to the system.""" 53 | handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 54 | tzkey = winreg.OpenKey(handle, TZKEYNAME) 55 | result = [winreg.EnumKey(tzkey, i) 56 | for i in range(winreg.QueryInfoKey(tzkey)[0])] 57 | tzkey.Close() 58 | handle.Close() 59 | return result 60 | list = staticmethod(list) 61 | 62 | def display(self): 63 | return self._display 64 | 65 | def _isdst(self, dt): 66 | if not self._dstmonth: 67 | # dstmonth == 0 signals the zone has no daylight saving time 68 | return False 69 | dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, 70 | self._dsthour, self._dstminute, 71 | self._dstweeknumber) 72 | dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, 73 | self._stdhour, self._stdminute, 74 | self._stdweeknumber) 75 | if dston < dstoff: 76 | return dston <= dt.replace(tzinfo=None) < dstoff 77 | else: 78 | return not dstoff <= dt.replace(tzinfo=None) < dston 79 | 80 | 81 | class tzwin(tzwinbase): 82 | 83 | def __init__(self, name): 84 | self._name = name 85 | 86 | # multiple contexts only possible in 2.7 and 3.1, we still support 2.6 87 | with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: 88 | with winreg.OpenKey(handle, 89 | "%s\%s" % (TZKEYNAME, name)) as tzkey: 90 | keydict = valuestodict(tzkey) 91 | 92 | self._stdname = keydict["Std"].encode("iso-8859-1") 93 | self._dstname = keydict["Dlt"].encode("iso-8859-1") 94 | 95 | self._display = keydict["Display"] 96 | 97 | # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm 98 | tup = struct.unpack("=3l16h", keydict["TZI"]) 99 | self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 100 | self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 101 | 102 | # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs 103 | # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx 104 | (self._stdmonth, 105 | self._stddayofweek, # Sunday = 0 106 | self._stdweeknumber, # Last = 5 107 | self._stdhour, 108 | self._stdminute) = tup[4:9] 109 | 110 | (self._dstmonth, 111 | self._dstdayofweek, # Sunday = 0 112 | self._dstweeknumber, # Last = 5 113 | self._dsthour, 114 | self._dstminute) = tup[12:17] 115 | 116 | def __repr__(self): 117 | return "tzwin(%s)" % repr(self._name) 118 | 119 | def __reduce__(self): 120 | return (self.__class__, (self._name,)) 121 | 122 | 123 | class tzwinlocal(tzwinbase): 124 | 125 | def __init__(self): 126 | 127 | with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: 128 | 129 | with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: 130 | keydict = valuestodict(tzlocalkey) 131 | 132 | self._stdname = keydict["StandardName"].encode("iso-8859-1") 133 | self._dstname = keydict["DaylightName"].encode("iso-8859-1") 134 | 135 | try: 136 | with winreg.OpenKey( 137 | handle, "%s\%s" % (TZKEYNAME, self._stdname)) as tzkey: 138 | _keydict = valuestodict(tzkey) 139 | self._display = _keydict["Display"] 140 | except OSError: 141 | self._display = None 142 | 143 | self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] 144 | self._dstoffset = self._stdoffset-keydict["DaylightBias"] 145 | 146 | # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm 147 | tup = struct.unpack("=8h", keydict["StandardStart"]) 148 | 149 | (self._stdmonth, 150 | self._stddayofweek, # Sunday = 0 151 | self._stdweeknumber, # Last = 5 152 | self._stdhour, 153 | self._stdminute) = tup[1:6] 154 | 155 | tup = struct.unpack("=8h", keydict["DaylightStart"]) 156 | 157 | (self._dstmonth, 158 | self._dstdayofweek, # Sunday = 0 159 | self._dstweeknumber, # Last = 5 160 | self._dsthour, 161 | self._dstminute) = tup[1:6] 162 | 163 | def __reduce__(self): 164 | return (self.__class__, ()) 165 | 166 | 167 | def picknthweekday(year, month, dayofweek, hour, minute, whichweek): 168 | """dayofweek == 0 means Sunday, whichweek 5 means last instance""" 169 | first = datetime.datetime(year, month, 1, hour, minute) 170 | weekdayone = first.replace(day=((dayofweek-first.isoweekday()) % 7+1)) 171 | for n in range(whichweek): 172 | dt = weekdayone+(whichweek-n)*ONEWEEK 173 | if dt.month == month: 174 | return dt 175 | 176 | 177 | def valuestodict(key): 178 | """Convert a registry key's values to a dictionary.""" 179 | dict = {} 180 | size = winreg.QueryInfoKey(key)[1] 181 | for i in range(size): 182 | data = winreg.EnumValue(key, i) 183 | dict[data[0]] = data[1] 184 | return dict 185 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dateutil.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dateutil.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/dateutil" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dateutil" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\dateutil.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\dateutil.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # dateutil documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Nov 20 23:18:41 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | sys.path.insert(0, os.path.abspath('../')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = 'dateutil' 53 | copyright = '2015, dateutil' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '2.4.2' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '2.4.2' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | html_theme = 'default' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # Add any extra paths that contain custom files (such as robots.txt or 139 | # .htaccess) here, relative to this directory. These files are copied 140 | # directly to the root of the documentation. 141 | #html_extra_path = [] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'dateutildoc' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'dateutil.tex', 'dateutil Documentation', 206 | 'dateutil', 'manual'), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'dateutil', 'dateutil Documentation', 236 | ['dateutil'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'dateutil', 'dateutil Documentation', 250 | 'dateutil', 'dateutil', 'One line description of project.', 251 | 'Miscellaneous'), 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | -------------------------------------------------------------------------------- /dateutil/relativedelta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import calendar 4 | 5 | from six import integer_types 6 | 7 | __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 8 | 9 | 10 | class weekday(object): 11 | __slots__ = ["weekday", "n"] 12 | 13 | def __init__(self, weekday, n=None): 14 | self.weekday = weekday 15 | self.n = n 16 | 17 | def __call__(self, n): 18 | if n == self.n: 19 | return self 20 | else: 21 | return self.__class__(self.weekday, n) 22 | 23 | def __eq__(self, other): 24 | try: 25 | if self.weekday != other.weekday or self.n != other.n: 26 | return False 27 | except AttributeError: 28 | return False 29 | return True 30 | 31 | def __repr__(self): 32 | s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] 33 | if not self.n: 34 | return s 35 | else: 36 | return "%s(%+d)" % (s, self.n) 37 | 38 | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) 39 | 40 | 41 | class relativedelta(object): 42 | """ 43 | The relativedelta type is based on the specification of the excellent 44 | work done by M.-A. Lemburg in his 45 | `mx.DateTime `_ extension. 46 | However, notice that this type does *NOT* implement the same algorithm as 47 | his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. 48 | 49 | There are two different ways to build a relativedelta instance. The 50 | first one is passing it two date/datetime classes:: 51 | 52 | relativedelta(datetime1, datetime2) 53 | 54 | The second one is passing it any number of the following keyword arguments:: 55 | 56 | relativedelta(arg1=x,arg2=y,arg3=z...) 57 | 58 | year, month, day, hour, minute, second, microsecond: 59 | Absolute information (argument is singular); adding or subtracting a 60 | relativedelta with absolute information does not perform an aritmetic 61 | operation, but rather REPLACES the corresponding value in the 62 | original datetime with the value(s) in relativedelta. 63 | 64 | years, months, weeks, days, hours, minutes, seconds, microseconds: 65 | Relative information, may be negative (argument is plural); adding 66 | or subtracting a relativedelta with relative information performs 67 | the corresponding aritmetic operation on the original datetime value 68 | with the information in the relativedelta. 69 | 70 | weekday: 71 | One of the weekday instances (MO, TU, etc). These instances may 72 | receive a parameter N, specifying the Nth weekday, which could 73 | be positive or negative (like MO(+1) or MO(-2). Not specifying 74 | it is the same as specifying +1. You can also use an integer, 75 | where 0=MO. 76 | 77 | leapdays: 78 | Will add given days to the date found, if year is a leap 79 | year, and the date found is post 28 of february. 80 | 81 | yearday, nlyearday: 82 | Set the yearday or the non-leap year day (jump leap days). 83 | These are converted to day/month/leapdays information. 84 | 85 | Here is the behavior of operations with relativedelta: 86 | 87 | 1. Calculate the absolute year, using the 'year' argument, or the 88 | original datetime year, if the argument is not present. 89 | 90 | 2. Add the relative 'years' argument to the absolute year. 91 | 92 | 3. Do steps 1 and 2 for month/months. 93 | 94 | 4. Calculate the absolute day, using the 'day' argument, or the 95 | original datetime day, if the argument is not present. Then, 96 | subtract from the day until it fits in the year and month 97 | found after their operations. 98 | 99 | 5. Add the relative 'days' argument to the absolute day. Notice 100 | that the 'weeks' argument is multiplied by 7 and added to 101 | 'days'. 102 | 103 | 6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, 104 | microsecond/microseconds. 105 | 106 | 7. If the 'weekday' argument is present, calculate the weekday, 107 | with the given (wday, nth) tuple. wday is the index of the 108 | weekday (0-6, 0=Mon), and nth is the number of weeks to add 109 | forward or backward, depending on its signal. Notice that if 110 | the calculated date is already Monday, for example, using 111 | (0, 1) or (0, -1) won't change the day. 112 | """ 113 | 114 | def __init__(self, dt1=None, dt2=None, 115 | years=0, months=0, days=0, leapdays=0, weeks=0, 116 | hours=0, minutes=0, seconds=0, microseconds=0, 117 | year=None, month=None, day=None, weekday=None, 118 | yearday=None, nlyearday=None, 119 | hour=None, minute=None, second=None, microsecond=None): 120 | if dt1 and dt2: 121 | # datetime is a subclass of date. So both must be date 122 | if not (isinstance(dt1, datetime.date) and 123 | isinstance(dt2, datetime.date)): 124 | raise TypeError("relativedelta only diffs datetime/date") 125 | # We allow two dates, or two datetimes, so we coerce them to be 126 | # of the same type 127 | if (isinstance(dt1, datetime.datetime) != 128 | isinstance(dt2, datetime.datetime)): 129 | if not isinstance(dt1, datetime.datetime): 130 | dt1 = datetime.datetime.fromordinal(dt1.toordinal()) 131 | elif not isinstance(dt2, datetime.datetime): 132 | dt2 = datetime.datetime.fromordinal(dt2.toordinal()) 133 | self.years = 0 134 | self.months = 0 135 | self.days = 0 136 | self.leapdays = 0 137 | self.hours = 0 138 | self.minutes = 0 139 | self.seconds = 0 140 | self.microseconds = 0 141 | self.year = None 142 | self.month = None 143 | self.day = None 144 | self.weekday = None 145 | self.hour = None 146 | self.minute = None 147 | self.second = None 148 | self.microsecond = None 149 | self._has_time = 0 150 | 151 | months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) 152 | self._set_months(months) 153 | dtm = self.__radd__(dt2) 154 | if dt1 < dt2: 155 | while dt1 > dtm: 156 | months += 1 157 | self._set_months(months) 158 | dtm = self.__radd__(dt2) 159 | else: 160 | while dt1 < dtm: 161 | months -= 1 162 | self._set_months(months) 163 | dtm = self.__radd__(dt2) 164 | delta = dt1 - dtm 165 | self.seconds = delta.seconds+delta.days*86400 166 | self.microseconds = delta.microseconds 167 | else: 168 | self.years = years 169 | self.months = months 170 | self.days = days + weeks * 7 171 | self.leapdays = leapdays 172 | self.hours = hours 173 | self.minutes = minutes 174 | self.seconds = seconds 175 | self.microseconds = microseconds 176 | self.year = year 177 | self.month = month 178 | self.day = day 179 | self.hour = hour 180 | self.minute = minute 181 | self.second = second 182 | self.microsecond = microsecond 183 | 184 | if isinstance(weekday, integer_types): 185 | self.weekday = weekdays[weekday] 186 | else: 187 | self.weekday = weekday 188 | 189 | yday = 0 190 | if nlyearday: 191 | yday = nlyearday 192 | elif yearday: 193 | yday = yearday 194 | if yearday > 59: 195 | self.leapdays = -1 196 | if yday: 197 | ydayidx = [31, 59, 90, 120, 151, 181, 212, 198 | 243, 273, 304, 334, 366] 199 | for idx, ydays in enumerate(ydayidx): 200 | if yday <= ydays: 201 | self.month = idx+1 202 | if idx == 0: 203 | self.day = yday 204 | else: 205 | self.day = yday-ydayidx[idx-1] 206 | break 207 | else: 208 | raise ValueError("invalid year day (%d)" % yday) 209 | 210 | self._fix() 211 | 212 | def _fix(self): 213 | if abs(self.microseconds) > 999999: 214 | s = self.microseconds//abs(self.microseconds) 215 | div, mod = divmod(self.microseconds*s, 1000000) 216 | self.microseconds = mod*s 217 | self.seconds += div*s 218 | if abs(self.seconds) > 59: 219 | s = self.seconds//abs(self.seconds) 220 | div, mod = divmod(self.seconds*s, 60) 221 | self.seconds = mod*s 222 | self.minutes += div*s 223 | if abs(self.minutes) > 59: 224 | s = self.minutes//abs(self.minutes) 225 | div, mod = divmod(self.minutes*s, 60) 226 | self.minutes = mod*s 227 | self.hours += div*s 228 | if abs(self.hours) > 23: 229 | s = self.hours//abs(self.hours) 230 | div, mod = divmod(self.hours*s, 24) 231 | self.hours = mod*s 232 | self.days += div*s 233 | if abs(self.months) > 11: 234 | s = self.months//abs(self.months) 235 | div, mod = divmod(self.months*s, 12) 236 | self.months = mod*s 237 | self.years += div*s 238 | if (self.hours or self.minutes or self.seconds or self.microseconds 239 | or self.hour is not None or self.minute is not None or 240 | self.second is not None or self.microsecond is not None): 241 | self._has_time = 1 242 | else: 243 | self._has_time = 0 244 | 245 | @property 246 | def weeks(self): 247 | return self.days // 7 248 | @weeks.setter 249 | def weeks(self, value): 250 | self.days = self.days - (self.weeks * 7) + value*7 251 | 252 | def _set_months(self, months): 253 | self.months = months 254 | if abs(self.months) > 11: 255 | s = self.months//abs(self.months) 256 | div, mod = divmod(self.months*s, 12) 257 | self.months = mod*s 258 | self.years = div*s 259 | else: 260 | self.years = 0 261 | 262 | def __add__(self, other): 263 | if isinstance(other, relativedelta): 264 | return self.__class__(years=other.years+self.years, 265 | months=other.months+self.months, 266 | days=other.days+self.days, 267 | hours=other.hours+self.hours, 268 | minutes=other.minutes+self.minutes, 269 | seconds=other.seconds+self.seconds, 270 | microseconds=(other.microseconds + 271 | self.microseconds), 272 | leapdays=other.leapdays or self.leapdays, 273 | year=other.year or self.year, 274 | month=other.month or self.month, 275 | day=other.day or self.day, 276 | weekday=other.weekday or self.weekday, 277 | hour=other.hour or self.hour, 278 | minute=other.minute or self.minute, 279 | second=other.second or self.second, 280 | microsecond=(other.microsecond or 281 | self.microsecond)) 282 | if not isinstance(other, datetime.date): 283 | raise TypeError("unsupported type for add operation") 284 | elif self._has_time and not isinstance(other, datetime.datetime): 285 | other = datetime.datetime.fromordinal(other.toordinal()) 286 | year = (self.year or other.year)+self.years 287 | month = self.month or other.month 288 | if self.months: 289 | assert 1 <= abs(self.months) <= 12 290 | month += self.months 291 | if month > 12: 292 | year += 1 293 | month -= 12 294 | elif month < 1: 295 | year -= 1 296 | month += 12 297 | day = min(calendar.monthrange(year, month)[1], 298 | self.day or other.day) 299 | repl = {"year": year, "month": month, "day": day} 300 | for attr in ["hour", "minute", "second", "microsecond"]: 301 | value = getattr(self, attr) 302 | if value is not None: 303 | repl[attr] = value 304 | days = self.days 305 | if self.leapdays and month > 2 and calendar.isleap(year): 306 | days += self.leapdays 307 | ret = (other.replace(**repl) 308 | + datetime.timedelta(days=days, 309 | hours=self.hours, 310 | minutes=self.minutes, 311 | seconds=self.seconds, 312 | microseconds=self.microseconds)) 313 | if self.weekday: 314 | weekday, nth = self.weekday.weekday, self.weekday.n or 1 315 | jumpdays = (abs(nth)-1)*7 316 | if nth > 0: 317 | jumpdays += (7-ret.weekday()+weekday) % 7 318 | else: 319 | jumpdays += (ret.weekday()-weekday) % 7 320 | jumpdays *= -1 321 | ret += datetime.timedelta(days=jumpdays) 322 | return ret 323 | 324 | def __radd__(self, other): 325 | return self.__add__(other) 326 | 327 | def __rsub__(self, other): 328 | return self.__neg__().__radd__(other) 329 | 330 | def __sub__(self, other): 331 | if not isinstance(other, relativedelta): 332 | raise TypeError("unsupported type for sub operation") 333 | return self.__class__(years=self.years-other.years, 334 | months=self.months-other.months, 335 | days=self.days-other.days, 336 | hours=self.hours-other.hours, 337 | minutes=self.minutes-other.minutes, 338 | seconds=self.seconds-other.seconds, 339 | microseconds=self.microseconds-other.microseconds, 340 | leapdays=self.leapdays or other.leapdays, 341 | year=self.year or other.year, 342 | month=self.month or other.month, 343 | day=self.day or other.day, 344 | weekday=self.weekday or other.weekday, 345 | hour=self.hour or other.hour, 346 | minute=self.minute or other.minute, 347 | second=self.second or other.second, 348 | microsecond=self.microsecond or other.microsecond) 349 | 350 | def __neg__(self): 351 | return self.__class__(years=-self.years, 352 | months=-self.months, 353 | days=-self.days, 354 | hours=-self.hours, 355 | minutes=-self.minutes, 356 | seconds=-self.seconds, 357 | microseconds=-self.microseconds, 358 | leapdays=self.leapdays, 359 | year=self.year, 360 | month=self.month, 361 | day=self.day, 362 | weekday=self.weekday, 363 | hour=self.hour, 364 | minute=self.minute, 365 | second=self.second, 366 | microsecond=self.microsecond) 367 | 368 | def __bool__(self): 369 | return not (not self.years and 370 | not self.months and 371 | not self.days and 372 | not self.hours and 373 | not self.minutes and 374 | not self.seconds and 375 | not self.microseconds and 376 | not self.leapdays and 377 | self.year is None and 378 | self.month is None and 379 | self.day is None and 380 | self.weekday is None and 381 | self.hour is None and 382 | self.minute is None and 383 | self.second is None and 384 | self.microsecond is None) 385 | # Compatibility with Python 2.x 386 | __nonzero__ = __bool__ 387 | 388 | def __mul__(self, other): 389 | f = float(other) 390 | return self.__class__(years=int(self.years*f), 391 | months=int(self.months*f), 392 | days=int(self.days*f), 393 | hours=int(self.hours*f), 394 | minutes=int(self.minutes*f), 395 | seconds=int(self.seconds*f), 396 | microseconds=int(self.microseconds*f), 397 | leapdays=self.leapdays, 398 | year=self.year, 399 | month=self.month, 400 | day=self.day, 401 | weekday=self.weekday, 402 | hour=self.hour, 403 | minute=self.minute, 404 | second=self.second, 405 | microsecond=self.microsecond) 406 | 407 | __rmul__ = __mul__ 408 | 409 | def __eq__(self, other): 410 | if not isinstance(other, relativedelta): 411 | return False 412 | if self.weekday or other.weekday: 413 | if not self.weekday or not other.weekday: 414 | return False 415 | if self.weekday.weekday != other.weekday.weekday: 416 | return False 417 | n1, n2 = self.weekday.n, other.weekday.n 418 | if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): 419 | return False 420 | return (self.years == other.years and 421 | self.months == other.months and 422 | self.days == other.days and 423 | self.hours == other.hours and 424 | self.minutes == other.minutes and 425 | self.seconds == other.seconds and 426 | self.leapdays == other.leapdays and 427 | self.year == other.year and 428 | self.month == other.month and 429 | self.day == other.day and 430 | self.hour == other.hour and 431 | self.minute == other.minute and 432 | self.second == other.second and 433 | self.microsecond == other.microsecond) 434 | 435 | def __ne__(self, other): 436 | return not self.__eq__(other) 437 | 438 | def __div__(self, other): 439 | return self.__mul__(1/float(other)) 440 | 441 | __truediv__ = __div__ 442 | 443 | def __repr__(self): 444 | l = [] 445 | for attr in ["years", "months", "days", "leapdays", 446 | "hours", "minutes", "seconds", "microseconds"]: 447 | value = getattr(self, attr) 448 | if value: 449 | l.append("%s=%+d" % (attr, value)) 450 | for attr in ["year", "month", "day", "weekday", 451 | "hour", "minute", "second", "microsecond"]: 452 | value = getattr(self, attr) 453 | if value is not None: 454 | l.append("%s=%s" % (attr, repr(value))) 455 | return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) 456 | 457 | # vim:ts=4:sw=4:et 458 | -------------------------------------------------------------------------------- /dateutil/tz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module offers timezone implementations subclassing the abstract 4 | :py:`datetime.tzinfo` type. There are classes to handle tzfile format files 5 | (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ 6 | environment string (in all known formats), given ranges (with help from 7 | relative deltas), local machine timezone, fixed offset timezone, and UTC 8 | timezone. 9 | """ 10 | import datetime 11 | import struct 12 | import time 13 | import sys 14 | import os 15 | 16 | from six import string_types, PY3 17 | 18 | try: 19 | from dateutil.tzwin import tzwin, tzwinlocal 20 | except ImportError: 21 | tzwin = tzwinlocal = None 22 | 23 | relativedelta = None 24 | parser = None 25 | rrule = None 26 | 27 | __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", 28 | "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] 29 | 30 | 31 | def tzname_in_python2(namefunc): 32 | """Change unicode output into bytestrings in Python 2 33 | 34 | tzname() API changed in Python 3. It used to return bytes, but was changed 35 | to unicode strings 36 | """ 37 | def adjust_encoding(*args, **kwargs): 38 | name = namefunc(*args, **kwargs) 39 | if name is not None and not PY3: 40 | name = name.encode() 41 | 42 | return name 43 | 44 | return adjust_encoding 45 | 46 | ZERO = datetime.timedelta(0) 47 | EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() 48 | 49 | 50 | class tzutc(datetime.tzinfo): 51 | 52 | def utcoffset(self, dt): 53 | return ZERO 54 | 55 | def dst(self, dt): 56 | return ZERO 57 | 58 | @tzname_in_python2 59 | def tzname(self, dt): 60 | return "UTC" 61 | 62 | def __eq__(self, other): 63 | return (isinstance(other, tzutc) or 64 | (isinstance(other, tzoffset) and other._offset == ZERO)) 65 | 66 | def __ne__(self, other): 67 | return not self.__eq__(other) 68 | 69 | def __repr__(self): 70 | return "%s()" % self.__class__.__name__ 71 | 72 | __reduce__ = object.__reduce__ 73 | 74 | 75 | class tzoffset(datetime.tzinfo): 76 | 77 | def __init__(self, name, offset): 78 | self._name = name 79 | self._offset = datetime.timedelta(seconds=offset) 80 | 81 | def utcoffset(self, dt): 82 | return self._offset 83 | 84 | def dst(self, dt): 85 | return ZERO 86 | 87 | @tzname_in_python2 88 | def tzname(self, dt): 89 | return self._name 90 | 91 | def __eq__(self, other): 92 | return (isinstance(other, tzoffset) and 93 | self._offset == other._offset) 94 | 95 | def __ne__(self, other): 96 | return not self.__eq__(other) 97 | 98 | def __repr__(self): 99 | return "%s(%s, %s)" % (self.__class__.__name__, 100 | repr(self._name), 101 | self._offset.days*86400+self._offset.seconds) 102 | 103 | __reduce__ = object.__reduce__ 104 | 105 | 106 | class tzlocal(datetime.tzinfo): 107 | 108 | _std_offset = datetime.timedelta(seconds=-time.timezone) 109 | if time.daylight: 110 | _dst_offset = datetime.timedelta(seconds=-time.altzone) 111 | else: 112 | _dst_offset = _std_offset 113 | 114 | def utcoffset(self, dt): 115 | if self._isdst(dt): 116 | return self._dst_offset 117 | else: 118 | return self._std_offset 119 | 120 | def dst(self, dt): 121 | if self._isdst(dt): 122 | return self._dst_offset-self._std_offset 123 | else: 124 | return ZERO 125 | 126 | @tzname_in_python2 127 | def tzname(self, dt): 128 | return time.tzname[self._isdst(dt)] 129 | 130 | def _isdst(self, dt): 131 | # We can't use mktime here. It is unstable when deciding if 132 | # the hour near to a change is DST or not. 133 | # 134 | # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, 135 | # dt.minute, dt.second, dt.weekday(), 0, -1)) 136 | # return time.localtime(timestamp).tm_isdst 137 | # 138 | # The code above yields the following result: 139 | # 140 | # >>> import tz, datetime 141 | # >>> t = tz.tzlocal() 142 | # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 143 | # 'BRDT' 144 | # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() 145 | # 'BRST' 146 | # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 147 | # 'BRST' 148 | # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() 149 | # 'BRDT' 150 | # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 151 | # 'BRDT' 152 | # 153 | # Here is a more stable implementation: 154 | # 155 | timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 156 | + dt.hour * 3600 157 | + dt.minute * 60 158 | + dt.second) 159 | return time.localtime(timestamp+time.timezone).tm_isdst 160 | 161 | def __eq__(self, other): 162 | if not isinstance(other, tzlocal): 163 | return False 164 | return (self._std_offset == other._std_offset and 165 | self._dst_offset == other._dst_offset) 166 | return True 167 | 168 | def __ne__(self, other): 169 | return not self.__eq__(other) 170 | 171 | def __repr__(self): 172 | return "%s()" % self.__class__.__name__ 173 | 174 | __reduce__ = object.__reduce__ 175 | 176 | 177 | class _ttinfo(object): 178 | __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] 179 | 180 | def __init__(self): 181 | for attr in self.__slots__: 182 | setattr(self, attr, None) 183 | 184 | def __repr__(self): 185 | l = [] 186 | for attr in self.__slots__: 187 | value = getattr(self, attr) 188 | if value is not None: 189 | l.append("%s=%s" % (attr, repr(value))) 190 | return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) 191 | 192 | def __eq__(self, other): 193 | if not isinstance(other, _ttinfo): 194 | return False 195 | return (self.offset == other.offset and 196 | self.delta == other.delta and 197 | self.isdst == other.isdst and 198 | self.abbr == other.abbr and 199 | self.isstd == other.isstd and 200 | self.isgmt == other.isgmt) 201 | 202 | def __ne__(self, other): 203 | return not self.__eq__(other) 204 | 205 | def __getstate__(self): 206 | state = {} 207 | for name in self.__slots__: 208 | state[name] = getattr(self, name, None) 209 | return state 210 | 211 | def __setstate__(self, state): 212 | for name in self.__slots__: 213 | if name in state: 214 | setattr(self, name, state[name]) 215 | 216 | 217 | class tzfile(datetime.tzinfo): 218 | 219 | # http://www.twinsun.com/tz/tz-link.htm 220 | # ftp://ftp.iana.org/tz/tz*.tar.gz 221 | 222 | def __init__(self, fileobj, filename=None): 223 | file_opened_here = False 224 | if isinstance(fileobj, string_types): 225 | self._filename = fileobj 226 | fileobj = open(fileobj, 'rb') 227 | file_opened_here = True 228 | elif filename is not None: 229 | self._filename = filename 230 | elif hasattr(fileobj, "name"): 231 | self._filename = fileobj.name 232 | else: 233 | self._filename = repr(fileobj) 234 | 235 | # From tzfile(5): 236 | # 237 | # The time zone information files used by tzset(3) 238 | # begin with the magic characters "TZif" to identify 239 | # them as time zone information files, followed by 240 | # sixteen bytes reserved for future use, followed by 241 | # six four-byte values of type long, written in a 242 | # ``standard'' byte order (the high-order byte 243 | # of the value is written first). 244 | try: 245 | if fileobj.read(4).decode() != "TZif": 246 | raise ValueError("magic not found") 247 | 248 | fileobj.read(16) 249 | 250 | ( 251 | # The number of UTC/local indicators stored in the file. 252 | ttisgmtcnt, 253 | 254 | # The number of standard/wall indicators stored in the file. 255 | ttisstdcnt, 256 | 257 | # The number of leap seconds for which data is 258 | # stored in the file. 259 | leapcnt, 260 | 261 | # The number of "transition times" for which data 262 | # is stored in the file. 263 | timecnt, 264 | 265 | # The number of "local time types" for which data 266 | # is stored in the file (must not be zero). 267 | typecnt, 268 | 269 | # The number of characters of "time zone 270 | # abbreviation strings" stored in the file. 271 | charcnt, 272 | 273 | ) = struct.unpack(">6l", fileobj.read(24)) 274 | 275 | # The above header is followed by tzh_timecnt four-byte 276 | # values of type long, sorted in ascending order. 277 | # These values are written in ``standard'' byte order. 278 | # Each is used as a transition time (as returned by 279 | # time(2)) at which the rules for computing local time 280 | # change. 281 | 282 | if timecnt: 283 | self._trans_list = struct.unpack(">%dl" % timecnt, 284 | fileobj.read(timecnt*4)) 285 | else: 286 | self._trans_list = [] 287 | 288 | # Next come tzh_timecnt one-byte values of type unsigned 289 | # char; each one tells which of the different types of 290 | # ``local time'' types described in the file is associated 291 | # with the same-indexed transition time. These values 292 | # serve as indices into an array of ttinfo structures that 293 | # appears next in the file. 294 | 295 | if timecnt: 296 | self._trans_idx = struct.unpack(">%dB" % timecnt, 297 | fileobj.read(timecnt)) 298 | else: 299 | self._trans_idx = [] 300 | 301 | # Each ttinfo structure is written as a four-byte value 302 | # for tt_gmtoff of type long, in a standard byte 303 | # order, followed by a one-byte value for tt_isdst 304 | # and a one-byte value for tt_abbrind. In each 305 | # structure, tt_gmtoff gives the number of 306 | # seconds to be added to UTC, tt_isdst tells whether 307 | # tm_isdst should be set by localtime(3), and 308 | # tt_abbrind serves as an index into the array of 309 | # time zone abbreviation characters that follow the 310 | # ttinfo structure(s) in the file. 311 | 312 | ttinfo = [] 313 | 314 | for i in range(typecnt): 315 | ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) 316 | 317 | abbr = fileobj.read(charcnt).decode() 318 | 319 | # Then there are tzh_leapcnt pairs of four-byte 320 | # values, written in standard byte order; the 321 | # first value of each pair gives the time (as 322 | # returned by time(2)) at which a leap second 323 | # occurs; the second gives the total number of 324 | # leap seconds to be applied after the given time. 325 | # The pairs of values are sorted in ascending order 326 | # by time. 327 | 328 | # Not used, for now 329 | # if leapcnt: 330 | # leap = struct.unpack(">%dl" % (leapcnt*2), 331 | # fileobj.read(leapcnt*8)) 332 | 333 | # Then there are tzh_ttisstdcnt standard/wall 334 | # indicators, each stored as a one-byte value; 335 | # they tell whether the transition times associated 336 | # with local time types were specified as standard 337 | # time or wall clock time, and are used when 338 | # a time zone file is used in handling POSIX-style 339 | # time zone environment variables. 340 | 341 | if ttisstdcnt: 342 | isstd = struct.unpack(">%db" % ttisstdcnt, 343 | fileobj.read(ttisstdcnt)) 344 | 345 | # Finally, there are tzh_ttisgmtcnt UTC/local 346 | # indicators, each stored as a one-byte value; 347 | # they tell whether the transition times associated 348 | # with local time types were specified as UTC or 349 | # local time, and are used when a time zone file 350 | # is used in handling POSIX-style time zone envi- 351 | # ronment variables. 352 | 353 | if ttisgmtcnt: 354 | isgmt = struct.unpack(">%db" % ttisgmtcnt, 355 | fileobj.read(ttisgmtcnt)) 356 | 357 | # ** Everything has been read ** 358 | finally: 359 | if file_opened_here: 360 | fileobj.close() 361 | 362 | # Build ttinfo list 363 | self._ttinfo_list = [] 364 | for i in range(typecnt): 365 | gmtoff, isdst, abbrind = ttinfo[i] 366 | # Round to full-minutes if that's not the case. Python's 367 | # datetime doesn't accept sub-minute timezones. Check 368 | # http://python.org/sf/1447945 for some information. 369 | gmtoff = (gmtoff+30)//60*60 370 | tti = _ttinfo() 371 | tti.offset = gmtoff 372 | tti.delta = datetime.timedelta(seconds=gmtoff) 373 | tti.isdst = isdst 374 | tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] 375 | tti.isstd = (ttisstdcnt > i and isstd[i] != 0) 376 | tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) 377 | self._ttinfo_list.append(tti) 378 | 379 | # Replace ttinfo indexes for ttinfo objects. 380 | trans_idx = [] 381 | for idx in self._trans_idx: 382 | trans_idx.append(self._ttinfo_list[idx]) 383 | self._trans_idx = tuple(trans_idx) 384 | 385 | # Set standard, dst, and before ttinfos. before will be 386 | # used when a given time is before any transitions, 387 | # and will be set to the first non-dst ttinfo, or to 388 | # the first dst, if all of them are dst. 389 | self._ttinfo_std = None 390 | self._ttinfo_dst = None 391 | self._ttinfo_before = None 392 | if self._ttinfo_list: 393 | if not self._trans_list: 394 | self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] 395 | else: 396 | for i in range(timecnt-1, -1, -1): 397 | tti = self._trans_idx[i] 398 | if not self._ttinfo_std and not tti.isdst: 399 | self._ttinfo_std = tti 400 | elif not self._ttinfo_dst and tti.isdst: 401 | self._ttinfo_dst = tti 402 | if self._ttinfo_std and self._ttinfo_dst: 403 | break 404 | else: 405 | if self._ttinfo_dst and not self._ttinfo_std: 406 | self._ttinfo_std = self._ttinfo_dst 407 | 408 | for tti in self._ttinfo_list: 409 | if not tti.isdst: 410 | self._ttinfo_before = tti 411 | break 412 | else: 413 | self._ttinfo_before = self._ttinfo_list[0] 414 | 415 | # Now fix transition times to become relative to wall time. 416 | # 417 | # I'm not sure about this. In my tests, the tz source file 418 | # is setup to wall time, and in the binary file isstd and 419 | # isgmt are off, so it should be in wall time. OTOH, it's 420 | # always in gmt time. Let me know if you have comments 421 | # about this. 422 | laststdoffset = 0 423 | self._trans_list = list(self._trans_list) 424 | for i in range(len(self._trans_list)): 425 | tti = self._trans_idx[i] 426 | if not tti.isdst: 427 | # This is std time. 428 | self._trans_list[i] += tti.offset 429 | laststdoffset = tti.offset 430 | else: 431 | # This is dst time. Convert to std. 432 | self._trans_list[i] += laststdoffset 433 | self._trans_list = tuple(self._trans_list) 434 | 435 | def _find_ttinfo(self, dt, laststd=0): 436 | timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 437 | + dt.hour * 3600 438 | + dt.minute * 60 439 | + dt.second) 440 | idx = 0 441 | for trans in self._trans_list: 442 | if timestamp < trans: 443 | break 444 | idx += 1 445 | else: 446 | return self._ttinfo_std 447 | if idx == 0: 448 | return self._ttinfo_before 449 | if laststd: 450 | while idx > 0: 451 | tti = self._trans_idx[idx-1] 452 | if not tti.isdst: 453 | return tti 454 | idx -= 1 455 | else: 456 | return self._ttinfo_std 457 | else: 458 | return self._trans_idx[idx-1] 459 | 460 | def utcoffset(self, dt): 461 | if not self._ttinfo_std: 462 | return ZERO 463 | return self._find_ttinfo(dt).delta 464 | 465 | def dst(self, dt): 466 | if not self._ttinfo_dst: 467 | return ZERO 468 | tti = self._find_ttinfo(dt) 469 | if not tti.isdst: 470 | return ZERO 471 | 472 | # The documentation says that utcoffset()-dst() must 473 | # be constant for every dt. 474 | return tti.delta-self._find_ttinfo(dt, laststd=1).delta 475 | 476 | # An alternative for that would be: 477 | # 478 | # return self._ttinfo_dst.offset-self._ttinfo_std.offset 479 | # 480 | # However, this class stores historical changes in the 481 | # dst offset, so I belive that this wouldn't be the right 482 | # way to implement this. 483 | 484 | @tzname_in_python2 485 | def tzname(self, dt): 486 | if not self._ttinfo_std: 487 | return None 488 | return self._find_ttinfo(dt).abbr 489 | 490 | def __eq__(self, other): 491 | if not isinstance(other, tzfile): 492 | return False 493 | return (self._trans_list == other._trans_list and 494 | self._trans_idx == other._trans_idx and 495 | self._ttinfo_list == other._ttinfo_list) 496 | 497 | def __ne__(self, other): 498 | return not self.__eq__(other) 499 | 500 | def __repr__(self): 501 | return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) 502 | 503 | def __reduce__(self): 504 | if not os.path.isfile(self._filename): 505 | raise ValueError("Unpickable %s class" % self.__class__.__name__) 506 | return (self.__class__, (self._filename,)) 507 | 508 | 509 | class tzrange(datetime.tzinfo): 510 | def __init__(self, stdabbr, stdoffset=None, 511 | dstabbr=None, dstoffset=None, 512 | start=None, end=None): 513 | global relativedelta 514 | if not relativedelta: 515 | from dateutil import relativedelta 516 | self._std_abbr = stdabbr 517 | self._dst_abbr = dstabbr 518 | if stdoffset is not None: 519 | self._std_offset = datetime.timedelta(seconds=stdoffset) 520 | else: 521 | self._std_offset = ZERO 522 | if dstoffset is not None: 523 | self._dst_offset = datetime.timedelta(seconds=dstoffset) 524 | elif dstabbr and stdoffset is not None: 525 | self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) 526 | else: 527 | self._dst_offset = ZERO 528 | if dstabbr and start is None: 529 | self._start_delta = relativedelta.relativedelta( 530 | hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) 531 | else: 532 | self._start_delta = start 533 | if dstabbr and end is None: 534 | self._end_delta = relativedelta.relativedelta( 535 | hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) 536 | else: 537 | self._end_delta = end 538 | 539 | def utcoffset(self, dt): 540 | if self._isdst(dt): 541 | return self._dst_offset 542 | else: 543 | return self._std_offset 544 | 545 | def dst(self, dt): 546 | if self._isdst(dt): 547 | return self._dst_offset-self._std_offset 548 | else: 549 | return ZERO 550 | 551 | @tzname_in_python2 552 | def tzname(self, dt): 553 | if self._isdst(dt): 554 | return self._dst_abbr 555 | else: 556 | return self._std_abbr 557 | 558 | def _isdst(self, dt): 559 | if not self._start_delta: 560 | return False 561 | year = datetime.datetime(dt.year, 1, 1) 562 | start = year+self._start_delta 563 | end = year+self._end_delta 564 | dt = dt.replace(tzinfo=None) 565 | if start < end: 566 | return dt >= start and dt < end 567 | else: 568 | return dt >= start or dt < end 569 | 570 | def __eq__(self, other): 571 | if not isinstance(other, tzrange): 572 | return False 573 | return (self._std_abbr == other._std_abbr and 574 | self._dst_abbr == other._dst_abbr and 575 | self._std_offset == other._std_offset and 576 | self._dst_offset == other._dst_offset and 577 | self._start_delta == other._start_delta and 578 | self._end_delta == other._end_delta) 579 | 580 | def __ne__(self, other): 581 | return not self.__eq__(other) 582 | 583 | def __repr__(self): 584 | return "%s(...)" % self.__class__.__name__ 585 | 586 | __reduce__ = object.__reduce__ 587 | 588 | 589 | class tzstr(tzrange): 590 | 591 | def __init__(self, s): 592 | global parser 593 | if not parser: 594 | from dateutil import parser 595 | self._s = s 596 | 597 | res = parser._parsetz(s) 598 | if res is None: 599 | raise ValueError("unknown string format") 600 | 601 | # Here we break the compatibility with the TZ variable handling. 602 | # GMT-3 actually *means* the timezone -3. 603 | if res.stdabbr in ("GMT", "UTC"): 604 | res.stdoffset *= -1 605 | 606 | # We must initialize it first, since _delta() needs 607 | # _std_offset and _dst_offset set. Use False in start/end 608 | # to avoid building it two times. 609 | tzrange.__init__(self, res.stdabbr, res.stdoffset, 610 | res.dstabbr, res.dstoffset, 611 | start=False, end=False) 612 | 613 | if not res.dstabbr: 614 | self._start_delta = None 615 | self._end_delta = None 616 | else: 617 | self._start_delta = self._delta(res.start) 618 | if self._start_delta: 619 | self._end_delta = self._delta(res.end, isend=1) 620 | 621 | def _delta(self, x, isend=0): 622 | kwargs = {} 623 | if x.month is not None: 624 | kwargs["month"] = x.month 625 | if x.weekday is not None: 626 | kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) 627 | if x.week > 0: 628 | kwargs["day"] = 1 629 | else: 630 | kwargs["day"] = 31 631 | elif x.day: 632 | kwargs["day"] = x.day 633 | elif x.yday is not None: 634 | kwargs["yearday"] = x.yday 635 | elif x.jyday is not None: 636 | kwargs["nlyearday"] = x.jyday 637 | if not kwargs: 638 | # Default is to start on first sunday of april, and end 639 | # on last sunday of october. 640 | if not isend: 641 | kwargs["month"] = 4 642 | kwargs["day"] = 1 643 | kwargs["weekday"] = relativedelta.SU(+1) 644 | else: 645 | kwargs["month"] = 10 646 | kwargs["day"] = 31 647 | kwargs["weekday"] = relativedelta.SU(-1) 648 | if x.time is not None: 649 | kwargs["seconds"] = x.time 650 | else: 651 | # Default is 2AM. 652 | kwargs["seconds"] = 7200 653 | if isend: 654 | # Convert to standard time, to follow the documented way 655 | # of working with the extra hour. See the documentation 656 | # of the tzinfo class. 657 | delta = self._dst_offset-self._std_offset 658 | kwargs["seconds"] -= delta.seconds+delta.days*86400 659 | return relativedelta.relativedelta(**kwargs) 660 | 661 | def __repr__(self): 662 | return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 663 | 664 | 665 | class _tzicalvtzcomp(object): 666 | def __init__(self, tzoffsetfrom, tzoffsetto, isdst, 667 | tzname=None, rrule=None): 668 | self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) 669 | self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) 670 | self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom 671 | self.isdst = isdst 672 | self.tzname = tzname 673 | self.rrule = rrule 674 | 675 | 676 | class _tzicalvtz(datetime.tzinfo): 677 | def __init__(self, tzid, comps=[]): 678 | self._tzid = tzid 679 | self._comps = comps 680 | self._cachedate = [] 681 | self._cachecomp = [] 682 | 683 | def _find_comp(self, dt): 684 | if len(self._comps) == 1: 685 | return self._comps[0] 686 | dt = dt.replace(tzinfo=None) 687 | try: 688 | return self._cachecomp[self._cachedate.index(dt)] 689 | except ValueError: 690 | pass 691 | lastcomp = None 692 | lastcompdt = None 693 | for comp in self._comps: 694 | if not comp.isdst: 695 | # Handle the extra hour in DST -> STD 696 | compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) 697 | else: 698 | compdt = comp.rrule.before(dt, inc=True) 699 | if compdt and (not lastcompdt or lastcompdt < compdt): 700 | lastcompdt = compdt 701 | lastcomp = comp 702 | if not lastcomp: 703 | # RFC says nothing about what to do when a given 704 | # time is before the first onset date. We'll look for the 705 | # first standard component, or the first component, if 706 | # none is found. 707 | for comp in self._comps: 708 | if not comp.isdst: 709 | lastcomp = comp 710 | break 711 | else: 712 | lastcomp = comp[0] 713 | self._cachedate.insert(0, dt) 714 | self._cachecomp.insert(0, lastcomp) 715 | if len(self._cachedate) > 10: 716 | self._cachedate.pop() 717 | self._cachecomp.pop() 718 | return lastcomp 719 | 720 | def utcoffset(self, dt): 721 | return self._find_comp(dt).tzoffsetto 722 | 723 | def dst(self, dt): 724 | comp = self._find_comp(dt) 725 | if comp.isdst: 726 | return comp.tzoffsetdiff 727 | else: 728 | return ZERO 729 | 730 | @tzname_in_python2 731 | def tzname(self, dt): 732 | return self._find_comp(dt).tzname 733 | 734 | def __repr__(self): 735 | return "" % repr(self._tzid) 736 | 737 | __reduce__ = object.__reduce__ 738 | 739 | 740 | class tzical(object): 741 | def __init__(self, fileobj): 742 | global rrule 743 | if not rrule: 744 | from dateutil import rrule 745 | 746 | if isinstance(fileobj, string_types): 747 | self._s = fileobj 748 | # ical should be encoded in UTF-8 with CRLF 749 | fileobj = open(fileobj, 'r') 750 | elif hasattr(fileobj, "name"): 751 | self._s = fileobj.name 752 | else: 753 | self._s = repr(fileobj) 754 | 755 | self._vtz = {} 756 | 757 | self._parse_rfc(fileobj.read()) 758 | 759 | def keys(self): 760 | return list(self._vtz.keys()) 761 | 762 | def get(self, tzid=None): 763 | if tzid is None: 764 | keys = list(self._vtz.keys()) 765 | if len(keys) == 0: 766 | raise ValueError("no timezones defined") 767 | elif len(keys) > 1: 768 | raise ValueError("more than one timezone available") 769 | tzid = keys[0] 770 | return self._vtz.get(tzid) 771 | 772 | def _parse_offset(self, s): 773 | s = s.strip() 774 | if not s: 775 | raise ValueError("empty offset") 776 | if s[0] in ('+', '-'): 777 | signal = (-1, +1)[s[0] == '+'] 778 | s = s[1:] 779 | else: 780 | signal = +1 781 | if len(s) == 4: 782 | return (int(s[:2])*3600+int(s[2:])*60)*signal 783 | elif len(s) == 6: 784 | return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal 785 | else: 786 | raise ValueError("invalid offset: "+s) 787 | 788 | def _parse_rfc(self, s): 789 | lines = s.splitlines() 790 | if not lines: 791 | raise ValueError("empty string") 792 | 793 | # Unfold 794 | i = 0 795 | while i < len(lines): 796 | line = lines[i].rstrip() 797 | if not line: 798 | del lines[i] 799 | elif i > 0 and line[0] == " ": 800 | lines[i-1] += line[1:] 801 | del lines[i] 802 | else: 803 | i += 1 804 | 805 | tzid = None 806 | comps = [] 807 | invtz = False 808 | comptype = None 809 | for line in lines: 810 | if not line: 811 | continue 812 | name, value = line.split(':', 1) 813 | parms = name.split(';') 814 | if not parms: 815 | raise ValueError("empty property name") 816 | name = parms[0].upper() 817 | parms = parms[1:] 818 | if invtz: 819 | if name == "BEGIN": 820 | if value in ("STANDARD", "DAYLIGHT"): 821 | # Process component 822 | pass 823 | else: 824 | raise ValueError("unknown component: "+value) 825 | comptype = value 826 | founddtstart = False 827 | tzoffsetfrom = None 828 | tzoffsetto = None 829 | rrulelines = [] 830 | tzname = None 831 | elif name == "END": 832 | if value == "VTIMEZONE": 833 | if comptype: 834 | raise ValueError("component not closed: "+comptype) 835 | if not tzid: 836 | raise ValueError("mandatory TZID not found") 837 | if not comps: 838 | raise ValueError( 839 | "at least one component is needed") 840 | # Process vtimezone 841 | self._vtz[tzid] = _tzicalvtz(tzid, comps) 842 | invtz = False 843 | elif value == comptype: 844 | if not founddtstart: 845 | raise ValueError("mandatory DTSTART not found") 846 | if tzoffsetfrom is None: 847 | raise ValueError( 848 | "mandatory TZOFFSETFROM not found") 849 | if tzoffsetto is None: 850 | raise ValueError( 851 | "mandatory TZOFFSETFROM not found") 852 | # Process component 853 | rr = None 854 | if rrulelines: 855 | rr = rrule.rrulestr("\n".join(rrulelines), 856 | compatible=True, 857 | ignoretz=True, 858 | cache=True) 859 | comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, 860 | (comptype == "DAYLIGHT"), 861 | tzname, rr) 862 | comps.append(comp) 863 | comptype = None 864 | else: 865 | raise ValueError("invalid component end: "+value) 866 | elif comptype: 867 | if name == "DTSTART": 868 | rrulelines.append(line) 869 | founddtstart = True 870 | elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): 871 | rrulelines.append(line) 872 | elif name == "TZOFFSETFROM": 873 | if parms: 874 | raise ValueError( 875 | "unsupported %s parm: %s " % (name, parms[0])) 876 | tzoffsetfrom = self._parse_offset(value) 877 | elif name == "TZOFFSETTO": 878 | if parms: 879 | raise ValueError( 880 | "unsupported TZOFFSETTO parm: "+parms[0]) 881 | tzoffsetto = self._parse_offset(value) 882 | elif name == "TZNAME": 883 | if parms: 884 | raise ValueError( 885 | "unsupported TZNAME parm: "+parms[0]) 886 | tzname = value 887 | elif name == "COMMENT": 888 | pass 889 | else: 890 | raise ValueError("unsupported property: "+name) 891 | else: 892 | if name == "TZID": 893 | if parms: 894 | raise ValueError( 895 | "unsupported TZID parm: "+parms[0]) 896 | tzid = value 897 | elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): 898 | pass 899 | else: 900 | raise ValueError("unsupported property: "+name) 901 | elif name == "BEGIN" and value == "VTIMEZONE": 902 | tzid = None 903 | comps = [] 904 | invtz = True 905 | 906 | def __repr__(self): 907 | return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 908 | 909 | if sys.platform != "win32": 910 | TZFILES = ["/etc/localtime", "localtime"] 911 | TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] 912 | else: 913 | TZFILES = [] 914 | TZPATHS = [] 915 | 916 | 917 | def gettz(name=None): 918 | tz = None 919 | if not name: 920 | try: 921 | name = os.environ["TZ"] 922 | except KeyError: 923 | pass 924 | if name is None or name == ":": 925 | for filepath in TZFILES: 926 | if not os.path.isabs(filepath): 927 | filename = filepath 928 | for path in TZPATHS: 929 | filepath = os.path.join(path, filename) 930 | if os.path.isfile(filepath): 931 | break 932 | else: 933 | continue 934 | if os.path.isfile(filepath): 935 | try: 936 | tz = tzfile(filepath) 937 | break 938 | except (IOError, OSError, ValueError): 939 | pass 940 | else: 941 | tz = tzlocal() 942 | else: 943 | if name.startswith(":"): 944 | name = name[:-1] 945 | if os.path.isabs(name): 946 | if os.path.isfile(name): 947 | tz = tzfile(name) 948 | else: 949 | tz = None 950 | else: 951 | for path in TZPATHS: 952 | filepath = os.path.join(path, name) 953 | if not os.path.isfile(filepath): 954 | filepath = filepath.replace(' ', '_') 955 | if not os.path.isfile(filepath): 956 | continue 957 | try: 958 | tz = tzfile(filepath) 959 | break 960 | except (IOError, OSError, ValueError): 961 | pass 962 | else: 963 | tz = None 964 | if tzwin is not None: 965 | try: 966 | tz = tzwin(name) 967 | except WindowsError: 968 | tz = None 969 | if not tz: 970 | from dateutil.zoneinfo import gettz 971 | tz = gettz(name) 972 | if not tz: 973 | for c in name: 974 | # name must have at least one offset to be a tzstr 975 | if c in "0123456789": 976 | try: 977 | tz = tzstr(name) 978 | except ValueError: 979 | pass 980 | break 981 | else: 982 | if name in ("GMT", "UTC"): 983 | tz = tzutc() 984 | elif name in time.tzname: 985 | tz = tzlocal() 986 | return tz 987 | 988 | # vim:ts=4:sw=4:et 989 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | dateutil examples 2 | ================= 3 | 4 | .. contents:: 5 | 6 | relativedelta examples 7 | ---------------------- 8 | 9 | .. testsetup:: relativedelta 10 | 11 | from datetime import *; from dateutil.relativedelta import * 12 | import calendar 13 | NOW = datetime(2003, 9, 17, 20, 54, 47, 282310) 14 | TODAY = date(2003, 9, 17) 15 | 16 | Let's begin our trip:: 17 | 18 | >>> from datetime import *; from dateutil.relativedelta import * 19 | >>> import calendar 20 | 21 | Store some values:: 22 | 23 | >>> NOW = datetime.now() 24 | >>> TODAY = date.today() 25 | >>> NOW 26 | datetime.datetime(2003, 9, 17, 20, 54, 47, 282310) 27 | >>> TODAY 28 | datetime.date(2003, 9, 17) 29 | 30 | Next month 31 | 32 | .. doctest:: relativedelta 33 | 34 | >>> NOW+relativedelta(months=+1) 35 | datetime.datetime(2003, 10, 17, 20, 54, 47, 282310) 36 | 37 | Next month, plus one week. 38 | 39 | .. doctest:: relativedelta 40 | 41 | >>> NOW+relativedelta(months=+1, weeks=+1) 42 | datetime.datetime(2003, 10, 24, 20, 54, 47, 282310) 43 | 44 | Next month, plus one week, at 10am. 45 | 46 | .. doctest:: relativedelta 47 | 48 | >>> TODAY+relativedelta(months=+1, weeks=+1, hour=10) 49 | datetime.datetime(2003, 10, 24, 10, 0) 50 | 51 | Here is another example using an absolute relativedelta. Notice the use of 52 | year and month (both singular) which causes the values to be *replaced* in the 53 | original datetime rather than performing an arithmetic operation on them. 54 | 55 | .. doctest:: relativedelta 56 | 57 | >>> NOW+relativedelta(year=1, month=1) 58 | datetime(1, 1, 17, 20, 54, 47, 282310) 59 | 60 | Let's try the other way around. Notice that the 61 | hour setting we get in the relativedelta is relative, 62 | since it's a difference, and the weeks parameter 63 | has gone. 64 | 65 | .. doctest:: relativedelta 66 | 67 | >>> relativedelta(datetime(2003, 10, 24, 10, 0), TODAY) 68 | relativedelta(months=+1, days=+7, hours=+10) 69 | 70 | One month before one year. 71 | 72 | .. doctest:: relativedelta 73 | 74 | >>> NOW+relativedelta(years=+1, months=-1) 75 | datetime.datetime(2004, 8, 17, 20, 54, 47, 282310) 76 | 77 | How does it handle months with different numbers of days? 78 | Notice that adding one month will never cross the month 79 | boundary. 80 | 81 | .. doctest:: relativedelta 82 | 83 | >>> date(2003,1,27)+relativedelta(months=+1) 84 | datetime.date(2003, 2, 27) 85 | >>> date(2003,1,31)+relativedelta(months=+1) 86 | datetime.date(2003, 2, 28) 87 | >>> date(2003,1,31)+relativedelta(months=+2) 88 | datetime.date(2003, 3, 31) 89 | 90 | The logic for years is the same, even on leap years. 91 | 92 | .. doctest:: relativedelta 93 | 94 | >>> date(2000,2,28)+relativedelta(years=+1) 95 | datetime.date(2001, 2, 28) 96 | >>> date(2000,2,29)+relativedelta(years=+1) 97 | datetime.date(2001, 2, 28) 98 | 99 | >>> date(1999,2,28)+relativedelta(years=+1) 100 | datetime.date(2000, 2, 28) 101 | >>> date(1999,3,1)+relativedelta(years=+1) 102 | datetime.date(2000, 3, 1) 103 | 104 | >>> date(2001,2,28)+relativedelta(years=-1) 105 | datetime.date(2000, 2, 28) 106 | >>> date(2001,3,1)+relativedelta(years=-1) 107 | datetime.date(2000, 3, 1) 108 | 109 | Next friday 110 | 111 | .. doctest:: relativedelta 112 | 113 | >>> TODAY+relativedelta(weekday=FR) 114 | datetime.date(2003, 9, 19) 115 | 116 | >>> TODAY+relativedelta(weekday=calendar.FRIDAY) 117 | datetime.date(2003, 9, 19) 118 | 119 | Last friday in this month. 120 | 121 | .. doctest:: relativedelta 122 | 123 | >>> TODAY+relativedelta(day=31, weekday=FR(-1)) 124 | datetime.date(2003, 9, 26) 125 | 126 | Next wednesday (it's today!). 127 | 128 | .. doctest:: relativedelta 129 | 130 | >>> TODAY+relativedelta(weekday=WE(+1)) 131 | datetime.date(2003, 9, 17) 132 | 133 | Next wednesday, but not today. 134 | 135 | .. doctest:: relativedelta 136 | 137 | >>> TODAY+relativedelta(days=+1, weekday=WE(+1)) 138 | datetime.date(2003, 9, 24) 139 | 140 | Following 141 | [http://www.cl.cam.ac.uk/~mgk25/iso-time.html ISO year week number notation] 142 | find the first day of the 15th week of 1997. 143 | 144 | .. doctest:: relativedelta 145 | 146 | >>> datetime(1997,1,1)+relativedelta(day=4, weekday=MO(-1), weeks=+14) 147 | datetime.datetime(1997, 4, 7, 0, 0) 148 | 149 | How long ago has the millennium changed? 150 | 151 | .. doctest:: relativedelta 152 | 153 | >>> relativedelta(NOW, date(2001,1,1)) 154 | relativedelta(years=+2, months=+8, days=+16, 155 | hours=+20, minutes=+54, seconds=+47, microseconds=+282310) 156 | 157 | How old is John? 158 | 159 | .. doctest:: relativedelta 160 | 161 | >>> johnbirthday = datetime(1978, 4, 5, 12, 0) 162 | >>> relativedelta(NOW, johnbirthday) 163 | relativedelta(years=+25, months=+5, days=+12, 164 | hours=+8, minutes=+54, seconds=+47, microseconds=+282310) 165 | 166 | It works with dates too. 167 | 168 | .. doctest:: relativedelta 169 | 170 | >>> relativedelta(TODAY, johnbirthday) 171 | relativedelta(years=+25, months=+5, days=+11, hours=+12) 172 | 173 | Obtain today's date using the yearday: 174 | 175 | .. doctest:: relativedelta 176 | 177 | >>> date(2003, 1, 1)+relativedelta(yearday=260) 178 | datetime.date(2003, 9, 17) 179 | 180 | We can use today's date, since yearday should be absolute 181 | in the given year: 182 | 183 | .. doctest:: relativedelta 184 | 185 | >>> TODAY+relativedelta(yearday=260) 186 | datetime.date(2003, 9, 17) 187 | 188 | Last year it should be in the same day: 189 | 190 | .. doctest:: relativedelta 191 | 192 | >>> date(2002, 1, 1)+relativedelta(yearday=260) 193 | datetime.date(2002, 9, 17) 194 | 195 | But not in a leap year: 196 | 197 | .. doctest:: relativedelta 198 | 199 | >>> date(2000, 1, 1)+relativedelta(yearday=260) 200 | datetime.date(2000, 9, 16) 201 | 202 | We can use the non-leap year day to ignore this: 203 | 204 | .. doctest:: relativedelta 205 | 206 | >>> date(2000, 1, 1)+relativedelta(nlyearday=260) 207 | datetime.date(2000, 9, 17) 208 | 209 | rrule examples 210 | -------------- 211 | These examples were converted from the RFC. 212 | 213 | Prepare the environment. 214 | 215 | .. testsetup:: rrule 216 | 217 | from dateutil.rrule import * 218 | from dateutil.parser import * 219 | from datetime import * 220 | import pprint 221 | import sys 222 | sys.displayhook = pprint.pprint 223 | 224 | .. doctest:: rrule 225 | 226 | >>> from dateutil.rrule import * 227 | >>> from dateutil.parser import * 228 | >>> from datetime import * 229 | 230 | >>> import pprint 231 | >>> import sys 232 | >>> sys.displayhook = pprint.pprint 233 | 234 | Daily, for 10 occurrences. 235 | 236 | .. doctest:: rrule 237 | 238 | >>> list(rrule(DAILY, count=10, 239 | dtstart=parse("19970902T090000"))) 240 | [datetime.datetime(1997, 9, 2, 9, 0), 241 | datetime.datetime(1997, 9, 3, 9, 0), 242 | datetime.datetime(1997, 9, 4, 9, 0), 243 | datetime.datetime(1997, 9, 5, 9, 0), 244 | datetime.datetime(1997, 9, 6, 9, 0), 245 | datetime.datetime(1997, 9, 7, 9, 0), 246 | datetime.datetime(1997, 9, 8, 9, 0), 247 | datetime.datetime(1997, 9, 9, 9, 0), 248 | datetime.datetime(1997, 9, 10, 9, 0), 249 | datetime.datetime(1997, 9, 11, 9, 0)] 250 | 251 | Daily until December 24, 1997 252 | 253 | .. doctest:: rrule 254 | 255 | >>> list(rrule(DAILY, 256 | dtstart=parse("19970902T090000"), 257 | until=parse("19971224T000000"))) 258 | [datetime.datetime(1997, 9, 2, 9, 0), 259 | datetime.datetime(1997, 9, 3, 9, 0), 260 | datetime.datetime(1997, 9, 4, 9, 0), 261 | (...) 262 | datetime.datetime(1997, 12, 21, 9, 0), 263 | datetime.datetime(1997, 12, 22, 9, 0), 264 | datetime.datetime(1997, 12, 23, 9, 0)] 265 | 266 | Every other day, 5 occurrences. 267 | 268 | .. doctest:: rrule 269 | 270 | >>> list(rrule(DAILY, interval=2, count=5, 271 | dtstart=parse("19970902T090000"))) 272 | [datetime.datetime(1997, 9, 2, 9, 0), 273 | datetime.datetime(1997, 9, 4, 9, 0), 274 | datetime.datetime(1997, 9, 6, 9, 0), 275 | datetime.datetime(1997, 9, 8, 9, 0), 276 | datetime.datetime(1997, 9, 10, 9, 0)] 277 | 278 | Every 10 days, 5 occurrences. 279 | 280 | .. doctest:: rrule 281 | 282 | >>> list(rrule(DAILY, interval=10, count=5, 283 | dtstart=parse("19970902T090000"))) 284 | [datetime.datetime(1997, 9, 2, 9, 0), 285 | datetime.datetime(1997, 9, 12, 9, 0), 286 | datetime.datetime(1997, 9, 22, 9, 0), 287 | datetime.datetime(1997, 10, 2, 9, 0), 288 | datetime.datetime(1997, 10, 12, 9, 0)] 289 | 290 | Everyday in January, for 3 years. 291 | 292 | .. doctest:: rrule 293 | 294 | >>> list(rrule(YEARLY, bymonth=1, byweekday=range(7), 295 | dtstart=parse("19980101T090000"), 296 | until=parse("20000131T090000"))) 297 | [datetime.datetime(1998, 1, 1, 9, 0), 298 | datetime.datetime(1998, 1, 2, 9, 0), 299 | (...) 300 | datetime.datetime(1998, 1, 30, 9, 0), 301 | datetime.datetime(1998, 1, 31, 9, 0), 302 | datetime.datetime(1999, 1, 1, 9, 0), 303 | datetime.datetime(1999, 1, 2, 9, 0), 304 | (...) 305 | datetime.datetime(1999, 1, 30, 9, 0), 306 | datetime.datetime(1999, 1, 31, 9, 0), 307 | datetime.datetime(2000, 1, 1, 9, 0), 308 | datetime.datetime(2000, 1, 2, 9, 0), 309 | (...) 310 | datetime.datetime(2000, 1, 29, 9, 0), 311 | datetime.datetime(2000, 1, 31, 9, 0)] 312 | 313 | Same thing, in another way. 314 | 315 | .. doctest:: rrule 316 | 317 | >>> list(rrule(DAILY, bymonth=1, 318 | dtstart=parse("19980101T090000"), 319 | until=parse("20000131T090000"))) 320 | (...) 321 | 322 | Weekly for 10 occurrences. 323 | 324 | .. doctest:: rrule 325 | 326 | >>> list(rrule(WEEKLY, count=10, 327 | dtstart=parse("19970902T090000"))) 328 | [datetime.datetime(1997, 9, 2, 9, 0), 329 | datetime.datetime(1997, 9, 9, 9, 0), 330 | datetime.datetime(1997, 9, 16, 9, 0), 331 | datetime.datetime(1997, 9, 23, 9, 0), 332 | datetime.datetime(1997, 9, 30, 9, 0), 333 | datetime.datetime(1997, 10, 7, 9, 0), 334 | datetime.datetime(1997, 10, 14, 9, 0), 335 | datetime.datetime(1997, 10, 21, 9, 0), 336 | datetime.datetime(1997, 10, 28, 9, 0), 337 | datetime.datetime(1997, 11, 4, 9, 0)] 338 | 339 | Every other week, 6 occurrences. 340 | 341 | .. doctest:: rrule 342 | 343 | >>> list(rrule(WEEKLY, interval=2, count=6, 344 | dtstart=parse("19970902T090000"))) 345 | [datetime.datetime(1997, 9, 2, 9, 0), 346 | datetime.datetime(1997, 9, 16, 9, 0), 347 | datetime.datetime(1997, 9, 30, 9, 0), 348 | datetime.datetime(1997, 10, 14, 9, 0), 349 | datetime.datetime(1997, 10, 28, 9, 0), 350 | datetime.datetime(1997, 11, 11, 9, 0)] 351 | 352 | Weekly on Tuesday and Thursday for 5 weeks. 353 | 354 | .. doctest:: rrule 355 | 356 | >>> list(rrule(WEEKLY, count=10, wkst=SU, byweekday=(TU,TH), 357 | dtstart=parse("19970902T090000"))) 358 | [datetime.datetime(1997, 9, 2, 9, 0), 359 | datetime.datetime(1997, 9, 4, 9, 0), 360 | datetime.datetime(1997, 9, 9, 9, 0), 361 | datetime.datetime(1997, 9, 11, 9, 0), 362 | datetime.datetime(1997, 9, 16, 9, 0), 363 | datetime.datetime(1997, 9, 18, 9, 0), 364 | datetime.datetime(1997, 9, 23, 9, 0), 365 | datetime.datetime(1997, 9, 25, 9, 0), 366 | datetime.datetime(1997, 9, 30, 9, 0), 367 | datetime.datetime(1997, 10, 2, 9, 0)] 368 | 369 | Every other week on Tuesday and Thursday, for 8 occurrences. 370 | 371 | .. doctest:: rrule 372 | 373 | >>> list(rrule(WEEKLY, interval=2, count=8, 374 | wkst=SU, byweekday=(TU,TH), 375 | dtstart=parse("19970902T090000"))) 376 | [datetime.datetime(1997, 9, 2, 9, 0), 377 | datetime.datetime(1997, 9, 4, 9, 0), 378 | datetime.datetime(1997, 9, 16, 9, 0), 379 | datetime.datetime(1997, 9, 18, 9, 0), 380 | datetime.datetime(1997, 9, 30, 9, 0), 381 | datetime.datetime(1997, 10, 2, 9, 0), 382 | datetime.datetime(1997, 10, 14, 9, 0), 383 | datetime.datetime(1997, 10, 16, 9, 0)] 384 | 385 | Monthly on the 1st Friday for ten occurrences. 386 | 387 | .. doctest:: rrule 388 | 389 | >>> list(rrule(MONTHLY, count=10, byweekday=FR(1), 390 | dtstart=parse("19970905T090000"))) 391 | [datetime.datetime(1997, 9, 5, 9, 0), 392 | datetime.datetime(1997, 10, 3, 9, 0), 393 | datetime.datetime(1997, 11, 7, 9, 0), 394 | datetime.datetime(1997, 12, 5, 9, 0), 395 | datetime.datetime(1998, 1, 2, 9, 0), 396 | datetime.datetime(1998, 2, 6, 9, 0), 397 | datetime.datetime(1998, 3, 6, 9, 0), 398 | datetime.datetime(1998, 4, 3, 9, 0), 399 | datetime.datetime(1998, 5, 1, 9, 0), 400 | datetime.datetime(1998, 6, 5, 9, 0)] 401 | 402 | Every other month on the 1st and last Sunday of the month for 10 occurrences. 403 | 404 | .. doctest:: rrule 405 | 406 | >>> list(rrule(MONTHLY, interval=2, count=10, 407 | byweekday=(SU(1), SU(-1)), 408 | dtstart=parse("19970907T090000"))) 409 | [datetime.datetime(1997, 9, 7, 9, 0), 410 | datetime.datetime(1997, 9, 28, 9, 0), 411 | datetime.datetime(1997, 11, 2, 9, 0), 412 | datetime.datetime(1997, 11, 30, 9, 0), 413 | datetime.datetime(1998, 1, 4, 9, 0), 414 | datetime.datetime(1998, 1, 25, 9, 0), 415 | datetime.datetime(1998, 3, 1, 9, 0), 416 | datetime.datetime(1998, 3, 29, 9, 0), 417 | datetime.datetime(1998, 5, 3, 9, 0), 418 | datetime.datetime(1998, 5, 31, 9, 0)] 419 | 420 | Monthly on the second to last Monday of the month for 6 months. 421 | 422 | .. doctest:: rrule 423 | 424 | >>> list(rrule(MONTHLY, count=6, byweekday=MO(-2), 425 | dtstart=parse("19970922T090000"))) 426 | [datetime.datetime(1997, 9, 22, 9, 0), 427 | datetime.datetime(1997, 10, 20, 9, 0), 428 | datetime.datetime(1997, 11, 17, 9, 0), 429 | datetime.datetime(1997, 12, 22, 9, 0), 430 | datetime.datetime(1998, 1, 19, 9, 0), 431 | datetime.datetime(1998, 2, 16, 9, 0)] 432 | 433 | 434 | Monthly on the third to the last day of the month, for 6 months. 435 | 436 | .. doctest:: rrule 437 | 438 | >>> list(rrule(MONTHLY, count=6, bymonthday=-3, 439 | dtstart=parse("19970928T090000"))) 440 | [datetime.datetime(1997, 9, 28, 9, 0), 441 | datetime.datetime(1997, 10, 29, 9, 0), 442 | datetime.datetime(1997, 11, 28, 9, 0), 443 | datetime.datetime(1997, 12, 29, 9, 0), 444 | datetime.datetime(1998, 1, 29, 9, 0), 445 | datetime.datetime(1998, 2, 26, 9, 0)] 446 | 447 | 448 | Monthly on the 2nd and 15th of the month for 5 occurrences. 449 | 450 | .. doctest:: rrule 451 | 452 | >>> list(rrule(MONTHLY, count=5, bymonthday=(2,15), 453 | dtstart=parse("19970902T090000"))) 454 | [datetime.datetime(1997, 9, 2, 9, 0), 455 | datetime.datetime(1997, 9, 15, 9, 0), 456 | datetime.datetime(1997, 10, 2, 9, 0), 457 | datetime.datetime(1997, 10, 15, 9, 0), 458 | datetime.datetime(1997, 11, 2, 9, 0)] 459 | 460 | 461 | Monthly on the first and last day of the month for 3 occurrences. 462 | 463 | .. doctest:: rrule 464 | 465 | >>> list(rrule(MONTHLY, count=5, bymonthday=(-1,1,), 466 | dtstart=parse("1997090 467 | 2T090000"))) 468 | [datetime.datetime(1997, 9, 30, 9, 0), 469 | datetime.datetime(1997, 10, 1, 9, 0), 470 | datetime.datetime(1997, 10, 31, 9, 0), 471 | datetime.datetime(1997, 11, 1, 9, 0), 472 | datetime.datetime(1997, 11, 30, 9, 0)] 473 | 474 | 475 | Every 18 months on the 10th thru 15th of the month for 10 occurrences. 476 | 477 | .. doctest:: rrule 478 | 479 | >>> list(rrule(MONTHLY, interval=18, count=10, 480 | bymonthday=range(10,16), 481 | dtstart=parse("19970910T090000"))) 482 | [datetime.datetime(1997, 9, 10, 9, 0), 483 | datetime.datetime(1997, 9, 11, 9, 0), 484 | datetime.datetime(1997, 9, 12, 9, 0), 485 | datetime.datetime(1997, 9, 13, 9, 0), 486 | datetime.datetime(1997, 9, 14, 9, 0), 487 | datetime.datetime(1997, 9, 15, 9, 0), 488 | datetime.datetime(1999, 3, 10, 9, 0), 489 | datetime.datetime(1999, 3, 11, 9, 0), 490 | datetime.datetime(1999, 3, 12, 9, 0), 491 | datetime.datetime(1999, 3, 13, 9, 0)] 492 | 493 | 494 | Every Tuesday, every other month, 6 occurences. 495 | 496 | .. doctest:: rrule 497 | 498 | >>> list(rrule(MONTHLY, interval=2, count=6, byweekday=TU, 499 | dtstart=parse("19970902T090000"))) 500 | [datetime.datetime(1997, 9, 2, 9, 0), 501 | datetime.datetime(1997, 9, 9, 9, 0), 502 | datetime.datetime(1997, 9, 16, 9, 0), 503 | datetime.datetime(1997, 9, 23, 9, 0), 504 | datetime.datetime(1997, 9, 30, 9, 0), 505 | datetime.datetime(1997, 11, 4, 9, 0)] 506 | 507 | 508 | Yearly in June and July for 10 occurrences. 509 | 510 | .. doctest:: rrule 511 | 512 | >>> list(rrule(YEARLY, count=4, bymonth=(6,7), 513 | dtstart=parse("19970610T0900 514 | 00"))) 515 | [datetime.datetime(1997, 6, 10, 9, 0), 516 | datetime.datetime(1997, 7, 10, 9, 0), 517 | datetime.datetime(1998, 6, 10, 9, 0), 518 | datetime.datetime(1998, 7, 10, 9, 0)] 519 | 520 | 521 | Every 3rd year on the 1st, 100th and 200th day for 4 occurrences. 522 | 523 | .. doctest:: rrule 524 | 525 | >>> list(rrule(YEARLY, count=4, interval=3, byyearday=(1,100,200), 526 | dtstart=parse("19970101T090000"))) 527 | [datetime.datetime(1997, 1, 1, 9, 0), 528 | datetime.datetime(1997, 4, 10, 9, 0), 529 | datetime.datetime(1997, 7, 19, 9, 0), 530 | datetime.datetime(2000, 1, 1, 9, 0)] 531 | 532 | 533 | Every 20th Monday of the year, 3 occurrences. 534 | 535 | .. doctest:: rrule 536 | 537 | >>> list(rrule(YEARLY, count=3, byweekday=MO(20), 538 | dtstart=parse("19970519T090000"))) 539 | [datetime.datetime(1997, 5, 19, 9, 0), 540 | datetime.datetime(1998, 5, 18, 9, 0), 541 | datetime.datetime(1999, 5, 17, 9, 0)] 542 | 543 | 544 | Monday of week number 20 (where the default start of the week is Monday), 545 | 3 occurrences. 546 | 547 | .. doctest:: rrule 548 | 549 | >>> list(rrule(YEARLY, count=3, byweekno=20, byweekday=MO, 550 | dtstart=parse("19970512T090000"))) 551 | [datetime.datetime(1997, 5, 12, 9, 0), 552 | datetime.datetime(1998, 5, 11, 9, 0), 553 | datetime.datetime(1999, 5, 17, 9, 0)] 554 | 555 | 556 | The week number 1 may be in the last year. 557 | 558 | .. doctest:: rrule 559 | 560 | >>> list(rrule(WEEKLY, count=3, byweekno=1, byweekday=MO, 561 | dtstart=parse("19970902T090000"))) 562 | [datetime.datetime(1997, 12, 29, 9, 0), 563 | datetime.datetime(1999, 1, 4, 9, 0), 564 | datetime.datetime(2000, 1, 3, 9, 0)] 565 | 566 | 567 | And the week numbers greater than 51 may be in the next year. 568 | 569 | .. doctest:: rrule 570 | 571 | >>> list(rrule(WEEKLY, count=3, byweekno=52, byweekday=SU, 572 | dtstart=parse("19970902T090000"))) 573 | [datetime.datetime(1997, 12, 28, 9, 0), 574 | datetime.datetime(1998, 12, 27, 9, 0), 575 | datetime.datetime(2000, 1, 2, 9, 0)] 576 | 577 | 578 | Only some years have week number 53: 579 | 580 | .. doctest:: rrule 581 | 582 | >>> list(rrule(WEEKLY, count=3, byweekno=53, byweekday=MO, 583 | dtstart=parse("19970902T090000"))) 584 | [datetime.datetime(1998, 12, 28, 9, 0), 585 | datetime.datetime(2004, 12, 27, 9, 0), 586 | datetime.datetime(2009, 12, 28, 9, 0)] 587 | 588 | 589 | Every Friday the 13th, 4 occurrences. 590 | 591 | .. doctest:: rrule 592 | 593 | >>> list(rrule(YEARLY, count=4, byweekday=FR, bymonthday=13, 594 | dtstart=parse("19970902T090000"))) 595 | [datetime.datetime(1998, 2, 13, 9, 0), 596 | datetime.datetime(1998, 3, 13, 9, 0), 597 | datetime.datetime(1998, 11, 13, 9, 0), 598 | datetime.datetime(1999, 8, 13, 9, 0)] 599 | 600 | 601 | Every four years, the first Tuesday after a Monday in November, 602 | 3 occurrences (U.S. Presidential Election day): 603 | 604 | .. doctest:: rrule 605 | 606 | >>> list(rrule(YEARLY, interval=4, count=3, bymonth=11, 607 | byweekday=TU, bymonthday=(2,3,4,5,6,7,8), 608 | dtstart=parse("19961105T090000"))) 609 | [datetime.datetime(1996, 11, 5, 9, 0), 610 | datetime.datetime(2000, 11, 7, 9, 0), 611 | datetime.datetime(2004, 11, 2, 9, 0)] 612 | 613 | 614 | The 3rd instance into the month of one of Tuesday, Wednesday or 615 | Thursday, for the next 3 months: 616 | 617 | .. doctest:: rrule 618 | 619 | >>> list(rrule(MONTHLY, count=3, byweekday=(TU,WE,TH), 620 | bysetpos=3, dtstart=parse("19970904T090000"))) 621 | [datetime.datetime(1997, 9, 4, 9, 0), 622 | datetime.datetime(1997, 10, 7, 9, 0), 623 | datetime.datetime(1997, 11, 6, 9, 0)] 624 | 625 | 626 | The 2nd to last weekday of the month, 3 occurrences. 627 | 628 | .. doctest:: rrule 629 | 630 | >>> list(rrule(MONTHLY, count=3, byweekday=(MO,TU,WE,TH,FR), 631 | bysetpos=-2, dtstart=parse("19970929T090000"))) 632 | [datetime.datetime(1997, 9, 29, 9, 0), 633 | datetime.datetime(1997, 10, 30, 9, 0), 634 | datetime.datetime(1997, 11, 27, 9, 0)] 635 | 636 | 637 | Every 3 hours from 9:00 AM to 5:00 PM on a specific day. 638 | 639 | .. doctest:: rrule 640 | 641 | >>> list(rrule(HOURLY, interval=3, 642 | dtstart=parse("19970902T090000"), 643 | until=parse("19970902T170000"))) 644 | [datetime.datetime(1997, 9, 2, 9, 0), 645 | datetime.datetime(1997, 9, 2, 12, 0), 646 | datetime.datetime(1997, 9, 2, 15, 0)] 647 | 648 | 649 | Every 15 minutes for 6 occurrences. 650 | 651 | .. doctest:: rrule 652 | 653 | >>> list(rrule(MINUTELY, interval=15, count=6, 654 | dtstart=parse("19970902T090000"))) 655 | [datetime.datetime(1997, 9, 2, 9, 0), 656 | datetime.datetime(1997, 9, 2, 9, 15), 657 | datetime.datetime(1997, 9, 2, 9, 30), 658 | datetime.datetime(1997, 9, 2, 9, 45), 659 | datetime.datetime(1997, 9, 2, 10, 0), 660 | datetime.datetime(1997, 9, 2, 10, 15)] 661 | 662 | 663 | Every hour and a half for 4 occurrences. 664 | 665 | .. doctest:: rrule 666 | 667 | >>> list(rrule(MINUTELY, interval=90, count=4, 668 | dtstart=parse("19970902T090000"))) 669 | [datetime.datetime(1997, 9, 2, 9, 0), 670 | datetime.datetime(1997, 9, 2, 10, 30), 671 | datetime.datetime(1997, 9, 2, 12, 0), 672 | datetime.datetime(1997, 9, 2, 13, 30)] 673 | 674 | 675 | Every 20 minutes from 9:00 AM to 4:40 PM for two days. 676 | 677 | .. doctest:: rrule 678 | 679 | >>> list(rrule(MINUTELY, interval=20, count=48, 680 | byhour=range(9,17), byminute=(0,20,40), 681 | dtstart=parse("19970902T090000"))) 682 | [datetime.datetime(1997, 9, 2, 9, 0), 683 | datetime.datetime(1997, 9, 2, 9, 20), 684 | (...) 685 | datetime.datetime(1997, 9, 2, 16, 20), 686 | datetime.datetime(1997, 9, 2, 16, 40), 687 | datetime.datetime(1997, 9, 3, 9, 0), 688 | datetime.datetime(1997, 9, 3, 9, 20), 689 | (...) 690 | datetime.datetime(1997, 9, 3, 16, 20), 691 | datetime.datetime(1997, 9, 3, 16, 40)] 692 | 693 | 694 | An example where the days generated makes a difference because of `wkst`. 695 | 696 | .. doctest:: rrule 697 | 698 | >>> list(rrule(WEEKLY, interval=2, count=4, 699 | byweekday=(TU,SU), wkst=MO, 700 | dtstart=parse("19970805T090000"))) 701 | [datetime.datetime(1997, 8, 5, 9, 0), 702 | datetime.datetime(1997, 8, 10, 9, 0), 703 | datetime.datetime(1997, 8, 19, 9, 0), 704 | datetime.datetime(1997, 8, 24, 9, 0)] 705 | 706 | >>> list(rrule(WEEKLY, interval=2, count=4, 707 | byweekday=(TU,SU), wkst=SU, 708 | dtstart=parse("19970805T090000"))) 709 | [datetime.datetime(1997, 8, 5, 9, 0), 710 | datetime.datetime(1997, 8, 17, 9, 0), 711 | datetime.datetime(1997, 8, 19, 9, 0), 712 | datetime.datetime(1997, 8, 31, 9, 0)] 713 | 714 | 715 | rruleset examples 716 | ----------------- 717 | Daily, for 7 days, jumping Saturday and Sunday occurrences. 718 | 719 | .. doctest:: rruleset 720 | 721 | >>> set = rruleset() 722 | >>> set.rrule(rrule(DAILY, count=7, 723 | dtstart=parse("19970902T090000"))) 724 | >>> set.exrule(rrule(YEARLY, byweekday=(SA,SU), 725 | dtstart=parse("19970902T090000"))) 726 | >>> list(set) 727 | [datetime.datetime(1997, 9, 2, 9, 0), 728 | datetime.datetime(1997, 9, 3, 9, 0), 729 | datetime.datetime(1997, 9, 4, 9, 0), 730 | datetime.datetime(1997, 9, 5, 9, 0), 731 | datetime.datetime(1997, 9, 8, 9, 0)] 732 | 733 | 734 | Weekly, for 4 weeks, plus one time on day 7, and not on day 16. 735 | 736 | .. doctest:: rruleset 737 | 738 | >>> set = rruleset() 739 | >>> set.rrule(rrule(WEEKLY, count=4, 740 | dtstart=parse("19970902T090000"))) 741 | >>> set.rdate(datetime.datetime(1997, 9, 7, 9, 0)) 742 | >>> set.exdate(datetime.datetime(1997, 9, 16, 9, 0)) 743 | >>> list(set) 744 | [datetime.datetime(1997, 9, 2, 9, 0), 745 | datetime.datetime(1997, 9, 7, 9, 0), 746 | datetime.datetime(1997, 9, 9, 9, 0), 747 | datetime.datetime(1997, 9, 23, 9, 0)] 748 | 749 | 750 | rrulestr() examples 751 | ------------------- 752 | 753 | Every 10 days, 5 occurrences. 754 | 755 | .. doctest:: rrulestr 756 | 757 | >>> list(rrulestr(""" 758 | ... DTSTART:19970902T090000 759 | ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 760 | ... """)) 761 | [datetime.datetime(1997, 9, 2, 9, 0), 762 | datetime.datetime(1997, 9, 12, 9, 0), 763 | datetime.datetime(1997, 9, 22, 9, 0), 764 | datetime.datetime(1997, 10, 2, 9, 0), 765 | datetime.datetime(1997, 10, 12, 9, 0)] 766 | 767 | 768 | Same thing, but passing only the `RRULE` value. 769 | 770 | .. doctest:: rrulestr 771 | 772 | >>> list(rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", 773 | dtstart=parse("19970902T090000"))) 774 | [datetime.datetime(1997, 9, 2, 9, 0), 775 | datetime.datetime(1997, 9, 12, 9, 0), 776 | datetime.datetime(1997, 9, 22, 9, 0), 777 | datetime.datetime(1997, 10, 2, 9, 0), 778 | datetime.datetime(1997, 10, 12, 9, 0)] 779 | 780 | 781 | Notice that when using a single rule, it returns an 782 | `rrule` instance, unless `forceset` was used. 783 | 784 | .. doctest:: rrulestr 785 | 786 | >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5") 787 | 788 | 789 | >>> rrulestr(""" 790 | ... DTSTART:19970902T090000 791 | ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 792 | ... """) 793 | 794 | 795 | >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", forceset=True) 796 | 797 | 798 | 799 | But when an `rruleset` is needed, it is automatically used. 800 | 801 | .. doctest:: rrulestr 802 | 803 | >>> rrulestr(""" 804 | ... DTSTART:19970902T090000 805 | ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 806 | ... RRULE:FREQ=DAILY;INTERVAL=5;COUNT=3 807 | ... """) 808 | 809 | 810 | 811 | parse examples 812 | ----------- 813 | The following code will prepare the environment: 814 | 815 | .. doctest:: tz 816 | 817 | >>> from dateutil.parser import * 818 | >>> from dateutil.tz import * 819 | >>> from datetime import * 820 | >>> TZOFFSETS = {"BRST": -10800} 821 | >>> BRSTTZ = tzoffset(-10800, "BRST") 822 | >>> DEFAULT = datetime(2003, 9, 25) 823 | 824 | 825 | Some simple examples based on the `date` command, using the 826 | `ZOFFSET` dictionary to provide the BRST timezone offset. 827 | 828 | .. doctest:: tz 829 | 830 | >>> parse("Thu Sep 25 10:36:28 BRST 2003", tzinfos=TZOFFSETS) 831 | datetime.datetime(2003, 9, 25, 10, 36, 28, 832 | tzinfo=tzoffset('BRST', -10800)) 833 | 834 | >>> parse("2003 10:36:28 BRST 25 Sep Thu", tzinfos=TZOFFSETS) 835 | datetime.datetime(2003, 9, 25, 10, 36, 28, 836 | tzinfo=tzoffset('BRST', -10800)) 837 | 838 | 839 | Notice that since BRST is my local timezone, parsing it without 840 | further timezone settings will yield a `tzlocal` timezone. 841 | 842 | .. doctest:: tz 843 | 844 | >>> parse("Thu Sep 25 10:36:28 BRST 2003") 845 | datetime.datetime(2003, 9, 25, 10, 36, 28, tzinfo=tzlocal()) 846 | 847 | 848 | We can also ask to ignore the timezone explicitly: 849 | 850 | .. doctest:: tz 851 | 852 | >>> parse("Thu Sep 25 10:36:28 BRST 2003", ignoretz=True) 853 | datetime.datetime(2003, 9, 25, 10, 36, 28) 854 | 855 | 856 | That's the same as processing a string without timezone: 857 | 858 | .. doctest:: tz 859 | 860 | >>> parse("Thu Sep 25 10:36:28 2003") 861 | datetime.datetime(2003, 9, 25, 10, 36, 28) 862 | 863 | 864 | Without the year, but passing our `DEFAULT` datetime to return 865 | the same year, no mattering what year we currently are in: 866 | 867 | .. doctest:: tz 868 | 869 | >>> parse("Thu Sep 25 10:36:28", default=DEFAULT) 870 | datetime.datetime(2003, 9, 25, 10, 36, 28) 871 | 872 | 873 | Strip it further: 874 | 875 | .. doctest:: tz 876 | 877 | >>> parse("Thu Sep 10:36:28", default=DEFAULT) 878 | datetime.datetime(2003, 9, 25, 10, 36, 28) 879 | 880 | >>> parse("Thu 10:36:28", default=DEFAULT) 881 | datetime.datetime(2003, 9, 25, 10, 36, 28) 882 | 883 | >>> parse("Thu 10:36", default=DEFAULT) 884 | datetime.datetime(2003, 9, 25, 10, 36) 885 | 886 | >>> parse("10:36", default=DEFAULT) 887 | datetime.datetime(2003, 9, 25, 10, 36) 888 | >>> 889 | 890 | 891 | Strip in a different way: 892 | 893 | .. doctest:: tz 894 | 895 | >>> parse("Thu Sep 25 2003") 896 | datetime.datetime(2003, 9, 25, 0, 0) 897 | 898 | >>> parse("Sep 25 2003") 899 | datetime.datetime(2003, 9, 25, 0, 0) 900 | 901 | >>> parse("Sep 2003", default=DEFAULT) 902 | datetime.datetime(2003, 9, 25, 0, 0) 903 | 904 | >>> parse("Sep", default=DEFAULT) 905 | datetime.datetime(2003, 9, 25, 0, 0) 906 | 907 | >>> parse("2003", default=DEFAULT) 908 | datetime.datetime(2003, 9, 25, 0, 0) 909 | 910 | 911 | Another format, based on `date -R` (RFC822): 912 | 913 | .. doctest:: tz 914 | 915 | >>> parse("Thu, 25 Sep 2003 10:49:41 -0300") 916 | datetime.datetime(2003, 9, 25, 10, 49, 41, 917 | tzinfo=tzoffset(None, -10800)) 918 | 919 | 920 | ISO format: 921 | 922 | .. doctest:: tz 923 | 924 | >>> parse("2003-09-25T10:49:41.5-03:00") 925 | datetime.datetime(2003, 9, 25, 10, 49, 41, 500000, 926 | tzinfo=tzoffset(None, -10800)) 927 | 928 | 929 | Some variations: 930 | 931 | .. doctest:: tz 932 | 933 | >>> parse("2003-09-25T10:49:41") 934 | datetime.datetime(2003, 9, 25, 10, 49, 41) 935 | 936 | >>> parse("2003-09-25T10:49") 937 | datetime.datetime(2003, 9, 25, 10, 49) 938 | 939 | >>> parse("2003-09-25T10") 940 | datetime.datetime(2003, 9, 25, 10, 0) 941 | 942 | >>> parse("2003-09-25") 943 | datetime.datetime(2003, 9, 25, 0, 0) 944 | 945 | 946 | ISO format, without separators: 947 | 948 | .. doctest:: tz 949 | 950 | >>> parse("20030925T104941.5-0300") 951 | datetime.datetime(2003, 9, 25, 10, 49, 41, 500000, 952 | tzinfo=tzinfo=tzoffset(None, -10800)) 953 | 954 | >>> parse("20030925T104941-0300") 955 | datetime.datetime(2003, 9, 25, 10, 49, 41, 956 | tzinfo=tzoffset(None, -10800)) 957 | 958 | >>> parse("20030925T104941") 959 | datetime.datetime(2003, 9, 25, 10, 49, 41) 960 | 961 | >>> parse("20030925T1049") 962 | datetime.datetime(2003, 9, 25, 10, 49) 963 | 964 | >>> parse("20030925T10") 965 | datetime.datetime(2003, 9, 25, 10, 0) 966 | 967 | >>> parse("20030925") 968 | datetime.datetime(2003, 9, 25, 0, 0) 969 | 970 | 971 | Everything together. 972 | 973 | .. doctest:: tz 974 | 975 | >>> parse("199709020900") 976 | datetime.datetime(1997, 9, 2, 9, 0) 977 | >>> parse("19970902090059") 978 | datetime.datetime(1997, 9, 2, 9, 0, 59) 979 | 980 | 981 | Different date orderings: 982 | 983 | .. doctest:: tz 984 | 985 | >>> parse("2003-09-25") 986 | datetime.datetime(2003, 9, 25, 0, 0) 987 | 988 | >>> parse("2003-Sep-25") 989 | datetime.datetime(2003, 9, 25, 0, 0) 990 | 991 | >>> parse("25-Sep-2003") 992 | datetime.datetime(2003, 9, 25, 0, 0) 993 | 994 | >>> parse("Sep-25-2003") 995 | datetime.datetime(2003, 9, 25, 0, 0) 996 | 997 | >>> parse("09-25-2003") 998 | datetime.datetime(2003, 9, 25, 0, 0) 999 | 1000 | >>> parse("25-09-2003") 1001 | datetime.datetime(2003, 9, 25, 0, 0) 1002 | 1003 | 1004 | Check some ambiguous dates: 1005 | 1006 | .. doctest:: tz 1007 | 1008 | >>> parse("10-09-2003") 1009 | datetime.datetime(2003, 10, 9, 0, 0) 1010 | 1011 | >>> parse("10-09-2003", dayfirst=True) 1012 | datetime.datetime(2003, 9, 10, 0, 0) 1013 | 1014 | >>> parse("10-09-03") 1015 | datetime.datetime(2003, 10, 9, 0, 0) 1016 | 1017 | >>> parse("10-09-03", yearfirst=True) 1018 | datetime.datetime(2010, 9, 3, 0, 0) 1019 | 1020 | 1021 | Other date separators are allowed: 1022 | 1023 | .. doctest:: tz 1024 | 1025 | >>> parse("2003.Sep.25") 1026 | datetime.datetime(2003, 9, 25, 0, 0) 1027 | 1028 | >>> parse("2003/09/25") 1029 | datetime.datetime(2003, 9, 25, 0, 0) 1030 | 1031 | 1032 | Even with spaces: 1033 | 1034 | .. doctest:: tz 1035 | 1036 | >>> parse("2003 Sep 25") 1037 | datetime.datetime(2003, 9, 25, 0, 0) 1038 | 1039 | >>> parse("2003 09 25") 1040 | datetime.datetime(2003, 9, 25, 0, 0) 1041 | 1042 | 1043 | Hours with letters work: 1044 | 1045 | .. doctest:: tz 1046 | 1047 | >>> parse("10h36m28.5s", default=DEFAULT) 1048 | datetime.datetime(2003, 9, 25, 10, 36, 28, 500000) 1049 | 1050 | >>> parse("01s02h03m", default=DEFAULT) 1051 | datetime.datetime(2003, 9, 25, 2, 3, 1) 1052 | 1053 | >>> parse("01h02m03", default=DEFAULT) 1054 | datetime.datetime(2003, 9, 3, 1, 2) 1055 | 1056 | >>> parse("01h02", default=DEFAULT) 1057 | datetime.datetime(2003, 9, 2, 1, 0) 1058 | 1059 | >>> parse("01h02s", default=DEFAULT) 1060 | datetime.datetime(2003, 9, 25, 1, 0, 2) 1061 | 1062 | 1063 | With AM/PM: 1064 | 1065 | .. doctest:: tz 1066 | 1067 | >>> parse("10h am", default=DEFAULT) 1068 | datetime.datetime(2003, 9, 25, 10, 0) 1069 | 1070 | >>> parse("10pm", default=DEFAULT) 1071 | datetime.datetime(2003, 9, 25, 22, 0) 1072 | 1073 | >>> parse("12:00am", default=DEFAULT) 1074 | datetime.datetime(2003, 9, 25, 0, 0) 1075 | 1076 | >>> parse("12pm", default=DEFAULT) 1077 | datetime.datetime(2003, 9, 25, 12, 0) 1078 | 1079 | 1080 | Some special treating for ''pertain'' relations: 1081 | 1082 | .. doctest:: tz 1083 | 1084 | >>> parse("Sep 03", default=DEFAULT) 1085 | datetime.datetime(2003, 9, 3, 0, 0) 1086 | 1087 | >>> parse("Sep of 03", default=DEFAULT) 1088 | datetime.datetime(2003, 9, 25, 0, 0) 1089 | 1090 | 1091 | Fuzzy parsing: 1092 | 1093 | .. doctest:: tz 1094 | 1095 | >>> s = "Today is 25 of September of 2003, exactly " \ 1096 | ... "at 10:49:41 with timezone -03:00." 1097 | >>> parse(s, fuzzy=True) 1098 | datetime.datetime(2003, 9, 25, 10, 49, 41, 1099 | tzinfo=tzoffset(None, -10800)) 1100 | 1101 | 1102 | Other random formats: 1103 | 1104 | .. doctest:: tz 1105 | 1106 | >>> parse("Wed, July 10, '96") 1107 | datetime.datetime(1996, 7, 10, 0, 0) 1108 | 1109 | >>> parse("1996.07.10 AD at 15:08:56 PDT", ignoretz=True) 1110 | datetime.datetime(1996, 7, 10, 15, 8, 56) 1111 | 1112 | >>> parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", ignoretz=True) 1113 | datetime.datetime(1952, 4, 12, 15, 30, 42) 1114 | 1115 | >>> parse("November 5, 1994, 8:15:30 am EST", ignoretz=True) 1116 | datetime.datetime(1994, 11, 5, 8, 15, 30) 1117 | 1118 | >>> parse("3rd of May 2001") 1119 | datetime.datetime(2001, 5, 3, 0, 0) 1120 | 1121 | >>> parse("5:50 A.M. on June 13, 1990") 1122 | datetime.datetime(1990, 6, 13, 5, 50) 1123 | 1124 | 1125 | tzutc examples 1126 | -------------- 1127 | 1128 | .. doctest:: tzutc 1129 | 1130 | >>> from datetime import * 1131 | >>> from dateutil.tz import * 1132 | 1133 | >>> datetime.now() 1134 | datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) 1135 | 1136 | >>> datetime.now(tzutc()) 1137 | datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) 1138 | 1139 | >>> datetime.now(tzutc()).tzname() 1140 | 'UTC' 1141 | 1142 | 1143 | tzoffset examples 1144 | ----------------- 1145 | 1146 | .. doctest:: tzoffset 1147 | 1148 | >>> from datetime import * 1149 | >>> from dateutil.tz import * 1150 | 1151 | >>> datetime.now(tzoffset("BRST", -10800)) 1152 | datetime.datetime(2003, 9, 27, 9, 52, 43, 624904, 1153 | tzinfo=tzinfo=tzoffset('BRST', -10800)) 1154 | 1155 | >>> datetime.now(tzoffset("BRST", -10800)).tzname() 1156 | 'BRST' 1157 | 1158 | >>> datetime.now(tzoffset("BRST", -10800)).astimezone(tzutc()) 1159 | datetime.datetime(2003, 9, 27, 12, 53, 11, 446419, 1160 | tzinfo=tzutc()) 1161 | 1162 | 1163 | tzlocal examples 1164 | ---------------- 1165 | 1166 | .. doctest:: tzlocal 1167 | 1168 | >>> from datetime import * 1169 | >>> from dateutil.tz import * 1170 | 1171 | >>> datetime.now(tzlocal()) 1172 | datetime.datetime(2003, 9, 27, 10, 1, 43, 673605, 1173 | tzinfo=tzlocal()) 1174 | 1175 | >>> datetime.now(tzlocal()).tzname() 1176 | 'BRST' 1177 | 1178 | >>> datetime.now(tzlocal()).astimezone(tzoffset(None, 0)) 1179 | datetime.datetime(2003, 9, 27, 13, 3, 0, 11493, 1180 | tzinfo=tzoffset(None, 0)) 1181 | 1182 | 1183 | tzstr examples 1184 | -------------- 1185 | Here are examples of the recognized formats: 1186 | 1187 | * `EST5EDT` 1188 | * `EST5EDT,4,0,6,7200,10,0,26,7200,3600` 1189 | * `EST5EDT,4,1,0,7200,10,-1,0,7200,3600` 1190 | * `EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00` 1191 | * `EST5EDT4,95/02:00:00,298/02:00` 1192 | * `EST5EDT4,J96/02:00:00,J299/02:00` 1193 | 1194 | Notice that if daylight information is not present, but a 1195 | daylight abbreviation was provided, `tzstr` will follow the 1196 | convention of using the first sunday of April to start daylight 1197 | saving, and the last sunday of October to end it. If start or 1198 | end time is not present, 2AM will be used, and if the daylight 1199 | offset is not present, the standard offset plus one hour will 1200 | be used. This convention is the same as used in the GNU libc. 1201 | 1202 | This also means that some of the above examples are exactly 1203 | equivalent, and all of these examples are equivalent 1204 | in the year of 2003. 1205 | 1206 | Here is the example mentioned in the 1207 | 1208 | [http://www.python.org/doc/current/lib/module-time.html time module documentation]. 1209 | 1210 | 1211 | .. doctest:: tzstr 1212 | 1213 | >>> os.environ['TZ'] = 'EST+05EDT,M4.1.0,M10.5.0' 1214 | >>> time.tzset() 1215 | >>> time.strftime('%X %x %Z') 1216 | '02:07:36 05/08/03 EDT' 1217 | >>> os.environ['TZ'] = 'AEST-10AEDT-11,M10.5.0,M3.5.0' 1218 | >>> time.tzset() 1219 | >>> time.strftime('%X %x %Z') 1220 | '16:08:12 05/08/03 AEST' 1221 | 1222 | 1223 | And here is an example showing the same information using `tzstr`, 1224 | without touching system settings. 1225 | 1226 | .. doctest:: tzstr 1227 | 1228 | >>> tz1 = tzstr('EST+05EDT,M4.1.0,M10.5.0') 1229 | >>> tz2 = tzstr('AEST-10AEDT-11,M10.5.0,M3.5.0') 1230 | >>> dt = datetime(2003, 5, 8, 2, 7, 36, tzinfo=tz1) 1231 | >>> dt.strftime('%X %x %Z') 1232 | '02:07:36 05/08/03 EDT' 1233 | >>> dt.astimezone(tz2).strftime('%X %x %Z') 1234 | '16:07:36 05/08/03 AEST' 1235 | 1236 | 1237 | Are these really equivalent? 1238 | 1239 | .. doctest:: tzstr 1240 | 1241 | >>> tzstr('EST5EDT') == tzstr('EST5EDT,4,1,0,7200,10,-1,0,7200,3600') 1242 | True 1243 | 1244 | 1245 | Check the daylight limit. 1246 | 1247 | .. doctest:: tzstr 1248 | 1249 | >>> datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname() 1250 | 'EST' 1251 | >>> datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname() 1252 | 'EDT' 1253 | >>> datetime(2003, 10, 26, 0, 59, tzinfo=tz).tzname() 1254 | 'EDT' 1255 | >>> datetime(2003, 10, 26, 1, 00, tzinfo=tz).tzname() 1256 | 'EST' 1257 | 1258 | 1259 | tzrange examples 1260 | ---------------- 1261 | 1262 | .. doctest:: tzrange 1263 | 1264 | >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") 1265 | True 1266 | 1267 | >>> from dateutil.relativedelta import * 1268 | >>> range1 = tzrange("EST", -18000, "EDT") 1269 | >>> range2 = tzrange("EST", -18000, "EDT", -14400, 1270 | ... relativedelta(hours=+2, month=4, day=1, 1271 | weekday=SU(+1)), 1272 | ... relativedelta(hours=+1, month=10, day=31, 1273 | weekday=SU(-1))) 1274 | >>> tzstr('EST5EDT') == range1 == range2 1275 | True 1276 | 1277 | 1278 | Notice a minor detail in the last example: while the DST should end 1279 | at 2AM, the delta will catch 1AM. That's because the daylight saving 1280 | time should end at 2AM standard time (the difference between STD and 1281 | DST is 1h in the given example) instead of the DST time. That's how 1282 | the `tzinfo` subtypes should deal with the extra hour that happens 1283 | when going back to the standard time. Check 1284 | 1285 | [http://www.python.org/doc/current/lib/datetime-tzinfo.html tzinfo documentation] 1286 | 1287 | for more information. 1288 | 1289 | tzfile examples 1290 | --------------- 1291 | 1292 | .. doctest:: tzfile 1293 | 1294 | >>> tz = tzfile("/etc/localtime") 1295 | >>> datetime.now(tz) 1296 | datetime.datetime(2003, 9, 27, 12, 3, 48, 392138, 1297 | tzinfo=tzfile('/etc/localtime')) 1298 | 1299 | >>> datetime.now(tz).astimezone(tzutc()) 1300 | datetime.datetime(2003, 9, 27, 15, 3, 53, 70863, 1301 | tzinfo=tzutc()) 1302 | 1303 | >>> datetime.now(tz).tzname() 1304 | 'BRST' 1305 | >>> datetime(2003, 1, 1, tzinfo=tz).tzname() 1306 | 'BRDT' 1307 | 1308 | 1309 | Check the daylight limit. 1310 | 1311 | .. doctest:: tzfile 1312 | 1313 | >>> tz = tzfile('/usr/share/zoneinfo/EST5EDT') 1314 | >>> datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname() 1315 | 'EST' 1316 | >>> datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname() 1317 | 'EDT' 1318 | >>> datetime(2003, 10, 26, 0, 59, tzinfo=tz).tzname() 1319 | 'EDT' 1320 | >>> datetime(2003, 10, 26, 1, 00, tzinfo=tz).tzname() 1321 | 'EST' 1322 | 1323 | 1324 | tzical examples 1325 | --------------- 1326 | Here is a sample file extracted from the RFC. This file defines 1327 | the `EST5EDT` timezone, and will be used in the following example. 1328 | 1329 | BEGIN:VTIMEZONE 1330 | TZID:US-Eastern 1331 | LAST-MODIFIED:19870101T000000Z 1332 | TZURL:http://zones.stds_r_us.net/tz/US-Eastern 1333 | BEGIN:STANDARD 1334 | DTSTART:19671029T020000 1335 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 1336 | TZOFFSETFROM:-0400 1337 | TZOFFSETTO:-0500 1338 | TZNAME:EST 1339 | END:STANDARD 1340 | BEGIN:DAYLIGHT 1341 | DTSTART:19870405T020000 1342 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 1343 | TZOFFSETFROM:-0500 1344 | TZOFFSETTO:-0400 1345 | TZNAME:EDT 1346 | END:DAYLIGHT 1347 | END:VTIMEZONE 1348 | 1349 | And here is an example exploring a `tzical` type: 1350 | 1351 | .. doctest:: tzfile 1352 | 1353 | >>> from dateutil.tz import *; from datetime import * 1354 | 1355 | >>> tz = tzical('EST5EDT.ics') 1356 | >>> tz.keys() 1357 | ['US-Eastern'] 1358 | 1359 | >>> est = tz.get('US-Eastern') 1360 | >>> est 1361 | 1362 | 1363 | >>> datetime.now(est) 1364 | datetime.datetime(2003, 10, 6, 19, 44, 18, 667987, 1365 | tzinfo=) 1366 | 1367 | >>> est == tz.get() 1368 | True 1369 | 1370 | 1371 | Let's check the daylight ranges, as usual: 1372 | 1373 | .. doctest:: tzfile 1374 | 1375 | >>> datetime(2003, 4, 6, 1, 59, tzinfo=est).tzname() 1376 | 'EST' 1377 | >>> datetime(2003, 4, 6, 2, 00, tzinfo=est).tzname() 1378 | 'EDT' 1379 | 1380 | >>> datetime(2003, 10, 26, 0, 59, tzinfo=est).tzname() 1381 | 'EDT' 1382 | >>> datetime(2003, 10, 26, 1, 00, tzinfo=est).tzname() 1383 | 'EST' 1384 | 1385 | 1386 | tzwin examples 1387 | -------------- 1388 | 1389 | .. doctest:: tzwin 1390 | 1391 | >>> tz = tzwin("E. South America Standard Time") 1392 | 1393 | 1394 | tzwinlocal examples 1395 | ------------------- 1396 | 1397 | 1398 | .. doctest:: tzwinlocal 1399 | 1400 | >>> tz = tzwinlocal() 1401 | 1402 | # vim:ts=4:sw=4:et 1403 | -------------------------------------------------------------------------------- /dateutil/rrule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The rrule module offers a small, complete, and very fast, implementation of 4 | the recurrence rules documented in the 5 | `iCalendar RFC `_, 6 | including support for caching of results. 7 | """ 8 | import itertools 9 | import datetime 10 | import calendar 11 | import pytz 12 | import sys 13 | 14 | from fractions import gcd 15 | 16 | from six import advance_iterator, integer_types 17 | from six.moves import _thread 18 | 19 | __all__ = ["rrule", "rruleset", "rrulestr", 20 | "YEARLY", "MONTHLY", "WEEKLY", "DAILY", 21 | "HOURLY", "MINUTELY", "SECONDLY", 22 | "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 23 | 24 | # Every mask is 7 days longer to handle cross-year weekly periods. 25 | M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + 26 | [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) 27 | M365MASK = list(M366MASK) 28 | M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) 29 | MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 30 | MDAY365MASK = list(MDAY366MASK) 31 | M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) 32 | NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 33 | NMDAY365MASK = list(NMDAY366MASK) 34 | M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) 35 | M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) 36 | WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 37 | del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] 38 | MDAY365MASK = tuple(MDAY365MASK) 39 | M365MASK = tuple(M365MASK) 40 | 41 | FREQNAMES = ['YEARLY','MONTHLY','WEEKLY','DAILY','HOURLY','MINUTELY','SECONDLY'] 42 | 43 | (YEARLY, 44 | MONTHLY, 45 | WEEKLY, 46 | DAILY, 47 | HOURLY, 48 | MINUTELY, 49 | SECONDLY) = list(range(7)) 50 | 51 | # Imported on demand. 52 | easter = None 53 | parser = None 54 | 55 | 56 | class weekday(object): 57 | __slots__ = ["weekday", "n"] 58 | 59 | def __init__(self, weekday, n=None): 60 | if n == 0: 61 | raise ValueError("Can't create weekday with n == 0") 62 | self.weekday = weekday 63 | self.n = n 64 | 65 | def __call__(self, n): 66 | if n == self.n: 67 | return self 68 | else: 69 | return self.__class__(self.weekday, n) 70 | 71 | def __eq__(self, other): 72 | try: 73 | if self.weekday != other.weekday or self.n != other.n: 74 | return False 75 | except AttributeError: 76 | return False 77 | return True 78 | 79 | def __repr__(self): 80 | s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] 81 | if not self.n: 82 | return s 83 | else: 84 | return "%s(%+d)" % (s, self.n) 85 | 86 | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) 87 | 88 | 89 | class rrulebase(object): 90 | def __init__(self, cache=False): 91 | if cache: 92 | self._cache = [] 93 | self._cache_lock = _thread.allocate_lock() 94 | self._cache_gen = self._iter() 95 | self._cache_complete = False 96 | else: 97 | self._cache = None 98 | self._cache_complete = False 99 | self._len = None 100 | 101 | def __iter__(self): 102 | if self._cache_complete: 103 | return iter(self._cache) 104 | elif self._cache is None: 105 | return self._iter() 106 | else: 107 | return self._iter_cached() 108 | 109 | def _iter_cached(self): 110 | i = 0 111 | gen = self._cache_gen 112 | cache = self._cache 113 | acquire = self._cache_lock.acquire 114 | release = self._cache_lock.release 115 | while gen: 116 | if i == len(cache): 117 | acquire() 118 | if self._cache_complete: 119 | break 120 | try: 121 | for j in range(10): 122 | cache.append(advance_iterator(gen)) 123 | except StopIteration: 124 | self._cache_gen = gen = None 125 | self._cache_complete = True 126 | break 127 | release() 128 | yield cache[i] 129 | i += 1 130 | while i < self._len: 131 | yield cache[i] 132 | i += 1 133 | 134 | def __getitem__(self, item): 135 | if self._cache_complete: 136 | return self._cache[item] 137 | elif isinstance(item, slice): 138 | if item.step and item.step < 0: 139 | return list(iter(self))[item] 140 | else: 141 | return list(itertools.islice(self, 142 | item.start or 0, 143 | item.stop or sys.maxsize, 144 | item.step or 1)) 145 | elif item >= 0: 146 | gen = iter(self) 147 | try: 148 | for i in range(item+1): 149 | res = advance_iterator(gen) 150 | except StopIteration: 151 | raise IndexError 152 | return res 153 | else: 154 | return list(iter(self))[item] 155 | 156 | def __contains__(self, item): 157 | if self._cache_complete: 158 | return item in self._cache 159 | else: 160 | for i in self: 161 | if i == item: 162 | return True 163 | elif i > item: 164 | return False 165 | return False 166 | 167 | # __len__() introduces a large performance penality. 168 | def count(self): 169 | """ Returns the number of recurrences in this set. It will have go 170 | trough the whole recurrence, if this hasn't been done before. """ 171 | if self._len is None: 172 | for x in self: 173 | pass 174 | return self._len 175 | 176 | def before(self, dt, inc=False): 177 | """ Returns the last recurrence before the given datetime instance. The 178 | inc keyword defines what happens if dt is an occurrence. With 179 | inc=True, if dt itself is an occurrence, it will be returned. """ 180 | if self._cache_complete: 181 | gen = self._cache 182 | else: 183 | gen = self 184 | last = None 185 | if inc: 186 | for i in gen: 187 | if i > dt: 188 | break 189 | last = i 190 | else: 191 | for i in gen: 192 | if i >= dt: 193 | break 194 | last = i 195 | return last 196 | 197 | def after(self, dt, inc=False): 198 | """ Returns the first recurrence after the given datetime instance. The 199 | inc keyword defines what happens if dt is an occurrence. With 200 | inc=True, if dt itself is an occurrence, it will be returned. """ 201 | if self._cache_complete: 202 | gen = self._cache 203 | else: 204 | gen = self 205 | if inc: 206 | for i in gen: 207 | if i >= dt: 208 | return i 209 | else: 210 | for i in gen: 211 | if i > dt: 212 | return i 213 | return None 214 | 215 | def xafter(self, dt, count=None, inc=False): 216 | """ 217 | Generator which yields up to `count` recurrences after the given 218 | datetime instance, equivalent to `after`. 219 | 220 | :param dt: 221 | The datetime at which to start generating recurrences. 222 | 223 | :param count: 224 | The maximum number of recurrences to generate. If `None` (default), 225 | dates are generated until the recurrence rule is exhausted. 226 | 227 | :param inc: 228 | If `dt` is an instance of the rule and `inc` is `True`, it is 229 | included in the output. 230 | 231 | :yields: Yields a sequence of `datetime` objects. 232 | """ 233 | 234 | if self._cache_complete: 235 | gen = self._cache 236 | else: 237 | gen = self 238 | 239 | # Select the comparison function 240 | if inc: 241 | comp = lambda dc, dtc: dc >= dtc 242 | else: 243 | comp = lambda dc, dtc: dc > dtc 244 | 245 | # Generate dates 246 | n = 0 247 | for d in gen: 248 | if comp(d, dt): 249 | yield d 250 | 251 | if count is not None: 252 | n += 1 253 | if n >= count: 254 | break 255 | 256 | def between(self, after, before, inc=False, count=1): 257 | """ Returns all the occurrences of the rrule between after and before. 258 | The inc keyword defines what happens if after and/or before are 259 | themselves occurrences. With inc=True, they will be included in the 260 | list, if they are found in the recurrence set. """ 261 | if self._cache_complete: 262 | gen = self._cache 263 | else: 264 | gen = self 265 | started = False 266 | l = [] 267 | if inc: 268 | for i in gen: 269 | if i > before: 270 | break 271 | elif not started: 272 | if i >= after: 273 | started = True 274 | l.append(i) 275 | else: 276 | l.append(i) 277 | else: 278 | for i in gen: 279 | if i >= before: 280 | break 281 | elif not started: 282 | if i > after: 283 | started = True 284 | l.append(i) 285 | else: 286 | l.append(i) 287 | return l 288 | 289 | 290 | class rrule(rrulebase): 291 | """ 292 | That's the base of the rrule operation. It accepts all the keywords 293 | defined in the RFC as its constructor parameters (except byday, 294 | which was renamed to byweekday) and more. The constructor prototype is:: 295 | 296 | rrule(freq) 297 | 298 | Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, 299 | or SECONDLY. 300 | 301 | Additionally, it supports the following keyword arguments: 302 | 303 | :param cache: 304 | If given, it must be a boolean value specifying to enable or disable 305 | caching of results. If you will use the same rrule instance multiple 306 | times, enabling caching will improve the performance considerably. 307 | :param dtstart: 308 | The recurrence start. Besides being the base for the recurrence, 309 | missing parameters in the final recurrence instances will also be 310 | extracted from this date. If not given, datetime.now() will be used 311 | instead. 312 | :param interval: 313 | The interval between each freq iteration. For example, when using 314 | YEARLY, an interval of 2 means once every two years, but with HOURLY, 315 | it means once every two hours. The default interval is 1. 316 | :param wkst: 317 | The week start day. Must be one of the MO, TU, WE constants, or an 318 | integer, specifying the first day of the week. This will affect 319 | recurrences based on weekly periods. The default week start is got 320 | from calendar.firstweekday(), and may be modified by 321 | calendar.setfirstweekday(). 322 | :param count: 323 | How many occurrences will be generated. 324 | :param until: 325 | If given, this must be a datetime instance, that will specify the 326 | limit of the recurrence. If a recurrence instance happens to be the 327 | same as the datetime instance given in the until keyword, this will 328 | be the last occurrence. 329 | :param bysetpos: 330 | If given, it must be either an integer, or a sequence of integers, 331 | positive or negative. Each given integer will specify an occurrence 332 | number, corresponding to the nth occurrence of the rule inside the 333 | frequency period. For example, a bysetpos of -1 if combined with a 334 | MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will 335 | result in the last work day of every month. 336 | :param bymonth: 337 | If given, it must be either an integer, or a sequence of integers, 338 | meaning the months to apply the recurrence to. 339 | :param bymonthday: 340 | If given, it must be either an integer, or a sequence of integers, 341 | meaning the month days to apply the recurrence to. 342 | :param byyearday: 343 | If given, it must be either an integer, or a sequence of integers, 344 | meaning the year days to apply the recurrence to. 345 | :param byweekno: 346 | If given, it must be either an integer, or a sequence of integers, 347 | meaning the week numbers to apply the recurrence to. Week numbers 348 | have the meaning described in ISO8601, that is, the first week of 349 | the year is that containing at least four days of the new year. 350 | :param byweekday: 351 | If given, it must be either an integer (0 == MO), a sequence of 352 | integers, one of the weekday constants (MO, TU, etc), or a sequence 353 | of these constants. When given, these variables will define the 354 | weekdays where the recurrence will be applied. It's also possible to 355 | use an argument n for the weekday instances, which will mean the nth 356 | occurrence of this weekday in the period. For example, with MONTHLY, 357 | or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the 358 | first friday of the month where the recurrence happens. Notice that in 359 | the RFC documentation, this is specified as BYDAY, but was renamed to 360 | avoid the ambiguity of that keyword. 361 | :param byhour: 362 | If given, it must be either an integer, or a sequence of integers, 363 | meaning the hours to apply the recurrence to. 364 | :param byminute: 365 | If given, it must be either an integer, or a sequence of integers, 366 | meaning the minutes to apply the recurrence to. 367 | :param bysecond: 368 | If given, it must be either an integer, or a sequence of integers, 369 | meaning the seconds to apply the recurrence to. 370 | :param byeaster: 371 | If given, it must be either an integer, or a sequence of integers, 372 | positive or negative. Each integer will define an offset from the 373 | Easter Sunday. Passing the offset 0 to byeaster will yield the Easter 374 | Sunday itself. This is an extension to the RFC specification. 375 | """ 376 | def __init__(self, freq, dtstart=None, 377 | interval=1, wkst=None, count=None, until=None, bysetpos=None, 378 | bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 379 | byweekno=None, byweekday=None, 380 | byhour=None, byminute=None, bysecond=None, 381 | cache=False): 382 | super(rrule, self).__init__(cache) 383 | global easter 384 | if not dtstart: 385 | dtstart = datetime.datetime.now().replace(microsecond=0) 386 | elif not isinstance(dtstart, datetime.datetime): 387 | dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) 388 | else: 389 | dtstart = dtstart.replace(microsecond=0) 390 | self._dtstart = dtstart 391 | self._tzinfo = dtstart.tzinfo 392 | self._freq = freq 393 | self._interval = interval 394 | self._count = count 395 | 396 | # Cache the original byxxx rules, if they are provided, as the _byxxx 397 | # attributes do not necessarily map to the inputs, and this can be 398 | # a problem in generating the strings. Only store things if they've 399 | # been supplied (the string retrieval will just use .get()) 400 | self._original_rule = {} 401 | 402 | if until and not isinstance(until, datetime.datetime): 403 | until = datetime.datetime.fromordinal(until.toordinal()) 404 | self._until = until 405 | 406 | if wkst is None: 407 | self._wkst = calendar.firstweekday() 408 | elif isinstance(wkst, integer_types): 409 | self._wkst = wkst 410 | else: 411 | self._wkst = wkst.weekday 412 | 413 | if bysetpos is None: 414 | self._bysetpos = None 415 | elif isinstance(bysetpos, integer_types): 416 | if bysetpos == 0 or not (-366 <= bysetpos <= 366): 417 | raise ValueError("bysetpos must be between 1 and 366, " 418 | "or between -366 and -1") 419 | self._bysetpos = (bysetpos,) 420 | else: 421 | self._bysetpos = tuple(bysetpos) 422 | for pos in self._bysetpos: 423 | if pos == 0 or not (-366 <= pos <= 366): 424 | raise ValueError("bysetpos must be between 1 and 366, " 425 | "or between -366 and -1") 426 | 427 | if self._bysetpos: 428 | self._original_rule['bysetpos'] = self._bysetpos 429 | 430 | if (byweekno is None and byyearday is None and bymonthday is None and 431 | byweekday is None and byeaster is None): 432 | if freq == YEARLY: 433 | if bymonth is None: 434 | bymonth = dtstart.month 435 | self._original_rule['bymonth'] = None 436 | bymonthday = dtstart.day 437 | self._original_rule['bymonthday'] = None 438 | elif freq == MONTHLY: 439 | bymonthday = dtstart.day 440 | self._original_rule['bymonthday'] = None 441 | elif freq == WEEKLY: 442 | byweekday = dtstart.weekday() 443 | self._original_rule['byweekday'] = None 444 | 445 | # bymonth 446 | if bymonth is None: 447 | self._bymonth = None 448 | else: 449 | if isinstance(bymonth, integer_types): 450 | bymonth = (bymonth,) 451 | 452 | self._bymonth = tuple(sorted(set(bymonth))) 453 | 454 | if 'bymonth' not in self._original_rule: 455 | self._original_rule['bymonth'] = self._bymonth 456 | 457 | # byyearday 458 | if byyearday is None: 459 | self._byyearday = None 460 | else: 461 | if isinstance(byyearday, integer_types): 462 | byyearday = (byyearday,) 463 | 464 | self._byyearday = tuple(sorted(set(byyearday))) 465 | self._original_rule['byyearday'] = self._byyearday 466 | 467 | # byeaster 468 | if byeaster is not None: 469 | if not easter: 470 | from dateutil import easter 471 | if isinstance(byeaster, integer_types): 472 | self._byeaster = (byeaster,) 473 | else: 474 | self._byeaster = tuple(sorted(byeaster)) 475 | 476 | self._original_rule['byeaster'] = self._byeaster 477 | else: 478 | self._byeaster = None 479 | 480 | # bymonthday 481 | if bymonthday is None: 482 | self._bymonthday = () 483 | self._bynmonthday = () 484 | else: 485 | if isinstance(bymonthday, integer_types): 486 | bymonthday = (bymonthday,) 487 | 488 | bymonthday = set(bymonthday) # Ensure it's unique 489 | 490 | self._bymonthday = tuple(sorted([x for x in bymonthday if x > 0])) 491 | self._bynmonthday = tuple(sorted([x for x in bymonthday if x < 0])) 492 | 493 | # Storing positive numbers first, then negative numbers 494 | if 'bymonthday' not in self._original_rule: 495 | self._original_rule['bymonthday'] = tuple( 496 | itertools.chain(self._bymonthday, self._bynmonthday)) 497 | 498 | # byweekno 499 | if byweekno is None: 500 | self._byweekno = None 501 | else: 502 | if isinstance(byweekno, integer_types): 503 | byweekno = (byweekno,) 504 | 505 | self._byweekno = tuple(sorted(set(byweekno))) 506 | 507 | self._original_rule['byweekno'] = self._byweekno 508 | 509 | # byweekday / bynweekday 510 | if byweekday is None: 511 | self._byweekday = None 512 | self._bynweekday = None 513 | else: 514 | # If it's one of the valid non-sequence types, convert to a 515 | # single-element sequence before the iterator that builds the 516 | # byweekday set. 517 | if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): 518 | byweekday = (byweekday,) 519 | 520 | self._byweekday = set() 521 | self._bynweekday = set() 522 | for wday in byweekday: 523 | if isinstance(wday, integer_types): 524 | self._byweekday.add(wday) 525 | elif not wday.n or freq > MONTHLY: 526 | self._byweekday.add(wday.weekday) 527 | else: 528 | self._bynweekday.add((wday.weekday, wday.n)) 529 | 530 | if not self._byweekday: 531 | self._byweekday = None 532 | elif not self._bynweekday: 533 | self._bynweekday = None 534 | 535 | if self._byweekday is not None: 536 | self._byweekday = tuple(sorted(self._byweekday)) 537 | orig_byweekday = [weekday(x) for x in self._byweekday] 538 | else: 539 | orig_byweekday = tuple() 540 | 541 | if self._bynweekday is not None: 542 | self._bynweekday = tuple(sorted(self._bynweekday)) 543 | orig_bynweekday = [weekday(*x) for x in self._bynweekday] 544 | else: 545 | orig_bynweekday = tuple() 546 | 547 | if 'byweekday' not in self._original_rule: 548 | self._original_rule['byweekday'] = tuple(itertools.chain( 549 | orig_byweekday, orig_bynweekday)) 550 | 551 | # byhour 552 | if byhour is None: 553 | if freq < HOURLY: 554 | self._byhour = set((dtstart.hour,)) 555 | else: 556 | self._byhour = None 557 | else: 558 | if isinstance(byhour, integer_types): 559 | byhour = (byhour,) 560 | 561 | if freq == HOURLY: 562 | self._byhour = self.__construct_byset(start=dtstart.hour, 563 | byxxx=byhour, 564 | base=24) 565 | else: 566 | self._byhour = set(byhour) 567 | 568 | self._byhour = tuple(sorted(self._byhour)) 569 | self._original_rule['byhour'] = self._byhour 570 | 571 | # byminute 572 | if byminute is None: 573 | if freq < MINUTELY: 574 | self._byminute = set((dtstart.minute,)) 575 | else: 576 | self._byminute = None 577 | else: 578 | if isinstance(byminute, integer_types): 579 | byminute = (byminute,) 580 | 581 | if freq == MINUTELY: 582 | self._byminute = self.__construct_byset(start=dtstart.minute, 583 | byxxx=byminute, 584 | base=60) 585 | else: 586 | self._byminute = set(byminute) 587 | 588 | self._byminute = tuple(sorted(self._byminute)) 589 | self._original_rule['byminute'] = self._byminute 590 | 591 | # bysecond 592 | if bysecond is None: 593 | if freq < SECONDLY: 594 | self._bysecond = ((dtstart.second,)) 595 | else: 596 | self._bysecond = None 597 | else: 598 | if isinstance(bysecond, integer_types): 599 | bysecond = (bysecond,) 600 | 601 | self._bysecond = set(bysecond) 602 | 603 | if freq == SECONDLY: 604 | self._bysecond = self.__construct_byset(start=dtstart.second, 605 | byxxx=bysecond, 606 | base=60) 607 | else: 608 | self._bysecond = set(bysecond) 609 | 610 | self._bysecond = tuple(sorted(self._bysecond)) 611 | self._original_rule['bysecond'] = self._bysecond 612 | 613 | if self._freq >= HOURLY: 614 | self._timeset = None 615 | else: 616 | self._timeset = [] 617 | for hour in self._byhour: 618 | for minute in self._byminute: 619 | for second in self._bysecond: 620 | self._timeset.append( 621 | datetime.time(hour, minute, second, 622 | tzinfo=self._tzinfo)) 623 | self._timeset.sort() 624 | self._timeset = tuple(self._timeset) 625 | 626 | def __str__(self): 627 | """ 628 | Output a string that would generate this RRULE if passed to rrulestr. 629 | This is mostly compatible with RFC2445, except for the 630 | dateutil-specific extension BYEASTER. 631 | """ 632 | 633 | output = [] 634 | h, m, s = [None] * 3 635 | if self._dtstart: 636 | output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) 637 | h, m, s = self._dtstart.timetuple()[3:6] 638 | 639 | parts = ['FREQ=' + FREQNAMES[self._freq]] 640 | if self._interval != 1: 641 | parts.append('INTERVAL=' + str(self._interval)) 642 | 643 | if self._wkst: 644 | parts.append('WKST=' + str(self._wkst)) 645 | 646 | if self._count: 647 | parts.append('COUNT=' + str(self._count)) 648 | 649 | if self._until: 650 | if self._until.tzinfo == pytz.utc: 651 | parts.append('UNTIL=' + self._until.strftime('%Y%m%dT%H%M%SZ')) 652 | else: 653 | parts.append('UNTIL=' + self._until.strftime('%Y%m%dT%H%M%S')) 654 | 655 | if self._original_rule.get('byweekday') is not None: 656 | # The str() method on weekday objects doesn't generate 657 | # RFC2445-compliant strings, so we should modify that. 658 | original_rule = dict(self._original_rule) 659 | wday_strings = [] 660 | for wday in original_rule['byweekday']: 661 | if wday.n: 662 | wday_strings.append('{n:+d}{wday}'.format( 663 | n=wday.n, 664 | wday=repr(wday)[0:2])) 665 | else: 666 | wday_strings.append(repr(wday)) 667 | 668 | original_rule['byweekday'] = wday_strings 669 | else: 670 | original_rule = self._original_rule 671 | 672 | partfmt = '{name}={vals}' 673 | for name, key in [('BYSETPOS', 'bysetpos'), 674 | ('BYMONTH', 'bymonth'), 675 | ('BYMONTHDAY', 'bymonthday'), 676 | ('BYYEARDAY', 'byyearday'), 677 | ('BYWEEKNO', 'byweekno'), 678 | ('BYDAY', 'byweekday'), 679 | ('BYHOUR', 'byhour'), 680 | ('BYMINUTE', 'byminute'), 681 | ('BYSECOND', 'bysecond'), 682 | ('BYEASTER', 'byeaster')]: 683 | value = original_rule.get(key) 684 | if value: 685 | parts.append(partfmt.format(name=name, vals=(','.join(str(v) 686 | for v in value)))) 687 | 688 | output.append(';'.join(parts)) 689 | return '\n'.join(output) 690 | 691 | def _iter(self): 692 | year, month, day, hour, minute, second, weekday, yearday, _ = \ 693 | self._dtstart.timetuple() 694 | 695 | # Some local variables to speed things up a bit 696 | freq = self._freq 697 | interval = self._interval 698 | wkst = self._wkst 699 | until = self._until 700 | bymonth = self._bymonth 701 | byweekno = self._byweekno 702 | byyearday = self._byyearday 703 | byweekday = self._byweekday 704 | byeaster = self._byeaster 705 | bymonthday = self._bymonthday 706 | bynmonthday = self._bynmonthday 707 | bysetpos = self._bysetpos 708 | byhour = self._byhour 709 | byminute = self._byminute 710 | bysecond = self._bysecond 711 | 712 | ii = _iterinfo(self) 713 | ii.rebuild(year, month) 714 | 715 | getdayset = {YEARLY: ii.ydayset, 716 | MONTHLY: ii.mdayset, 717 | WEEKLY: ii.wdayset, 718 | DAILY: ii.ddayset, 719 | HOURLY: ii.ddayset, 720 | MINUTELY: ii.ddayset, 721 | SECONDLY: ii.ddayset}[freq] 722 | 723 | if freq < HOURLY: 724 | timeset = self._timeset 725 | else: 726 | gettimeset = {HOURLY: ii.htimeset, 727 | MINUTELY: ii.mtimeset, 728 | SECONDLY: ii.stimeset}[freq] 729 | if ((freq >= HOURLY and 730 | self._byhour and hour not in self._byhour) or 731 | (freq >= MINUTELY and 732 | self._byminute and minute not in self._byminute) or 733 | (freq >= SECONDLY and 734 | self._bysecond and second not in self._bysecond)): 735 | timeset = () 736 | else: 737 | timeset = gettimeset(hour, minute, second) 738 | 739 | total = 0 740 | count = self._count 741 | while True: 742 | # Get dayset with the right frequency 743 | dayset, start, end = getdayset(year, month, day) 744 | 745 | # Do the "hard" work ;-) 746 | filtered = False 747 | for i in dayset[start:end]: 748 | if ((bymonth and ii.mmask[i] not in bymonth) or 749 | (byweekno and not ii.wnomask[i]) or 750 | (byweekday and ii.wdaymask[i] not in byweekday) or 751 | (ii.nwdaymask and not ii.nwdaymask[i]) or 752 | (byeaster and not ii.eastermask[i]) or 753 | ((bymonthday or bynmonthday) and 754 | ii.mdaymask[i] not in bymonthday and 755 | ii.nmdaymask[i] not in bynmonthday) or 756 | (byyearday and 757 | ((i < ii.yearlen and i+1 not in byyearday and 758 | -ii.yearlen+i not in byyearday) or 759 | (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and 760 | -ii.nextyearlen+i-ii.yearlen not in byyearday)))): 761 | dayset[i] = None 762 | filtered = True 763 | 764 | # Output results 765 | if bysetpos and timeset: 766 | poslist = [] 767 | for pos in bysetpos: 768 | if pos < 0: 769 | daypos, timepos = divmod(pos, len(timeset)) 770 | else: 771 | daypos, timepos = divmod(pos-1, len(timeset)) 772 | try: 773 | i = [x for x in dayset[start:end] 774 | if x is not None][daypos] 775 | time = timeset[timepos] 776 | except IndexError: 777 | pass 778 | else: 779 | date = datetime.date.fromordinal(ii.yearordinal+i) 780 | res = datetime.datetime.combine(date, time) 781 | if res not in poslist: 782 | poslist.append(res) 783 | poslist.sort() 784 | for res in poslist: 785 | if until and res > until: 786 | self._len = total 787 | return 788 | elif res >= self._dtstart: 789 | total += 1 790 | yield res 791 | if count: 792 | count -= 1 793 | if not count: 794 | self._len = total 795 | return 796 | else: 797 | for i in dayset[start:end]: 798 | if i is not None: 799 | date = datetime.date.fromordinal(ii.yearordinal + i) 800 | for time in timeset: 801 | res = datetime.datetime.combine(date, time) 802 | if until and res > until: 803 | self._len = total 804 | return 805 | elif res >= self._dtstart: 806 | total += 1 807 | yield res 808 | if count: 809 | count -= 1 810 | if not count: 811 | self._len = total 812 | return 813 | 814 | # Handle frequency and interval 815 | fixday = False 816 | if freq == YEARLY: 817 | year += interval 818 | if year > datetime.MAXYEAR: 819 | self._len = total 820 | return 821 | ii.rebuild(year, month) 822 | elif freq == MONTHLY: 823 | month += interval 824 | if month > 12: 825 | div, mod = divmod(month, 12) 826 | month = mod 827 | year += div 828 | if month == 0: 829 | month = 12 830 | year -= 1 831 | if year > datetime.MAXYEAR: 832 | self._len = total 833 | return 834 | ii.rebuild(year, month) 835 | elif freq == WEEKLY: 836 | if wkst > weekday: 837 | day += -(weekday+1+(6-wkst))+self._interval*7 838 | else: 839 | day += -(weekday-wkst)+self._interval*7 840 | weekday = wkst 841 | fixday = True 842 | elif freq == DAILY: 843 | day += interval 844 | fixday = True 845 | elif freq == HOURLY: 846 | if filtered: 847 | # Jump to one iteration before next day 848 | hour += ((23-hour)//interval)*interval 849 | 850 | if byhour: 851 | ndays, hour = self.__mod_distance(value=hour, 852 | byxxx=self._byhour, 853 | base=24) 854 | else: 855 | ndays, hour = divmod(hour+interval, 24) 856 | 857 | if ndays: 858 | day += ndays 859 | fixday = True 860 | 861 | timeset = gettimeset(hour, minute, second) 862 | elif freq == MINUTELY: 863 | if filtered: 864 | # Jump to one iteration before next day 865 | minute += ((1439-(hour*60+minute))//interval)*interval 866 | 867 | valid = False 868 | rep_rate = (24*60) 869 | for j in range(rep_rate // gcd(interval, rep_rate)): 870 | if byminute: 871 | nhours, minute = \ 872 | self.__mod_distance(value=minute, 873 | byxxx=self._byminute, 874 | base=60) 875 | else: 876 | nhours, minute = divmod(minute+interval, 60) 877 | 878 | div, hour = divmod(hour+nhours, 24) 879 | if div: 880 | day += div 881 | fixday = True 882 | filtered = False 883 | 884 | if not byhour or hour in byhour: 885 | valid = True 886 | break 887 | 888 | if not valid: 889 | raise ValueError('Invalid combination of interval and ' + 890 | 'byhour resulting in empty rule.') 891 | 892 | timeset = gettimeset(hour, minute, second) 893 | elif freq == SECONDLY: 894 | if filtered: 895 | # Jump to one iteration before next day 896 | second += (((86399 - (hour * 3600 + minute * 60 + second)) 897 | // interval) * interval) 898 | 899 | rep_rate = (24 * 3600) 900 | valid = False 901 | for j in range(0, rep_rate // gcd(interval, rep_rate)): 902 | if bysecond: 903 | nminutes, second = \ 904 | self.__mod_distance(value=second, 905 | byxxx=self._bysecond, 906 | base=60) 907 | else: 908 | nminutes, second = divmod(second+interval, 60) 909 | 910 | div, minute = divmod(minute+nminutes, 60) 911 | if div: 912 | hour += div 913 | div, hour = divmod(hour, 24) 914 | if div: 915 | day += div 916 | fixday = True 917 | 918 | if ((not byhour or hour in byhour) and 919 | (not byminute or minute in byminute) and 920 | (not bysecond or second in bysecond)): 921 | valid = True 922 | break 923 | 924 | if not valid: 925 | raise ValueError('Invalid combination of interval, ' + 926 | 'byhour and byminute resulting in empty' + 927 | ' rule.') 928 | 929 | timeset = gettimeset(hour, minute, second) 930 | 931 | if fixday and day > 28: 932 | daysinmonth = calendar.monthrange(year, month)[1] 933 | if day > daysinmonth: 934 | while day > daysinmonth: 935 | day -= daysinmonth 936 | month += 1 937 | if month == 13: 938 | month = 1 939 | year += 1 940 | if year > datetime.MAXYEAR: 941 | self._len = total 942 | return 943 | daysinmonth = calendar.monthrange(year, month)[1] 944 | ii.rebuild(year, month) 945 | 946 | def __construct_byset(self, start, byxxx, base): 947 | """ 948 | If a `BYXXX` sequence is passed to the constructor at the same level as 949 | `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some 950 | specifications which cannot be reached given some starting conditions. 951 | 952 | This occurs whenever the interval is not coprime with the base of a 953 | given unit and the difference between the starting position and the 954 | ending position is not coprime with the greatest common denominator 955 | between the interval and the base. For example, with a FREQ of hourly 956 | starting at 17:00 and an interval of 4, the only valid values for 957 | BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not 958 | coprime. 959 | 960 | :param start: 961 | Specifies the starting position. 962 | :param byxxx: 963 | An iterable containing the list of allowed values. 964 | :param base: 965 | The largest allowable value for the specified frequency (e.g. 966 | 24 hours, 60 minutes). 967 | 968 | This does not preserve the type of the iterable, returning a set, since 969 | the values should be unique and the order is irrelevant, this will 970 | speed up later lookups. 971 | 972 | In the event of an empty set, raises a :exception:`ValueError`, as this 973 | results in an empty rrule. 974 | """ 975 | 976 | cset = set() 977 | 978 | # Support a single byxxx value. 979 | if isinstance(byxxx, integer_types): 980 | byxxx = (byxxx, ) 981 | 982 | for num in byxxx: 983 | i_gcd = gcd(self._interval, base) 984 | # Use divmod rather than % because we need to wrap negative nums. 985 | if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: 986 | cset.add(num) 987 | 988 | if len(cset) == 0: 989 | raise ValueError("Invalid rrule byxxx generates an empty set.") 990 | 991 | return cset 992 | 993 | def __mod_distance(self, value, byxxx, base): 994 | """ 995 | Calculates the next value in a sequence where the `FREQ` parameter is 996 | specified along with a `BYXXX` parameter at the same "level" 997 | (e.g. `HOURLY` specified with `BYHOUR`). 998 | 999 | :param value: 1000 | The old value of the component. 1001 | :param byxxx: 1002 | The `BYXXX` set, which should have been generated by 1003 | `rrule._construct_byset`, or something else which checks that a 1004 | valid rule is present. 1005 | :param base: 1006 | The largest allowable value for the specified frequency (e.g. 1007 | 24 hours, 60 minutes). 1008 | 1009 | If a valid value is not found after `base` iterations (the maximum 1010 | number before the sequence would start to repeat), this raises a 1011 | :exception:`ValueError`, as no valid values were found. 1012 | 1013 | This returns a tuple of `divmod(n*interval, base)`, where `n` is the 1014 | smallest number of `interval` repetitions until the next specified 1015 | value in `byxxx` is found. 1016 | """ 1017 | accumulator = 0 1018 | for ii in range(1, base + 1): 1019 | # Using divmod() over % to account for negative intervals 1020 | div, value = divmod(value + self._interval, base) 1021 | accumulator += div 1022 | if value in byxxx: 1023 | return (accumulator, value) 1024 | 1025 | 1026 | class _iterinfo(object): 1027 | __slots__ = ["rrule", "lastyear", "lastmonth", 1028 | "yearlen", "nextyearlen", "yearordinal", "yearweekday", 1029 | "mmask", "mrange", "mdaymask", "nmdaymask", 1030 | "wdaymask", "wnomask", "nwdaymask", "eastermask"] 1031 | 1032 | def __init__(self, rrule): 1033 | for attr in self.__slots__: 1034 | setattr(self, attr, None) 1035 | self.rrule = rrule 1036 | 1037 | def rebuild(self, year, month): 1038 | # Every mask is 7 days longer to handle cross-year weekly periods. 1039 | rr = self.rrule 1040 | if year != self.lastyear: 1041 | self.yearlen = 365 + calendar.isleap(year) 1042 | self.nextyearlen = 365 + calendar.isleap(year + 1) 1043 | firstyday = datetime.date(year, 1, 1) 1044 | self.yearordinal = firstyday.toordinal() 1045 | self.yearweekday = firstyday.weekday() 1046 | 1047 | wday = datetime.date(year, 1, 1).weekday() 1048 | if self.yearlen == 365: 1049 | self.mmask = M365MASK 1050 | self.mdaymask = MDAY365MASK 1051 | self.nmdaymask = NMDAY365MASK 1052 | self.wdaymask = WDAYMASK[wday:] 1053 | self.mrange = M365RANGE 1054 | else: 1055 | self.mmask = M366MASK 1056 | self.mdaymask = MDAY366MASK 1057 | self.nmdaymask = NMDAY366MASK 1058 | self.wdaymask = WDAYMASK[wday:] 1059 | self.mrange = M366RANGE 1060 | 1061 | if not rr._byweekno: 1062 | self.wnomask = None 1063 | else: 1064 | self.wnomask = [0]*(self.yearlen+7) 1065 | # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) 1066 | no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 1067 | if no1wkst >= 4: 1068 | no1wkst = 0 1069 | # Number of days in the year, plus the days we got 1070 | # from last year. 1071 | wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 1072 | else: 1073 | # Number of days in the year, minus the days we 1074 | # left in last year. 1075 | wyearlen = self.yearlen-no1wkst 1076 | div, mod = divmod(wyearlen, 7) 1077 | numweeks = div+mod//4 1078 | for n in rr._byweekno: 1079 | if n < 0: 1080 | n += numweeks+1 1081 | if not (0 < n <= numweeks): 1082 | continue 1083 | if n > 1: 1084 | i = no1wkst+(n-1)*7 1085 | if no1wkst != firstwkst: 1086 | i -= 7-firstwkst 1087 | else: 1088 | i = no1wkst 1089 | for j in range(7): 1090 | self.wnomask[i] = 1 1091 | i += 1 1092 | if self.wdaymask[i] == rr._wkst: 1093 | break 1094 | if 1 in rr._byweekno: 1095 | # Check week number 1 of next year as well 1096 | # TODO: Check -numweeks for next year. 1097 | i = no1wkst+numweeks*7 1098 | if no1wkst != firstwkst: 1099 | i -= 7-firstwkst 1100 | if i < self.yearlen: 1101 | # If week starts in next year, we 1102 | # don't care about it. 1103 | for j in range(7): 1104 | self.wnomask[i] = 1 1105 | i += 1 1106 | if self.wdaymask[i] == rr._wkst: 1107 | break 1108 | if no1wkst: 1109 | # Check last week number of last year as 1110 | # well. If no1wkst is 0, either the year 1111 | # started on week start, or week number 1 1112 | # got days from last year, so there are no 1113 | # days from last year's last week number in 1114 | # this year. 1115 | if -1 not in rr._byweekno: 1116 | lyearweekday = datetime.date(year-1, 1, 1).weekday() 1117 | lno1wkst = (7-lyearweekday+rr._wkst) % 7 1118 | lyearlen = 365+calendar.isleap(year-1) 1119 | if lno1wkst >= 4: 1120 | lno1wkst = 0 1121 | lnumweeks = 52+(lyearlen + 1122 | (lyearweekday-rr._wkst) % 7) % 7//4 1123 | else: 1124 | lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 1125 | else: 1126 | lnumweeks = -1 1127 | if lnumweeks in rr._byweekno: 1128 | for i in range(no1wkst): 1129 | self.wnomask[i] = 1 1130 | 1131 | if (rr._bynweekday and (month != self.lastmonth or 1132 | year != self.lastyear)): 1133 | ranges = [] 1134 | if rr._freq == YEARLY: 1135 | if rr._bymonth: 1136 | for month in rr._bymonth: 1137 | ranges.append(self.mrange[month-1:month+1]) 1138 | else: 1139 | ranges = [(0, self.yearlen)] 1140 | elif rr._freq == MONTHLY: 1141 | ranges = [self.mrange[month-1:month+1]] 1142 | if ranges: 1143 | # Weekly frequency won't get here, so we may not 1144 | # care about cross-year weekly periods. 1145 | self.nwdaymask = [0]*self.yearlen 1146 | for first, last in ranges: 1147 | last -= 1 1148 | for wday, n in rr._bynweekday: 1149 | if n < 0: 1150 | i = last+(n+1)*7 1151 | i -= (self.wdaymask[i]-wday) % 7 1152 | else: 1153 | i = first+(n-1)*7 1154 | i += (7-self.wdaymask[i]+wday) % 7 1155 | if first <= i <= last: 1156 | self.nwdaymask[i] = 1 1157 | 1158 | if rr._byeaster: 1159 | self.eastermask = [0]*(self.yearlen+7) 1160 | eyday = easter.easter(year).toordinal()-self.yearordinal 1161 | for offset in rr._byeaster: 1162 | self.eastermask[eyday+offset] = 1 1163 | 1164 | self.lastyear = year 1165 | self.lastmonth = month 1166 | 1167 | def ydayset(self, year, month, day): 1168 | return list(range(self.yearlen)), 0, self.yearlen 1169 | 1170 | def mdayset(self, year, month, day): 1171 | dset = [None]*self.yearlen 1172 | start, end = self.mrange[month-1:month+1] 1173 | for i in range(start, end): 1174 | dset[i] = i 1175 | return dset, start, end 1176 | 1177 | def wdayset(self, year, month, day): 1178 | # We need to handle cross-year weeks here. 1179 | dset = [None]*(self.yearlen+7) 1180 | i = datetime.date(year, month, day).toordinal()-self.yearordinal 1181 | start = i 1182 | for j in range(7): 1183 | dset[i] = i 1184 | i += 1 1185 | # if (not (0 <= i < self.yearlen) or 1186 | # self.wdaymask[i] == self.rrule._wkst): 1187 | # This will cross the year boundary, if necessary. 1188 | if self.wdaymask[i] == self.rrule._wkst: 1189 | break 1190 | return dset, start, i 1191 | 1192 | def ddayset(self, year, month, day): 1193 | dset = [None] * self.yearlen 1194 | i = datetime.date(year, month, day).toordinal() - self.yearordinal 1195 | dset[i] = i 1196 | return dset, i, i + 1 1197 | 1198 | def htimeset(self, hour, minute, second): 1199 | tset = [] 1200 | rr = self.rrule 1201 | for minute in rr._byminute: 1202 | for second in rr._bysecond: 1203 | tset.append(datetime.time(hour, minute, second, 1204 | tzinfo=rr._tzinfo)) 1205 | tset.sort() 1206 | return tset 1207 | 1208 | def mtimeset(self, hour, minute, second): 1209 | tset = [] 1210 | rr = self.rrule 1211 | for second in rr._bysecond: 1212 | tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) 1213 | tset.sort() 1214 | return tset 1215 | 1216 | def stimeset(self, hour, minute, second): 1217 | return (datetime.time(hour, minute, second, 1218 | tzinfo=self.rrule._tzinfo),) 1219 | 1220 | 1221 | class rruleset(rrulebase): 1222 | """ The rruleset type allows more complex recurrence setups, mixing 1223 | multiple rules, dates, exclusion rules, and exclusion dates. The type 1224 | constructor takes the following keyword arguments: 1225 | 1226 | :param cache: If True, caching of results will be enabled, improving 1227 | performance of multiple queries considerably. """ 1228 | 1229 | class _genitem(object): 1230 | def __init__(self, genlist, gen): 1231 | try: 1232 | self.dt = advance_iterator(gen) 1233 | genlist.append(self) 1234 | except StopIteration: 1235 | pass 1236 | self.genlist = genlist 1237 | self.gen = gen 1238 | 1239 | def __next__(self): 1240 | try: 1241 | self.dt = advance_iterator(self.gen) 1242 | except StopIteration: 1243 | self.genlist.remove(self) 1244 | 1245 | next = __next__ 1246 | 1247 | def __lt__(self, other): 1248 | return self.dt < other.dt 1249 | 1250 | def __gt__(self, other): 1251 | return self.dt > other.dt 1252 | 1253 | def __eq__(self, other): 1254 | return self.dt == other.dt 1255 | 1256 | def __ne__(self, other): 1257 | return self.dt != other.dt 1258 | 1259 | def __init__(self, cache=False): 1260 | super(rruleset, self).__init__(cache) 1261 | self._rrule = [] 1262 | self._rdate = [] 1263 | self._exrule = [] 1264 | self._exdate = [] 1265 | 1266 | def rrule(self, rrule): 1267 | """ Include the given :py:class:`rrule` instance in the recurrence set 1268 | generation. """ 1269 | self._rrule.append(rrule) 1270 | 1271 | def rdate(self, rdate): 1272 | """ Include the given :py:class:`datetime` instance in the recurrence 1273 | set generation. """ 1274 | self._rdate.append(rdate) 1275 | 1276 | def exrule(self, exrule): 1277 | """ Include the given rrule instance in the recurrence set exclusion 1278 | list. Dates which are part of the given recurrence rules will not 1279 | be generated, even if some inclusive rrule or rdate matches them. 1280 | """ 1281 | self._exrule.append(exrule) 1282 | 1283 | def exdate(self, exdate): 1284 | """ Include the given datetime instance in the recurrence set 1285 | exclusion list. Dates included that way will not be generated, 1286 | even if some inclusive rrule or rdate matches them. """ 1287 | self._exdate.append(exdate) 1288 | 1289 | def _iter(self): 1290 | rlist = [] 1291 | self._rdate.sort() 1292 | self._genitem(rlist, iter(self._rdate)) 1293 | for gen in [iter(x) for x in self._rrule]: 1294 | self._genitem(rlist, gen) 1295 | rlist.sort() 1296 | exlist = [] 1297 | self._exdate.sort() 1298 | self._genitem(exlist, iter(self._exdate)) 1299 | for gen in [iter(x) for x in self._exrule]: 1300 | self._genitem(exlist, gen) 1301 | exlist.sort() 1302 | lastdt = None 1303 | total = 0 1304 | while rlist: 1305 | ritem = rlist[0] 1306 | if not lastdt or lastdt != ritem.dt: 1307 | while exlist and exlist[0] < ritem: 1308 | advance_iterator(exlist[0]) 1309 | exlist.sort() 1310 | if not exlist or ritem != exlist[0]: 1311 | total += 1 1312 | yield ritem.dt 1313 | lastdt = ritem.dt 1314 | advance_iterator(ritem) 1315 | rlist.sort() 1316 | self._len = total 1317 | 1318 | 1319 | class _rrulestr(object): 1320 | 1321 | _freq_map = {"YEARLY": YEARLY, 1322 | "MONTHLY": MONTHLY, 1323 | "WEEKLY": WEEKLY, 1324 | "DAILY": DAILY, 1325 | "HOURLY": HOURLY, 1326 | "MINUTELY": MINUTELY, 1327 | "SECONDLY": SECONDLY} 1328 | 1329 | _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, 1330 | "FR": 4, "SA": 5, "SU": 6} 1331 | 1332 | def _handle_int(self, rrkwargs, name, value, **kwargs): 1333 | rrkwargs[name.lower()] = int(value) 1334 | 1335 | def _handle_int_list(self, rrkwargs, name, value, **kwargs): 1336 | rrkwargs[name.lower()] = [int(x) for x in value.split(',')] 1337 | 1338 | _handle_INTERVAL = _handle_int 1339 | _handle_COUNT = _handle_int 1340 | _handle_BYSETPOS = _handle_int_list 1341 | _handle_BYMONTH = _handle_int_list 1342 | _handle_BYMONTHDAY = _handle_int_list 1343 | _handle_BYYEARDAY = _handle_int_list 1344 | _handle_BYEASTER = _handle_int_list 1345 | _handle_BYWEEKNO = _handle_int_list 1346 | _handle_BYHOUR = _handle_int_list 1347 | _handle_BYMINUTE = _handle_int_list 1348 | _handle_BYSECOND = _handle_int_list 1349 | 1350 | def _handle_FREQ(self, rrkwargs, name, value, **kwargs): 1351 | rrkwargs["freq"] = self._freq_map[value] 1352 | 1353 | def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): 1354 | global parser 1355 | if not parser: 1356 | from dateutil import parser 1357 | try: 1358 | rrkwargs["until"] = parser.parse(value, 1359 | ignoretz=kwargs.get("ignoretz"), 1360 | tzinfos=kwargs.get("tzinfos")) 1361 | except ValueError: 1362 | raise ValueError("invalid until date") 1363 | 1364 | def _handle_WKST(self, rrkwargs, name, value, **kwargs): 1365 | rrkwargs["wkst"] = self._weekday_map[value] 1366 | 1367 | def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): 1368 | """ 1369 | Two ways to specify this: +1MO or MO(+1) 1370 | """ 1371 | l = [] 1372 | for wday in value.split(','): 1373 | if '(' in wday: 1374 | # If it's of the form TH(+1), etc. 1375 | splt = wday.split('(') 1376 | w = splt[0] 1377 | n = int(splt[1][:-1]) 1378 | else: 1379 | # If it's of the form +1MO 1380 | for i in range(len(wday)): 1381 | if wday[i] not in '+-0123456789': 1382 | break 1383 | n = wday[:i] or None 1384 | w = wday[i:] 1385 | if n: 1386 | n = int(n) 1387 | l.append(weekdays[self._weekday_map[w]](n)) 1388 | rrkwargs["byweekday"] = l 1389 | 1390 | _handle_BYDAY = _handle_BYWEEKDAY 1391 | 1392 | def _parse_rfc_rrule(self, line, 1393 | dtstart=None, 1394 | cache=False, 1395 | ignoretz=False, 1396 | tzinfos=None): 1397 | if line.find(':') != -1: 1398 | name, value = line.split(':') 1399 | if name != "RRULE": 1400 | raise ValueError("unknown parameter name") 1401 | else: 1402 | value = line 1403 | rrkwargs = {} 1404 | for pair in value.split(';'): 1405 | name, value = pair.split('=') 1406 | name = name.upper() 1407 | value = value.upper() 1408 | try: 1409 | getattr(self, "_handle_"+name)(rrkwargs, name, value, 1410 | ignoretz=ignoretz, 1411 | tzinfos=tzinfos) 1412 | except AttributeError: 1413 | raise ValueError("unknown parameter '%s'" % name) 1414 | except (KeyError, ValueError): 1415 | raise ValueError("invalid '%s': %s" % (name, value)) 1416 | return rrule(dtstart=dtstart, cache=cache, **rrkwargs) 1417 | 1418 | def _parse_rfc(self, s, 1419 | dtstart=None, 1420 | cache=False, 1421 | unfold=False, 1422 | forceset=False, 1423 | compatible=False, 1424 | ignoretz=False, 1425 | tzinfos=None): 1426 | global parser 1427 | if compatible: 1428 | forceset = True 1429 | unfold = True 1430 | s = s.upper() 1431 | if not s.strip(): 1432 | raise ValueError("empty string") 1433 | if unfold: 1434 | lines = s.splitlines() 1435 | i = 0 1436 | while i < len(lines): 1437 | line = lines[i].rstrip() 1438 | if not line: 1439 | del lines[i] 1440 | elif i > 0 and line[0] == " ": 1441 | lines[i-1] += line[1:] 1442 | del lines[i] 1443 | else: 1444 | i += 1 1445 | else: 1446 | lines = s.split() 1447 | if (not forceset and len(lines) == 1 and (s.find(':') == -1 or 1448 | s.startswith('RRULE:'))): 1449 | return self._parse_rfc_rrule(lines[0], cache=cache, 1450 | dtstart=dtstart, ignoretz=ignoretz, 1451 | tzinfos=tzinfos) 1452 | else: 1453 | rrulevals = [] 1454 | rdatevals = [] 1455 | exrulevals = [] 1456 | exdatevals = [] 1457 | for line in lines: 1458 | if not line: 1459 | continue 1460 | if line.find(':') == -1: 1461 | name = "RRULE" 1462 | value = line 1463 | else: 1464 | name, value = line.split(':', 1) 1465 | parms = name.split(';') 1466 | if not parms: 1467 | raise ValueError("empty property name") 1468 | name = parms[0] 1469 | parms = parms[1:] 1470 | if name == "RRULE": 1471 | for parm in parms: 1472 | raise ValueError("unsupported RRULE parm: "+parm) 1473 | rrulevals.append(value) 1474 | elif name == "RDATE": 1475 | for parm in parms: 1476 | if parm != "VALUE=DATE-TIME": 1477 | raise ValueError("unsupported RDATE parm: "+parm) 1478 | rdatevals.append(value) 1479 | elif name == "EXRULE": 1480 | for parm in parms: 1481 | raise ValueError("unsupported EXRULE parm: "+parm) 1482 | exrulevals.append(value) 1483 | elif name == "EXDATE": 1484 | for parm in parms: 1485 | if parm != "VALUE=DATE-TIME": 1486 | raise ValueError("unsupported RDATE parm: "+parm) 1487 | exdatevals.append(value) 1488 | elif name == "DTSTART": 1489 | for parm in parms: 1490 | raise ValueError("unsupported DTSTART parm: "+parm) 1491 | if not parser: 1492 | from dateutil import parser 1493 | dtstart = parser.parse(value, ignoretz=ignoretz, 1494 | tzinfos=tzinfos) 1495 | else: 1496 | raise ValueError("unsupported property: "+name) 1497 | if (forceset or len(rrulevals) > 1 or rdatevals 1498 | or exrulevals or exdatevals): 1499 | if not parser and (rdatevals or exdatevals): 1500 | from dateutil import parser 1501 | rset = rruleset(cache=cache) 1502 | for value in rrulevals: 1503 | rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, 1504 | ignoretz=ignoretz, 1505 | tzinfos=tzinfos)) 1506 | for value in rdatevals: 1507 | for datestr in value.split(','): 1508 | rset.rdate(parser.parse(datestr, 1509 | ignoretz=ignoretz, 1510 | tzinfos=tzinfos)) 1511 | for value in exrulevals: 1512 | rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, 1513 | ignoretz=ignoretz, 1514 | tzinfos=tzinfos)) 1515 | for value in exdatevals: 1516 | for datestr in value.split(','): 1517 | rset.exdate(parser.parse(datestr, 1518 | ignoretz=ignoretz, 1519 | tzinfos=tzinfos)) 1520 | if compatible and dtstart: 1521 | rset.rdate(dtstart) 1522 | return rset 1523 | else: 1524 | return self._parse_rfc_rrule(rrulevals[0], 1525 | dtstart=dtstart, 1526 | cache=cache, 1527 | ignoretz=ignoretz, 1528 | tzinfos=tzinfos) 1529 | 1530 | def __call__(self, s, **kwargs): 1531 | return self._parse_rfc(s, **kwargs) 1532 | 1533 | rrulestr = _rrulestr() 1534 | 1535 | # vim:ts=4:sw=4:et 1536 | --------------------------------------------------------------------------------