summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGerhard Weis <gweis@gmx.at>2009-01-05 05:32:56 +1000
committerGerhard Weis <gweis@gmx.at>2009-01-05 05:32:56 +1000
commitb95749437bdb961a98b79a10114567eb31547e9d (patch)
treefdaa9e7e9b74d0e4de6b61990fc9c2a4a26160ce
downloadisodate-b95749437bdb961a98b79a10114567eb31547e9d.tar.gz
* initial commit (version 0.3.0)
-rw-r--r--.project17
-rw-r--r--CHANGES.txt9
-rw-r--r--MANIFEST.in2
-rw-r--r--README.txt69
-rw-r--r--TODO.txt37
-rw-r--r--buildout.cfg32
-rw-r--r--setup.py66
-rw-r--r--src/isodate/__init__.py37
-rw-r--r--src/isodate/isodates.py195
-rw-r--r--src/isodate/isodatetime.py49
-rw-r--r--src/isodate/isoduration.py324
-rw-r--r--src/isodate/isoerror.py32
-rw-r--r--src/isodate/isotime.py152
-rw-r--r--src/isodate/tzinfo.py137
-rw-r--r--src/tests/__init__.py45
-rw-r--r--src/tests/test_date.py102
-rw-r--r--src/tests/test_datetime.py82
-rw-r--r--src/tests/test_duration.py409
-rw-r--r--src/tests/test_time.py105
19 files changed, 1901 insertions, 0 deletions
diff --git a/.project b/.project
new file mode 100644
index 0000000..b4db36c
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>isodate</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.python.pydev.PyDevBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.python.pydev.pythonNature</nature>
+ </natures>
+</projectDescription>
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..949e95e
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,9 @@
+
+CHANGES
+=======
+
+0.3.0 (2009-1-05)
+------------------
+
+- Initial release
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..426ffc6
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include CHANGES.txt
+include TODO.txt \ No newline at end of file
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..8cbeb63
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,69 @@
+
+ISO 8601 date/time parser
+=========================
+
+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, that if no time zone designation is given for a time,
+the it is a 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.
+
+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.
+
+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.
+
+Installation:
+-------------
+
+This module can easily be installed with Python standard installation methods.
+Just use *setuptools* or *easy_instal* as usual.
+
+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.
+
+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 `<http://hg.proclos.com/isodate>`_.
+
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..cc3a07a
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,37 @@
+
+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:
+-----------------
+
+ * methods to format *date*, *time*, *datetime*, *timedelta* and *Duration*
+ objects to various ISO strings.
+ * parser for ISO intervals.
+
+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? (`<http://www.w3.org/TR/xmlschema-2/#duration-order>`_)
+ - refactor to have duration mathematics only at one place.
+ - localize __str__ method (does timedelta do this?)
+ \ No newline at end of file
diff --git a/buildout.cfg b/buildout.cfg
new file mode 100644
index 0000000..dd34859
--- /dev/null
+++ b/buildout.cfg
@@ -0,0 +1,32 @@
+[buildout]
+develop = .
+parts = iso8601date importchecker test pydev coverage coverage-report
+
+[iso8601date]
+recipe = zc.recipe.egg
+eggs = iso8601date
+interpreter = python
+
+[pydev]
+recipe = pb.recipes.pydev
+eggs = ${iso8601date:eggs}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = iso8601date
+
+[coverage]
+recipe = zc.recipe.testrunner
+eggs = iso8601date
+defaults = ['--coverage', '.']
+
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+scripts = coverage=coverage-report
+arguments = ('parts/coverage', 'parts/coverage-report')
+
+[importchecker]
+recipe = zc.recipe.egg
+eggs = importchecker
+arguments = "${buildout:directory}/src"
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f7b478c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,66 @@
+#!/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
+from setuptools import setup
+from setuptools import find_packages
+# for setuptools see: http://peak.telecommunity.com/DevCenter/setuptools
+
+def read(*rnames):
+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+setup(name = 'iso8601date',
+ version = '0.3.0',
+ packages = find_packages('src', exclude=["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 formater',
+ license = 'BSD',
+ #keywords = '',
+ url='http://cheeseshop.python.org/pypi/isodate',
+
+ long_description=read('README.txt') +
+ 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',
+ 'Topic :: Internet',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ test_suite = "tests.test_suite"
+ )
diff --git a/src/isodate/__init__.py b/src/isodate/__init__.py
new file mode 100644
index 0000000..4e119dd
--- /dev/null
+++ b/src/isodate/__init__.py
@@ -0,0 +1,37 @@
+##############################################################################
+# 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.
+'''
+
+from isodate.isodates import parse_date
+from isodate.isotime import parse_time
+from isodate.isodatetime import parse_datetime
+from isodate.isoduration import parse_duration, Duration
+from isodate.isoerror import ISO8601Error
+from isodate.tzinfo import UTC, FixedOffset, LOCAL
diff --git a/src/isodate/isodates.py b/src/isodate/isodates.py
new file mode 100644
index 0000000..136757b
--- /dev/null
+++ b/src/isodate/isodates.py
@@ -0,0 +1,195 @@
+##############################################################################
+# 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.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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})"
+ % (sign, yeardigits)))
+ # YYYYMMDD or +- YYYYYYMMDD... basic date format
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"(?P<month>[0-9]{2})(?P<day>[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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})"
+ % (sign, yeardigits)))
+ # YYYYWwwD or +-YYYYYYWwwD ... basic week date
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
+ r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})"
+ % (sign, yeardigits)))
+ # 3. ordinal dates:
+ # YYYY-DDD or +-YYYYYY-DDD ... extended format
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"-(?P<day>[0-9]{3})"
+ % (sign, yeardigits)))
+ # YYYYDDD or +-YYYYYYDDD ... basic format
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"(?P<day>[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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"-W(?P<week>[0-9]{2})"
+ % (sign, yeardigits)))
+ # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
+ r"(?P<week>[0-9]{2})"
+ % (sign, yeardigits)))
+ # 5. month dates:
+ # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ r"-(?P<month>[0-9]{2})"
+ % (sign, yeardigits)))
+ # 6. year dates:
+ # YYYY or +-YYYYYY ... reduced accuracy specific year
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+ % (sign, yeardigits)))
+ # 7. century dates:
+ # YY or +-YYYY ... reduced accuracy specific century
+ cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}"
+ r"(?P<century>[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,
+ if groups['sign'] == '-': # FIXME: not possible with datetime, date
+ sign = -1
+ else:
+ sign = 1
+ if 'century' in groups and groups['century'] is not None:
+ return date(sign * (int(groups['century']) * 100 + 1), 1, 1)
+ if not 'month' in groups: # weekdate or ordinal date
+ ret = date(sign * int(groups['year']), 1, 1)
+ if 'week' in groups and groups['week'] is not None:
+ isotuple = ret.isocalendar()
+ if 'day' in groups:
+ days = int(groups['day'] or 1)
+ else:
+ days = 1
+ if isotuple[1] == 1: # this is the first week in the year
+ return ret + timedelta(weeks=int(groups['week'])-1,
+ days=-isotuple[2]+days)
+ else:
+ return ret + timedelta(weeks=int(groups['week']),
+ 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)
diff --git a/src/isodate/isodatetime.py b/src/isodate/isodatetime.py
new file mode 100644
index 0000000..4cc6fd4
--- /dev/null
+++ b/src/isodate/isodatetime.py
@@ -0,0 +1,49 @@
+##############################################################################
+# 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.isodates import parse_date
+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.
+ '''
+ datestring, timestring = datetimestring.split('T')
+ tmpdate = parse_date(datestring)
+ tmptime = parse_time(timestring)
+ return datetime.combine(tmpdate, tmptime)
diff --git a/src/isodate/isoduration.py b/src/isodate/isoduration.py
new file mode 100644
index 0000000..ebbd7eb
--- /dev/null
+++ b/src/isodate/isoduration.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 provides an ISO 8601:2004 duration parser.
+
+It also defines a class Duration, which allows to define
+durations in years and months.
+'''
+from datetime import date, datetime, timedelta
+import re
+
+from isodate.isoerror import ISO8601Error
+from isodate.isodatetime import parse_datetime
+
+ISO8601_PERIOD_REGEX = re.compile(r"^(?P<sign>[+-])?"
+ r"P(?P<years>[0-9]+([,.][0-9]+)?Y)?"
+ r"(?P<months>[0-9]+([,.][0-9]+)?M)?"
+ r"(?P<weeks>[0-9]+([,.][0-9]+)?W)?"
+ r"(?P<days>[0-9]+([,.][0-9]+)?D)?"
+ r"((?P<separator>T)(?P<hours>[0-9]+([,.][0-9]+)?H)?"
+ r"(?P<minutes>[0-9]+([,.][0-9]+)?M)?"
+ r"(?P<seconds>[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]
+ 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 fquotmod(val, low, high):
+ '''
+ A divmod function with boundaries.
+ '''
+ 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.
+ '''
+ self.months = months
+ self.years = years
+ self.tdelta = timedelta(days, seconds, microseconds, milliseconds,
+ minutes, hours, weeks)
+
+ 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 __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)):
+ 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)):
+ 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 __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)):
+ 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 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 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 todatetime(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/isoerror.py b/src/isodate/isoerror.py
new file mode 100644
index 0000000..b690bfe
--- /dev/null
+++ b/src/isodate/isoerror.py
@@ -0,0 +1,32 @@
+##############################################################################
+# 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(Exception):
+ '''Raised when the given ISO string can not be parsed.'''
diff --git a/src/isodate/isotime.py b/src/isodate/isotime.py
new file mode 100644
index 0000000..563d668
--- /dev/null
+++ b/src/isodate/isotime.py
@@ -0,0 +1,152 @@
+##############################################################################
+# 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
+import math
+from datetime import time
+
+from isodate.isoerror import ISO8601Error
+from isodate.tzinfo import UTC, FixedOffset
+
+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 =>
+ tz_regex = r"(?P<tz>Z|(?P<tzh>[+-][0-9]{2})(:?(?P<tzm>[0-9]{2})?))?"
+ # 1. complete time:
+ # hh:mm:ss.ss ... extended format
+ TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
+ r"(?P<minute>[0-9]{2}):"
+ r"(?P<second>[0-9]{2}([,.][0-9]+)?)"
+ + tz_regex))
+ # hhmmss.ss ... basic format
+ TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
+ r"(?P<minute>[0-9]{2})"
+ r"(?P<second>[0-9]{2}([,.][0-9]+)?)"
+ + tz_regex))
+ # 2. reduced accuracy:
+ # hh:mm.mm ... extended format
+ TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
+ r"(?P<minute>[0-9]{2}([,.][0-9]+)?)"
+ + tz_regex))
+ # hhmm.mm ... basic format
+ TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
+ r"(?P<minute>[0-9]{2}([,.][0-9]+)?)"
+ + tz_regex))
+ # hh.hh ... basic format
+ TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}([,.][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(',', '.')
+ if groups['tz'] is not None:
+ if groups['tz'] == 'Z':
+ tzinfo = UTC
+ else:
+ if groups['tzh'].startswith('-'):
+ tzinfo = FixedOffset(int(groups['tzh']),
+ -int(groups['tzm'] or 0),
+ groups['tz'])
+ else:
+ tzinfo = FixedOffset(int(groups['tzh']),
+ int(groups['tzm'] or 0),
+ groups['tz'])
+ else:
+ tzinfo = None
+ if 'second' in groups:
+ frac, second = math.modf(float(groups['second']))
+ microsecond = frac * 1e6
+ return time(int(groups['hour']), int(groups['minute']),
+ int(second), int(microsecond), tzinfo)
+ if 'minute' in groups:
+ frac, minute = math.modf(float(groups['minute']))
+ frac, second = math.modf(frac * 60.0)
+ microsecond = frac * 1e6
+ return time(int(groups['hour']), int(minute), int(second),
+ int(microsecond), tzinfo)
+ else:
+ microsecond, second, minute = 0, 0, 0
+ frac, hour = math.modf(float(groups['hour']))
+ frac, minute = math.modf(frac * 60.0)
+ frac, second = math.modf(frac * 60.0)
+ microsecond = frac * 1e6
+ return time(int(hour), int(minute), int(second), int(microsecond),
+ tzinfo)
+ raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring)
diff --git a/src/isodate/tzinfo.py b/src/isodate/tzinfo.py
new file mode 100644
index 0000000..03b3a83
--- /dev/null
+++ b/src/isodate/tzinfo.py
@@ -0,0 +1,137 @@
+'''
+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, "UTC") is a different way to build a
+ UTC tzinfo object.
+ '''
+
+ def __init__(self, offset_hours, offset_minutes, name):
+ '''
+ 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 "<FixedOffset %r>" % 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.
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
new file mode 100644
index 0000000..b9783f8
--- /dev/null
+++ b/src/tests/__init__.py
@@ -0,0 +1,45 @@
+##############################################################################
+# 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 tests import test_date, test_time, test_datetime, test_duration
+
+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()])
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/src/tests/test_date.py b/src/tests/test_date.py
new file mode 100644
index 0000000..238f8fc
--- /dev/null
+++ b/src/tests/test_date.py
@@ -0,0 +1,102 @@
+##############################################################################
+# 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
+
+# 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)),
+ ('1985', date(1985, 1, 1)),
+ ('1985-04', date(1985, 4, 1)),
+ ('1985-04-12', date(1985, 4, 12)),
+ ('19850412', date(1985, 4, 12)),
+ ('1985102', date(1985, 4, 12)),
+ ('1985-102', date(1985, 4, 12)),
+ ('1985W155', date(1985, 4, 12)),
+ ('1985-W15-5', date(1985, 4, 12)),
+ ('1985W15', date(1985, 4, 8)),
+ ('1985-W15', date(1985, 4, 8)),
+ ('1989-W15', date(1989, 4, 10)),
+ ('1989-W15-5', date(1989, 4, 14)),
+ ('1-W1-1', None)],
+ 6: [('+0019', date(1901, 1, 1)),
+ ('+001985', date(1985, 1, 1)),
+ ('+001985-04', date(1985, 4, 1)),
+ ('+001985-04-12', date(1985, 4, 12)),
+ ('+0019850412', date(1985, 4, 12)),
+ ('+001985102', date(1985, 4, 12)),
+ ('+001985-102', date(1985, 4, 12)),
+ ('+001985W155', date(1985, 4, 12)),
+ ('+001985-W15-5', date(1985, 4, 12)),
+ ('+001985W15', date(1985, 4, 8)),
+ ('+001985-W15', date(1985, 4, 8))]}
+
+def create_testcase(datestring, yeardigits, 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 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)
+
+ 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 in tests:
+ suite.addTest(create_testcase(datestring, yeardigits, expectation))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/src/tests/test_datetime.py b/src/tests/test_datetime.py
new file mode 100644
index 0000000..98daaba
--- /dev/null
+++ b/src/tests/test_datetime.py
@@ -0,0 +1,82 @@
+##############################################################################
+# 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
+
+# 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)),
+ ('1985-04-12T10:15', datetime(1985, 4, 12, 10, 15)),
+ ('1985102T1015Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC)),
+ ('1985-102T10:15Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC)),
+ ('1985W155T1015+0400', datetime(1985, 4, 12, 10, 15,
+ tzinfo=FixedOffset(4, 0,
+ '+0400'))),
+ ('1985-W15-5T10:15+04', datetime(1985, 4, 12, 10, 15,
+ tzinfo=FixedOffset(4, 0,
+ '+0400')))]
+
+def create_testcase(datetimestring, 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 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.
+ '''
+ result = parse_datetime(datetimestring)
+ self.assertEqual(result, expectation)
+
+ return unittest.TestLoader().loadTestsFromTestCase(TestDateTime)
+
+def test_suite():
+ '''
+ Construct a TestSuite instance for all test cases.
+ '''
+ suite = unittest.TestSuite()
+ for datetimestring, expectation in TEST_CASES:
+ suite.addTest(create_testcase(datetimestring, expectation))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/src/tests/test_duration.py b/src/tests/test_duration.py
new file mode 100644
index 0000000..e95008e
--- /dev/null
+++ b/src/tests/test_duration.py
@@ -0,0 +1,409 @@
+##############################################################################
+# 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
+
+# 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),
+ 'P2W': timedelta(weeks = 2),
+ 'P3Y6M4DT12H30M5S':Duration(4, 5, 0, 0, 30, 12, 0, 6, 3),
+ 'P23DT23H': timedelta(hours=23, days=23),
+ 'P4Y': Duration(years=4),
+ 'P1M': Duration(months=1),
+ 'PT1M': timedelta(minutes=1),
+ 'P0.5Y': Duration(years=0.5), # ?????????
+ 'PT36H': timedelta(hours=36),
+ 'P1DT12H': timedelta(days=1, hours=12),
+ '+P11D': timedelta(days=11),
+ '-P2W': timedelta(weeks=-2),
+ '-P2.2W': timedelta(weeks=-2.2),
+ 'P1DT2H3M4S': timedelta(days=1, hours=2, minutes=3,
+ seconds=4),
+ 'P1DT2H3M': timedelta(days=1, hours=2, minutes=3),
+ 'P1DT2H': timedelta(days=1, hours=2),
+ 'PT2H': timedelta(hours=2),
+ 'PT2.3H': timedelta(hours=2.3),
+ 'PT2H3M4S': timedelta(hours=2, minutes=3, seconds=4),
+ 'PT3M4S': timedelta(minutes=3, seconds=4),
+ 'PT22S': timedelta(seconds=22),
+ 'PT22.22S': timedelta(seconds=22.22),
+ '-P2Y': Duration(years=-2),
+ '-P3Y6M4DT12H30M5S': Duration(-4, -5, 0, 0, -30, -12, 0,
+ -6, -3),
+ '-P1DT2H3M4S': timedelta(days=-1, hours=-2, minutes=-3,
+ seconds=-4),
+ # alternative format
+ 'P0018-09-04T11:09:08': Duration(4, 8, 0, 0, 9, 11, 0, 9,
+ 18),
+ #'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)),
+ (Duration(years=1, months=1, weeks=5),
+ 'raise exception',
+ None),
+ ('raise exception',
+ Duration(years=1, months=1, weeks=5),
+ None),
+ (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)),
+ #(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)),
+ )
+
+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))
+
+ 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 obects.
+ '''
+ dur = Duration(10, 10, years=10, months=10)
+ self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur))
+ self.assertEqual('isodate.isoduration.Duration(10, 10, 0,'
+ ' years=10, months=10)', repr(dur))
+
+ 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))
+
+def create_parsetestcase(durationstring, expectation):
+ '''
+ 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)
+
+ 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(TypeError, operator.add, start, duration)
+ else:
+ self.assertEqual(start + duration, expectation)
+
+ return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)
+
+def test_suite():
+ '''
+ Return a test suite containing all test defined above.
+ '''
+ suite = unittest.TestSuite()
+ for durationstring, expectation in PARSE_TEST_CASES.items():
+ suite.addTest(create_parsetestcase(durationstring, expectation))
+ 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))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/src/tests/test_time.py b/src/tests/test_time.py
new file mode 100644
index 0000000..a1f8f4e
--- /dev/null
+++ b/src/tests/test_time.py
@@ -0,0 +1,105 @@
+##############################################################################
+# 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
+
+# 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)),
+ ('23:20:50', time(23, 20, 50)),
+ ('2320', time(23, 20)),
+ ('23:20', time(23, 20)),
+ ('23', time(23)),
+ ('232050,5', time(23, 20, 50, 500000)),
+ ('23:20:50.5', time(23, 20, 50, 500000)),
+ ('2320,8', time(23, 20, 48)),
+ ('23:20,8', time(23, 20, 48)),
+ ('23,3', time(23, 18)),
+ ('232030Z', time(23, 20, 30, tzinfo=UTC)),
+ ('2320Z', time(23, 20, tzinfo=UTC)),
+ ('23Z', time(23, tzinfo=UTC)),
+ ('23:20:30Z', time(23, 20, 30, tzinfo=UTC)),
+ ('23:20Z', time(23, 20, tzinfo=UTC)),
+ ('152746+0100', time(14, 27, 46, tzinfo=UTC)),
+ ('152746-0500', time(20, 27, 46, tzinfo=UTC)),
+ ('152746+01', time(15, 27, 46,
+ tzinfo=FixedOffset(1, 0, '+01:00'))),
+ ('152746-05', time(15, 27, 46,
+ tzinfo=FixedOffset(-5, -0, '-05:00'))),
+ ('15:27:46+01:00', time(15, 27, 46,
+ tzinfo=FixedOffset(1, 0, '+01:00'))),
+ ('15:27:46-05:00', time(15, 27, 46,
+ tzinfo=FixedOffset(-5, -0, '-05:00'))),
+ ('15:27:46+01', time(15, 27, 46,
+ tzinfo=FixedOffset(1, 0, '+01:00'))),
+ ('15:27:46-05', time(15, 27, 46,
+ tzinfo=FixedOffset(-5, -0, '-05:00'))),
+ ('1:17:30', None)]
+
+
+def create_testcase(timestring, 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 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)
+
+ return unittest.TestLoader().loadTestsFromTestCase(TestTime)
+
+def test_suite():
+ '''
+ Construct a TestSuite instance for all test cases.
+ '''
+ suite = unittest.TestSuite()
+ for timestring, expectation in TEST_CASES:
+ suite.addTest(create_testcase(timestring, expectation))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')