summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGerhard Weis <gweis@gmx.at>2009-02-09 10:25:14 +1000
committerGerhard Weis <gweis@gmx.at>2009-02-09 10:25:14 +1000
commit40a99905da4df23a4d68753258c0354abd8ab932 (patch)
treea826fd5e96346c962319cffb9fd8caa40a6cb353
parent44c432ba38078eff35b1ab7cde07fc205e1564d3 (diff)
downloadisodate-40a99905da4df23a4d68753258c0354abd8ab932.tar.gz
* added ISO 8601 formating methods
* still problems with negative durations * refactored Duration class to separate module * allow access to wrapped timedelta attributes directly on Duration. * refactored tzinfo parsing and formating into separate module * updated test cases for new functionality
-rw-r--r--.hgignore4
-rw-r--r--CHANGES.txt8
-rw-r--r--README.txt37
-rw-r--r--TODO.txt8
-rw-r--r--setup.py2
-rw-r--r--src/isodate/__init__.py30
-rw-r--r--src/isodate/duration.py244
-rw-r--r--src/isodate/isodates.py32
-rw-r--r--src/isodate/isodatetime.py12
-rw-r--r--src/isodate/isoduration.py225
-rw-r--r--src/isodate/isostrf.py196
-rw-r--r--src/isodate/isotime.py41
-rw-r--r--src/isodate/isotzinfo.py108
-rw-r--r--src/isodate/tzinfo.py2
-rw-r--r--src/tests/test_date.py78
-rw-r--r--src/tests/test_datetime.py42
-rw-r--r--src/tests/test_duration.py95
-rw-r--r--src/tests/test_time.py79
18 files changed, 888 insertions, 355 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..97bd802
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,4 @@
+syntax: glob
+*.pyc
+isodate.egg-info
+build/
diff --git a/CHANGES.txt b/CHANGES.txt
index 949e95e..6b32ae1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,9 +1,17 @@
+
CHANGES
=======
+0.4.0 (unreleased)
+------------------
+
+- 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/README.txt b/README.txt
index 4989fa2..48f50a8 100644
--- a/README.txt
+++ b/README.txt
@@ -11,8 +11,8 @@ 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.
+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
@@ -31,18 +31,46 @@ Currently there are four parsing methods available.
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.
+ 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 formating 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.
-Just use *setuptools* or *easy_instal* as usual.
+Just use *setuptools* or *easy_install* as usual.
Limitations:
------------
@@ -55,6 +83,7 @@ Limitations:
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:
--------------------
diff --git a/TODO.txt b/TODO.txt
index 5cba68f..89a9ec3 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,4 +1,5 @@
+
TODOs
=====
@@ -9,8 +10,7 @@ not complete.
Missing features:
-----------------
- * methods to format *date*, *time*, *datetime*, *timedelta* and *Duration*
- objects to various ISO strings.
+ * time formating does not allow to create fractional representations.
* parser for ISO intervals.
Documentation:
@@ -34,5 +34,7 @@ Documentation:
- 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?)
+ - when is a Duration negative?
+ - normalize Durations. months [00-12] and years ]-inf,+inf[
+
-
diff --git a/setup.py b/setup.py
index 6b4d615..a1cbc57 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@ def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
setup(name = 'isodate',
- version = '0.3.0',
+ version = '0.4.0',
packages = find_packages('src', exclude=["tests"]),
package_dir={'': 'src'},
diff --git a/src/isodate/__init__.py b/src/isodate/__init__.py
index 4e119dd..091af0a 100644
--- a/src/isodate/__init__.py
+++ b/src/isodate/__init__.py
@@ -26,12 +26,30 @@
##############################################################################
'''
Import all essential functions and constants to re-export them here for easy
-access.
-'''
+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
+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, Duration
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
diff --git a/src/isodate/duration.py b/src/isodate/duration.py
new file mode 100644
index 0000000..25d94af
--- /dev/null
+++ b/src/isodate/duration.py
@@ -0,0 +1,244 @@
+##############################################################################
+# 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
+
+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 __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 __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/isodates.py b/src/isodate/isodates.py
index 136757b..8bafa20 100644
--- a/src/isodate/isodates.py
+++ b/src/isodate/isodates.py
@@ -35,6 +35,7 @@ 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 = {}
@@ -161,26 +162,22 @@ def parse_date(datestring, yeardigits=4, expanded=False):
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:
+ # 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 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:
+ if 'week' in groups:
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)
+ # 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
@@ -190,6 +187,15 @@ def parse_date(datestring, yeardigits=4, expanded=False):
day = 1
else:
day = int(groups['day'])
- return date(sign*int(groups['year']),
+ 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
index 4cc6fd4..7e4d570 100644
--- a/src/isodate/isodatetime.py
+++ b/src/isodate/isodatetime.py
@@ -32,6 +32,8 @@ 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.isotime import parse_time
@@ -47,3 +49,13 @@ def parse_datetime(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
index ebbd7eb..5002e51 100644
--- a/src/isodate/isoduration.py
+++ b/src/isodate/isoduration.py
@@ -27,14 +27,16 @@
'''
This module provides an ISO 8601:2004 duration parser.
-It also defines a class Duration, which allows to define
-durations in years and months.
+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 date, datetime, timedelta
+from datetime import timedelta
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<sign>[+-])?"
r"P(?P<years>[0-9]+([,.][0-9]+)?Y)?"
@@ -117,208 +119,21 @@ def parse_duration(datestring):
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.
+def duration_isoformat(tduration, format=D_DEFAULT):
'''
- 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.
+ Format duration strings.
- The algorithm to add a duration to a date is defined at
- http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes
+ This method is just a wrapper around isodate.isostrf.strftime and uses
+ P%P (D_DEFAULT) as default format.
'''
-
- 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)
+ # 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/isostrf.py b/src/isodate/isostrf.py
new file mode 100644
index 0000000..aeda09f
--- /dev/null
+++ b/src/isodate/isostrf.py
@@ -0,0 +1,196 @@
+##############################################################################
+# 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: '%d' % tdt.microseconds,
+ '%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.tzinfo, '%h'),
+ '%Z': lambda tdt, yds: tz_isoformat(tdt.tzinfo, '%Z'),
+ '%z': lambda tdt, yds: tz_isoformat(tdt.tzinfo, '%z'),
+ '%%': lambda tdt, yds: '%'}
+
+STRF_D_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.days,
+ '%f': lambda tdt, yds: '%d' % 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 += str(abs(tdt.years)) + 'Y'
+ if tdt.months:
+ ret += str(abs(tdt.months)) + 'M'
+ seconds = abs(tdt.days * 24 * 60 * 60 + tdt.seconds)
+ minutes, seconds = divmod(seconds, 60)
+ hours, minutes = divmod(minutes, 60)
+ days, hours = divmod(hours, 24)
+ if days:
+ ret += str(days) + 'D'
+ if hours or minutes or seconds:
+ ret += 'T'
+ if hours:
+ ret += str(hours) + 'H'
+ if minutes:
+ ret += str(minutes) + 'M'
+ if seconds:
+ ret += str(seconds) + 'S'
+ return ret or '0D' # at least one component has to be there.
+ 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 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
index 563d668..c677f74 100644
--- a/src/isodate/isotime.py
+++ b/src/isodate/isotime.py
@@ -35,8 +35,9 @@ import re
import math
from datetime import time
+from isodate.isostrf import strftime, TIME_EXT_COMPLETE, TZ_EXT
from isodate.isoerror import ISO8601Error
-from isodate.tzinfo import UTC, FixedOffset
+from isodate.isotzinfo import TZ_REGEX, build_tzinfo
TIME_REGEX_CACHE = []
# used to cache regular expressions to parse ISO time strings.
@@ -64,30 +65,30 @@ def build_time_regexps():
# +-hh:mm
# +-hhmm
# +-hh =>
- tz_regex = r"(?P<tz>Z|(?P<tzh>[+-][0-9]{2})(:?(?P<tzm>[0-9]{2})?))?"
+ # isotzinfo.TZ_REGEX
# 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))
+ + 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))
+ + 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))
+ + 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))
+ + TZ_REGEX))
# hh.hh ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}([,.][0-9]+)?)"
- + tz_regex))
+ + TZ_REGEX))
return TIME_REGEX_CACHE
def parse_time(timestring):
@@ -116,20 +117,9 @@ def parse_time(timestring):
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
+ tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'],
+ int(groups['tzhour'] or 0),
+ int(groups['tzmin'] or 0))
if 'second' in groups:
frac, second = math.modf(float(groups['second']))
microsecond = frac * 1e6
@@ -150,3 +140,12 @@ def parse_time(timestring):
return time(int(hour), int(minute), int(second), int(microsecond),
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..79797fa
--- /dev/null
+++ b/src/isodate/isotzinfo.py
@@ -0,0 +1,108 @@
+##############################################################################
+# 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<tzname>(Z|(?P<tzsign>[+-])"\
+ r"(?P<tzhour>[0-9]{2})(:(?P<tzmin>[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(tzinfo, 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
+ '''
+ if (tzinfo is None) or (tzinfo.utcoffset(None) is None):
+ return ''
+ if tzinfo.utcoffset(None) == ZERO and tzinfo.dst(None) == ZERO:
+ return 'Z'
+ tdelta = tzinfo.utcoffset(None)
+ 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 AttributeError('unknown format string "%s"' % format)
diff --git a/src/isodate/tzinfo.py b/src/isodate/tzinfo.py
index 03b3a83..776d905 100644
--- a/src/isodate/tzinfo.py
+++ b/src/isodate/tzinfo.py
@@ -7,7 +7,7 @@ from datetime import timedelta, tzinfo
import time
ZERO = timedelta(0)
-# constant for zero time offset.
+# constant for zero time offset.
class Utc(tzinfo):
'''UTC
diff --git a/src/tests/test_date.py b/src/tests/test_date.py
index 238f8fc..fb6ba8c 100644
--- a/src/tests/test_date.py
+++ b/src/tests/test_date.py
@@ -29,39 +29,44 @@ Test cases for the isodate module.
'''
import unittest
from datetime import date
-from isodate import parse_date, ISO8601Error
+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)),
- ('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))]}
+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(datestring, yeardigits, expectation):
+def create_testcase(yeardigits, datestring, expectation, format):
'''
Create a TestCase class for a specific test.
@@ -85,6 +90,20 @@ def create_testcase(datestring, yeardigits, expectation):
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)
@@ -94,8 +113,9 @@ def test_suite():
'''
suite = unittest.TestSuite()
for yeardigits, tests in TEST_CASES.items():
- for datestring, expectation in tests:
- suite.addTest(create_testcase(datestring, yeardigits, expectation))
+ for datestring, expectation, format in tests:
+ suite.addTest(create_testcase(yeardigits, datestring,
+ expectation, format))
return suite
if __name__ == '__main__':
diff --git a/src/tests/test_datetime.py b/src/tests/test_datetime.py
index 98daaba..842f312 100644
--- a/src/tests/test_datetime.py
+++ b/src/tests/test_datetime.py
@@ -30,23 +30,33 @@ Test cases for the isodatetime module.
import unittest
from datetime import datetime
-from isodate import parse_datetime, UTC, FixedOffset
+from isodate import parse_datetime, UTC, FixedOffset, datetime_isoformat
+from isodate import DATE_BAS_COMPLETE, TIME_BAS_MINUTE, TZ_BAS
+from isodate import DATE_EXT_COMPLETE, TIME_EXT_MINUTE, 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)),
- ('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)),
+TEST_CASES = [('19850412T1015', datetime(1985, 4, 12, 10, 15),
+ DATE_BAS_COMPLETE + 'T' + TIME_BAS_MINUTE),
+ ('1985-04-12T10:15', datetime(1985, 4, 12, 10, 15),
+ DATE_EXT_COMPLETE + 'T' + TIME_EXT_MINUTE),
+ ('1985102T1015Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
+ DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS),
+ ('1985-102T10:15Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
+ DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_EXT),
('1985W155T1015+0400', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
- '+0400'))),
+ '+0400')),
+ DATE_BAS_WEEK_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS),
('1985-W15-5T10:15+04', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
- '+0400')))]
+ '+0400')),
+ DATE_EXT_WEEK_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_HOUR)]
-def create_testcase(datetimestring, expectation):
+def create_testcase(datetimestring, expectation, format):
'''
Create a TestCase class for a specific test.
@@ -67,6 +77,18 @@ def create_testcase(datetimestring, expectation):
result = parse_datetime(datetimestring)
self.assertEqual(result, 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),
+ datetimestring)
+
return unittest.TestLoader().loadTestsFromTestCase(TestDateTime)
def test_suite():
@@ -74,8 +96,8 @@ 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))
+ for datetimestring, expectation, format in TEST_CASES:
+ suite.addTest(create_testcase(datetimestring, expectation, format))
return suite
if __name__ == '__main__':
diff --git a/src/tests/test_duration.py b/src/tests/test_duration.py
index e95008e..d8244b5 100644
--- a/src/tests/test_duration.py
+++ b/src/tests/test_duration.py
@@ -32,41 +32,49 @@ 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),
- '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),
+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),
+ '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),
}
@@ -242,9 +250,15 @@ class DurationTest(unittest.TestCase):
'''
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,'
+ self.assertEqual('isodate.duration.Duration(10, 10, 0,'
' years=10, months=10)', repr(dur))
+ def test_format(self):
+ '''
+ Test various other strftime combinations.
+ '''
+ self.assertEqual(duration_isoformat(Duration(0)), 'P0D')
+
def test_equal(self):
'''
Test __eq__ and __ne__ methods.
@@ -264,7 +278,7 @@ class DurationTest(unittest.TestCase):
self.assertTrue(Duration(years=1, months=1) != Duration(months=14))
self.assertTrue(Duration(years=1) != timedelta(days=365))
-def create_parsetestcase(durationstring, expectation):
+def create_parsetestcase(durationstring, expectation, format, altstr):
'''
Create a TestCase class for a specific test.
@@ -285,6 +299,18 @@ def create_parsetestcase(durationstring, expectation):
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:
+ self.assertEqual(duration_isoformat(expectation, format),
+ durationstring)
+
return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration)
def create_mathtestcase(dur1, dur2, resadd, ressub, resge):
@@ -394,8 +420,9 @@ 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 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:
diff --git a/src/tests/test_time.py b/src/tests/test_time.py
index a1f8f4e..062995b 100644
--- a/src/tests/test_time.py
+++ b/src/tests/test_time.py
@@ -29,44 +29,55 @@ Test cases for the isotime module.
'''
import unittest
from datetime import time
-from isodate import parse_time, UTC, FixedOffset, ISO8601Error
+
+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)),
- ('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)),
+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),
+ ('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'))),
+ tzinfo=FixedOffset(1, 0, '+01:00')),
+ TIME_BAS_COMPLETE + TZ_HOUR),
('152746-05', time(15, 27, 46,
- tzinfo=FixedOffset(-5, -0, '-05:00'))),
+ 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'))),
+ 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'))),
+ 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'))),
+ 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'))),
- ('1:17:30', None)]
+ tzinfo=FixedOffset(-5, -0, '-05:00')),
+ TIME_EXT_COMPLETE + TZ_HOUR),
+ ('1:17:30', None, TIME_EXT_COMPLETE)]
-def create_testcase(timestring, expectation):
+def create_testcase(timestring, expectation, format):
'''
Create a TestCase class for a specific test.
@@ -89,6 +100,18 @@ def create_testcase(timestring, expectation):
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)
@@ -97,8 +120,8 @@ 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))
+ for timestring, expectation, format in TEST_CASES:
+ suite.addTest(create_testcase(timestring, expectation, format))
return suite
if __name__ == '__main__':