From 45a2b87a8f19bd55d027895b0cebbb2c347c0677 Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Fri, 22 Jun 2018 13:09:12 +0200 Subject: [PATCH] Import Upstream version 0.5.4 --- CHANGES.txt | 92 ++++ MANIFEST.in | 2 + PKG-INFO | 273 ++++++++++ README.rst | 119 +++++ TODO.txt | 39 ++ setup.cfg | 5 + setup.py | 84 +++ src/isodate.egg-info/PKG-INFO | 273 ++++++++++ src/isodate.egg-info/SOURCES.txt | 26 + src/isodate.egg-info/dependency_links.txt | 1 + src/isodate.egg-info/top_level.txt | 1 + src/isodate/__init__.py | 70 +++ src/isodate/duration.py | 324 ++++++++++++ src/isodate/isodates.py | 204 ++++++++ src/isodate/isodatetime.py | 68 +++ src/isodate/isoduration.py | 149 ++++++ src/isodate/isoerror.py | 33 ++ src/isodate/isostrf.py | 213 ++++++++ src/isodate/isotime.py | 158 ++++++ src/isodate/isotzinfo.py | 112 ++++ src/isodate/tests/__init__.py | 50 ++ src/isodate/tests/test_date.py | 129 +++++ src/isodate/tests/test_datetime.py | 146 ++++++ src/isodate/tests/test_duration.py | 601 ++++++++++++++++++++++ src/isodate/tests/test_pickle.py | 54 ++ src/isodate/tests/test_strf.py | 135 +++++ src/isodate/tests/test_time.py | 143 +++++ src/isodate/tzinfo.py | 142 +++++ 28 files changed, 3646 insertions(+) create mode 100644 CHANGES.txt create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 TODO.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/isodate.egg-info/PKG-INFO create mode 100644 src/isodate.egg-info/SOURCES.txt create mode 100644 src/isodate.egg-info/dependency_links.txt create mode 100644 src/isodate.egg-info/top_level.txt create mode 100644 src/isodate/__init__.py create mode 100644 src/isodate/duration.py create mode 100644 src/isodate/isodates.py create mode 100644 src/isodate/isodatetime.py create mode 100644 src/isodate/isoduration.py create mode 100644 src/isodate/isoerror.py create mode 100644 src/isodate/isostrf.py create mode 100644 src/isodate/isotime.py create mode 100644 src/isodate/isotzinfo.py create mode 100644 src/isodate/tests/__init__.py create mode 100644 src/isodate/tests/test_date.py create mode 100644 src/isodate/tests/test_datetime.py create mode 100644 src/isodate/tests/test_duration.py create mode 100644 src/isodate/tests/test_pickle.py create mode 100644 src/isodate/tests/test_strf.py create mode 100644 src/isodate/tests/test_time.py create mode 100644 src/isodate/tzinfo.py diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..31af061 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,92 @@ + +CHANGES +======= + +0.5.4 (2015-08-06) +------------------ + +- Fix parsing of Periods (Fabien Bochu) +- Make Duration objects hashable (Geoffrey Fairchild) +- Add multiplication to duration (Reinoud Elhorst) + + +0.5.1 (2014-11-07) +------------------ + +- fixed pickling of Duration objects +- raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) + + +0.5.0 (2014-02-23) +------------------ + +- ISO8601Error are subclasses of ValueError now (Michael Hrivnak) +- improve compatibility across various python variants and versions +- raise exceptions when using fractional years and months in date + maths with durations +- renamed method todatetime on Duraction objects to totimedelta + + +0.4.9 (2012-10-30) +------------------ + +- support pickling FixedOffset instances +- make sure parsed fractional seconds are in microseconds +- add leading zeros when formattig microseconds (Jarom Loveridge) + + +0.4.8 (2012-05-04) +------------------ + +- fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 + and 3.2) + + +0.4.7 (2012-01-26) +------------------ + +- fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) + + +0.4.6 (2012-01-06) +------------------ + +- added Python 3 compatibility via 2to3 + +0.4.5 (2012-01-06) +------------------ + +- made setuptools dependency optional + +0.4.4 (2011-04-16) +------------------ + +- Fixed formatting of microseconds for datetime objects + +0.4.3 (2010-10-29) +------------------ + +- Fixed problem with %P formating and fractions (supplied by David Brooks) + +0.4.2 (2010-10-28) +------------------ + +- Implemented unary - for Duration (supplied by David Brooks) +- Output fractional seconds with '%P' format. (partly supplied by David Brooks) + +0.4.1 (2010-10-13) +------------------ + +- fixed bug in comparison between timedelta and Duration. +- fixed precision problem with microseconds (reported by Tommi Virtanen) + +0.4.0 (2009-02-09) +------------------ + +- added method to parse ISO 8601 time zone strings +- added methods to create ISO 8601 conforming strings + +0.3.0 (2009-1-05) +------------------ + +- Initial release diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..127f5af --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include CHANGES.txt +include TODO.txt diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..f609f9b --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,273 @@ +Metadata-Version: 1.1 +Name: isodate +Version: 0.5.4 +Summary: An ISO 8601 date/time/duration parser and formatter +Home-page: http://cheeseshop.python.org/pypi/isodate +Author: Gerhard Weis +Author-email: gerhard.weis@proclos.com +License: BSD +Description: + ISO 8601 date/time parser + ========================= + + .. image:: https://travis-ci.org/gweis/isodate.svg?branch=master + :target: https://travis-ci.org/gweis/isodate + :alt: Travis-CI + .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master + :target: https://coveralls.io/r/gweis/isodate?branch=master + :alt: Coveralls + .. image:: https://pypip.in/version/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Latest Version + .. image:: https://pypip.in/download/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Downloads + .. image:: https://pypip.in/license/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: License + + + This module implements ISO 8601 date, time and duration parsing. + The implementation follows ISO8601:2004 standard, and implements only + date/time representations mentioned in the standard. If something is not + mentioned there, then it is treated as non existent, and not as an allowed + option. + + For instance, ISO8601:2004 never mentions 2 digit years. So, it is not + intended by this module to support 2 digit years. (while it may still + be valid as ISO date, because it is not explicitly forbidden.) + Another example is, when no time zone information is given for a time, + then it should be interpreted as local time, and not UTC. + + As this module maps ISO 8601 dates/times to standard Python data types, like + *date*, *time*, *datetime* and *timedelta*, it is not possible to convert + all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are + not allowed by the Python *date* and *datetime* classes. Additionally + fractional seconds are limited to microseconds. That means if the parser finds + for instance nanoseconds it will round it to microseconds. + + Documentation + ------------- + + Currently there are four parsing methods available. + * parse_time: + parses an ISO 8601 time string into a *time* object + * parse_date: + parses an ISO 8601 date string into a *date* object + * parse_datetime: + parses an ISO 8601 date-time string into a *datetime* object + * parse_duration: + parses an ISO 8601 duration string into a *timedelta* or *Duration* + object. + * parse_tzinfo: + parses the time zone info part of an ISO 8601 string into a + *tzinfo* object. + + As ISO 8601 allows to define durations in years and months, and *timedelta* + does not handle years and months, this module provides a *Duration* class, + which can be used almost like a *timedelta* object (with some limitations). + However, a *Duration* object can be converted into a *timedelta* object. + + There are also ISO formatting methods for all supported data types. Each + *xxx_isoformat* method accepts a format parameter. The default format is + always the ISO 8601 expanded format. This is the same format used by + *datetime.isoformat*: + + * time_isoformat: + Intended to create ISO time strings with default format + *hh:mm:ssZ*. + * date_isoformat: + Intended to create ISO date strings with default format + *yyyy-mm-dd*. + * datetime_isoformat: + Intended to create ISO date-time strings with default format + *yyyy-mm-ddThh:mm:ssZ*. + * duration_isoformat: + Intended to create ISO duration strings with default format + *PnnYnnMnnDTnnHnnMnnS*. + * tz_isoformat: + Intended to create ISO time zone strings with default format + *hh:mm*. + * strftime: + A re-implementation mostly compatible with Python's *strftime*, but + supports only those format strings, which can also be used for dates + prior 1900. This method also understands how to format *datetime* and + *Duration* instances. + + Installation: + ------------- + + This module can easily be installed with Python standard installation methods. + + Either use *python setup.py install* or in case you have *setuptools* or + *distribute* available, you can also use *easy_install*. + + Limitations: + ------------ + + * The parser accepts several date/time representation which should be invalid + according to ISO 8601 standard. + + 1. for date and time together, this parser accepts a mixture of basic and extended format. + e.g. the date could be in basic format, while the time is accepted in extended format. + It also allows short dates and times in date-time strings. + 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of + 1901-01-01. + 3. negative *Duration* and *timedelta* value are not fully supported yet. + + Further information: + -------------------- + + The doc strings and unit tests should provide rather detailed information about + the methods and their limitations. + + The source release provides a *setup.py* script and a *buildout.cfg*. Both can + be used to run the unit tests included. + + Source code is available at ``_. + + CHANGES + ======= + + 0.5.4 (2015-08-06) + ------------------ + + - Fix parsing of Periods (Fabien Bochu) + - Make Duration objects hashable (Geoffrey Fairchild) + - Add multiplication to duration (Reinoud Elhorst) + + + 0.5.1 (2014-11-07) + ------------------ + + - fixed pickling of Duration objects + - raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) + + + 0.5.0 (2014-02-23) + ------------------ + + - ISO8601Error are subclasses of ValueError now (Michael Hrivnak) + - improve compatibility across various python variants and versions + - raise exceptions when using fractional years and months in date + maths with durations + - renamed method todatetime on Duraction objects to totimedelta + + + 0.4.9 (2012-10-30) + ------------------ + + - support pickling FixedOffset instances + - make sure parsed fractional seconds are in microseconds + - add leading zeros when formattig microseconds (Jarom Loveridge) + + + 0.4.8 (2012-05-04) + ------------------ + + - fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 + and 3.2) + + + 0.4.7 (2012-01-26) + ------------------ + + - fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) + + + 0.4.6 (2012-01-06) + ------------------ + + - added Python 3 compatibility via 2to3 + + 0.4.5 (2012-01-06) + ------------------ + + - made setuptools dependency optional + + 0.4.4 (2011-04-16) + ------------------ + + - Fixed formatting of microseconds for datetime objects + + 0.4.3 (2010-10-29) + ------------------ + + - Fixed problem with %P formating and fractions (supplied by David Brooks) + + 0.4.2 (2010-10-28) + ------------------ + + - Implemented unary - for Duration (supplied by David Brooks) + - Output fractional seconds with '%P' format. (partly supplied by David Brooks) + + 0.4.1 (2010-10-13) + ------------------ + + - fixed bug in comparison between timedelta and Duration. + - fixed precision problem with microseconds (reported by Tommi Virtanen) + + 0.4.0 (2009-02-09) + ------------------ + + - added method to parse ISO 8601 time zone strings + - added methods to create ISO 8601 conforming strings + + 0.3.0 (2009-1-05) + ------------------ + + - Initial release + + TODOs + ===== + + This to do list contains some thoughts and ideas about missing features, and + parts to think about, whether to implement them or not. This list is probably + not complete. + + Missing features: + ----------------- + + * time formating does not allow to create fractional representations. + * parser for ISO intervals. + * currently microseconds are always padded to a length of 6 characters. + trailing 0s should be optional + + Documentation: + -------------- + + * parse_datetime: + - complete documentation to show what this function allows, but ISO forbids. + and vice verse. + - support other separators between date and time than 'T' + + * parse_date: + - yeardigits should be always greater than 4 + - dates before 0001-01-01 are not supported + + * parse_duration: + - alternative formats are not fully supported due to parse_date restrictions + - standard duration format is fully supported but not very restrictive. + + * Duration: + - support fractional years and month in calculations + - implement w3c order relation? (``_) + - refactor to have duration mathematics only at one place. + - localize __str__ method (does timedelta do this?) + - when is a Duration negative? + - normalize Durations. months [00-12] and years ]-inf,+inf[ + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..084c8c6 --- /dev/null +++ b/README.rst @@ -0,0 +1,119 @@ + +ISO 8601 date/time parser +========================= + +.. image:: https://travis-ci.org/gweis/isodate.svg?branch=master + :target: https://travis-ci.org/gweis/isodate + :alt: Travis-CI +.. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master + :target: https://coveralls.io/r/gweis/isodate?branch=master + :alt: Coveralls +.. image:: https://pypip.in/version/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Latest Version +.. image:: https://pypip.in/download/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Downloads +.. image:: https://pypip.in/license/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: License + + +This module implements ISO 8601 date, time and duration parsing. +The implementation follows ISO8601:2004 standard, and implements only +date/time representations mentioned in the standard. If something is not +mentioned there, then it is treated as non existent, and not as an allowed +option. + +For instance, ISO8601:2004 never mentions 2 digit years. So, it is not +intended by this module to support 2 digit years. (while it may still +be valid as ISO date, because it is not explicitly forbidden.) +Another example is, when no time zone information is given for a time, +then it should be interpreted as local time, and not UTC. + +As this module maps ISO 8601 dates/times to standard Python data types, like +*date*, *time*, *datetime* and *timedelta*, it is not possible to convert +all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are +not allowed by the Python *date* and *datetime* classes. Additionally +fractional seconds are limited to microseconds. That means if the parser finds +for instance nanoseconds it will round it to microseconds. + +Documentation +------------- + +Currently there are four parsing methods available. + * parse_time: + parses an ISO 8601 time string into a *time* object + * parse_date: + parses an ISO 8601 date string into a *date* object + * parse_datetime: + parses an ISO 8601 date-time string into a *datetime* object + * parse_duration: + parses an ISO 8601 duration string into a *timedelta* or *Duration* + object. + * parse_tzinfo: + parses the time zone info part of an ISO 8601 string into a + *tzinfo* object. + +As ISO 8601 allows to define durations in years and months, and *timedelta* +does not handle years and months, this module provides a *Duration* class, +which can be used almost like a *timedelta* object (with some limitations). +However, a *Duration* object can be converted into a *timedelta* object. + +There are also ISO formatting methods for all supported data types. Each +*xxx_isoformat* method accepts a format parameter. The default format is +always the ISO 8601 expanded format. This is the same format used by +*datetime.isoformat*: + + * time_isoformat: + Intended to create ISO time strings with default format + *hh:mm:ssZ*. + * date_isoformat: + Intended to create ISO date strings with default format + *yyyy-mm-dd*. + * datetime_isoformat: + Intended to create ISO date-time strings with default format + *yyyy-mm-ddThh:mm:ssZ*. + * duration_isoformat: + Intended to create ISO duration strings with default format + *PnnYnnMnnDTnnHnnMnnS*. + * tz_isoformat: + Intended to create ISO time zone strings with default format + *hh:mm*. + * strftime: + A re-implementation mostly compatible with Python's *strftime*, but + supports only those format strings, which can also be used for dates + prior 1900. This method also understands how to format *datetime* and + *Duration* instances. + +Installation: +------------- + +This module can easily be installed with Python standard installation methods. + +Either use *python setup.py install* or in case you have *setuptools* or +*distribute* available, you can also use *easy_install*. + +Limitations: +------------ + + * The parser accepts several date/time representation which should be invalid + according to ISO 8601 standard. + + 1. for date and time together, this parser accepts a mixture of basic and extended format. + e.g. the date could be in basic format, while the time is accepted in extended format. + It also allows short dates and times in date-time strings. + 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of + 1901-01-01. + 3. negative *Duration* and *timedelta* value are not fully supported yet. + +Further information: +-------------------- + +The doc strings and unit tests should provide rather detailed information about +the methods and their limitations. + +The source release provides a *setup.py* script and a *buildout.cfg*. Both can +be used to run the unit tests included. + +Source code is available at ``_. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..6843fa3 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,39 @@ + +TODOs +===== + +This to do list contains some thoughts and ideas about missing features, and +parts to think about, whether to implement them or not. This list is probably +not complete. + +Missing features: +----------------- + + * time formating does not allow to create fractional representations. + * parser for ISO intervals. + * currently microseconds are always padded to a length of 6 characters. + trailing 0s should be optional + +Documentation: +-------------- + + * parse_datetime: + - complete documentation to show what this function allows, but ISO forbids. + and vice verse. + - support other separators between date and time than 'T' + + * parse_date: + - yeardigits should be always greater than 4 + - dates before 0001-01-01 are not supported + + * parse_duration: + - alternative formats are not fully supported due to parse_date restrictions + - standard duration format is fully supported but not very restrictive. + + * Duration: + - support fractional years and month in calculations + - implement w3c order relation? (``_) + - refactor to have duration mathematics only at one place. + - localize __str__ method (does timedelta do this?) + - when is a Duration negative? + - normalize Durations. months [00-12] and years ]-inf,+inf[ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ebbec92 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_svn_revision = 0 +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3fc0075 --- /dev/null +++ b/setup.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +import os +import sys + +setupargs = {} + +try: + from setuptools import setup + setupargs['test_suite'] = 'isodate.tests.test_suite' + if sys.version[0] == '3': + setupargs['use_2to3'] = True +except ImportError: + from distutils.core import setup + if sys.version[0] == '3': + from distutils.command.build_py import build_py_2to3 + setupargs['cmdclass'] = {'build_py': build_py_2to3} + + +def read(*rnames): + return open(os.path.join(os.path.dirname(__file__), *rnames)).read() + +setup(name='isodate', + version='0.5.4', + packages=['isodate', 'isodate.tests'], + package_dir={'': 'src'}, + + # dependencies: + # install_requires = [], + + # PyPI metadata + author='Gerhard Weis', + author_email='gerhard.weis@proclos.com', + description='An ISO 8601 date/time/duration parser and formatter', + license='BSD', + # keywords = '', + url='http://cheeseshop.python.org/pypi/isodate', + + long_description=(read('README.rst') + + read('CHANGES.txt') + + read('TODO.txt')), + + classifiers=['Development Status :: 4 - Beta', + # 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Internet', + ('Topic :: Software Development :' + ': Libraries :: Python Modules'), + ], + **setupargs) diff --git a/src/isodate.egg-info/PKG-INFO b/src/isodate.egg-info/PKG-INFO new file mode 100644 index 0000000..f609f9b --- /dev/null +++ b/src/isodate.egg-info/PKG-INFO @@ -0,0 +1,273 @@ +Metadata-Version: 1.1 +Name: isodate +Version: 0.5.4 +Summary: An ISO 8601 date/time/duration parser and formatter +Home-page: http://cheeseshop.python.org/pypi/isodate +Author: Gerhard Weis +Author-email: gerhard.weis@proclos.com +License: BSD +Description: + ISO 8601 date/time parser + ========================= + + .. image:: https://travis-ci.org/gweis/isodate.svg?branch=master + :target: https://travis-ci.org/gweis/isodate + :alt: Travis-CI + .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master + :target: https://coveralls.io/r/gweis/isodate?branch=master + :alt: Coveralls + .. image:: https://pypip.in/version/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Latest Version + .. image:: https://pypip.in/download/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: Downloads + .. image:: https://pypip.in/license/isodate/badge.svg + :target: https://pypi.python.org/pypi/isodate/ + :alt: License + + + This module implements ISO 8601 date, time and duration parsing. + The implementation follows ISO8601:2004 standard, and implements only + date/time representations mentioned in the standard. If something is not + mentioned there, then it is treated as non existent, and not as an allowed + option. + + For instance, ISO8601:2004 never mentions 2 digit years. So, it is not + intended by this module to support 2 digit years. (while it may still + be valid as ISO date, because it is not explicitly forbidden.) + Another example is, when no time zone information is given for a time, + then it should be interpreted as local time, and not UTC. + + As this module maps ISO 8601 dates/times to standard Python data types, like + *date*, *time*, *datetime* and *timedelta*, it is not possible to convert + all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are + not allowed by the Python *date* and *datetime* classes. Additionally + fractional seconds are limited to microseconds. That means if the parser finds + for instance nanoseconds it will round it to microseconds. + + Documentation + ------------- + + Currently there are four parsing methods available. + * parse_time: + parses an ISO 8601 time string into a *time* object + * parse_date: + parses an ISO 8601 date string into a *date* object + * parse_datetime: + parses an ISO 8601 date-time string into a *datetime* object + * parse_duration: + parses an ISO 8601 duration string into a *timedelta* or *Duration* + object. + * parse_tzinfo: + parses the time zone info part of an ISO 8601 string into a + *tzinfo* object. + + As ISO 8601 allows to define durations in years and months, and *timedelta* + does not handle years and months, this module provides a *Duration* class, + which can be used almost like a *timedelta* object (with some limitations). + However, a *Duration* object can be converted into a *timedelta* object. + + There are also ISO formatting methods for all supported data types. Each + *xxx_isoformat* method accepts a format parameter. The default format is + always the ISO 8601 expanded format. This is the same format used by + *datetime.isoformat*: + + * time_isoformat: + Intended to create ISO time strings with default format + *hh:mm:ssZ*. + * date_isoformat: + Intended to create ISO date strings with default format + *yyyy-mm-dd*. + * datetime_isoformat: + Intended to create ISO date-time strings with default format + *yyyy-mm-ddThh:mm:ssZ*. + * duration_isoformat: + Intended to create ISO duration strings with default format + *PnnYnnMnnDTnnHnnMnnS*. + * tz_isoformat: + Intended to create ISO time zone strings with default format + *hh:mm*. + * strftime: + A re-implementation mostly compatible with Python's *strftime*, but + supports only those format strings, which can also be used for dates + prior 1900. This method also understands how to format *datetime* and + *Duration* instances. + + Installation: + ------------- + + This module can easily be installed with Python standard installation methods. + + Either use *python setup.py install* or in case you have *setuptools* or + *distribute* available, you can also use *easy_install*. + + Limitations: + ------------ + + * The parser accepts several date/time representation which should be invalid + according to ISO 8601 standard. + + 1. for date and time together, this parser accepts a mixture of basic and extended format. + e.g. the date could be in basic format, while the time is accepted in extended format. + It also allows short dates and times in date-time strings. + 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of + 1901-01-01. + 3. negative *Duration* and *timedelta* value are not fully supported yet. + + Further information: + -------------------- + + The doc strings and unit tests should provide rather detailed information about + the methods and their limitations. + + The source release provides a *setup.py* script and a *buildout.cfg*. Both can + be used to run the unit tests included. + + Source code is available at ``_. + + CHANGES + ======= + + 0.5.4 (2015-08-06) + ------------------ + + - Fix parsing of Periods (Fabien Bochu) + - Make Duration objects hashable (Geoffrey Fairchild) + - Add multiplication to duration (Reinoud Elhorst) + + + 0.5.1 (2014-11-07) + ------------------ + + - fixed pickling of Duration objects + - raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) + + + 0.5.0 (2014-02-23) + ------------------ + + - ISO8601Error are subclasses of ValueError now (Michael Hrivnak) + - improve compatibility across various python variants and versions + - raise exceptions when using fractional years and months in date + maths with durations + - renamed method todatetime on Duraction objects to totimedelta + + + 0.4.9 (2012-10-30) + ------------------ + + - support pickling FixedOffset instances + - make sure parsed fractional seconds are in microseconds + - add leading zeros when formattig microseconds (Jarom Loveridge) + + + 0.4.8 (2012-05-04) + ------------------ + + - fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 + and 3.2) + + + 0.4.7 (2012-01-26) + ------------------ + + - fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) + + + 0.4.6 (2012-01-06) + ------------------ + + - added Python 3 compatibility via 2to3 + + 0.4.5 (2012-01-06) + ------------------ + + - made setuptools dependency optional + + 0.4.4 (2011-04-16) + ------------------ + + - Fixed formatting of microseconds for datetime objects + + 0.4.3 (2010-10-29) + ------------------ + + - Fixed problem with %P formating and fractions (supplied by David Brooks) + + 0.4.2 (2010-10-28) + ------------------ + + - Implemented unary - for Duration (supplied by David Brooks) + - Output fractional seconds with '%P' format. (partly supplied by David Brooks) + + 0.4.1 (2010-10-13) + ------------------ + + - fixed bug in comparison between timedelta and Duration. + - fixed precision problem with microseconds (reported by Tommi Virtanen) + + 0.4.0 (2009-02-09) + ------------------ + + - added method to parse ISO 8601 time zone strings + - added methods to create ISO 8601 conforming strings + + 0.3.0 (2009-1-05) + ------------------ + + - Initial release + + TODOs + ===== + + This to do list contains some thoughts and ideas about missing features, and + parts to think about, whether to implement them or not. This list is probably + not complete. + + Missing features: + ----------------- + + * time formating does not allow to create fractional representations. + * parser for ISO intervals. + * currently microseconds are always padded to a length of 6 characters. + trailing 0s should be optional + + Documentation: + -------------- + + * parse_datetime: + - complete documentation to show what this function allows, but ISO forbids. + and vice verse. + - support other separators between date and time than 'T' + + * parse_date: + - yeardigits should be always greater than 4 + - dates before 0001-01-01 are not supported + + * parse_duration: + - alternative formats are not fully supported due to parse_date restrictions + - standard duration format is fully supported but not very restrictive. + + * Duration: + - support fractional years and month in calculations + - implement w3c order relation? (``_) + - refactor to have duration mathematics only at one place. + - localize __str__ method (does timedelta do this?) + - when is a Duration negative? + - normalize Durations. months [00-12] and years ]-inf,+inf[ + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/src/isodate.egg-info/SOURCES.txt b/src/isodate.egg-info/SOURCES.txt new file mode 100644 index 0000000..37d9c1f --- /dev/null +++ b/src/isodate.egg-info/SOURCES.txt @@ -0,0 +1,26 @@ +CHANGES.txt +MANIFEST.in +README.rst +TODO.txt +setup.py +src/isodate/__init__.py +src/isodate/duration.py +src/isodate/isodates.py +src/isodate/isodatetime.py +src/isodate/isoduration.py +src/isodate/isoerror.py +src/isodate/isostrf.py +src/isodate/isotime.py +src/isodate/isotzinfo.py +src/isodate/tzinfo.py +src/isodate.egg-info/PKG-INFO +src/isodate.egg-info/SOURCES.txt +src/isodate.egg-info/dependency_links.txt +src/isodate.egg-info/top_level.txt +src/isodate/tests/__init__.py +src/isodate/tests/test_date.py +src/isodate/tests/test_datetime.py +src/isodate/tests/test_duration.py +src/isodate/tests/test_pickle.py +src/isodate/tests/test_strf.py +src/isodate/tests/test_time.py \ No newline at end of file diff --git a/src/isodate.egg-info/dependency_links.txt b/src/isodate.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/isodate.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/isodate.egg-info/top_level.txt b/src/isodate.egg-info/top_level.txt new file mode 100644 index 0000000..817963d --- /dev/null +++ b/src/isodate.egg-info/top_level.txt @@ -0,0 +1 @@ +isodate diff --git a/src/isodate/__init__.py b/src/isodate/__init__.py new file mode 100644 index 0000000..83c8cf8 --- /dev/null +++ b/src/isodate/__init__.py @@ -0,0 +1,70 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Import all essential functions and constants to re-export them here for easy +access. + +This module contains also various pre-defined ISO 8601 format strings. +''' +from isodate.isodates import parse_date, date_isoformat +from isodate.isotime import parse_time, time_isoformat +from isodate.isodatetime import parse_datetime, datetime_isoformat +from isodate.isoduration import parse_duration, duration_isoformat +from isodate.isoerror import ISO8601Error +from isodate.isotzinfo import parse_tzinfo, tz_isoformat +from isodate.tzinfo import UTC, FixedOffset, LOCAL +from isodate.duration import Duration +from isodate.isostrf import strftime +from isodate.isostrf import DATE_BAS_COMPLETE, DATE_BAS_ORD_COMPLETE +from isodate.isostrf import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE +from isodate.isostrf import DATE_CENTURY, DATE_EXT_COMPLETE +from isodate.isostrf import DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK +from isodate.isostrf import DATE_EXT_WEEK_COMPLETE, DATE_MONTH, DATE_YEAR +from isodate.isostrf import TIME_BAS_COMPLETE, TIME_BAS_MINUTE +from isodate.isostrf import TIME_EXT_COMPLETE, TIME_EXT_MINUTE +from isodate.isostrf import TIME_HOUR +from isodate.isostrf import TZ_BAS, TZ_EXT, TZ_HOUR +from isodate.isostrf import DT_BAS_COMPLETE, DT_EXT_COMPLETE +from isodate.isostrf import DT_BAS_ORD_COMPLETE, DT_EXT_ORD_COMPLETE +from isodate.isostrf import DT_BAS_WEEK_COMPLETE, DT_EXT_WEEK_COMPLETE +from isodate.isostrf import D_DEFAULT, D_WEEK, D_ALT_EXT, D_ALT_BAS +from isodate.isostrf import D_ALT_BAS_ORD, D_ALT_EXT_ORD + +__all__ = ['parse_date', 'date_isoformat', 'parse_time', 'time_isoformat', + 'parse_datetime', 'datetime_isoformat', 'parse_duration', + 'duration_isoformat', 'ISO8601Error', 'parse_tzinfo', + 'tz_isoformat', 'UTC', 'FixedOffset', 'LOCAL', 'Duration', + 'strftime', 'DATE_BAS_COMPLETE', 'DATE_BAS_ORD_COMPLETE', + 'DATE_BAS_WEEK', 'DATE_BAS_WEEK_COMPLETE', 'DATE_CENTURY', + 'DATE_EXT_COMPLETE', 'DATE_EXT_ORD_COMPLETE', 'DATE_EXT_WEEK', + 'DATE_EXT_WEEK_COMPLETE', 'DATE_MONTH', 'DATE_YEAR', + 'TIME_BAS_COMPLETE', 'TIME_BAS_MINUTE', 'TIME_EXT_COMPLETE', + 'TIME_EXT_MINUTE', 'TIME_HOUR', 'TZ_BAS', 'TZ_EXT', 'TZ_HOUR', + 'DT_BAS_COMPLETE', 'DT_EXT_COMPLETE', 'DT_BAS_ORD_COMPLETE', + 'DT_EXT_ORD_COMPLETE', 'DT_BAS_WEEK_COMPLETE', + 'DT_EXT_WEEK_COMPLETE', 'D_DEFAULT', 'D_WEEK', 'D_ALT_EXT', + 'D_ALT_BAS', 'D_ALT_BAS_ORD', 'D_ALT_EXT_ORD'] diff --git a/src/isodate/duration.py b/src/isodate/duration.py new file mode 100644 index 0000000..a901865 --- /dev/null +++ b/src/isodate/duration.py @@ -0,0 +1,324 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This module defines a Duration class. + +The class Duration allows to define durations in years and months and can be +used as limited replacement for timedelta objects. +''' +from datetime import date, datetime, timedelta +from decimal import Decimal, ROUND_FLOOR + + +def fquotmod(val, low, high): + ''' + A divmod function with boundaries. + + ''' + # assumes that all the maths is done with Decimals. + # divmod for Decimal uses truncate instead of floor as builtin + # divmod, so we have to do it manually here. + a, b = val - low, high - low + div = (a / b).to_integral(ROUND_FLOOR) + mod = a - div * b + # if we were not usig Decimal, it would look like this. + # div, mod = divmod(val - low, high - low) + mod += low + return int(div), mod + + +def max_days_in_month(year, month): + ''' + Determines the number of days of a specific month in a specific year. + ''' + if month in (1, 3, 5, 7, 8, 10, 12): + return 31 + if month in (4, 6, 9, 11): + return 30 + if ((year % 400) == 0) or ((year % 100) != 0) and ((year % 4) == 0): + return 29 + return 28 + + +class Duration(object): + ''' + A class which represents a duration. + + The difference to datetime.timedelta is, that this class handles also + differences given in years and months. + A Duration treats differences given in year, months separately from all + other components. + + A Duration can be used almost like any timedelta object, however there + are some restrictions: + * It is not really possible to compare Durations, because it is unclear, + whether a duration of 1 year is bigger than 365 days or not. + * Equality is only tested between the two (year, month vs. timedelta) + basic components. + + A Duration can also be converted into a datetime object, but this requires + a start date or an end date. + + The algorithm to add a duration to a date is defined at + http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes + ''' + + def __init__(self, days=0, seconds=0, microseconds=0, milliseconds=0, + minutes=0, hours=0, weeks=0, months=0, years=0): + ''' + Initialise this Duration instance with the given parameters. + ''' + if not isinstance(months, Decimal): + months = Decimal(str(months)) + if not isinstance(years, Decimal): + years = Decimal(str(years)) + self.months = months + self.years = years + self.tdelta = timedelta(days, seconds, microseconds, milliseconds, + minutes, hours, weeks) + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self, state): + self.__dict__.update(state) + + def __getattr__(self, name): + ''' + Provide direct access to attributes of included timedelta instance. + ''' + return getattr(self.tdelta, name) + + def __str__(self): + ''' + Return a string representation of this duration similar to timedelta. + ''' + params = [] + if self.years: + params.append('%d years' % self.years) + if self.months: + params.append('%d months' % self.months) + params.append(str(self.tdelta)) + return ', '.join(params) + + def __repr__(self): + ''' + Return a string suitable for repr(x) calls. + ''' + return "%s.%s(%d, %d, %d, years=%d, months=%d)" % ( + self.__class__.__module__, self.__class__.__name__, + self.tdelta.days, self.tdelta.seconds, + self.tdelta.microseconds, self.years, self.months) + + def __hash__(self): + ''' + Return a hash of this instance so that it can be used in, for + example, dicts and sets. + ''' + return hash((self.tdelta, self.months, self.years)) + + def __neg__(self): + """ + A simple unary minus. + + Returns a new Duration instance with all it's negated. + """ + negduration = Duration(years=-self.years, months=-self.months) + negduration.tdelta = -self.tdelta + return negduration + + def __add__(self, other): + ''' + Durations can be added with Duration, timedelta, date and datetime + objects. + ''' + if isinstance(other, timedelta): + newduration = Duration(years=self.years, months=self.months) + newduration.tdelta = self.tdelta + other + return newduration + if isinstance(other, Duration): + newduration = Duration(years=self.years + other.years, + months=self.months + other.months) + newduration.tdelta = self.tdelta + other.tdelta + return newduration + if isinstance(other, (date, datetime)): + if (not(float(self.years).is_integer() and + float(self.months).is_integer())): + raise ValueError('fractional years or months not supported' + ' for date calculations') + newmonth = other.month + self.months + carry, newmonth = fquotmod(newmonth, 1, 13) + newyear = other.year + self.years + carry + maxdays = max_days_in_month(newyear, newmonth) + if other.day > maxdays: + newday = maxdays + else: + newday = other.day + newdt = other.replace(year=newyear, month=newmonth, day=newday) + return self.tdelta + newdt + raise TypeError('unsupported operand type(s) for +: %s and %s' % + (self.__class__, other.__class__)) + + def __radd__(self, other): + ''' + Add durations to timedelta, date and datetime objects. + ''' + if isinstance(other, timedelta): + newduration = Duration(years=self.years, months=self.months) + newduration.tdelta = self.tdelta + other + return newduration + if isinstance(other, (date, datetime)): + if (not(float(self.years).is_integer() and + float(self.months).is_integer())): + raise ValueError('fractional years or months not supported' + ' for date calculations') + newmonth = other.month + self.months + carry, newmonth = fquotmod(newmonth, 1, 13) + newyear = other.year + self.years + carry + maxdays = max_days_in_month(newyear, newmonth) + if other.day > maxdays: + newday = maxdays + else: + newday = other.day + newdt = other.replace(year=newyear, month=newmonth, day=newday) + return newdt + self.tdelta + raise TypeError('unsupported operand type(s) for +: %s and %s' % + (other.__class__, self.__class__)) + + def __mul__(self, other): + if isinstance(other, int): + newduration = Duration( + years=self.years * other, + months=self.months * other) + newduration.tdelta = self.tdelta * other + return newduration + raise TypeError('unsupported operand type(s) for +: %s and %s' % + (self.__class__, other.__class__)) + + def __rmul__(self, other): + + if isinstance(other, int): + newduration = Duration( + years=self.years * other, + months=self.months * other) + newduration.tdelta = self.tdelta * other + return newduration + raise TypeError('unsupported operand type(s) for +: %s and %s' % + (other.__class__, self.__class__)) + + def __sub__(self, other): + ''' + It is possible to subtract Duration and timedelta objects from Duration + objects. + ''' + if isinstance(other, Duration): + newduration = Duration(years=self.years - other.years, + months=self.months - other.months) + newduration.tdelta = self.tdelta - other.tdelta + return newduration + if isinstance(other, timedelta): + newduration = Duration(years=self.years, months=self.months) + newduration.tdelta = self.tdelta - other + return newduration + raise TypeError('unsupported operand type(s) for -: %s and %s' % + (self.__class__, other.__class__)) + + def __rsub__(self, other): + ''' + It is possible to subtract Duration objecs from date, datetime and + timedelta objects. + ''' + # print '__rsub__:', self, other + if isinstance(other, (date, datetime)): + if (not(float(self.years).is_integer() and + float(self.months).is_integer())): + raise ValueError('fractional years or months not supported' + ' for date calculations') + newmonth = other.month - self.months + carry, newmonth = fquotmod(newmonth, 1, 13) + newyear = other.year - self.years + carry + maxdays = max_days_in_month(newyear, newmonth) + if other.day > maxdays: + newday = maxdays + else: + newday = other.day + newdt = other.replace(year=newyear, month=newmonth, day=newday) + return newdt - self.tdelta + if isinstance(other, timedelta): + tmpdur = Duration() + tmpdur.tdelta = other + return tmpdur - self + raise TypeError('unsupported operand type(s) for -: %s and %s' % + (other.__class__, self.__class__)) + + def __eq__(self, other): + ''' + If the years, month part and the timedelta part are both equal, then + the two Durations are considered equal. + ''' + if ((isinstance(other, timedelta) and + self.years == 0 and self.months == 0)): + return self.tdelta == other + if not isinstance(other, Duration): + return NotImplemented + if (((self.years * 12 + self.months) == + (other.years * 12 + other.months) and + self.tdelta == other.tdelta)): + return True + return False + + def __ne__(self, other): + ''' + If the years, month part or the timedelta part is not equal, then + the two Durations are considered not equal. + ''' + if ((isinstance(other, timedelta) and + self.years == 0 and + self.months == 0)): + return self.tdelta != other + if not isinstance(other, Duration): + return NotImplemented + if (((self.years * 12 + self.months) != + (other.years * 12 + other.months) or + self.tdelta != other.tdelta)): + return True + return False + + def totimedelta(self, start=None, end=None): + ''' + Convert this duration into a timedelta object. + + This method requires a start datetime or end datetimem, but raises + an exception if both are given. + ''' + if start is None and end is None: + raise ValueError("start or end required") + if start is not None and end is not None: + raise ValueError("only start or end allowed") + if start is not None: + return (start + self) - start + return end - (end - self) diff --git a/src/isodate/isodates.py b/src/isodate/isodates.py new file mode 100644 index 0000000..37d42f8 --- /dev/null +++ b/src/isodate/isodates.py @@ -0,0 +1,204 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This modules provides a method to parse an ISO 8601:2004 date string to a +python datetime.date instance. + +It supports all basic, extended and expanded formats as described in the ISO +standard. The only limitations it has, are given by the Python datetime.date +implementation, which does not support dates before 0001-01-01. +''' +import re +from datetime import date, timedelta + +from isodate.isostrf import strftime, DATE_EXT_COMPLETE +from isodate.isoerror import ISO8601Error + +DATE_REGEX_CACHE = {} +# A dictionary to cache pre-compiled regular expressions. +# A set of regular expressions is identified, by number of year digits allowed +# and whether a plus/minus sign is required or not. (This option is changeable +# only for 4 digit years). + + +def build_date_regexps(yeardigits=4, expanded=False): + ''' + Compile set of regular expressions to parse ISO dates. The expressions will + be created only if they are not already in REGEX_CACHE. + + It is necessary to fix the number of year digits, else it is not possible + to automatically distinguish between various ISO date formats. + + ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/- + sign is required (expanded format). To support +/- sign for 4 digit years, + the expanded parameter needs to be set to True. + ''' + if yeardigits != 4: + expanded = True + if (yeardigits, expanded) not in DATE_REGEX_CACHE: + cache_entry = [] + # ISO 8601 expanded DATE formats allow an arbitrary number of year + # digits with a leading +/- sign. + if expanded: + sign = 1 + else: + sign = 0 + # 1. complete dates: + # YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{2})-(?P[0-9]{2})" + % (sign, yeardigits))) + # YYYYMMDD or +- YYYYYYMMDD... basic date format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"(?P[0-9]{2})(?P[0-9]{2})" + % (sign, yeardigits))) + # 2. complete week dates: + # YYYY-Www-D or +-YYYYYY-Www-D ... extended week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-W(?P[0-9]{2})-(?P[0-9]{1})" + % (sign, yeardigits))) + # YYYYWwwD or +-YYYYYYWwwD ... basic week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})W" + r"(?P[0-9]{2})(?P[0-9]{1})" + % (sign, yeardigits))) + # 3. ordinal dates: + # YYYY-DDD or +-YYYYYY-DDD ... extended format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{3})" + % (sign, yeardigits))) + # YYYYDDD or +-YYYYYYDDD ... basic format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"(?P[0-9]{3})" + % (sign, yeardigits))) + # 4. week dates: + # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-W(?P[0-9]{2})" + % (sign, yeardigits))) + # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})W" + r"(?P[0-9]{2})" + % (sign, yeardigits))) + # 5. month dates: + # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{2})" + % (sign, yeardigits))) + # 6. year dates: + # YYYY or +-YYYYYY ... reduced accuracy specific year + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + % (sign, yeardigits))) + # 7. century dates: + # YY or +-YYYY ... reduced accuracy specific century + cache_entry.append(re.compile(r"(?P[+-]){%d}" + r"(?P[0-9]{%d})" + % (sign, yeardigits - 2))) + + DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry + return DATE_REGEX_CACHE[(yeardigits, expanded)] + + +def parse_date(datestring, yeardigits=4, expanded=False): + ''' + Parse an ISO 8601 date string into a datetime.date object. + + As the datetime.date implementation is limited to dates starting from + 0001-01-01, negative dates (BC) and year 0 can not be parsed by this + method. + + For incomplete dates, this method chooses the first day for it. For + instance if only a century is given, this method returns the 1st of + January in year 1 of this century. + + supported formats: (expanded formats are shown with 6 digits for year) + YYYYMMDD +-YYYYYYMMDD basic complete date + YYYY-MM-DD +-YYYYYY-MM-DD extended complete date + YYYYWwwD +-YYYYYYWwwD basic complete week date + YYYY-Www-D +-YYYYYY-Www-D extended complete week date + YYYYDDD +-YYYYYYDDD basic ordinal date + YYYY-DDD +-YYYYYY-DDD extended ordinal date + YYYYWww +-YYYYYYWww basic incomplete week date + YYYY-Www +-YYYYYY-Www extended incomplete week date + YYY-MM +-YYYYYY-MM incomplete month date + YYYY +-YYYYYY incomplete year date + YY +-YYYY incomplete century date + + @param datestring: the ISO date string to parse + @param yeardigits: how many digits are used to represent a year + @param expanded: if True then +/- signs are allowed. This parameter + is forced to True, if yeardigits != 4 + + @return: a datetime.date instance represented by datestring + @raise ISO8601Error: if this function can not parse the datestring + @raise ValueError: if datestring can not be represented by datetime.date + ''' + if yeardigits != 4: + expanded = True + isodates = build_date_regexps(yeardigits, expanded) + for pattern in isodates: + match = pattern.match(datestring) + if match: + groups = match.groupdict() + # sign, century, year, month, week, day, + # FIXME: negative dates not possible with python standard types + sign = (groups['sign'] == '-' and -1) or 1 + if 'century' in groups: + return date(sign * (int(groups['century']) * 100 + 1), 1, 1) + if 'month' not in groups: # weekdate or ordinal date + ret = date(sign * int(groups['year']), 1, 1) + if 'week' in groups: + isotuple = ret.isocalendar() + if 'day' in groups: + days = int(groups['day'] or 1) + else: + days = 1 + # if first week in year, do weeks-1 + return ret + timedelta(weeks=int(groups['week']) - + (((isotuple[1] == 1) and 1) or 0), + days=-isotuple[2] + days) + elif 'day' in groups: # ordinal date + return ret + timedelta(days=int(groups['day'])-1) + else: # year date + return ret + # year-, month-, or complete date + if 'day' not in groups or groups['day'] is None: + day = 1 + else: + day = int(groups['day']) + return date(sign * int(groups['year']), + int(groups['month']) or 1, day) + raise ISO8601Error('Unrecognised ISO 8601 date format: %r' % datestring) + + +def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4): + ''' + Format date strings. + + This method is just a wrapper around isodate.isostrf.strftime and uses + Date-Extended-Complete as default format. + ''' + return strftime(tdate, format, yeardigits) diff --git a/src/isodate/isodatetime.py b/src/isodate/isodatetime.py new file mode 100644 index 0000000..9a5fce0 --- /dev/null +++ b/src/isodate/isodatetime.py @@ -0,0 +1,68 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This module defines a method to parse an ISO 8601:2004 date time string. + +For this job it uses the parse_date and parse_time methods defined in date +and time module. +''' +from datetime import datetime + +from isodate.isostrf import strftime +from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT +from isodate.isodates import parse_date +from isodate.isoerror import ISO8601Error +from isodate.isotime import parse_time + + +def parse_datetime(datetimestring): + ''' + Parses ISO 8601 date-times into datetime.datetime objects. + + This function uses parse_date and parse_time to do the job, so it allows + more combinations of date and time representations, than the actual + ISO 8601:2004 standard allows. + ''' + try: + datestring, timestring = datetimestring.split('T') + except ValueError: + raise ISO8601Error("ISO 8601 time designator 'T' missing. Unable to" + " parse datetime string %r" % datetimestring) + tmpdate = parse_date(datestring) + tmptime = parse_time(timestring) + return datetime.combine(tmpdate, tmptime) + + +def datetime_isoformat(tdt, format=DATE_EXT_COMPLETE + 'T' + + TIME_EXT_COMPLETE + TZ_EXT): + ''' + Format datetime strings. + + This method is just a wrapper around isodate.isostrf.strftime and uses + Extended-Complete as default format. + ''' + return strftime(tdt, format) diff --git a/src/isodate/isoduration.py b/src/isodate/isoduration.py new file mode 100644 index 0000000..6da69f5 --- /dev/null +++ b/src/isodate/isoduration.py @@ -0,0 +1,149 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This module provides an ISO 8601:2004 duration parser. + +It also provides a wrapper to strftime. This wrapper makes it easier to +format timedelta or Duration instances as ISO conforming strings. +''' +from datetime import timedelta +from decimal import Decimal +import re + +from isodate.duration import Duration +from isodate.isoerror import ISO8601Error +from isodate.isodatetime import parse_datetime +from isodate.isostrf import strftime, D_DEFAULT + +ISO8601_PERIOD_REGEX = re.compile( + r"^(?P[+-])?" + r"P(?!\b)" + r"(?P[0-9]+([,.][0-9]+)?Y)?" + r"(?P[0-9]+([,.][0-9]+)?M)?" + r"(?P[0-9]+([,.][0-9]+)?W)?" + r"(?P[0-9]+([,.][0-9]+)?D)?" + r"((?PT)(?P[0-9]+([,.][0-9]+)?H)?" + r"(?P[0-9]+([,.][0-9]+)?M)?" + r"(?P[0-9]+([,.][0-9]+)?S)?)?$") +# regular expression to parse ISO duartion strings. + + +def parse_duration(datestring): + """ + Parses an ISO 8601 durations into datetime.timedelta or Duration objects. + + If the ISO date string does not contain years or months, a timedelta + instance is returned, else a Duration instance is returned. + + The following duration formats are supported: + -PnnW duration in weeks + -PnnYnnMnnDTnnHnnMnnS complete duration specification + -PYYYYMMDDThhmmss basic alternative complete date format + -PYYYY-MM-DDThh:mm:ss extended alternative complete date format + -PYYYYDDDThhmmss basic alternative ordinal date format + -PYYYY-DDDThh:mm:ss extended alternative ordinal date format + + The '-' is optional. + + Limitations: ISO standard defines some restrictions about where to use + fractional numbers and which component and format combinations are + allowed. This parser implementation ignores all those restrictions and + returns something when it is able to find all necessary components. + In detail: + it does not check, whether only the last component has fractions. + it allows weeks specified with all other combinations + + The alternative format does not support durations with years, months or + days set to 0. + """ + if not isinstance(datestring, basestring): + raise TypeError("Expecting a string %r" % datestring) + match = ISO8601_PERIOD_REGEX.match(datestring) + if not match: + # try alternative format: + if datestring.startswith("P"): + durdt = parse_datetime(datestring[1:]) + if durdt.year != 0 or durdt.month != 0: + # create Duration + ret = Duration(days=durdt.day, seconds=durdt.second, + microseconds=durdt.microsecond, + minutes=durdt.minute, hours=durdt.hour, + months=durdt.month, years=durdt.year) + else: # FIXME: currently not possible in alternative format + # create timedelta + ret = timedelta(days=durdt.day, seconds=durdt.second, + microseconds=durdt.microsecond, + minutes=durdt.minute, hours=durdt.hour) + return ret + raise ISO8601Error("Unable to parse duration string %r" % datestring) + groups = match.groupdict() + for key, val in groups.items(): + if key not in ('separator', 'sign'): + if val is None: + groups[key] = "0n" + # print groups[key] + if key in ('years', 'months'): + groups[key] = Decimal(groups[key][:-1].replace(',', '.')) + else: + # these values are passed into a timedelta object, + # which works with floats. + groups[key] = float(groups[key][:-1].replace(',', '.')) + if groups["years"] == 0 and groups["months"] == 0: + ret = timedelta(days=groups["days"], hours=groups["hours"], + minutes=groups["minutes"], seconds=groups["seconds"], + weeks=groups["weeks"]) + if groups["sign"] == '-': + ret = timedelta(0) - ret + else: + ret = Duration(years=groups["years"], months=groups["months"], + days=groups["days"], hours=groups["hours"], + minutes=groups["minutes"], seconds=groups["seconds"], + weeks=groups["weeks"]) + if groups["sign"] == '-': + ret = Duration(0) - ret + return ret + + +def duration_isoformat(tduration, format=D_DEFAULT): + ''' + Format duration strings. + + This method is just a wrapper around isodate.isostrf.strftime and uses + P%P (D_DEFAULT) as default format. + ''' + # TODO: implement better decision for negative Durations. + # should be done in Duration class in consistent way with timedelta. + if (((isinstance(tduration, Duration) and + (tduration.years < 0 or tduration.months < 0 or + tduration.tdelta < timedelta(0))) or + (isinstance(tduration, timedelta) and + (tduration < timedelta(0))))): + ret = '-' + else: + ret = '' + ret += strftime(tduration, format) + return ret diff --git a/src/isodate/isoerror.py b/src/isodate/isoerror.py new file mode 100644 index 0000000..e7b211b --- /dev/null +++ b/src/isodate/isoerror.py @@ -0,0 +1,33 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This module defines all exception classes in the whole package. +''' + + +class ISO8601Error(ValueError): + '''Raised when the given ISO string can not be parsed.''' diff --git a/src/isodate/isostrf.py b/src/isodate/isostrf.py new file mode 100644 index 0000000..1afc810 --- /dev/null +++ b/src/isodate/isostrf.py @@ -0,0 +1,213 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +""" +This module provides an alternative strftime method. + +The strftime method in this module allows only a subset of Python's strftime +format codes, plus a few additional. It supports the full range of date values +possible with standard Python date/time objects. Furthermore there are several +pr-defined format strings in this module to make ease producing of ISO 8601 +conforming strings. +""" +import re +from datetime import date, timedelta + +from isodate.duration import Duration +from isodate.isotzinfo import tz_isoformat + +# Date specific format strings +DATE_BAS_COMPLETE = '%Y%m%d' +DATE_EXT_COMPLETE = '%Y-%m-%d' +DATE_BAS_WEEK_COMPLETE = '%YW%W%w' +DATE_EXT_WEEK_COMPLETE = '%Y-W%W-%w' +DATE_BAS_ORD_COMPLETE = '%Y%j' +DATE_EXT_ORD_COMPLETE = '%Y-%j' +DATE_BAS_WEEK = '%YW%W' +DATE_EXT_WEEK = '%Y-W%W' +DATE_MONTH = '%Y-%m' +DATE_YEAR = '%Y' +DATE_CENTURY = '%C' + +# Time specific format strings +TIME_BAS_COMPLETE = '%H%M%S' +TIME_EXT_COMPLETE = '%H:%M:%S' +TIME_BAS_MINUTE = '%H%M' +TIME_EXT_MINUTE = '%H:%M' +TIME_HOUR = '%H' + +# Time zone formats +TZ_BAS = '%z' +TZ_EXT = '%Z' +TZ_HOUR = '%h' + +# DateTime formats +DT_EXT_COMPLETE = DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT +DT_BAS_COMPLETE = DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS +DT_EXT_ORD_COMPLETE = DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT +DT_BAS_ORD_COMPLETE = DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS +DT_EXT_WEEK_COMPLETE = (DATE_EXT_WEEK_COMPLETE + 'T' + + TIME_EXT_COMPLETE + TZ_EXT) +DT_BAS_WEEK_COMPLETE = (DATE_BAS_WEEK_COMPLETE + 'T' + + TIME_BAS_COMPLETE + TZ_BAS) + +# Duration formts +D_DEFAULT = 'P%P' +D_WEEK = 'P%p' +D_ALT_EXT = 'P' + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE +D_ALT_BAS = 'P' + DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE +D_ALT_EXT_ORD = 'P' + DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE +D_ALT_BAS_ORD = 'P' + DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE + +STRF_DT_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.day, + '%f': lambda tdt, yds: '%06d' % tdt.microsecond, + '%H': lambda tdt, yds: '%02d' % tdt.hour, + '%j': lambda tdt, yds: '%03d' % (tdt.toordinal() - + date(tdt.year, + 1, 1).toordinal() + + 1), + '%m': lambda tdt, yds: '%02d' % tdt.month, + '%M': lambda tdt, yds: '%02d' % tdt.minute, + '%S': lambda tdt, yds: '%02d' % tdt.second, + '%w': lambda tdt, yds: '%1d' % tdt.isoweekday(), + '%W': lambda tdt, yds: '%02d' % tdt.isocalendar()[1], + '%Y': lambda tdt, yds: (((yds != 4) and '+') or '') + + (('%%0%dd' % yds) % tdt.year), + '%C': lambda tdt, yds: (((yds != 4) and '+') or '') + + (('%%0%dd' % (yds - 2)) % + (tdt.year / 100)), + '%h': lambda tdt, yds: tz_isoformat(tdt, '%h'), + '%Z': lambda tdt, yds: tz_isoformat(tdt, '%Z'), + '%z': lambda tdt, yds: tz_isoformat(tdt, '%z'), + '%%': lambda tdt, yds: '%'} + +STRF_D_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.days, + '%f': lambda tdt, yds: '%06d' % tdt.microseconds, + '%H': lambda tdt, yds: '%02d' % (tdt.seconds / 60 / 60), + '%m': lambda tdt, yds: '%02d' % tdt.months, + '%M': lambda tdt, yds: '%02d' % ((tdt.seconds / 60) % 60), + '%S': lambda tdt, yds: '%02d' % (tdt.seconds % 60), + '%W': lambda tdt, yds: '%02d' % (abs(tdt.days / 7)), + '%Y': lambda tdt, yds: (((yds != 4) and '+') or '') + + (('%%0%dd' % yds) % tdt.years), + '%C': lambda tdt, yds: (((yds != 4) and '+') or '') + + (('%%0%dd' % (yds - 2)) % + (tdt.years / 100)), + '%%': lambda tdt, yds: '%'} + + +def _strfduration(tdt, format, yeardigits=4): + ''' + this is the work method for timedelta and Duration instances. + + see strftime for more details. + ''' + def repl(match): + ''' + lookup format command and return corresponding replacement. + ''' + if match.group(0) in STRF_D_MAP: + return STRF_D_MAP[match.group(0)](tdt, yeardigits) + elif match.group(0) == '%P': + ret = [] + if isinstance(tdt, Duration): + if tdt.years: + ret.append('%sY' % abs(tdt.years)) + if tdt.months: + ret.append('%sM' % abs(tdt.months)) + usecs = abs((tdt.days * 24 * 60 * 60 + tdt.seconds) * 1000000 + + tdt.microseconds) + seconds, usecs = divmod(usecs, 1000000) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + if days: + ret.append('%sD' % days) + if hours or minutes or seconds or usecs: + ret.append('T') + if hours: + ret.append('%sH' % hours) + if minutes: + ret.append('%sM' % minutes) + if seconds or usecs: + if usecs: + ret.append(("%d.%06d" % (seconds, usecs)).rstrip('0')) + else: + ret.append("%d" % seconds) + ret.append('S') + # at least one component has to be there. + return ret and ''.join(ret) or '0D' + elif match.group(0) == '%p': + return str(abs(tdt.days // 7)) + 'W' + return match.group(0) + return re.sub('%d|%f|%H|%m|%M|%S|%W|%Y|%C|%%|%P|%p', repl, + format) + + +def _strfdt(tdt, format, yeardigits=4): + ''' + this is the work method for time and date instances. + + see strftime for more details. + ''' + def repl(match): + ''' + lookup format command and return corresponding replacement. + ''' + if match.group(0) in STRF_DT_MAP: + return STRF_DT_MAP[match.group(0)](tdt, yeardigits) + return match.group(0) + return re.sub('%d|%f|%H|%j|%m|%M|%S|%w|%W|%Y|%C|%z|%Z|%h|%%', repl, + format) + + +def strftime(tdt, format, yeardigits=4): + '''Directive Meaning Notes + %d Day of the month as a decimal number [01,31]. + %f Microsecond as a decimal number [0,999999], zero-padded + on the left (1) + %H Hour (24-hour clock) as a decimal number [00,23]. + %j Day of the year as a decimal number [001,366]. + %m Month as a decimal number [01,12]. + %M Minute as a decimal number [00,59]. + %S Second as a decimal number [00,61]. (3) + %w Weekday as a decimal number [0(Monday),6]. + %W Week number of the year (Monday as the first day of the week) + as a decimal number [00,53]. All days in a new year preceding the + first Monday are considered to be in week 0. (4) + %Y Year with century as a decimal number. [0000,9999] + %C Century as a decimal number. [00,99] + %z UTC offset in the form +HHMM or -HHMM (empty string if the + object is naive). (5) + %Z Time zone name (empty string if the object is naive). + %P ISO8601 duration format. + %p ISO8601 duration format in weeks. + %% A literal '%' character. + + ''' + if isinstance(tdt, (timedelta, Duration)): + return _strfduration(tdt, format, yeardigits) + return _strfdt(tdt, format, yeardigits) diff --git a/src/isodate/isotime.py b/src/isodate/isotime.py new file mode 100644 index 0000000..9650cda --- /dev/null +++ b/src/isodate/isotime.py @@ -0,0 +1,158 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This modules provides a method to parse an ISO 8601:2004 time string to a +Python datetime.time instance. + +It supports all basic and extended formats including time zone specifications +as described in the ISO standard. +''' +import re +from decimal import Decimal +from datetime import time + +from isodate.isostrf import strftime, TIME_EXT_COMPLETE, TZ_EXT +from isodate.isoerror import ISO8601Error +from isodate.isotzinfo import TZ_REGEX, build_tzinfo + +TIME_REGEX_CACHE = [] +# used to cache regular expressions to parse ISO time strings. + + +def build_time_regexps(): + ''' + Build regular expressions to parse ISO time string. + + The regular expressions are compiled and stored in TIME_REGEX_CACHE + for later reuse. + ''' + if not TIME_REGEX_CACHE: + # ISO 8601 time representations allow decimal fractions on least + # significant time component. Command and Full Stop are both valid + # fraction separators. + # The letter 'T' is allowed as time designator in front of a time + # expression. + # Immediately after a time expression, a time zone definition is + # allowed. + # a TZ may be missing (local time), be a 'Z' for UTC or a string of + # +-hh:mm where the ':mm' part can be skipped. + # TZ information patterns: + # '' + # Z + # +-hh:mm + # +-hhmm + # +-hh => + # isotzinfo.TZ_REGEX + # 1. complete time: + # hh:mm:ss.ss ... extended format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2}):" + r"(?P[0-9]{2}):" + r"(?P[0-9]{2}" + r"([,.][0-9]+)?)" + TZ_REGEX)) + # hhmmss.ss ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2})" + r"(?P[0-9]{2})" + r"(?P[0-9]{2}" + r"([,.][0-9]+)?)" + TZ_REGEX)) + # 2. reduced accuracy: + # hh:mm.mm ... extended format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2}):" + r"(?P[0-9]{2}" + r"([,.][0-9]+)?)" + TZ_REGEX)) + # hhmm.mm ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2})" + r"(?P[0-9]{2}" + r"([,.][0-9]+)?)" + TZ_REGEX)) + # hh.hh ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2}" + r"([,.][0-9]+)?)" + TZ_REGEX)) + return TIME_REGEX_CACHE + + +def parse_time(timestring): + ''' + Parses ISO 8601 times into datetime.time objects. + + Following ISO 8601 formats are supported: + (as decimal separator a ',' or a '.' is allowed) + hhmmss.ssTZD basic complete time + hh:mm:ss.ssTZD extended compelte time + hhmm.mmTZD basic reduced accuracy time + hh:mm.mmTZD extended reduced accuracy time + hh.hhTZD basic reduced accuracy time + TZD is the time zone designator which can be in the following format: + no designator indicates local time zone + Z UTC + +-hhmm basic hours and minutes + +-hh:mm extended hours and minutes + +-hh hours + ''' + isotimes = build_time_regexps() + for pattern in isotimes: + match = pattern.match(timestring) + if match: + groups = match.groupdict() + for key, value in groups.items(): + if value is not None: + groups[key] = value.replace(',', '.') + tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'], + int(groups['tzhour'] or 0), + int(groups['tzmin'] or 0)) + if 'second' in groups: + # round to microseconds if fractional seconds are more precise + second = Decimal(groups['second']).quantize(Decimal('.000001')) + microsecond = (second - int(second)) * long(1e6) + # int(...) ... no rounding + # to_integral() ... rounding + return time(int(groups['hour']), int(groups['minute']), + int(second), int(microsecond.to_integral()), + tzinfo) + if 'minute' in groups: + minute = Decimal(groups['minute']) + second = (minute - int(minute)) * 60 + microsecond = (second - int(second)) * long(1e6) + return time(int(groups['hour']), int(minute), int(second), + int(microsecond.to_integral()), tzinfo) + else: + microsecond, second, minute = 0, 0, 0 + hour = Decimal(groups['hour']) + minute = (hour - int(hour)) * 60 + second = (minute - int(minute)) * 60 + microsecond = (second - int(second)) * long(1e6) + return time(int(hour), int(minute), int(second), + int(microsecond.to_integral()), tzinfo) + raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring) + + +def time_isoformat(ttime, format=TIME_EXT_COMPLETE + TZ_EXT): + ''' + Format time strings. + + This method is just a wrapper around isodate.isostrf.strftime and uses + Time-Extended-Complete with extended time zone as default format. + ''' + return strftime(ttime, format) diff --git a/src/isodate/isotzinfo.py b/src/isodate/isotzinfo.py new file mode 100644 index 0000000..263afd7 --- /dev/null +++ b/src/isodate/isotzinfo.py @@ -0,0 +1,112 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +This module provides an ISO 8601:2004 time zone info parser. + +It offers a function to parse the time zone offset as specified by ISO 8601. +''' +import re + +from isodate.isoerror import ISO8601Error +from isodate.tzinfo import UTC, FixedOffset, ZERO + +TZ_REGEX = r"(?P(Z|(?P[+-])"\ + r"(?P[0-9]{2})(:(?P[0-9]{2}))?)?)" + +TZ_RE = re.compile(TZ_REGEX) + + +def build_tzinfo(tzname, tzsign='+', tzhour=0, tzmin=0): + ''' + create a tzinfo instance according to given parameters. + + tzname: + 'Z' ... return UTC + '' | None ... return None + other ... return FixedOffset + ''' + if tzname is None or tzname == '': + return None + if tzname == 'Z': + return UTC + tzsign = ((tzsign == '-') and -1) or 1 + return FixedOffset(tzsign * tzhour, tzsign * tzmin, tzname) + + +def parse_tzinfo(tzstring): + ''' + Parses ISO 8601 time zone designators to tzinfo objecs. + + A time zone designator can be in the following format: + no designator indicates local time zone + Z UTC + +-hhmm basic hours and minutes + +-hh:mm extended hours and minutes + +-hh hours + ''' + match = TZ_RE.match(tzstring) + if match: + groups = match.groupdict() + return build_tzinfo(groups['tzname'], groups['tzsign'], + int(groups['tzhour'] or 0), + int(groups['tzmin'] or 0)) + raise ISO8601Error('%s not a valid time zone info' % tzstring) + + +def tz_isoformat(dt, format='%Z'): + ''' + return time zone offset ISO 8601 formatted. + The various ISO formats can be chosen with the format parameter. + + if tzinfo is None returns '' + if tzinfo is UTC returns 'Z' + else the offset is rendered to the given format. + format: + %h ... +-HH + %z ... +-HHMM + %Z ... +-HH:MM + ''' + tzinfo = dt.tzinfo + if (tzinfo is None) or (tzinfo.utcoffset(dt) is None): + return '' + if tzinfo.utcoffset(dt) == ZERO and tzinfo.dst(dt) == ZERO: + return 'Z' + tdelta = tzinfo.utcoffset(dt) + seconds = tdelta.days * 24 * 60 * 60 + tdelta.seconds + sign = ((seconds < 0) and '-') or '+' + seconds = abs(seconds) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + if hours > 99: + raise OverflowError('can not handle differences > 99 hours') + if format == '%Z': + return '%s%02d:%02d' % (sign, hours, minutes) + elif format == '%z': + return '%s%02d%02d' % (sign, hours, minutes) + elif format == '%h': + return '%s%02d' % (sign, hours) + raise ValueError('unknown format string "%s"' % format) diff --git a/src/isodate/tests/__init__.py b/src/isodate/tests/__init__.py new file mode 100644 index 0000000..09dba2e --- /dev/null +++ b/src/isodate/tests/__init__.py @@ -0,0 +1,50 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Collect all test suites into one TestSuite instance. +''' + +import unittest +from isodate.tests import (test_date, test_time, test_datetime, test_duration, + test_strf, test_pickle) + + +def test_suite(): + ''' + Return a new TestSuite instance consisting of all available TestSuites. + ''' + return unittest.TestSuite([ + test_date.test_suite(), + test_time.test_suite(), + test_datetime.test_suite(), + test_duration.test_suite(), + test_strf.test_suite(), + test_pickle.test_suite(), + ]) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_date.py b/src/isodate/tests/test_date.py new file mode 100644 index 0000000..fdc1043 --- /dev/null +++ b/src/isodate/tests/test_date.py @@ -0,0 +1,129 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Test cases for the isodate module. +''' +import unittest +from datetime import date +from isodate import parse_date, ISO8601Error, date_isoformat +from isodate import DATE_CENTURY, DATE_YEAR, DATE_MONTH +from isodate import DATE_EXT_COMPLETE, DATE_BAS_COMPLETE +from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE +from isodate import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE +from isodate import DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE + +# the following list contains tuples of ISO date strings and the expected +# result from the parse_date method. A result of None means an ISO8601Error +# is expected. The test cases are grouped into dates with 4 digit years +# and 6 digit years. +TEST_CASES = {4: [('19', date(1901, 1, 1), DATE_CENTURY), + ('1985', date(1985, 1, 1), DATE_YEAR), + ('1985-04', date(1985, 4, 1), DATE_MONTH), + ('1985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE), + ('19850412', date(1985, 4, 12), DATE_BAS_COMPLETE), + ('1985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), + ('1985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), + ('1985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), + ('1985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), + ('1985W15', date(1985, 4, 8), DATE_BAS_WEEK), + ('1985-W15', date(1985, 4, 8), DATE_EXT_WEEK), + ('1989-W15', date(1989, 4, 10), DATE_EXT_WEEK), + ('1989-W15-5', date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE), + ('1-W1-1', None, DATE_BAS_WEEK_COMPLETE)], + 6: [('+0019', date(1901, 1, 1), DATE_CENTURY), + ('+001985', date(1985, 1, 1), DATE_YEAR), + ('+001985-04', date(1985, 4, 1), DATE_MONTH), + ('+001985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE), + ('+0019850412', date(1985, 4, 12), DATE_BAS_COMPLETE), + ('+001985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), + ('+001985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), + ('+001985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), + ('+001985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), + ('+001985W15', date(1985, 4, 8), DATE_BAS_WEEK), + ('+001985-W15', date(1985, 4, 8), DATE_EXT_WEEK)]} + + +def create_testcase(yeardigits, datestring, expectation, format): + ''' + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + TEST_CASES list, so that a failed test won't stop other tests. + ''' + + class TestDate(unittest.TestCase): + ''' + A test case template to parse an ISO date string into a date + object. + ''' + + def test_parse(self): + ''' + Parse an ISO date string and compare it to the expected value. + ''' + if expectation is None: + self.assertRaises(ISO8601Error, parse_date, datestring, + yeardigits) + else: + result = parse_date(datestring, yeardigits) + self.assertEqual(result, expectation) + + def test_format(self): + ''' + Take date object and create ISO string from it. + This is the reverse test to test_parse. + ''' + if expectation is None: + self.assertRaises(AttributeError, + date_isoformat, expectation, format, + yeardigits) + else: + self.assertEqual(date_isoformat(expectation, format, + yeardigits), + datestring) + + return unittest.TestLoader().loadTestsFromTestCase(TestDate) + + +def test_suite(): + ''' + Construct a TestSuite instance for all test cases. + ''' + suite = unittest.TestSuite() + for yeardigits, tests in TEST_CASES.items(): + for datestring, expectation, format in tests: + suite.addTest(create_testcase(yeardigits, datestring, + expectation, format)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_datetime.py b/src/isodate/tests/test_datetime.py new file mode 100644 index 0000000..ddad5da --- /dev/null +++ b/src/isodate/tests/test_datetime.py @@ -0,0 +1,146 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Test cases for the isodatetime module. +''' +import unittest +from datetime import datetime + +from isodate import parse_datetime, UTC, FixedOffset, datetime_isoformat +from isodate import ISO8601Error +from isodate import DATE_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_BAS_COMPLETE +from isodate import DATE_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_EXT_COMPLETE +from isodate import TZ_BAS, TZ_EXT, TZ_HOUR +from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE +from isodate import DATE_BAS_WEEK_COMPLETE, DATE_EXT_WEEK_COMPLETE + +# the following list contains tuples of ISO datetime strings and the expected +# result from the parse_datetime method. A result of None means an ISO8601Error +# is expected. +TEST_CASES = [('19850412T1015', datetime(1985, 4, 12, 10, 15), + DATE_BAS_COMPLETE + 'T' + TIME_BAS_MINUTE, + '19850412T1015'), + ('1985-04-12T10:15', datetime(1985, 4, 12, 10, 15), + DATE_EXT_COMPLETE + 'T' + TIME_EXT_MINUTE, + '1985-04-12T10:15'), + ('1985102T1015Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC), + DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS, + '1985102T1015Z'), + ('1985-102T10:15Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC), + DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_EXT, + '1985-102T10:15Z'), + ('1985W155T1015+0400', datetime(1985, 4, 12, 10, 15, + tzinfo=FixedOffset(4, 0, + '+0400')), + DATE_BAS_WEEK_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS, + '1985W155T1015+0400'), + ('1985-W15-5T10:15+04', datetime(1985, 4, 12, 10, 15, + tzinfo=FixedOffset(4, 0, + '+0400'),), + DATE_EXT_WEEK_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_HOUR, + '1985-W15-5T10:15+04'), + ('20110410T101225.123000Z', + datetime(2011, 4, 10, 10, 12, 25, 123000, tzinfo=UTC), + DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + ".%f" + TZ_BAS, + '20110410T101225.123000Z'), + ('2012-10-12T08:29:46.069178Z', + datetime(2012, 10, 12, 8, 29, 46, 69178, tzinfo=UTC), + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS, + '2012-10-12T08:29:46.069178Z'), + ('2012-10-12T08:29:46.691780Z', + datetime(2012, 10, 12, 8, 29, 46, 691780, tzinfo=UTC), + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS, + '2012-10-12T08:29:46.691780Z'), + ('2012-10-30T08:55:22.1234567Z', + datetime(2012, 10, 30, 8, 55, 22, 123457, tzinfo=UTC), + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS, + '2012-10-30T08:55:22.123457Z'), + ('2012-10-30T08:55:22.1234561Z', + datetime(2012, 10, 30, 8, 55, 22, 123456, tzinfo=UTC), + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS, + '2012-10-30T08:55:22.123456Z'), + ('2014-08-18 14:55:22.123456Z', None, + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS, + '2014-08-18T14:55:22.123456Z'), + ] + + +def create_testcase(datetimestring, expectation, format, output): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestDateTime(unittest.TestCase): + ''' + A test case template to parse an ISO datetime string into a + datetime object. + ''' + + def test_parse(self): + ''' + Parse an ISO datetime string and compare it to the expected value. + ''' + if expectation is None: + self.assertRaises(ISO8601Error, parse_datetime, datetimestring) + else: + self.assertEqual(parse_datetime(datetimestring), expectation) + + def test_format(self): + ''' + Take datetime object and create ISO string from it. + This is the reverse test to test_parse. + ''' + if expectation is None: + self.assertRaises(AttributeError, + datetime_isoformat, expectation, format) + else: + self.assertEqual(datetime_isoformat(expectation, format), + output) + + return unittest.TestLoader().loadTestsFromTestCase(TestDateTime) + + +def test_suite(): + ''' + Construct a TestSuite instance for all test cases. + ''' + suite = unittest.TestSuite() + for datetimestring, expectation, format, output in TEST_CASES: + suite.addTest(create_testcase(datetimestring, expectation, + format, output)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_duration.py b/src/isodate/tests/test_duration.py new file mode 100644 index 0000000..0b80a54 --- /dev/null +++ b/src/isodate/tests/test_duration.py @@ -0,0 +1,601 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Test cases for the isoduration module. +''' +import unittest +import operator +from datetime import timedelta, date, datetime + +from isodate import Duration, parse_duration, ISO8601Error +from isodate import D_DEFAULT, D_WEEK, D_ALT_EXT, duration_isoformat + +# the following list contains tuples of ISO duration strings and the expected +# result from the parse_duration method. A result of None means an ISO8601Error +# is expected. +PARSE_TEST_CASES = {'P18Y9M4DT11H9M8S': (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), + D_DEFAULT, None), + 'P2W': (timedelta(weeks=2), D_WEEK, None), + 'P3Y6M4DT12H30M5S': (Duration(4, 5, 0, 0, 30, 12, 0, 6, 3), + D_DEFAULT, None), + 'P23DT23H': (timedelta(hours=23, days=23), + D_DEFAULT, None), + 'P4Y': (Duration(years=4), D_DEFAULT, None), + 'P1M': (Duration(months=1), D_DEFAULT, None), + 'PT1M': (timedelta(minutes=1), D_DEFAULT, None), + 'P0.5Y': (Duration(years=0.5), D_DEFAULT, None), + 'PT36H': (timedelta(hours=36), D_DEFAULT, 'P1DT12H'), + 'P1DT12H': (timedelta(days=1, hours=12), D_DEFAULT, None), + '+P11D': (timedelta(days=11), D_DEFAULT, 'P11D'), + '-P2W': (timedelta(weeks=-2), D_WEEK, None), + '-P2.2W': (timedelta(weeks=-2.2), D_DEFAULT, + '-P15DT9H36M'), + 'P1DT2H3M4S': (timedelta(days=1, hours=2, minutes=3, + seconds=4), D_DEFAULT, None), + 'P1DT2H3M': (timedelta(days=1, hours=2, minutes=3), + D_DEFAULT, None), + 'P1DT2H': (timedelta(days=1, hours=2), D_DEFAULT, None), + 'PT2H': (timedelta(hours=2), D_DEFAULT, None), + 'PT2.3H': (timedelta(hours=2.3), D_DEFAULT, 'PT2H18M'), + 'PT2H3M4S': (timedelta(hours=2, minutes=3, seconds=4), + D_DEFAULT, None), + 'PT3M4S': (timedelta(minutes=3, seconds=4), D_DEFAULT, + None), + 'PT22S': (timedelta(seconds=22), D_DEFAULT, None), + 'PT22.22S': (timedelta(seconds=22.22), 'PT%S.%fS', + 'PT22.220000S'), + '-P2Y': (Duration(years=-2), D_DEFAULT, None), + '-P3Y6M4DT12H30M5S': (Duration(-4, -5, 0, 0, -30, -12, 0, + -6, -3), D_DEFAULT, None), + '-P1DT2H3M4S': (timedelta(days=-1, hours=-2, minutes=-3, + seconds=-4), D_DEFAULT, None), + # alternative format + 'P0018-09-04T11:09:08': (Duration(4, 8, 0, 0, 9, 11, 0, 9, + 18), D_ALT_EXT, None), + # 'PT000022.22': timedelta(seconds=22.22), + } + +# d1 d2 '+', '-', '>' +# A list of test cases to test addition and subtraction between datetime and +# Duration objects. +# each tuple contains 2 duration strings, and a result string for addition and +# one for subtraction. The last value says, if the first duration is greater +# than the second. +MATH_TEST_CASES = (('P5Y7M1DT9H45M16.72S', 'PT27M24.68S', + 'P5Y7M1DT10H12M41.4S', 'P5Y7M1DT9H17M52.04S', None), + ('PT28M12.73S', 'PT56M29.92S', + 'PT1H24M42.65S', '-PT28M17.19S', False), + ('P3Y7M23DT5H25M0.33S', 'PT1H1.95S', + 'P3Y7M23DT6H25M2.28S', 'P3Y7M23DT4H24M58.38S', None), + ('PT1H1.95S', 'P3Y7M23DT5H25M0.33S', + 'P3Y7M23DT6H25M2.28S', '-P3Y7M23DT4H24M58.38S', None), + ('P1332DT55M0.33S', 'PT1H1.95S', + 'P1332DT1H55M2.28S', 'P1331DT23H54M58.38S', True), + ('PT1H1.95S', 'P1332DT55M0.33S', + 'P1332DT1H55M2.28S', '-P1331DT23H54M58.38S', False)) + + +# A list of test cases to test addition and subtraction of date/datetime +# and Duration objects. They are tested against the results of an +# equal long timedelta duration. +DATE_TEST_CASES = ((date(2008, 2, 29), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (date(2008, 1, 31), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (datetime(2008, 2, 29), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (datetime(2008, 1, 31), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (datetime(2008, 4, 21), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (datetime(2008, 5, 5), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20)), + (datetime(2000, 1, 1), + timedelta(hours=-33), + Duration(hours=-33)), + (datetime(2008, 5, 5), + Duration(years=1, months=1, days=10, hours=12, + minutes=20), + Duration(months=13, days=10, hours=12, minutes=20)), + (datetime(2000, 3, 30), + Duration(years=1, months=1, days=10, hours=12, + minutes=20), + Duration(months=13, days=10, hours=12, minutes=20)), + ) + +# A list of test cases of additon of date/datetime and Duration. The results +# are compared against a given expected result. +DATE_CALC_TEST_CASES = ( + (date(2000, 2, 1), + Duration(years=1, months=1), + date(2001, 3, 1)), + (date(2000, 2, 29), + Duration(years=1, months=1), + date(2001, 3, 29)), + (date(2000, 2, 29), + Duration(years=1), + date(2001, 2, 28)), + (date(1996, 2, 29), + Duration(years=4), + date(2000, 2, 29)), + (date(2096, 2, 29), + Duration(years=4), + date(2100, 2, 28)), + (date(2000, 2, 1), + Duration(years=-1, months=-1), + date(1999, 1, 1)), + (date(2000, 2, 29), + Duration(years=-1, months=-1), + date(1999, 1, 29)), + (date(2000, 2, 1), + Duration(years=1, months=1, days=1), + date(2001, 3, 2)), + (date(2000, 2, 29), + Duration(years=1, months=1, days=1), + date(2001, 3, 30)), + (date(2000, 2, 29), + Duration(years=1, days=1), + date(2001, 3, 1)), + (date(1996, 2, 29), + Duration(years=4, days=1), + date(2000, 3, 1)), + (date(2096, 2, 29), + Duration(years=4, days=1), + date(2100, 3, 1)), + (date(2000, 2, 1), + Duration(years=-1, months=-1, days=-1), + date(1998, 12, 31)), + (date(2000, 2, 29), + Duration(years=-1, months=-1, days=-1), + date(1999, 1, 28)), + (date(2001, 4, 1), + Duration(years=-1, months=-1, days=-1), + date(2000, 2, 29)), + (date(2000, 4, 1), + Duration(years=-1, months=-1, days=-1), + date(1999, 2, 28)), + (Duration(years=1, months=2), + Duration(years=0, months=0, days=1), + Duration(years=1, months=2, days=1)), + (Duration(years=-1, months=-1, days=-1), + date(2000, 4, 1), + date(1999, 2, 28)), + (Duration(years=1, months=1, weeks=5), + date(2000, 1, 30), + date(2001, 4, 4)), + (parse_duration("P1Y1M5W"), + date(2000, 1, 30), + date(2001, 4, 4)), + (parse_duration("P0.5Y"), + date(2000, 1, 30), + None), + (Duration(years=1, months=1, hours=3), + datetime(2000, 1, 30, 12, 15, 00), + datetime(2001, 2, 28, 15, 15, 00)), + (parse_duration("P1Y1MT3H"), + datetime(2000, 1, 30, 12, 15, 00), + datetime(2001, 2, 28, 15, 15, 00)), + (Duration(years=1, months=2), + timedelta(days=1), + Duration(years=1, months=2, days=1)), + (timedelta(days=1), + Duration(years=1, months=2), + Duration(years=1, months=2, days=1)), + (datetime(2008, 1, 1, 0, 2), + Duration(months=1), + datetime(2008, 2, 1, 0, 2)), + (datetime.strptime("200802", "%Y%M"), + parse_duration("P1M"), + datetime(2008, 2, 1, 0, 2)), + (datetime(2008, 2, 1), + Duration(months=1), + datetime(2008, 3, 1)), + (datetime.strptime("200802", "%Y%m"), + parse_duration("P1M"), + datetime(2008, 3, 1)), + # (date(2000, 1, 1), + # Duration(years=1.5), + # date(2001, 6, 1)), + # (date(2000, 1, 1), + # Duration(years=1, months=1.5), + # date(2001, 2, 14)), + ) + +# A list of test cases of multiplications of durations +# are compared against a given expected result. +DATE_MUL_TEST_CASES = ( + (Duration(years=1, months=1), + 3, + Duration(years=3, months=3)), + (Duration(years=1, months=1), + -3, + Duration(years=-3, months=-3)), + (3, + Duration(years=1, months=1), + Duration(years=3, months=3)), + (-3, + Duration(years=1, months=1), + Duration(years=-3, months=-3)), + (5, + Duration(years=2, minutes=40), + Duration(years=10, hours=3, minutes=20)), + (-5, + Duration(years=2, minutes=40), + Duration(years=-10, hours=-3, minutes=-20)), + (7, + Duration(years=1, months=2, weeks=40), + Duration(years=8, months=2, weeks=280))) + + +class DurationTest(unittest.TestCase): + ''' + This class tests various other aspects of the isoduration module, + which are not covered with the test cases listed above. + ''' + + def test_associative(self): + ''' + Adding 2 durations to a date is not associative. + ''' + days1 = Duration(days=1) + months1 = Duration(months=1) + start = date(2000, 3, 30) + res1 = start + days1 + months1 + res2 = start + months1 + days1 + self.assertNotEqual(res1, res2) + + def test_typeerror(self): + ''' + Test if TypError is raised with certain parameters. + ''' + self.assertRaises(TypeError, parse_duration, date(2000, 1, 1)) + self.assertRaises(TypeError, operator.sub, Duration(years=1), + date(2000, 1, 1)) + self.assertRaises(TypeError, operator.sub, 'raise exc', + Duration(years=1)) + self.assertRaises(TypeError, operator.add, + Duration(years=1, months=1, weeks=5), + 'raise exception') + self.assertRaises(TypeError, operator.add, 'raise exception', + Duration(years=1, months=1, weeks=5)) + self.assertRaises(TypeError, operator.mul, + Duration(years=1, months=1, weeks=5), + 'raise exception') + self.assertRaises(TypeError, operator.mul, 'raise exception', + Duration(years=1, months=1, weeks=5)) + self.assertRaises(TypeError, operator.mul, + Duration(years=1, months=1, weeks=5), + 3.14) + self.assertRaises(TypeError, operator.mul, 3.14, + Duration(years=1, months=1, weeks=5)) + + def test_parseerror(self): + ''' + Test for unparseable duration string. + ''' + self.assertRaises(ISO8601Error, parse_duration, 'T10:10:10') + + def test_repr(self): + ''' + Test __repr__ and __str__ for Duration objects. + ''' + dur = Duration(10, 10, years=10, months=10) + self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur)) + self.assertEqual('isodate.duration.Duration(10, 10, 0,' + ' years=10, months=10)', repr(dur)) + + def test_hash(self): + ''' + Test __hash__ for Duration objects. + ''' + dur1 = Duration(10, 10, years=10, months=10) + dur2 = Duration(9, 9, years=9, months=9) + dur3 = Duration(10, 10, years=10, months=10) + self.assertNotEqual(hash(dur1), hash(dur2)) + self.assertNotEqual(id(dur1), id(dur2)) + self.assertEqual(hash(dur1), hash(dur3)) + self.assertNotEqual(id(dur1), id(dur3)) + durSet = set() + durSet.add(dur1) + durSet.add(dur2) + durSet.add(dur3) + self.assertEqual(len(durSet), 2) + + def test_neg(self): + ''' + Test __neg__ for Duration objects. + ''' + self.assertEqual(-Duration(0), Duration(0)) + self.assertEqual(-Duration(years=1, months=1), + Duration(years=-1, months=-1)) + self.assertEqual(-Duration(years=1, months=1), Duration(months=-13)) + self.assertNotEqual(-Duration(years=1), timedelta(days=-365)) + self.assertNotEqual(-timedelta(days=365), Duration(years=-1)) + # FIXME: this test fails in python 3... it seems like python3 + # treats a == b the same b == a + # self.assertNotEqual(-timedelta(days=10), -Duration(days=10)) + + def test_format(self): + ''' + Test various other strftime combinations. + ''' + self.assertEqual(duration_isoformat(Duration(0)), 'P0D') + self.assertEqual(duration_isoformat(-Duration(0)), 'P0D') + self.assertEqual(duration_isoformat(Duration(seconds=10)), 'PT10S') + self.assertEqual(duration_isoformat(Duration(years=-1, months=-1)), + '-P1Y1M') + self.assertEqual(duration_isoformat(-Duration(years=1, months=1)), + '-P1Y1M') + self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)), + 'P1Y1M') + self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)), + 'P1Y1M') + dur = Duration(years=3, months=7, days=23, hours=5, minutes=25, + milliseconds=330) + self.assertEqual(duration_isoformat(dur), 'P3Y7M23DT5H25M0.33S') + self.assertEqual(duration_isoformat(-dur), '-P3Y7M23DT5H25M0.33S') + + def test_equal(self): + ''' + Test __eq__ and __ne__ methods. + ''' + self.assertEqual(Duration(years=1, months=1), + Duration(years=1, months=1)) + self.assertEqual(Duration(years=1, months=1), Duration(months=13)) + self.assertNotEqual(Duration(years=1, months=2), + Duration(years=1, months=1)) + self.assertNotEqual(Duration(years=1, months=1), Duration(months=14)) + self.assertNotEqual(Duration(years=1), timedelta(days=365)) + self.assertFalse(Duration(years=1, months=1) != + Duration(years=1, months=1)) + self.assertFalse(Duration(years=1, months=1) != Duration(months=13)) + self.assertTrue(Duration(years=1, months=2) != + Duration(years=1, months=1)) + self.assertTrue(Duration(years=1, months=1) != Duration(months=14)) + self.assertTrue(Duration(years=1) != timedelta(days=365)) + self.assertEqual(Duration(days=1), timedelta(days=1)) + # FIXME: this test fails in python 3... it seems like python3 + # treats a != b the same b != a + # self.assertNotEqual(timedelta(days=1), Duration(days=1)) + + def test_totimedelta(self): + ''' + Test conversion form Duration to timedelta. + ''' + dur = Duration(years=1, months=2, days=10) + self.assertEqual(dur.totimedelta(datetime(1998, 2, 25)), + timedelta(434)) + # leap year has one day more in february + self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), + timedelta(435)) + dur = Duration(months=2) + # march is longer than february, but april is shorter than + # march (cause only one day difference compared to 2) + self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(60)) + self.assertEqual(dur.totimedelta(datetime(2001, 2, 25)), timedelta(59)) + self.assertEqual(dur.totimedelta(datetime(2001, 3, 25)), timedelta(61)) + + +def create_parsetestcase(durationstring, expectation, format, altstr): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + PARSE_TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestParseDuration(unittest.TestCase): + ''' + A test case template to parse an ISO duration string into a + timedelta or Duration object. + ''' + + def test_parse(self): + ''' + Parse an ISO duration string and compare it to the expected value. + ''' + result = parse_duration(durationstring) + self.assertEqual(result, expectation) + + def test_format(self): + ''' + Take duration/timedelta object and create ISO string from it. + This is the reverse test to test_parse. + ''' + if altstr: + self.assertEqual(duration_isoformat(expectation, format), + altstr) + else: + # if durationstring == '-P2W': + # import pdb; pdb.set_trace() + self.assertEqual(duration_isoformat(expectation, format), + durationstring) + + return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration) + + +def create_mathtestcase(dur1, dur2, resadd, ressub, resge): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + MATH_TEST_CASES list, so that a failed test won't stop other tests. + """ + + dur1 = parse_duration(dur1) + dur2 = parse_duration(dur2) + resadd = parse_duration(resadd) + ressub = parse_duration(ressub) + + class TestMathDuration(unittest.TestCase): + ''' + A test case template test addition, subtraction and > + operators for Duration objects. + ''' + + def test_add(self): + ''' + Test operator + (__add__, __radd__) + ''' + self.assertEqual(dur1 + dur2, resadd) + + def test_sub(self): + ''' + Test operator - (__sub__, __rsub__) + ''' + self.assertEqual(dur1 - dur2, ressub) + + def test_ge(self): + ''' + Test operator > and < + ''' + def dogetest(): + ''' Test greater than.''' + return dur1 > dur2 + + def doletest(): + ''' Test less than.''' + return dur1 < dur2 + if resge is None: + self.assertRaises(TypeError, dogetest) + self.assertRaises(TypeError, doletest) + else: + self.assertEqual(dogetest(), resge) + self.assertEqual(doletest(), not resge) + + return unittest.TestLoader().loadTestsFromTestCase(TestMathDuration) + + +def create_datetestcase(start, tdelta, duration): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + DATE_TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestDateCalc(unittest.TestCase): + ''' + A test case template test addition, subtraction + operators for Duration objects. + ''' + + def test_add(self): + ''' + Test operator +. + ''' + self.assertEqual(start + tdelta, start + duration) + + def test_sub(self): + ''' + Test operator -. + ''' + self.assertEqual(start - tdelta, start - duration) + + return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc) + + +def create_datecalctestcase(start, duration, expectation): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestDateCalc(unittest.TestCase): + ''' + A test case template test addition operators for Duration objects. + ''' + + def test_calc(self): + ''' + Test operator +. + ''' + if expectation is None: + self.assertRaises(ValueError, operator.add, start, duration) + else: + self.assertEqual(start + duration, expectation) + + return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc) + + +def create_datemultestcase(operand1, operand2, expectation): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestDateMul(unittest.TestCase): + ''' + A test case template test addition operators for Duration objects. + ''' + + def test_mul(self): + ''' + Test operator *. + ''' + self.assertEqual(operand1 * operand2, expectation) + + return unittest.TestLoader().loadTestsFromTestCase(TestDateMul) + + +def test_suite(): + ''' + Return a test suite containing all test defined above. + ''' + suite = unittest.TestSuite() + for durationstring, (expectation, format, + altstr) in PARSE_TEST_CASES.items(): + suite.addTest(create_parsetestcase(durationstring, expectation, + format, altstr)) + for testdata in MATH_TEST_CASES: + suite.addTest(create_mathtestcase(*testdata)) + for testdata in DATE_TEST_CASES: + suite.addTest(create_datetestcase(*testdata)) + for testdata in DATE_CALC_TEST_CASES: + suite.addTest(create_datecalctestcase(*testdata)) + for testdata in DATE_MUL_TEST_CASES: + suite.addTest(create_datemultestcase(*testdata)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_pickle.py b/src/isodate/tests/test_pickle.py new file mode 100644 index 0000000..b52f8cb --- /dev/null +++ b/src/isodate/tests/test_pickle.py @@ -0,0 +1,54 @@ +import unittest +import cPickle as pickle +import isodate + + +class TestPickle(unittest.TestCase): + ''' + A test case template to parse an ISO datetime string into a + datetime object. + ''' + + def test_pickle_datetime(self): + ''' + Parse an ISO datetime string and compare it to the expected value. + ''' + dti = isodate.parse_datetime('2012-10-26T09:33+00:00') + for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): + pikl = pickle.dumps(dti, proto) + self.assertEqual(dti, pickle.loads(pikl), + "pickle proto %d failed" % proto) + + def test_pickle_duration(self): + ''' + Pickle / unpickle duration objects. + ''' + from isodate.duration import Duration + dur = Duration() + failed = [] + for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): + try: + pikl = pickle.dumps(dur, proto) + if dur != pickle.loads(pikl): + raise Exception("not equal") + except Exception, e: + failed.append("pickle proto %d failed (%s)" % (proto, repr(e))) + self.assertEqual(len(failed), 0, "pickle protos failed: %s" % + str(failed)) + + +def test_suite(): + ''' + Construct a TestSuite instance for all test cases. + ''' + suite = unittest.TestSuite() + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestPickle)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_strf.py b/src/isodate/tests/test_strf.py new file mode 100644 index 0000000..37a135b --- /dev/null +++ b/src/isodate/tests/test_strf.py @@ -0,0 +1,135 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Test cases for the isodate module. +''' +import unittest +import time +from datetime import datetime, timedelta +from isodate import strftime +from isodate import LOCAL +from isodate import DT_EXT_COMPLETE +from isodate import tzinfo + + +TEST_CASES = ((datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE, + "2012-12-25T13:30:00+10:00"), + # DST ON + (datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE, + "1999-12-25T13:30:00+11:00"), + # microseconds + (datetime(2012, 10, 12, 8, 29, 46, 69178), + "%Y-%m-%dT%H:%M:%S.%f", + "2012-10-12T08:29:46.069178"), + (datetime(2012, 10, 12, 8, 29, 46, 691780), + "%Y-%m-%dT%H:%M:%S.%f", + "2012-10-12T08:29:46.691780"), + ) + + +def create_testcase(dt, format, expectation): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestDate(unittest.TestCase): + ''' + A test case template to test ISO date formatting. + ''' + + # local time zone mock function + def localtime_mock(self, secs): + """ + mock time.localtime so that it always returns a time_struct with + tm_idst=1 + """ + tt = self.ORIG['localtime'](secs) + # befor 2000 everything is dst, after 2000 no dst. + if tt.tm_year < 2000: + dst = 1 + else: + dst = 0 + tt = (tt.tm_year, tt.tm_mon, tt.tm_mday, + tt.tm_hour, tt.tm_min, tt.tm_sec, + tt.tm_wday, tt.tm_yday, dst) + return time.struct_time(tt) + + def setUp(self): + self.ORIG = {} + self.ORIG['STDOFFSET'] = tzinfo.STDOFFSET + self.ORIG['DSTOFFSET'] = tzinfo.DSTOFFSET + self.ORIG['DSTDIFF'] = tzinfo.DSTDIFF + self.ORIG['localtime'] = time.localtime + # ovveride all saved values with fixtures. + # calculate LOCAL TZ offset, so that this test runs in + # every time zone + tzinfo.STDOFFSET = timedelta(seconds=36000) # assume LOC = +10:00 + tzinfo.DSTOFFSET = timedelta(seconds=39600) # assume DST = +11:00 + tzinfo.DSTDIFF = tzinfo.DSTOFFSET - tzinfo.STDOFFSET + time.localtime = self.localtime_mock + + def tearDown(self): + # restore test fixtures + tzinfo.STDOFFSET = self.ORIG['STDOFFSET'] + tzinfo.DSTOFFSET = self.ORIG['DSTOFFSET'] + tzinfo.DSTDIFF = self.ORIG['DSTDIFF'] + time.localtime = self.ORIG['localtime'] + + def test_format(self): + ''' + Take date object and create ISO string from it. + This is the reverse test to test_parse. + ''' + if expectation is None: + self.assertRaises(AttributeError, + strftime(dt, format)) + else: + self.assertEqual(strftime(dt, format), + expectation) + + return unittest.TestLoader().loadTestsFromTestCase(TestDate) + + +def test_suite(): + ''' + Construct a TestSuite instance for all test cases. + ''' + suite = unittest.TestSuite() + for dt, format, expectation in TEST_CASES: + suite.addTest(create_testcase(dt, format, expectation)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_time.py b/src/isodate/tests/test_time.py new file mode 100644 index 0000000..cc5ec08 --- /dev/null +++ b/src/isodate/tests/test_time.py @@ -0,0 +1,143 @@ +############################################################################## +# Copyright 2009, Gerhard Weis +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the authors nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT +############################################################################## +''' +Test cases for the isotime module. +''' +import unittest +from datetime import time + +from isodate import parse_time, UTC, FixedOffset, ISO8601Error, time_isoformat +from isodate import TIME_BAS_COMPLETE, TIME_BAS_MINUTE +from isodate import TIME_EXT_COMPLETE, TIME_EXT_MINUTE +from isodate import TIME_HOUR +from isodate import TZ_BAS, TZ_EXT, TZ_HOUR + +# the following list contains tuples of ISO time strings and the expected +# result from the parse_time method. A result of None means an ISO8601Error +# is expected. +TEST_CASES = [('232050', time(23, 20, 50), TIME_BAS_COMPLETE + TZ_BAS), + ('23:20:50', time(23, 20, 50), TIME_EXT_COMPLETE + TZ_EXT), + ('2320', time(23, 20), TIME_BAS_MINUTE), + ('23:20', time(23, 20), TIME_EXT_MINUTE), + ('23', time(23), TIME_HOUR), + ('232050,5', time(23, 20, 50, 500000), None), + ('23:20:50.5', time(23, 20, 50, 500000), None), + # test precision + ('15:33:42.123456', time(15, 33, 42, 123456), None), + ('15:33:42.1234564', time(15, 33, 42, 123456), None), + ('15:33:42.1234557', time(15, 33, 42, 123456), None), + ('2320,8', time(23, 20, 48), None), + ('23:20,8', time(23, 20, 48), None), + ('23,3', time(23, 18), None), + ('232030Z', time(23, 20, 30, tzinfo=UTC), + TIME_BAS_COMPLETE + TZ_BAS), + ('2320Z', time(23, 20, tzinfo=UTC), TIME_BAS_MINUTE + TZ_BAS), + ('23Z', time(23, tzinfo=UTC), TIME_HOUR + TZ_BAS), + ('23:20:30Z', time(23, 20, 30, tzinfo=UTC), + TIME_EXT_COMPLETE + TZ_EXT), + ('23:20Z', time(23, 20, tzinfo=UTC), TIME_EXT_MINUTE + TZ_EXT), + ('152746+0100', time(15, 27, 46, + tzinfo=FixedOffset(1, 0, '+0100')), TIME_BAS_COMPLETE + TZ_BAS), + ('152746-0500', time(15, 27, 46, + tzinfo=FixedOffset(-5, 0, '-0500')), + TIME_BAS_COMPLETE + TZ_BAS), + ('152746+01', time(15, 27, 46, + tzinfo=FixedOffset(1, 0, '+01:00')), + TIME_BAS_COMPLETE + TZ_HOUR), + ('152746-05', time(15, 27, 46, + tzinfo=FixedOffset(-5, -0, '-05:00')), + TIME_BAS_COMPLETE + TZ_HOUR), + ('15:27:46+01:00', time(15, 27, 46, + tzinfo=FixedOffset(1, 0, '+01:00')), + TIME_EXT_COMPLETE + TZ_EXT), + ('15:27:46-05:00', time(15, 27, 46, + tzinfo=FixedOffset(-5, -0, '-05:00')), + TIME_EXT_COMPLETE + TZ_EXT), + ('15:27:46+01', time(15, 27, 46, + tzinfo=FixedOffset(1, 0, '+01:00')), + TIME_EXT_COMPLETE + TZ_HOUR), + ('15:27:46-05', time(15, 27, 46, + tzinfo=FixedOffset(-5, -0, '-05:00')), + TIME_EXT_COMPLETE + TZ_HOUR), + ('1:17:30', None, TIME_EXT_COMPLETE)] + + +def create_testcase(timestring, expectation, format): + """ + Create a TestCase class for a specific test. + + This allows having a separate TestCase for each test tuple from the + TEST_CASES list, so that a failed test won't stop other tests. + """ + + class TestTime(unittest.TestCase): + ''' + A test case template to parse an ISO time string into a time + object. + ''' + + def test_parse(self): + ''' + Parse an ISO time string and compare it to the expected value. + ''' + if expectation is None: + self.assertRaises(ISO8601Error, parse_time, timestring) + else: + result = parse_time(timestring) + self.assertEqual(result, expectation) + + def test_format(self): + ''' + Take time object and create ISO string from it. + This is the reverse test to test_parse. + ''' + if expectation is None: + self.assertRaises(AttributeError, + time_isoformat, expectation, format) + elif format is not None: + self.assertEqual(time_isoformat(expectation, format), + timestring) + + return unittest.TestLoader().loadTestsFromTestCase(TestTime) + + +def test_suite(): + ''' + Construct a TestSuite instance for all test cases. + ''' + suite = unittest.TestSuite() + for timestring, expectation, format in TEST_CASES: + suite.addTest(create_testcase(timestring, expectation, format)) + return suite + + +# load_tests Protocol +def load_tests(loader, tests, pattern): + return test_suite() + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tzinfo.py b/src/isodate/tzinfo.py new file mode 100644 index 0000000..b41f058 --- /dev/null +++ b/src/isodate/tzinfo.py @@ -0,0 +1,142 @@ +''' +This module provides some datetime.tzinfo implementations. + +All those classes are taken from the Python documentation. +''' +from datetime import timedelta, tzinfo +import time + +ZERO = timedelta(0) +# constant for zero time offset. + + +class Utc(tzinfo): + '''UTC + + Universal time coordinated time zone. + ''' + + def utcoffset(self, dt): + ''' + Return offset from UTC in minutes east of UTC, which is ZERO for UTC. + ''' + return ZERO + + def tzname(self, dt): + ''' + Return the time zone name corresponding to the datetime object dt, + as a string. + ''' + return "UTC" + + def dst(self, dt): + ''' + Return the daylight saving time (DST) adjustment, in minutes east + of UTC. + ''' + return ZERO + +UTC = Utc() +# the default instance for UTC. + + +class FixedOffset(tzinfo): + ''' + A class building tzinfo objects for fixed-offset time zones. + + Note that FixedOffset(0, 0, "UTC") or FixedOffset() is a different way to + build a UTC tzinfo object. + ''' + + def __init__(self, offset_hours=0, offset_minutes=0, name="UTC"): + ''' + Initialise an instance with time offset and name. + The time offset should be positive for time zones east of UTC + and negate for time zones west of UTC. + ''' + self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) + self.__name = name + + def utcoffset(self, dt): + ''' + Return offset from UTC in minutes of UTC. + ''' + return self.__offset + + def tzname(self, dt): + ''' + Return the time zone name corresponding to the datetime object dt, as a + string. + ''' + return self.__name + + def dst(self, dt): + ''' + Return the daylight saving time (DST) adjustment, in minutes east of + UTC. + ''' + return ZERO + + def __repr__(self): + ''' + Return nicely formatted repr string. + ''' + return "" % self.__name + + +STDOFFSET = timedelta(seconds=-time.timezone) +# locale time zone offset + +# calculate local daylight saving offset if any. +if time.daylight: + DSTOFFSET = timedelta(seconds=-time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET +# difference between local time zone and local DST time zone + + +class LocalTimezone(tzinfo): + """ + A class capturing the platform's idea of local time. + """ + + def utcoffset(self, dt): + ''' + Return offset from UTC in minutes of UTC. + ''' + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + ''' + Return daylight saving offset. + ''' + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + ''' + Return the time zone name corresponding to the datetime object dt, as a + string. + ''' + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + ''' + Returns true if DST is active for given datetime object dt. + ''' + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + +LOCAL = LocalTimezone() +# the default instance for local time zone.