diff options
author | Gerhard Weis <gweis@gmx.at> | 2009-01-05 05:32:56 +1000 |
---|---|---|
committer | Gerhard Weis <gweis@gmx.at> | 2009-01-05 05:32:56 +1000 |
commit | b95749437bdb961a98b79a10114567eb31547e9d (patch) | |
tree | fdaa9e7e9b74d0e4de6b61990fc9c2a4a26160ce /src/isodate/isoduration.py | |
download | isodate-b95749437bdb961a98b79a10114567eb31547e9d.tar.gz |
* initial commit (version 0.3.0)
Diffstat (limited to 'src/isodate/isoduration.py')
-rw-r--r-- | src/isodate/isoduration.py | 324 |
1 files changed, 324 insertions, 0 deletions
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) |