jpayne@68: ############################################################################## jpayne@68: # jpayne@68: # Copyright (c) 2002 Zope Foundation and Contributors. jpayne@68: # jpayne@68: # This software is subject to the provisions of the Zope Public License, jpayne@68: # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. jpayne@68: # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED jpayne@68: # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED jpayne@68: # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS jpayne@68: # FOR A PARTICULAR PURPOSE jpayne@68: # jpayne@68: ############################################################################## jpayne@68: jpayne@68: import copyreg as copy_reg jpayne@68: import math jpayne@68: import re jpayne@68: from datetime import datetime jpayne@68: from time import altzone jpayne@68: from time import daylight jpayne@68: from time import gmtime jpayne@68: from time import localtime jpayne@68: from time import time jpayne@68: from time import timezone jpayne@68: from time import tzname jpayne@68: jpayne@68: from zope.interface import implementer jpayne@68: jpayne@68: from .interfaces import DateError jpayne@68: from .interfaces import DateTimeError jpayne@68: from .interfaces import IDateTime jpayne@68: from .interfaces import SyntaxError jpayne@68: from .interfaces import TimeError jpayne@68: from .pytz_support import PytzCache jpayne@68: jpayne@68: jpayne@68: basestring = str jpayne@68: long = int jpayne@68: explicit_unicode_type = type(None) jpayne@68: jpayne@68: default_datefmt = None jpayne@68: jpayne@68: jpayne@68: def getDefaultDateFormat(): jpayne@68: global default_datefmt jpayne@68: if default_datefmt is None: jpayne@68: try: jpayne@68: from App.config import getConfiguration jpayne@68: default_datefmt = getConfiguration().datetime_format jpayne@68: return default_datefmt jpayne@68: except Exception: jpayne@68: return 'us' jpayne@68: else: jpayne@68: return default_datefmt jpayne@68: jpayne@68: jpayne@68: # To control rounding errors, we round system time to the nearest jpayne@68: # microsecond. Then delicate calculations can rely on the fact that the jpayne@68: # maximum precision that needs to be preserved is known. jpayne@68: _system_time = time jpayne@68: jpayne@68: jpayne@68: def time(): jpayne@68: return round(_system_time(), 6) jpayne@68: jpayne@68: jpayne@68: # Determine machine epoch jpayne@68: tm = ((0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334), jpayne@68: (0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335)) jpayne@68: yr, mo, dy, hr, mn, sc = gmtime(0)[:6] jpayne@68: i = int(yr - 1) jpayne@68: to_year = int(i * 365 + i // 4 - i // 100 + i // 400 - 693960.0) jpayne@68: to_month = tm[yr % 4 == 0 and (yr % 100 != 0 or yr % 400 == 0)][mo] jpayne@68: EPOCH = ((to_year + to_month + dy + jpayne@68: (hr / 24.0 + mn / 1440.0 + sc / 86400.0)) * 86400) jpayne@68: jd1901 = 2415385 jpayne@68: jpayne@68: _TZINFO = PytzCache() jpayne@68: jpayne@68: INT_PATTERN = re.compile(r'([0-9]+)') jpayne@68: FLT_PATTERN = re.compile(r':([0-9]+\.[0-9]+)') jpayne@68: NAME_PATTERN = re.compile(r'([a-zA-Z]+)', re.I) jpayne@68: SPACE_CHARS = ' \t\n' jpayne@68: DELIMITERS = '-/.:,+' jpayne@68: jpayne@68: _MONTH_LEN = ((0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), jpayne@68: (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)) jpayne@68: _MONTHS = ('', 'January', 'February', 'March', 'April', 'May', 'June', jpayne@68: 'July', 'August', 'September', 'October', 'November', 'December') jpayne@68: _MONTHS_A = ('', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', jpayne@68: 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') jpayne@68: _MONTHS_P = ('', 'Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'June', jpayne@68: 'July', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.') jpayne@68: _MONTHMAP = {'january': 1, 'jan': 1, jpayne@68: 'february': 2, 'feb': 2, jpayne@68: 'march': 3, 'mar': 3, jpayne@68: 'april': 4, 'apr': 4, jpayne@68: 'may': 5, jpayne@68: 'june': 6, 'jun': 6, jpayne@68: 'july': 7, 'jul': 7, jpayne@68: 'august': 8, 'aug': 8, jpayne@68: 'september': 9, 'sep': 9, 'sept': 9, jpayne@68: 'october': 10, 'oct': 10, jpayne@68: 'november': 11, 'nov': 11, jpayne@68: 'december': 12, 'dec': 12} jpayne@68: _DAYS = ('Sunday', 'Monday', 'Tuesday', 'Wednesday', jpayne@68: 'Thursday', 'Friday', 'Saturday') jpayne@68: _DAYS_A = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat') jpayne@68: _DAYS_P = ('Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.') jpayne@68: _DAYMAP = {'sunday': 1, 'sun': 1, jpayne@68: 'monday': 2, 'mon': 2, jpayne@68: 'tuesday': 3, 'tues': 3, 'tue': 3, jpayne@68: 'wednesday': 4, 'wed': 4, jpayne@68: 'thursday': 5, 'thurs': 5, 'thur': 5, 'thu': 5, jpayne@68: 'friday': 6, 'fri': 6, jpayne@68: 'saturday': 7, 'sat': 7} jpayne@68: jpayne@68: numericTimeZoneMatch = re.compile(r'[+-][0-9][0-9][0-9][0-9]').match jpayne@68: iso8601Match = re.compile(r''' jpayne@68: (?P\d\d\d\d) # four digits year jpayne@68: (?:-? # one optional dash jpayne@68: (?: # followed by: jpayne@68: (?P\d\d\d # three digits year day jpayne@68: (?!\d)) # when there is no fourth digit jpayne@68: | # or: jpayne@68: W # one W jpayne@68: (?P\d\d) # two digits week jpayne@68: (?:-? # one optional dash jpayne@68: (?P\d) # one digit week day jpayne@68: )? # week day is optional jpayne@68: | # or: jpayne@68: (?P\d\d)? # two digits month jpayne@68: (?:-? # one optional dash jpayne@68: (?P\d\d)? # two digits day jpayne@68: )? # after day is optional jpayne@68: ) # jpayne@68: )? # after year is optional jpayne@68: (?:[T ] # one T or one whitespace jpayne@68: (?P\d\d) # two digits hour jpayne@68: (?::? # one optional colon jpayne@68: (?P\d\d)? # two digits minute jpayne@68: (?::? # one optional colon jpayne@68: (?P\d\d)? # two digits second jpayne@68: (?:[.,] # one dot or one comma jpayne@68: (?P\d+) # n digits fraction jpayne@68: )? # after second is optional jpayne@68: )? # after minute is optional jpayne@68: )? # after hour is optional jpayne@68: (?: # timezone: jpayne@68: (?PZ) # one Z jpayne@68: | # or: jpayne@68: (?P[-+]) # one plus or one minus as signal jpayne@68: (?P\d # one digit for hour offset... jpayne@68: (?:\d(?!\d$) # ...or two, if not the last two digits jpayne@68: )?) # second hour offset digit is optional jpayne@68: (?::? # one optional colon jpayne@68: (?P\d\d) # two digits minute offset jpayne@68: )? # after hour offset is optional jpayne@68: )? # timezone is optional jpayne@68: )? # time is optional jpayne@68: (?P.*) # store the extra garbage jpayne@68: ''', re.VERBOSE).match jpayne@68: jpayne@68: jpayne@68: def _findLocalTimeZoneName(isDST): jpayne@68: if not daylight: jpayne@68: # Daylight savings does not occur in this time zone. jpayne@68: isDST = 0 jpayne@68: try: jpayne@68: # Get the name of the current time zone depending jpayne@68: # on DST. jpayne@68: _localzone = PytzCache._zmap[tzname[isDST].lower()] jpayne@68: except BaseException: jpayne@68: try: jpayne@68: # Generate a GMT-offset zone name. jpayne@68: if isDST: jpayne@68: localzone = altzone jpayne@68: else: jpayne@68: localzone = timezone jpayne@68: offset = (-localzone / 3600.0) jpayne@68: majorOffset = int(offset) jpayne@68: if majorOffset != 0: jpayne@68: minorOffset = abs(int((offset % majorOffset) * 60.0)) jpayne@68: else: jpayne@68: minorOffset = 0 jpayne@68: m = majorOffset >= 0 and '+' or '' jpayne@68: lz = '%s%0.02d%0.02d' % (m, majorOffset, minorOffset) jpayne@68: _localzone = PytzCache._zmap[('GMT%s' % lz).lower()] jpayne@68: except BaseException: jpayne@68: _localzone = '' jpayne@68: return _localzone jpayne@68: jpayne@68: jpayne@68: _localzone0 = _findLocalTimeZoneName(0) jpayne@68: _localzone1 = _findLocalTimeZoneName(1) jpayne@68: _multipleZones = (_localzone0 != _localzone1) jpayne@68: jpayne@68: # Some utility functions for calculating dates: jpayne@68: jpayne@68: jpayne@68: def _calcSD(t): jpayne@68: # Returns timezone-independent days since epoch and the fractional jpayne@68: # part of the days. jpayne@68: dd = t + EPOCH - 86400.0 jpayne@68: d = dd / 86400.0 jpayne@68: s = d - math.floor(d) jpayne@68: return s, d jpayne@68: jpayne@68: jpayne@68: def _calcDependentSecond(tz, t): jpayne@68: # Calculates the timezone-dependent second (integer part only) jpayne@68: # from the timezone-independent second. jpayne@68: fset = _tzoffset(tz, t) jpayne@68: return fset + long(math.floor(t)) + long(EPOCH) - 86400 jpayne@68: jpayne@68: jpayne@68: def _calcDependentSecond2(yr, mo, dy, hr, mn, sc): jpayne@68: # Calculates the timezone-dependent second (integer part only) jpayne@68: # from the date given. jpayne@68: ss = int(hr) * 3600 + int(mn) * 60 + int(sc) jpayne@68: x = long(_julianday(yr, mo, dy) - jd1901) * 86400 + ss jpayne@68: return x jpayne@68: jpayne@68: jpayne@68: def _calcIndependentSecondEtc(tz, x, ms): jpayne@68: # Derive the timezone-independent second from the timezone jpayne@68: # dependent second. jpayne@68: fsetAtEpoch = _tzoffset(tz, 0.0) jpayne@68: nearTime = x - fsetAtEpoch - long(EPOCH) + 86400 + ms jpayne@68: # nearTime is now within an hour of being correct. jpayne@68: # Recalculate t according to DST. jpayne@68: fset = long(_tzoffset(tz, nearTime)) jpayne@68: d = (x - fset) / 86400.0 + (ms / 86400.0) jpayne@68: t = x - fset - long(EPOCH) + 86400 + ms jpayne@68: micros = (x + 86400 - fset) * 1000000 + \ jpayne@68: long(round(ms * 1000000.0)) - long(EPOCH * 1000000.0) jpayne@68: s = d - math.floor(d) jpayne@68: return (s, d, t, micros) jpayne@68: jpayne@68: jpayne@68: def _calcHMS(x, ms): jpayne@68: # hours, minutes, seconds from integer and float. jpayne@68: hr = x // 3600 jpayne@68: x = x - hr * 3600 jpayne@68: mn = x // 60 jpayne@68: sc = x - mn * 60 + ms jpayne@68: return (hr, mn, sc) jpayne@68: jpayne@68: jpayne@68: def _calcYMDHMS(x, ms): jpayne@68: # x is a timezone-dependent integer of seconds. jpayne@68: # Produces yr,mo,dy,hr,mn,sc. jpayne@68: yr, mo, dy = _calendarday(x // 86400 + jd1901) jpayne@68: x = int(x - (x // 86400) * 86400) jpayne@68: hr = x // 3600 jpayne@68: x = x - hr * 3600 jpayne@68: mn = x // 60 jpayne@68: sc = x - mn * 60 + ms jpayne@68: return (yr, mo, dy, hr, mn, sc) jpayne@68: jpayne@68: jpayne@68: def _julianday(yr, mo, dy): jpayne@68: y, m, d = long(yr), long(mo), long(dy) jpayne@68: if m > 12: jpayne@68: y = y + m // 12 jpayne@68: m = m % 12 jpayne@68: elif m < 1: jpayne@68: m = -m jpayne@68: y = y - m // 12 - 1 jpayne@68: m = 12 - m % 12 jpayne@68: if y > 0: jpayne@68: yr_correct = 0 jpayne@68: else: jpayne@68: yr_correct = 3 jpayne@68: if m < 3: jpayne@68: y, m = y - 1, m + 12 jpayne@68: if y * 10000 + m * 100 + d > 15821014: jpayne@68: b = 2 - y // 100 + y // 400 jpayne@68: else: jpayne@68: b = 0 jpayne@68: return ((1461 * y - yr_correct) // 4 + jpayne@68: 306001 * (m + 1) // 10000 + d + 1720994 + b) jpayne@68: jpayne@68: jpayne@68: def _calendarday(j): jpayne@68: j = long(j) jpayne@68: if (j < 2299160): jpayne@68: b = j + 1525 jpayne@68: else: jpayne@68: a = (4 * j - 7468861) // 146097 jpayne@68: b = j + 1526 + a - a // 4 jpayne@68: c = (20 * b - 2442) // 7305 jpayne@68: d = 1461 * c // 4 jpayne@68: e = 10000 * (b - d) // 306001 jpayne@68: dy = int(b - d - 306001 * e // 10000) jpayne@68: mo = (e < 14) and int(e - 1) or int(e - 13) jpayne@68: yr = (mo > 2) and (c - 4716) or (c - 4715) jpayne@68: return (int(yr), int(mo), int(dy)) jpayne@68: jpayne@68: jpayne@68: def _tzoffset(tz, t): jpayne@68: """Returns the offset in seconds to GMT from a specific timezone (tz) at jpayne@68: a specific time (t). NB! The _tzoffset result is the same same sign as jpayne@68: the time zone, i.e. GMT+2 has a 7200 second offset. This is the opposite jpayne@68: sign of time.timezone which (confusingly) is -7200 for GMT+2.""" jpayne@68: try: jpayne@68: return _TZINFO[tz].info(t)[0] jpayne@68: except Exception: jpayne@68: if numericTimeZoneMatch(tz) is not None: jpayne@68: return int(tz[0:3]) * 3600 + int(tz[0] + tz[3:5]) * 60 jpayne@68: else: jpayne@68: return 0 # ?? jpayne@68: jpayne@68: jpayne@68: def _correctYear(year): jpayne@68: # Y2K patch. jpayne@68: if year >= 0 and year < 100: jpayne@68: # 00-69 means 2000-2069, 70-99 means 1970-1999. jpayne@68: if year < 70: jpayne@68: year = 2000 + year jpayne@68: else: jpayne@68: year = 1900 + year jpayne@68: return year jpayne@68: jpayne@68: jpayne@68: def safegmtime(t): jpayne@68: '''gmtime with a safety zone.''' jpayne@68: try: jpayne@68: return gmtime(t) jpayne@68: except (ValueError, OverflowError): jpayne@68: raise TimeError('The time %f is beyond the range of this Python ' jpayne@68: 'implementation.' % float(t)) jpayne@68: jpayne@68: jpayne@68: def safelocaltime(t): jpayne@68: '''localtime with a safety zone.''' jpayne@68: try: jpayne@68: return localtime(t) jpayne@68: except (ValueError, OverflowError): jpayne@68: raise TimeError('The time %f is beyond the range of this Python ' jpayne@68: 'implementation.' % float(t)) jpayne@68: jpayne@68: jpayne@68: def _tzoffset2rfc822zone(seconds): jpayne@68: """Takes an offset, such as from _tzoffset(), and returns an rfc822 jpayne@68: compliant zone specification. Please note that the result of jpayne@68: _tzoffset() is the negative of what time.localzone and time.altzone is. jpayne@68: """ jpayne@68: return "%+03d%02d" % divmod((seconds // 60), 60) jpayne@68: jpayne@68: jpayne@68: def _tzoffset2iso8601zone(seconds): jpayne@68: """Takes an offset, such as from _tzoffset(), and returns an ISO 8601 jpayne@68: compliant zone specification. Please note that the result of jpayne@68: _tzoffset() is the negative of what time.localzone and time.altzone is. jpayne@68: """ jpayne@68: return "%+03d:%02d" % divmod((seconds // 60), 60) jpayne@68: jpayne@68: jpayne@68: def Timezones(): jpayne@68: """Return the list of recognized timezone names""" jpayne@68: return sorted(list(PytzCache._zmap.values())) jpayne@68: jpayne@68: jpayne@68: class strftimeFormatter: jpayne@68: jpayne@68: def __init__(self, dt, format): jpayne@68: self.dt = dt jpayne@68: self.format = format jpayne@68: jpayne@68: def __call__(self): jpayne@68: return self.dt.strftime(self.format) jpayne@68: jpayne@68: jpayne@68: @implementer(IDateTime) jpayne@68: class DateTime: jpayne@68: """DateTime objects represent instants in time and provide jpayne@68: interfaces for controlling its representation without jpayne@68: affecting the absolute value of the object. jpayne@68: jpayne@68: DateTime objects may be created from a wide variety of string jpayne@68: or numeric data, or may be computed from other DateTime objects. jpayne@68: DateTimes support the ability to convert their representations jpayne@68: to many major timezones, as well as the ability to create a jpayne@68: DateTime object in the context of a given timezone. jpayne@68: jpayne@68: DateTime objects provide partial numerical behavior: jpayne@68: jpayne@68: - Two date-time objects can be subtracted to obtain a time, jpayne@68: in days between the two. jpayne@68: jpayne@68: - A date-time object and a positive or negative number may jpayne@68: be added to obtain a new date-time object that is the given jpayne@68: number of days later than the input date-time object. jpayne@68: jpayne@68: - A positive or negative number and a date-time object may jpayne@68: be added to obtain a new date-time object that is the given jpayne@68: number of days later than the input date-time object. jpayne@68: jpayne@68: - A positive or negative number may be subtracted from a jpayne@68: date-time object to obtain a new date-time object that is jpayne@68: the given number of days earlier than the input date-time jpayne@68: object. jpayne@68: jpayne@68: DateTime objects may be converted to integer, long, or float jpayne@68: numbers of days since January 1, 1901, using the standard int, jpayne@68: long, and float functions (Compatibility Note: int, long and jpayne@68: float return the number of days since 1901 in GMT rather than jpayne@68: local machine timezone). DateTime objects also provide access jpayne@68: to their value in a float format usable with the Python time jpayne@68: module, provided that the value of the object falls in the jpayne@68: range of the epoch-based time module, and as a datetime.datetime jpayne@68: object. jpayne@68: jpayne@68: A DateTime object should be considered immutable; all conversion jpayne@68: and numeric operations return a new DateTime object rather than jpayne@68: modify the current object.""" jpayne@68: jpayne@68: # For security machinery: jpayne@68: __roles__ = None jpayne@68: __allow_access_to_unprotected_subobjects__ = 1 jpayne@68: jpayne@68: # Limit the amount of instance attributes jpayne@68: __slots__ = ( jpayne@68: '_timezone_naive', jpayne@68: '_tz', jpayne@68: '_dayoffset', jpayne@68: '_year', jpayne@68: '_month', jpayne@68: '_day', jpayne@68: '_hour', jpayne@68: '_minute', jpayne@68: '_second', jpayne@68: '_nearsec', jpayne@68: '_d', jpayne@68: '_micros', jpayne@68: 'time', jpayne@68: ) jpayne@68: jpayne@68: def __init__(self, *args, **kw): jpayne@68: """Return a new date-time object""" jpayne@68: try: jpayne@68: return self._parse_args(*args, **kw) jpayne@68: except (DateError, TimeError, DateTimeError): jpayne@68: raise jpayne@68: except Exception: jpayne@68: raise SyntaxError('Unable to parse {}, {}'.format(args, kw)) jpayne@68: jpayne@68: def __getstate__(self): jpayne@68: return (self._micros, jpayne@68: getattr(self, '_timezone_naive', False), jpayne@68: self._tz) jpayne@68: jpayne@68: def __setstate__(self, value): jpayne@68: if isinstance(value, tuple): jpayne@68: micros, tz_naive, tz = value jpayne@68: if isinstance(micros, float): jpayne@68: # BBB: support for pickle where micros was a float jpayne@68: micros = int(micros * 1000000) jpayne@68: self._parse_args(micros / 1000000., tz) jpayne@68: self._micros = micros jpayne@68: self._timezone_naive = tz_naive jpayne@68: else: jpayne@68: for k, v in value.items(): jpayne@68: if k in self.__slots__: jpayne@68: setattr(self, k, v) jpayne@68: # BBB: support for very old DateTime pickles jpayne@68: if '_micros' not in value: jpayne@68: self._micros = long(value['_t'] * 1000000) jpayne@68: if '_timezone_naive' not in value: jpayne@68: self._timezone_naive = False jpayne@68: jpayne@68: def _parse_args(self, *args, **kw): jpayne@68: """Return a new date-time object. jpayne@68: jpayne@68: A DateTime object always maintains its value as an absolute jpayne@68: UTC time, and is represented in the context of some timezone jpayne@68: based on the arguments used to create the object. A DateTime jpayne@68: object's methods return values based on the timezone context. jpayne@68: jpayne@68: Note that in all cases the local machine timezone is used for jpayne@68: representation if no timezone is specified. jpayne@68: jpayne@68: DateTimes may be created with zero to seven arguments. jpayne@68: jpayne@68: - If the function is called with no arguments or with None, jpayne@68: then the current date/time is returned, represented in the jpayne@68: timezone of the local machine. jpayne@68: jpayne@68: - If the function is invoked with a single string argument jpayne@68: which is a recognized timezone name, an object representing jpayne@68: the current time is returned, represented in the specified jpayne@68: timezone. jpayne@68: jpayne@68: - If the function is invoked with a single string argument jpayne@68: representing a valid date/time, an object representing jpayne@68: that date/time will be returned. jpayne@68: jpayne@68: As a general rule, any date-time representation that is jpayne@68: recognized and unambiguous to a resident of North America jpayne@68: is acceptable. The reason for this qualification is that jpayne@68: in North America, a date like: 2/1/1994 is interpreted jpayne@68: as February 1, 1994, while in some parts of the world, jpayne@68: it is interpreted as January 2, 1994. jpayne@68: jpayne@68: A date/time string consists of two components, a date jpayne@68: component and an optional time component, separated by one jpayne@68: or more spaces. If the time component is omitted, 12:00am is jpayne@68: assumed. Any recognized timezone name specified as the final jpayne@68: element of the date/time string will be used for computing jpayne@68: the date/time value. If you create a DateTime with the jpayne@68: string 'Mar 9, 1997 1:45pm US/Pacific', the value will jpayne@68: essentially be the same as if you had captured time.time() jpayne@68: at the specified date and time on a machine in that timezone: jpayne@68: jpayne@68:
jpayne@68:             e = DateTime('US/Eastern')
jpayne@68:             # returns current date/time, represented in US/Eastern.
jpayne@68: 
jpayne@68:             x = DateTime('1997/3/9 1:45pm')
jpayne@68:             # returns specified time, represented in local machine zone.
jpayne@68: 
jpayne@68:             y = DateTime('Mar 9, 1997 13:45:00')
jpayne@68:             # y is equal to x
jpayne@68:             
jpayne@68: jpayne@68: The date component consists of year, month, and day jpayne@68: values. The year value must be a one-, two-, or jpayne@68: four-digit integer. If a one- or two-digit year is jpayne@68: used, the year is assumed to be in the twentieth jpayne@68: century. The month may be an integer, from 1 to 12, a jpayne@68: month name, or a month abbreviation, where a period may jpayne@68: optionally follow the abbreviation. The day must be an jpayne@68: integer from 1 to the number of days in the month. The jpayne@68: year, month, and day values may be separated by jpayne@68: periods, hyphens, forward slashes, or spaces. Extra jpayne@68: spaces are permitted around the delimiters. Year, jpayne@68: month, and day values may be given in any order as long jpayne@68: as it is possible to distinguish the components. If all jpayne@68: three components are numbers that are less than 13, jpayne@68: then a month-day-year ordering is assumed. jpayne@68: jpayne@68: The time component consists of hour, minute, and second jpayne@68: values separated by colons. The hour value must be an jpayne@68: integer between 0 and 23 inclusively. The minute value jpayne@68: must be an integer between 0 and 59 inclusively. The jpayne@68: second value may be an integer value between 0 and jpayne@68: 59.999 inclusively. The second value or both the minute jpayne@68: and second values may be omitted. The time may be jpayne@68: followed by am or pm in upper or lower case, in which jpayne@68: case a 12-hour clock is assumed. jpayne@68: jpayne@68: New in Zope 2.4: jpayne@68: The DateTime constructor automatically detects and handles jpayne@68: ISO8601 compliant dates (YYYY-MM-DDThh:ss:mmTZD). jpayne@68: jpayne@68: New in Zope 2.9.6: jpayne@68: The existing ISO8601 parser was extended to support almost jpayne@68: the whole ISO8601 specification. New formats includes: jpayne@68: jpayne@68:
jpayne@68:             y = DateTime('1993-045')
jpayne@68:             # returns the 45th day from 1993, which is 14th February
jpayne@68: 
jpayne@68:             w = DateTime('1993-W06-7')
jpayne@68:             # returns the 7th day from the 6th week from 1993, which
jpayne@68:             # is also 14th February
jpayne@68:             
jpayne@68: jpayne@68: See http://en.wikipedia.org/wiki/ISO_8601 for full specs. jpayne@68: jpayne@68: Note that the Zope DateTime parser assumes timezone naive ISO jpayne@68: strings to be in UTC rather than local time as specified. jpayne@68: jpayne@68: - If the DateTime function is invoked with a single numeric jpayne@68: argument, the number is assumed to be a floating point value jpayne@68: such as that returned by time.time(). jpayne@68: jpayne@68: A DateTime object is returned that represents the GMT value jpayne@68: of the time.time() float represented in the local machine's jpayne@68: timezone. jpayne@68: jpayne@68: - If the DateTime function is invoked with a single argument jpayne@68: that is a DateTime instance, a copy of the passed object will jpayne@68: be created. jpayne@68: jpayne@68: - New in 2.11: jpayne@68: The DateTime function may now be invoked with a single argument jpayne@68: that is a datetime.datetime instance. DateTimes may be converted jpayne@68: back to datetime.datetime objects with asdatetime(). jpayne@68: DateTime instances may be converted to a timezone naive jpayne@68: datetime.datetime in UTC with utcdatetime(). jpayne@68: jpayne@68: - If the function is invoked with two numeric arguments, then jpayne@68: the first is taken to be an integer year and the second jpayne@68: argument is taken to be an offset in days from the beginning jpayne@68: of the year, in the context of the local machine timezone. jpayne@68: jpayne@68: The date-time value returned is the given offset number of jpayne@68: days from the beginning of the given year, represented in jpayne@68: the timezone of the local machine. The offset may be positive jpayne@68: or negative. jpayne@68: jpayne@68: Two-digit years are assumed to be in the twentieth jpayne@68: century. jpayne@68: jpayne@68: - If the function is invoked with two arguments, the first jpayne@68: a float representing a number of seconds past the epoch jpayne@68: in gmt (such as those returned by time.time()) and the jpayne@68: second a string naming a recognized timezone, a DateTime jpayne@68: with a value of that gmt time will be returned, represented jpayne@68: in the given timezone. jpayne@68: jpayne@68:
jpayne@68:             import time
jpayne@68:             t = time.time()
jpayne@68: 
jpayne@68:             now_east = DateTime(t,'US/Eastern')
jpayne@68:             # Time t represented as US/Eastern
jpayne@68: 
jpayne@68:             now_west = DateTime(t,'US/Pacific')
jpayne@68:             # Time t represented as US/Pacific
jpayne@68: 
jpayne@68:             # now_east == now_west
jpayne@68:             # only their representations are different
jpayne@68:             
jpayne@68: jpayne@68: - If the function is invoked with three or more numeric jpayne@68: arguments, then the first is taken to be an integer jpayne@68: year, the second is taken to be an integer month, and jpayne@68: the third is taken to be an integer day. If the jpayne@68: combination of values is not valid, then a jpayne@68: DateError is raised. Two-digit years are assumed jpayne@68: to be in the twentieth century. The fourth, fifth, and jpayne@68: sixth arguments specify a time in hours, minutes, and jpayne@68: seconds; hours and minutes should be positive integers jpayne@68: and seconds is a positive floating point value, all of jpayne@68: these default to zero if not given. An optional string may jpayne@68: be given as the final argument to indicate timezone (the jpayne@68: effect of this is as if you had taken the value of time.time() jpayne@68: at that time on a machine in the specified timezone). jpayne@68: jpayne@68: New in Zope 2.7: jpayne@68: A new keyword parameter "datefmt" can be passed to the jpayne@68: constructor. If set to "international", the constructor jpayne@68: is forced to treat ambiguous dates as "days before month jpayne@68: before year". This useful if you need to parse non-US jpayne@68: dates in a reliable way jpayne@68: jpayne@68: In any case that a floating point number of seconds is given jpayne@68: or derived, it's rounded to the nearest millisecond. jpayne@68: jpayne@68: If a string argument passed to the DateTime constructor cannot be jpayne@68: parsed, it will raise DateTime.SyntaxError. Invalid date components jpayne@68: will raise a DateError, while invalid time or timezone components jpayne@68: will raise a DateTimeError. jpayne@68: jpayne@68: The module function Timezones() will return a list of the (common) jpayne@68: timezones recognized by the DateTime module. Recognition of jpayne@68: timezone names is case-insensitive. jpayne@68: """ jpayne@68: jpayne@68: datefmt = kw.get('datefmt', getDefaultDateFormat()) jpayne@68: d = t = s = None jpayne@68: ac = len(args) jpayne@68: microsecs = None jpayne@68: jpayne@68: if ac == 10: jpayne@68: # Internal format called only by DateTime jpayne@68: yr, mo, dy, hr, mn, sc, tz, t, d, s = args jpayne@68: elif ac == 11: jpayne@68: # Internal format that includes milliseconds (from the epoch) jpayne@68: yr, mo, dy, hr, mn, sc, tz, t, d, s, millisecs = args jpayne@68: microsecs = millisecs * 1000 jpayne@68: jpayne@68: elif ac == 12: jpayne@68: # Internal format that includes microseconds (from the epoch) and a jpayne@68: # flag indicating whether this was constructed in a timezone naive jpayne@68: # manner jpayne@68: yr, mo, dy, hr, mn, sc, tz, t, d, s, microsecs, tznaive = args jpayne@68: if tznaive is not None: # preserve this information jpayne@68: self._timezone_naive = tznaive jpayne@68: jpayne@68: elif not args or (ac and args[0] is None): jpayne@68: # Current time, to be displayed in local timezone jpayne@68: t = time() jpayne@68: lt = safelocaltime(t) jpayne@68: tz = self.localZone(lt) jpayne@68: ms = (t - math.floor(t)) jpayne@68: s, d = _calcSD(t) jpayne@68: yr, mo, dy, hr, mn, sc = lt[:6] jpayne@68: sc = sc + ms jpayne@68: self._timezone_naive = False jpayne@68: jpayne@68: elif ac == 1: jpayne@68: arg = args[0] jpayne@68: jpayne@68: if arg == '': jpayne@68: raise SyntaxError(arg) jpayne@68: jpayne@68: if isinstance(arg, DateTime): jpayne@68: """Construct a new DateTime instance from a given jpayne@68: DateTime instance. jpayne@68: """ jpayne@68: t = arg.timeTime() jpayne@68: s, d = _calcSD(t) jpayne@68: yr, mo, dy, hr, mn, sc, tz = arg.parts() jpayne@68: jpayne@68: elif isinstance(arg, datetime): jpayne@68: yr, mo, dy, hr, mn, sc, numerictz, tznaive = \ jpayne@68: self._parse_iso8601_preserving_tznaive(arg.isoformat()) jpayne@68: if arg.tzinfo is None: jpayne@68: self._timezone_naive = True jpayne@68: tz = None jpayne@68: else: jpayne@68: self._timezone_naive = False jpayne@68: # if we have a pytz tzinfo, use the `zone` attribute jpayne@68: # as a key jpayne@68: tz = getattr(arg.tzinfo, 'zone', numerictz) jpayne@68: ms = sc - math.floor(sc) jpayne@68: x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc) jpayne@68: jpayne@68: if tz: jpayne@68: try: jpayne@68: zone = _TZINFO[tz] jpayne@68: except DateTimeError: jpayne@68: try: jpayne@68: zone = _TZINFO[numerictz] jpayne@68: except DateTimeError: jpayne@68: raise DateTimeError( jpayne@68: 'Unknown time zone in date: %s' % arg) jpayne@68: tz = zone.tzinfo.zone jpayne@68: else: jpayne@68: tz = self._calcTimezoneName(x, ms) jpayne@68: s, d, t, microsecs = _calcIndependentSecondEtc(tz, x, ms) jpayne@68: jpayne@68: elif (isinstance(arg, basestring) and jpayne@68: arg.lower() in _TZINFO._zidx): jpayne@68: # Current time, to be displayed in specified timezone jpayne@68: t, tz = time(), _TZINFO._zmap[arg.lower()] jpayne@68: ms = (t - math.floor(t)) jpayne@68: # Use integer arithmetic as much as possible. jpayne@68: s, d = _calcSD(t) jpayne@68: x = _calcDependentSecond(tz, t) jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, ms) jpayne@68: jpayne@68: elif isinstance(arg, basestring): jpayne@68: # Date/time string jpayne@68: iso8601 = iso8601Match(arg.strip()) jpayne@68: fields_iso8601 = iso8601 and iso8601.groupdict() or {} jpayne@68: if fields_iso8601 and not fields_iso8601.get('garbage'): jpayne@68: yr, mo, dy, hr, mn, sc, tz, tznaive = \ jpayne@68: self._parse_iso8601_preserving_tznaive(arg) jpayne@68: self._timezone_naive = tznaive jpayne@68: else: jpayne@68: yr, mo, dy, hr, mn, sc, tz = self._parse(arg, datefmt) jpayne@68: jpayne@68: if not self._validDate(yr, mo, dy): jpayne@68: raise DateError('Invalid date: %s' % arg) jpayne@68: if not self._validTime(hr, mn, int(sc)): jpayne@68: raise TimeError('Invalid time: %s' % arg) jpayne@68: ms = sc - math.floor(sc) jpayne@68: x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc) jpayne@68: jpayne@68: if tz: jpayne@68: try: jpayne@68: tz = _TZINFO._zmap[tz.lower()] jpayne@68: except KeyError: jpayne@68: if numericTimeZoneMatch(tz) is None: jpayne@68: raise DateTimeError( jpayne@68: 'Unknown time zone in date: %s' % arg) jpayne@68: else: jpayne@68: tz = self._calcTimezoneName(x, ms) jpayne@68: s, d, t, microsecs = _calcIndependentSecondEtc(tz, x, ms) jpayne@68: jpayne@68: else: jpayne@68: # Seconds from epoch, gmt jpayne@68: t = arg jpayne@68: lt = safelocaltime(t) jpayne@68: tz = self.localZone(lt) jpayne@68: ms = (t - math.floor(t)) jpayne@68: s, d = _calcSD(t) jpayne@68: yr, mo, dy, hr, mn, sc = lt[:6] jpayne@68: sc = sc + ms jpayne@68: jpayne@68: elif ac == 2: jpayne@68: if isinstance(args[1], basestring): jpayne@68: # Seconds from epoch (gmt) and timezone jpayne@68: t, tz = args jpayne@68: ms = (t - math.floor(t)) jpayne@68: try: jpayne@68: tz = _TZINFO._zmap[tz.lower()] jpayne@68: except KeyError: jpayne@68: if numericTimeZoneMatch(tz) is None: jpayne@68: raise DateTimeError('Unknown time zone: %s' % tz) jpayne@68: # Use integer arithmetic as much as possible. jpayne@68: s, d = _calcSD(t) jpayne@68: x = _calcDependentSecond(tz, t) jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, ms) jpayne@68: else: jpayne@68: # Year, julian expressed in local zone jpayne@68: t = time() jpayne@68: lt = safelocaltime(t) jpayne@68: tz = self.localZone(lt) jpayne@68: yr, jul = args jpayne@68: yr = _correctYear(yr) jpayne@68: d = (_julianday(yr, 1, 0) - jd1901) + jul jpayne@68: x_float = d * 86400.0 jpayne@68: x_floor = math.floor(x_float) jpayne@68: ms = x_float - x_floor jpayne@68: x = long(x_floor) jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, ms) jpayne@68: s, d, t, microsecs = _calcIndependentSecondEtc(tz, x, ms) jpayne@68: else: jpayne@68: # Explicit format jpayne@68: yr, mo, dy = args[:3] jpayne@68: hr, mn, sc, tz = 0, 0, 0, 0 jpayne@68: yr = _correctYear(yr) jpayne@68: if not self._validDate(yr, mo, dy): jpayne@68: raise DateError('Invalid date: {}'.format(args)) jpayne@68: args = args[3:] jpayne@68: if args: jpayne@68: hr, args = args[0], args[1:] jpayne@68: if args: jpayne@68: mn, args = args[0], args[1:] jpayne@68: if args: jpayne@68: sc, args = args[0], args[1:] jpayne@68: if args: jpayne@68: tz, args = args[0], args[1:] jpayne@68: if args: jpayne@68: raise DateTimeError('Too many arguments') jpayne@68: if not self._validTime(hr, mn, sc): jpayne@68: raise TimeError('Invalid time: %s' % repr(args)) jpayne@68: jpayne@68: x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc) jpayne@68: ms = sc - math.floor(sc) jpayne@68: if tz: jpayne@68: try: jpayne@68: tz = _TZINFO._zmap[tz.lower()] jpayne@68: except KeyError: jpayne@68: if numericTimeZoneMatch(tz) is None: jpayne@68: raise DateTimeError('Unknown time zone: %s' % tz) jpayne@68: else: jpayne@68: # Get local time zone name jpayne@68: tz = self._calcTimezoneName(x, ms) jpayne@68: s, d, t, microsecs = _calcIndependentSecondEtc(tz, x, ms) jpayne@68: jpayne@68: self._dayoffset = int((_julianday(yr, mo, dy) + 2) % 7) jpayne@68: # Round to nearest microsecond in platform-independent way. You jpayne@68: # cannot rely on C sprintf (Python '%') formatting to round jpayne@68: # consistently; doing it ourselves ensures that all but truly jpayne@68: # horrid C sprintf implementations will yield the same result jpayne@68: # cross-platform, provided the format asks for exactly 6 digits after jpayne@68: # the decimal point. jpayne@68: sc = round(sc, 6) jpayne@68: if sc >= 60.0: # can happen if, e.g., orig sc was 59.9999999 jpayne@68: sc = 59.999999 jpayne@68: self._nearsec = math.floor(sc) jpayne@68: self._year, self._month, self._day = yr, mo, dy jpayne@68: self._hour, self._minute, self._second = hr, mn, sc jpayne@68: self.time, self._d, self._tz = s, d, tz jpayne@68: # self._micros is the time since the epoch jpayne@68: # in long integer microseconds. jpayne@68: if microsecs is None: jpayne@68: microsecs = long(round(t * 1000000.0)) jpayne@68: self._micros = microsecs jpayne@68: jpayne@68: def localZone(self, ltm=None): jpayne@68: '''Returns the time zone on the given date. The time zone jpayne@68: can change according to daylight savings.''' jpayne@68: if not _multipleZones: jpayne@68: return _localzone0 jpayne@68: if ltm is None: jpayne@68: ltm = localtime(time()) jpayne@68: isDST = ltm[8] jpayne@68: lz = isDST and _localzone1 or _localzone0 jpayne@68: return lz jpayne@68: jpayne@68: def _calcTimezoneName(self, x, ms): jpayne@68: # Derive the name of the local time zone at the given jpayne@68: # timezone-dependent second. jpayne@68: if not _multipleZones: jpayne@68: return _localzone0 jpayne@68: fsetAtEpoch = _tzoffset(_localzone0, 0.0) jpayne@68: nearTime = x - fsetAtEpoch - long(EPOCH) + 86400 + ms jpayne@68: # nearTime is within an hour of being correct. jpayne@68: try: jpayne@68: ltm = safelocaltime(nearTime) jpayne@68: except BaseException: jpayne@68: # We are beyond the range of Python's date support. jpayne@68: # Hopefully we can assume that daylight savings schedules jpayne@68: # repeat every 28 years. Calculate the name of the jpayne@68: # time zone using a supported range of years. jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, 0) jpayne@68: yr = ((yr - 1970) % 28) + 1970 jpayne@68: x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc) jpayne@68: nearTime = x - fsetAtEpoch - long(EPOCH) + 86400 + ms jpayne@68: jpayne@68: # nearTime might still be negative if we are east of Greenwich. jpayne@68: # But we can assume on 1969/12/31 were no timezone changes. jpayne@68: nearTime = max(0, nearTime) jpayne@68: jpayne@68: ltm = safelocaltime(nearTime) jpayne@68: tz = self.localZone(ltm) jpayne@68: return tz jpayne@68: jpayne@68: def _parse(self, st, datefmt=getDefaultDateFormat()): jpayne@68: # Parse date-time components from a string jpayne@68: month = year = tz = tm = None jpayne@68: ValidZones = _TZINFO._zidx jpayne@68: TimeModifiers = ['am', 'pm'] jpayne@68: jpayne@68: # Find timezone first, since it should always be the last jpayne@68: # element, and may contain a slash, confusing the parser. jpayne@68: st = st.strip() jpayne@68: sp = st.split() jpayne@68: tz = sp[-1] jpayne@68: if tz and (tz.lower() in ValidZones): jpayne@68: self._timezone_naive = False jpayne@68: st = ' '.join(sp[:-1]) jpayne@68: else: jpayne@68: self._timezone_naive = True jpayne@68: tz = None # Decide later, since the default time zone jpayne@68: # could depend on the date. jpayne@68: jpayne@68: ints = [] jpayne@68: i = 0 jpayne@68: len_st = len(st) jpayne@68: while i < len_st: jpayne@68: while i < len_st and st[i] in SPACE_CHARS: jpayne@68: i += 1 jpayne@68: if i < len_st and st[i] in DELIMITERS: jpayne@68: d = st[i] jpayne@68: i += 1 jpayne@68: else: jpayne@68: d = '' jpayne@68: while i < len_st and st[i] in SPACE_CHARS: jpayne@68: i += 1 jpayne@68: jpayne@68: # The float pattern needs to look back 1 character, because it jpayne@68: # actually looks for a preceding colon like ':33.33'. This is jpayne@68: # needed to avoid accidentally matching the date part of a jpayne@68: # dot-separated date string such as '1999.12.31'. jpayne@68: if i > 0: jpayne@68: b = i - 1 jpayne@68: else: jpayne@68: b = i jpayne@68: jpayne@68: ts_results = FLT_PATTERN.match(st, b) jpayne@68: if ts_results: jpayne@68: s = ts_results.group(1) jpayne@68: i = i + len(s) jpayne@68: ints.append(float(s)) jpayne@68: continue jpayne@68: jpayne@68: ts_results = INT_PATTERN.match(st, i) jpayne@68: if ts_results: jpayne@68: s = ts_results.group(0) jpayne@68: jpayne@68: ls = len(s) jpayne@68: i = i + ls jpayne@68: if (ls == 4 and d and d in '+-' and jpayne@68: (len(ints) + (not not month) >= 3)): jpayne@68: tz = '{}{}'.format(d, s) jpayne@68: else: jpayne@68: v = int(s) jpayne@68: ints.append(v) jpayne@68: continue jpayne@68: jpayne@68: ts_results = NAME_PATTERN.match(st, i) jpayne@68: if ts_results: jpayne@68: s = ts_results.group(0).lower() jpayne@68: i = i + len(s) jpayne@68: if i < len_st and st[i] == '.': jpayne@68: i += 1 jpayne@68: # Check for month name: jpayne@68: _v = _MONTHMAP.get(s) jpayne@68: if _v is not None: jpayne@68: if month is None: jpayne@68: month = _v jpayne@68: else: jpayne@68: raise SyntaxError(st) jpayne@68: continue jpayne@68: # Check for time modifier: jpayne@68: if s in TimeModifiers: jpayne@68: if tm is None: jpayne@68: tm = s jpayne@68: else: jpayne@68: raise SyntaxError(st) jpayne@68: continue jpayne@68: # Check for and skip day of week: jpayne@68: if s in _DAYMAP: jpayne@68: continue jpayne@68: jpayne@68: raise SyntaxError(st) jpayne@68: jpayne@68: day = None jpayne@68: if ints[-1] > 60 and d not in ('.', ':', '/') and len(ints) > 2: jpayne@68: year = ints[-1] jpayne@68: del ints[-1] jpayne@68: if month: jpayne@68: day = ints[0] jpayne@68: del ints[:1] jpayne@68: else: jpayne@68: if datefmt == "us": jpayne@68: month = ints[0] jpayne@68: day = ints[1] jpayne@68: else: jpayne@68: month = ints[1] jpayne@68: day = ints[0] jpayne@68: del ints[:2] jpayne@68: elif month: jpayne@68: if len(ints) > 1: jpayne@68: if ints[0] > 31: jpayne@68: year = ints[0] jpayne@68: day = ints[1] jpayne@68: else: jpayne@68: year = ints[1] jpayne@68: day = ints[0] jpayne@68: del ints[:2] jpayne@68: elif len(ints) > 2: jpayne@68: if ints[0] > 31: jpayne@68: year = ints[0] jpayne@68: if ints[1] > 12: jpayne@68: day = ints[1] jpayne@68: month = ints[2] jpayne@68: else: jpayne@68: day = ints[2] jpayne@68: month = ints[1] jpayne@68: if ints[1] > 31: jpayne@68: year = ints[1] jpayne@68: if ints[0] > 12 and ints[2] <= 12: jpayne@68: day = ints[0] jpayne@68: month = ints[2] jpayne@68: elif ints[2] > 12 and ints[0] <= 12: jpayne@68: day = ints[2] jpayne@68: month = ints[0] jpayne@68: elif ints[2] > 31: jpayne@68: year = ints[2] jpayne@68: if ints[0] > 12: jpayne@68: day = ints[0] jpayne@68: month = ints[1] jpayne@68: else: jpayne@68: if datefmt == "us": jpayne@68: day = ints[1] jpayne@68: month = ints[0] jpayne@68: else: jpayne@68: day = ints[0] jpayne@68: month = ints[1] jpayne@68: jpayne@68: elif ints[0] <= 12: jpayne@68: month = ints[0] jpayne@68: day = ints[1] jpayne@68: year = ints[2] jpayne@68: del ints[:3] jpayne@68: jpayne@68: if day is None: jpayne@68: # Use today's date. jpayne@68: year, month, day = localtime(time())[:3] jpayne@68: jpayne@68: year = _correctYear(year) jpayne@68: if year < 1000: jpayne@68: raise SyntaxError(st) jpayne@68: jpayne@68: leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) jpayne@68: try: jpayne@68: if not day or day > _MONTH_LEN[leap][month]: jpayne@68: raise DateError(st) jpayne@68: except IndexError: jpayne@68: raise DateError(st) jpayne@68: jpayne@68: tod = 0 jpayne@68: if ints: jpayne@68: i = ints[0] jpayne@68: # Modify hour to reflect am/pm jpayne@68: if tm and (tm == 'pm') and i < 12: jpayne@68: i += 12 jpayne@68: if tm and (tm == 'am') and i == 12: jpayne@68: i = 0 jpayne@68: if i > 24: jpayne@68: raise TimeError(st) jpayne@68: tod = tod + int(i) * 3600 jpayne@68: del ints[0] jpayne@68: if ints: jpayne@68: i = ints[0] jpayne@68: if i > 60: jpayne@68: raise TimeError(st) jpayne@68: tod = tod + int(i) * 60 jpayne@68: del ints[0] jpayne@68: if ints: jpayne@68: i = ints[0] jpayne@68: if i > 60: jpayne@68: raise TimeError(st) jpayne@68: tod = tod + i jpayne@68: del ints[0] jpayne@68: if ints: jpayne@68: raise SyntaxError(st) jpayne@68: jpayne@68: tod_int = int(math.floor(tod)) jpayne@68: ms = tod - tod_int jpayne@68: hr, mn, sc = _calcHMS(tod_int, ms) jpayne@68: if not tz: jpayne@68: # Figure out what time zone it is in the local area jpayne@68: # on the given date. jpayne@68: x = _calcDependentSecond2(year, month, day, hr, mn, sc) jpayne@68: tz = self._calcTimezoneName(x, ms) jpayne@68: jpayne@68: return year, month, day, hr, mn, sc, tz jpayne@68: jpayne@68: # Internal methods jpayne@68: def _validDate(self, y, m, d): jpayne@68: if m < 1 or m > 12 or y < 0 or d < 1 or d > 31: jpayne@68: return 0 jpayne@68: return d <= _MONTH_LEN[ jpayne@68: (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0))][m] jpayne@68: jpayne@68: def _validTime(self, h, m, s): jpayne@68: return h >= 0 and h <= 23 and m >= 0 and m <= 59 and s >= 0 and s < 60 jpayne@68: jpayne@68: def __getattr__(self, name): jpayne@68: if '%' in name: jpayne@68: return strftimeFormatter(self, name) jpayne@68: raise AttributeError(name) jpayne@68: jpayne@68: # Conversion and comparison methods jpayne@68: jpayne@68: def timeTime(self): jpayne@68: """Return the date/time as a floating-point number in UTC, jpayne@68: in the format used by the Python time module. jpayne@68: jpayne@68: Note that it is possible to create date/time values with jpayne@68: DateTime that have no meaningful value to the time module. jpayne@68: """ jpayne@68: return self._micros / 1000000.0 jpayne@68: jpayne@68: def toZone(self, z): jpayne@68: """Return a DateTime with the value as the current jpayne@68: object, represented in the indicated timezone. jpayne@68: """ jpayne@68: t, tz = self._t, _TZINFO._zmap[z.lower()] jpayne@68: micros = self.micros() jpayne@68: tznaive = False # you're performing a timzone change, can't be naive jpayne@68: jpayne@68: try: jpayne@68: # Try to use time module for speed. jpayne@68: yr, mo, dy, hr, mn, sc = safegmtime(t + _tzoffset(tz, t))[:6] jpayne@68: sc = self._second jpayne@68: return self.__class__(yr, mo, dy, hr, mn, sc, tz, t, jpayne@68: self._d, self.time, micros, tznaive) jpayne@68: except Exception: jpayne@68: # gmtime can't perform the calculation in the given range. jpayne@68: # Calculate the difference between the two time zones. jpayne@68: tzdiff = _tzoffset(tz, t) - _tzoffset(self._tz, t) jpayne@68: if tzdiff == 0: jpayne@68: return self jpayne@68: sc = self._second jpayne@68: ms = sc - math.floor(sc) jpayne@68: x = _calcDependentSecond2(self._year, self._month, self._day, jpayne@68: self._hour, self._minute, sc) jpayne@68: x_new = x + tzdiff jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x_new, ms) jpayne@68: return self.__class__(yr, mo, dy, hr, mn, sc, tz, t, jpayne@68: self._d, self.time, micros, tznaive) jpayne@68: jpayne@68: def isFuture(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: later than the time of the call. jpayne@68: """ jpayne@68: return (self._t > time()) jpayne@68: jpayne@68: def isPast(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: earlier than the time of the call. jpayne@68: """ jpayne@68: return (self._t < time()) jpayne@68: jpayne@68: def isCurrentYear(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: that falls within the current year, in the context jpayne@68: of this object's timezone representation. jpayne@68: """ jpayne@68: t = time() jpayne@68: return safegmtime(t + _tzoffset(self._tz, t))[0] == self._year jpayne@68: jpayne@68: def isCurrentMonth(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: that falls within the current month, in the context jpayne@68: of this object's timezone representation. jpayne@68: """ jpayne@68: t = time() jpayne@68: gmt = safegmtime(t + _tzoffset(self._tz, t)) jpayne@68: return gmt[0] == self._year and gmt[1] == self._month jpayne@68: jpayne@68: def isCurrentDay(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: that falls within the current day, in the context jpayne@68: of this object's timezone representation. jpayne@68: """ jpayne@68: t = time() jpayne@68: gmt = safegmtime(t + _tzoffset(self._tz, t)) jpayne@68: return (gmt[0] == self._year and gmt[1] == self._month and jpayne@68: gmt[2] == self._day) jpayne@68: jpayne@68: def isCurrentHour(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: that falls within the current hour, in the context jpayne@68: of this object's timezone representation. jpayne@68: """ jpayne@68: t = time() jpayne@68: gmt = safegmtime(t + _tzoffset(self._tz, t)) jpayne@68: return (gmt[0] == self._year and gmt[1] == self._month and jpayne@68: gmt[2] == self._day and gmt[3] == self._hour) jpayne@68: jpayne@68: def isCurrentMinute(self): jpayne@68: """Return true if this object represents a date/time jpayne@68: that falls within the current minute, in the context jpayne@68: of this object's timezone representation. jpayne@68: """ jpayne@68: t = time() jpayne@68: gmt = safegmtime(t + _tzoffset(self._tz, t)) jpayne@68: return (gmt[0] == self._year and gmt[1] == self._month and jpayne@68: gmt[2] == self._day and gmt[3] == self._hour and jpayne@68: gmt[4] == self._minute) jpayne@68: jpayne@68: def earliestTime(self): jpayne@68: """Return a new DateTime object that represents the earliest jpayne@68: possible time (in whole seconds) that still falls within jpayne@68: the current object's day, in the object's timezone context. jpayne@68: """ jpayne@68: return self.__class__( jpayne@68: self._year, self._month, self._day, 0, 0, 0, self._tz) jpayne@68: jpayne@68: def latestTime(self): jpayne@68: """Return a new DateTime object that represents the latest jpayne@68: possible time (in whole seconds) that still falls within jpayne@68: the current object's day, in the object's timezone context. jpayne@68: """ jpayne@68: return self.__class__( jpayne@68: self._year, self._month, self._day, 23, 59, 59, self._tz) jpayne@68: jpayne@68: def greaterThan(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time greater jpayne@68: than the specified DateTime or time module style time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: if t is None: jpayne@68: return True jpayne@68: if isinstance(t, (float, int)): jpayne@68: return self._micros > long(t * 1000000) jpayne@68: try: jpayne@68: return self._micros > t._micros jpayne@68: except AttributeError: jpayne@68: return self._micros > t jpayne@68: jpayne@68: __gt__ = greaterThan jpayne@68: jpayne@68: def greaterThanEqualTo(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time greater jpayne@68: than or equal to the specified DateTime or time module style jpayne@68: time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: if t is None: jpayne@68: return True jpayne@68: if isinstance(t, (float, int)): jpayne@68: return self._micros >= long(t * 1000000) jpayne@68: try: jpayne@68: return self._micros >= t._micros jpayne@68: except AttributeError: jpayne@68: return self._micros >= t jpayne@68: jpayne@68: __ge__ = greaterThanEqualTo jpayne@68: jpayne@68: def equalTo(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time equal to jpayne@68: the specified DateTime or time module style time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: if t is None: jpayne@68: return False jpayne@68: if isinstance(t, (float, int)): jpayne@68: return self._micros == long(t * 1000000) jpayne@68: try: jpayne@68: return self._micros == t._micros jpayne@68: except AttributeError: jpayne@68: return self._micros == t jpayne@68: jpayne@68: def notEqualTo(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time not equal jpayne@68: to the specified DateTime or time module style time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: return not self.equalTo(t) jpayne@68: jpayne@68: def __eq__(self, t): jpayne@68: """Compare this DateTime object to another DateTime object. jpayne@68: Return True if their internal state is the same. Two objects jpayne@68: representing the same time in different timezones are regared as jpayne@68: unequal. Use the equalTo method if you are only interested in them jpayne@68: referring to the same moment in time. jpayne@68: """ jpayne@68: if not isinstance(t, DateTime): jpayne@68: return False jpayne@68: return (self._micros, self._tz) == (t._micros, t._tz) jpayne@68: jpayne@68: def __ne__(self, t): jpayne@68: return not self.__eq__(t) jpayne@68: jpayne@68: def lessThan(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time less than jpayne@68: the specified DateTime or time module style time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: if t is None: jpayne@68: return False jpayne@68: if isinstance(t, (float, int)): jpayne@68: return self._micros < long(t * 1000000) jpayne@68: try: jpayne@68: return self._micros < t._micros jpayne@68: except AttributeError: jpayne@68: return self._micros < t jpayne@68: jpayne@68: __lt__ = lessThan jpayne@68: jpayne@68: def lessThanEqualTo(self, t): jpayne@68: """Compare this DateTime object to another DateTime object jpayne@68: OR a floating point number such as that which is returned jpayne@68: by the Python time module. jpayne@68: jpayne@68: Returns true if the object represents a date/time less than jpayne@68: or equal to the specified DateTime or time module style time. jpayne@68: jpayne@68: Revised to give more correct results through comparison of jpayne@68: long integer microseconds. jpayne@68: """ jpayne@68: if t is None: jpayne@68: return False jpayne@68: if isinstance(t, (float, int)): jpayne@68: return self._micros <= long(t * 1000000) jpayne@68: try: jpayne@68: return self._micros <= t._micros jpayne@68: except AttributeError: jpayne@68: return self._micros <= t jpayne@68: jpayne@68: __le__ = lessThanEqualTo jpayne@68: jpayne@68: def isLeapYear(self): jpayne@68: """Return true if the current year (in the context of the jpayne@68: object's timezone) is a leap year. jpayne@68: """ jpayne@68: return (self._year % 4 == 0 and jpayne@68: (self._year % 100 != 0 or self._year % 400 == 0)) jpayne@68: jpayne@68: def dayOfYear(self): jpayne@68: """Return the day of the year, in context of the timezone jpayne@68: representation of the object. jpayne@68: """ jpayne@68: d = int(self._d + (_tzoffset(self._tz, self._t) / 86400.0)) jpayne@68: return int((d + jd1901) - _julianday(self._year, 1, 0)) jpayne@68: jpayne@68: # Component access jpayne@68: def parts(self): jpayne@68: """Return a tuple containing the calendar year, month, jpayne@68: day, hour, minute second and timezone of the object. jpayne@68: """ jpayne@68: return (self._year, self._month, self._day, self._hour, jpayne@68: self._minute, self._second, self._tz) jpayne@68: jpayne@68: def timezone(self): jpayne@68: """Return the timezone in which the object is represented.""" jpayne@68: return self._tz jpayne@68: jpayne@68: def tzoffset(self): jpayne@68: """Return the timezone offset for the objects timezone.""" jpayne@68: return _tzoffset(self._tz, self._t) jpayne@68: jpayne@68: def year(self): jpayne@68: """Return the calendar year of the object.""" jpayne@68: return self._year jpayne@68: jpayne@68: def month(self): jpayne@68: """Return the month of the object as an integer.""" jpayne@68: return self._month jpayne@68: jpayne@68: @property jpayne@68: def _fmon(self): jpayne@68: return _MONTHS[self._month] jpayne@68: jpayne@68: def Month(self): jpayne@68: """Return the full month name.""" jpayne@68: return self._fmon jpayne@68: jpayne@68: @property jpayne@68: def _amon(self): jpayne@68: return _MONTHS_A[self._month] jpayne@68: jpayne@68: def aMonth(self): jpayne@68: """Return the abbreviated month name.""" jpayne@68: return self._amon jpayne@68: jpayne@68: def Mon(self): jpayne@68: """Compatibility: see aMonth.""" jpayne@68: return self._amon jpayne@68: jpayne@68: @property jpayne@68: def _pmon(self): jpayne@68: return _MONTHS_P[self._month] jpayne@68: jpayne@68: def pMonth(self): jpayne@68: """Return the abbreviated (with period) month name.""" jpayne@68: return self._pmon jpayne@68: jpayne@68: def Mon_(self): jpayne@68: """Compatibility: see pMonth.""" jpayne@68: return self._pmon jpayne@68: jpayne@68: def day(self): jpayne@68: """Return the integer day.""" jpayne@68: return self._day jpayne@68: jpayne@68: @property jpayne@68: def _fday(self): jpayne@68: return _DAYS[self._dayoffset] jpayne@68: jpayne@68: def Day(self): jpayne@68: """Return the full name of the day of the week.""" jpayne@68: return self._fday jpayne@68: jpayne@68: def DayOfWeek(self): jpayne@68: """Compatibility: see Day.""" jpayne@68: return self._fday jpayne@68: jpayne@68: @property jpayne@68: def _aday(self): jpayne@68: return _DAYS_A[self._dayoffset] jpayne@68: jpayne@68: def aDay(self): jpayne@68: """Return the abbreviated name of the day of the week.""" jpayne@68: return self._aday jpayne@68: jpayne@68: @property jpayne@68: def _pday(self): jpayne@68: return _DAYS_P[self._dayoffset] jpayne@68: jpayne@68: def pDay(self): jpayne@68: """Return the abbreviated (with period) name of the day of the week.""" jpayne@68: return self._pday jpayne@68: jpayne@68: def Day_(self): jpayne@68: """Compatibility: see pDay.""" jpayne@68: return self._pday jpayne@68: jpayne@68: def dow(self): jpayne@68: """Return the integer day of the week, where Sunday is 0.""" jpayne@68: return self._dayoffset jpayne@68: jpayne@68: def dow_1(self): jpayne@68: """Return the integer day of the week, where Sunday is 1.""" jpayne@68: return self._dayoffset + 1 jpayne@68: jpayne@68: @property jpayne@68: def _pmhour(self): jpayne@68: hr = self._hour jpayne@68: if hr > 12: jpayne@68: return hr - 12 jpayne@68: return hr or 12 jpayne@68: jpayne@68: def h_12(self): jpayne@68: """Return the 12-hour clock representation of the hour.""" jpayne@68: return self._pmhour jpayne@68: jpayne@68: def h_24(self): jpayne@68: """Return the 24-hour clock representation of the hour.""" jpayne@68: return self._hour jpayne@68: jpayne@68: @property jpayne@68: def _pm(self): jpayne@68: hr = self._hour jpayne@68: if hr >= 12: jpayne@68: return 'pm' jpayne@68: return 'am' jpayne@68: jpayne@68: def ampm(self): jpayne@68: """Return the appropriate time modifier (am or pm).""" jpayne@68: return self._pm jpayne@68: jpayne@68: def hour(self): jpayne@68: """Return the 24-hour clock representation of the hour.""" jpayne@68: return self._hour jpayne@68: jpayne@68: def minute(self): jpayne@68: """Return the minute.""" jpayne@68: return self._minute jpayne@68: jpayne@68: def second(self): jpayne@68: """Return the second.""" jpayne@68: return self._second jpayne@68: jpayne@68: def millis(self): jpayne@68: """Return the millisecond since the epoch in GMT.""" jpayne@68: return self._micros // 1000 jpayne@68: jpayne@68: def micros(self): jpayne@68: """Return the microsecond since the epoch in GMT.""" jpayne@68: return self._micros jpayne@68: jpayne@68: def timezoneNaive(self): jpayne@68: """The Python datetime module introduces the idea of distinguishing jpayne@68: between timezone aware and timezone naive datetime values. For lossless jpayne@68: conversion to and from datetime.datetime we record this jpayne@68: information using True / False. DateTime makes no distinction, if we jpayne@68: don't have any information we return None here. jpayne@68: """ jpayne@68: try: jpayne@68: return self._timezone_naive jpayne@68: except AttributeError: jpayne@68: return None jpayne@68: jpayne@68: def strftime(self, format): jpayne@68: """Format the date/time using the *current timezone representation*.""" jpayne@68: x = _calcDependentSecond2(self._year, self._month, self._day, jpayne@68: self._hour, self._minute, self._second) jpayne@68: ltz = self._calcTimezoneName(x, 0) jpayne@68: tzdiff = _tzoffset(ltz, self._t) - _tzoffset(self._tz, self._t) jpayne@68: zself = self + tzdiff / 86400.0 jpayne@68: microseconds = int((zself._second - zself._nearsec) * 1000000) jpayne@68: unicode_format = False jpayne@68: if isinstance(format, explicit_unicode_type): jpayne@68: format = format.encode('utf-8') jpayne@68: unicode_format = True jpayne@68: ds = datetime(zself._year, zself._month, zself._day, zself._hour, jpayne@68: zself._minute, int(zself._nearsec), jpayne@68: microseconds).strftime(format) jpayne@68: if unicode_format: jpayne@68: return ds.decode('utf-8') jpayne@68: return ds jpayne@68: jpayne@68: # General formats from previous DateTime jpayne@68: def Date(self): jpayne@68: """Return the date string for the object.""" jpayne@68: return "%s/%2.2d/%2.2d" % (self._year, self._month, self._day) jpayne@68: jpayne@68: def Time(self): jpayne@68: """Return the time string for an object to the nearest second.""" jpayne@68: return '%2.2d:%2.2d:%2.2d' % (self._hour, self._minute, self._nearsec) jpayne@68: jpayne@68: def TimeMinutes(self): jpayne@68: """Return the time string for an object not showing seconds.""" jpayne@68: return '%2.2d:%2.2d' % (self._hour, self._minute) jpayne@68: jpayne@68: def AMPM(self): jpayne@68: """Return the time string for an object to the nearest second.""" jpayne@68: return '%2.2d:%2.2d:%2.2d %s' % ( jpayne@68: self._pmhour, self._minute, self._nearsec, self._pm) jpayne@68: jpayne@68: def AMPMMinutes(self): jpayne@68: """Return the time string for an object not showing seconds.""" jpayne@68: return '%2.2d:%2.2d %s' % (self._pmhour, self._minute, self._pm) jpayne@68: jpayne@68: def PreciseTime(self): jpayne@68: """Return the time string for the object.""" jpayne@68: return '%2.2d:%2.2d:%06.3f' % (self._hour, self._minute, self._second) jpayne@68: jpayne@68: def PreciseAMPM(self): jpayne@68: """Return the time string for the object.""" jpayne@68: return '%2.2d:%2.2d:%06.3f %s' % ( jpayne@68: self._pmhour, self._minute, self._second, self._pm) jpayne@68: jpayne@68: def yy(self): jpayne@68: """Return calendar year as a 2 digit string.""" jpayne@68: return str(self._year)[-2:] jpayne@68: jpayne@68: def mm(self): jpayne@68: """Return month as a 2 digit string.""" jpayne@68: return '%02d' % self._month jpayne@68: jpayne@68: def dd(self): jpayne@68: """Return day as a 2 digit string.""" jpayne@68: return '%02d' % self._day jpayne@68: jpayne@68: def rfc822(self): jpayne@68: """Return the date in RFC 822 format.""" jpayne@68: tzoffset = _tzoffset2rfc822zone(_tzoffset(self._tz, self._t)) jpayne@68: return '%s, %2.2d %s %d %2.2d:%2.2d:%2.2d %s' % ( jpayne@68: self._aday, self._day, self._amon, self._year, jpayne@68: self._hour, self._minute, self._nearsec, tzoffset) jpayne@68: jpayne@68: # New formats jpayne@68: def fCommon(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: March 1, 1997 1:45 pm. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %s:%2.2d %s' % ( jpayne@68: self._fmon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm) jpayne@68: jpayne@68: def fCommonZ(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: March 1, 1997 1:45 pm US/Eastern. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %d:%2.2d %s %s' % ( jpayne@68: self._fmon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm, self._tz) jpayne@68: jpayne@68: def aCommon(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: Mar 1, 1997 1:45 pm. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %s:%2.2d %s' % ( jpayne@68: self._amon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm) jpayne@68: jpayne@68: def aCommonZ(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: Mar 1, 1997 1:45 pm US/Eastern. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %d:%2.2d %s %s' % ( jpayne@68: self._amon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm, self._tz) jpayne@68: jpayne@68: def pCommon(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: Mar. 1, 1997 1:45 pm. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %s:%2.2d %s' % ( jpayne@68: self._pmon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm) jpayne@68: jpayne@68: def pCommonZ(self): jpayne@68: """Return a string representing the object's value jpayne@68: in the format: Mar. 1, 1997 1:45 pm US/Eastern. jpayne@68: """ jpayne@68: return '%s %s, %4.4d %d:%2.2d %s %s' % ( jpayne@68: self._pmon, self._day, self._year, self._pmhour, jpayne@68: self._minute, self._pm, self._tz) jpayne@68: jpayne@68: def ISO(self): jpayne@68: """Return the object in ISO standard format. jpayne@68: jpayne@68: Note: this is *not* ISO 8601-format! See the ISO8601 and jpayne@68: HTML4 methods below for ISO 8601-compliant output. jpayne@68: jpayne@68: Dates are output as: YYYY-MM-DD HH:MM:SS jpayne@68: """ jpayne@68: return "%.4d-%.2d-%.2d %.2d:%.2d:%.2d" % ( jpayne@68: self._year, self._month, self._day, jpayne@68: self._hour, self._minute, self._second) jpayne@68: jpayne@68: def ISO8601(self): jpayne@68: """Return the object in ISO 8601-compatible format containing the jpayne@68: date, time with seconds-precision and the time zone identifier. jpayne@68: jpayne@68: See: http://www.w3.org/TR/NOTE-datetime jpayne@68: jpayne@68: Dates are output as: YYYY-MM-DDTHH:MM:SSTZD jpayne@68: T is a literal character. jpayne@68: TZD is Time Zone Designator, format +HH:MM or -HH:MM jpayne@68: jpayne@68: If the instance is timezone naive (it was not specified with a timezone jpayne@68: when it was constructed) then the timezone is omitted. jpayne@68: jpayne@68: The HTML4 method below offers the same formatting, but converts jpayne@68: to UTC before returning the value and sets the TZD "Z". jpayne@68: """ jpayne@68: if self.timezoneNaive(): jpayne@68: return "%0.4d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d" % ( jpayne@68: self._year, self._month, self._day, jpayne@68: self._hour, self._minute, self._second) jpayne@68: tzoffset = _tzoffset2iso8601zone(_tzoffset(self._tz, self._t)) jpayne@68: return "%0.4d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d%s" % ( jpayne@68: self._year, self._month, self._day, jpayne@68: self._hour, self._minute, self._second, tzoffset) jpayne@68: jpayne@68: def HTML4(self): jpayne@68: """Return the object in the format used in the HTML4.0 specification, jpayne@68: one of the standard forms in ISO8601. jpayne@68: jpayne@68: See: http://www.w3.org/TR/NOTE-datetime jpayne@68: jpayne@68: Dates are output as: YYYY-MM-DDTHH:MM:SSZ jpayne@68: T, Z are literal characters. jpayne@68: The time is in UTC. jpayne@68: """ jpayne@68: newdate = self.toZone('UTC') jpayne@68: return "%0.4d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2dZ" % ( jpayne@68: newdate._year, newdate._month, newdate._day, jpayne@68: newdate._hour, newdate._minute, newdate._second) jpayne@68: jpayne@68: def asdatetime(self): jpayne@68: """Return a standard library datetime.datetime jpayne@68: """ jpayne@68: tznaive = self.timezoneNaive() jpayne@68: if tznaive: jpayne@68: tzinfo = None jpayne@68: else: jpayne@68: tzinfo = _TZINFO[self._tz].tzinfo jpayne@68: second = int(self._second) jpayne@68: microsec = self.micros() % 1000000 jpayne@68: dt = datetime(self._year, self._month, self._day, self._hour, jpayne@68: self._minute, second, microsec, tzinfo) jpayne@68: return dt jpayne@68: jpayne@68: def utcdatetime(self): jpayne@68: """Convert the time to UTC and return a timezone naive datetime object jpayne@68: """ jpayne@68: utc = self.toZone('UTC') jpayne@68: second = int(utc._second) jpayne@68: microsec = utc.micros() % 1000000 jpayne@68: dt = datetime(utc._year, utc._month, utc._day, utc._hour, jpayne@68: utc._minute, second, microsec) jpayne@68: return dt jpayne@68: jpayne@68: def __add__(self, other): jpayne@68: """A DateTime may be added to a number and a number may be jpayne@68: added to a DateTime; two DateTimes cannot be added. jpayne@68: """ jpayne@68: if hasattr(other, '_t'): jpayne@68: raise DateTimeError('Cannot add two DateTimes') jpayne@68: o = float(other) jpayne@68: tz = self._tz jpayne@68: omicros = round(o * 86400000000) jpayne@68: tmicros = self.micros() + omicros jpayne@68: t = tmicros / 1000000.0 jpayne@68: d = (tmicros + long(EPOCH * 1000000)) / 86400000000.0 jpayne@68: s = d - math.floor(d) jpayne@68: ms = t - math.floor(t) jpayne@68: x = _calcDependentSecond(tz, t) jpayne@68: yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, ms) jpayne@68: return self.__class__(yr, mo, dy, hr, mn, sc, self._tz, jpayne@68: t, d, s, tmicros, self.timezoneNaive()) jpayne@68: jpayne@68: __radd__ = __add__ jpayne@68: jpayne@68: def __sub__(self, other): jpayne@68: """Either a DateTime or a number may be subtracted from a jpayne@68: DateTime, however, a DateTime may not be subtracted from jpayne@68: a number. jpayne@68: """ jpayne@68: if hasattr(other, '_d'): jpayne@68: return (self.micros() - other.micros()) / 86400000000.0 jpayne@68: else: jpayne@68: return self.__add__(-(other)) jpayne@68: jpayne@68: def __repr__(self): jpayne@68: """Convert a DateTime to a string that looks like a Python jpayne@68: expression. jpayne@68: """ jpayne@68: return '{}(\'{}\')'.format(self.__class__.__name__, str(self)) jpayne@68: jpayne@68: def __str__(self): jpayne@68: """Convert a DateTime to a string.""" jpayne@68: y, m, d = self._year, self._month, self._day jpayne@68: h, mn, s, t = self._hour, self._minute, self._second, self._tz jpayne@68: if s == int(s): jpayne@68: # A whole number of seconds -- suppress milliseconds. jpayne@68: return '%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%2.2d %s' % ( jpayne@68: y, m, d, h, mn, s, t) jpayne@68: else: jpayne@68: # s is already rounded to the nearest microsecond, and jpayne@68: # it's not a whole number of seconds. Be sure to print jpayne@68: # 2 digits before the decimal point. jpayne@68: return '%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%06.6f %s' % ( jpayne@68: y, m, d, h, mn, s, t) jpayne@68: jpayne@68: def __format__(self, fmt): jpayne@68: """Render a DateTime in an f-string.""" jpayne@68: if not isinstance(fmt, str): jpayne@68: raise TypeError("must be str, not %s" % type(fmt).__name__) jpayne@68: if len(fmt) != 0: jpayne@68: return self.strftime(fmt) jpayne@68: return str(self) jpayne@68: jpayne@68: def __hash__(self): jpayne@68: """Compute a hash value for a DateTime.""" jpayne@68: return int(((self._year % 100 * 12 + self._month) * 31 + jpayne@68: self._day + self.time) * 100) jpayne@68: jpayne@68: def __int__(self): jpayne@68: """Convert to an integer number of seconds since the epoch (gmt).""" jpayne@68: return int(self.micros() // 1000000) jpayne@68: jpayne@68: def __long__(self): jpayne@68: """Convert to a long-int number of seconds since the epoch (gmt).""" jpayne@68: return long(self.micros() // 1000000) # pragma: PY2 jpayne@68: jpayne@68: def __float__(self): jpayne@68: """Convert to floating-point number of seconds since the epoch (gmt). jpayne@68: """ jpayne@68: return self.micros() / 1000000.0 jpayne@68: jpayne@68: @property jpayne@68: def _t(self): jpayne@68: return self._micros / 1000000.0 jpayne@68: jpayne@68: def _parse_iso8601(self, s): jpayne@68: # preserve the previously implied contract jpayne@68: # who knows where this could be used... jpayne@68: return self._parse_iso8601_preserving_tznaive(s)[:7] jpayne@68: jpayne@68: def _parse_iso8601_preserving_tznaive(self, s): jpayne@68: try: jpayne@68: return self.__parse_iso8601(s) jpayne@68: except IndexError: jpayne@68: raise SyntaxError( jpayne@68: 'Not an ISO 8601 compliant date string: "%s"' % s) jpayne@68: jpayne@68: def __parse_iso8601(self, s): jpayne@68: """Parse an ISO 8601 compliant date. jpayne@68: jpayne@68: See: http://en.wikipedia.org/wiki/ISO_8601 jpayne@68: """ jpayne@68: month = day = week_day = 1 jpayne@68: year = hour = minute = seconds = hour_off = min_off = 0 jpayne@68: tznaive = True jpayne@68: jpayne@68: iso8601 = iso8601Match(s.strip()) jpayne@68: fields = iso8601 and iso8601.groupdict() or {} jpayne@68: if not iso8601 or fields.get('garbage'): jpayne@68: raise IndexError jpayne@68: jpayne@68: if fields['year']: jpayne@68: year = int(fields['year']) jpayne@68: if fields['month']: jpayne@68: month = int(fields['month']) jpayne@68: if fields['day']: jpayne@68: day = int(fields['day']) jpayne@68: jpayne@68: if fields['year_day']: jpayne@68: d = DateTime('%s-01-01' % year) + int(fields['year_day']) - 1 jpayne@68: month = d.month() jpayne@68: day = d.day() jpayne@68: jpayne@68: if fields['week']: jpayne@68: week = int(fields['week']) jpayne@68: if fields['week_day']: jpayne@68: week_day = int(fields['week_day']) jpayne@68: d = DateTime('%s-01-04' % year) jpayne@68: d = d - (d.dow() + 6) % 7 + week * 7 + week_day - 8 jpayne@68: month = d.month() jpayne@68: day = d.day() jpayne@68: jpayne@68: if fields['hour']: jpayne@68: hour = int(fields['hour']) jpayne@68: jpayne@68: if fields['minute']: jpayne@68: minute = int(fields['minute']) jpayne@68: elif fields['fraction']: jpayne@68: minute = 60.0 * float('0.%s' % fields['fraction']) jpayne@68: seconds, minute = math.modf(minute) jpayne@68: minute = int(minute) jpayne@68: seconds = 60.0 * seconds jpayne@68: # Avoid reprocess when handling seconds, bellow jpayne@68: fields['fraction'] = None jpayne@68: jpayne@68: if fields['second']: jpayne@68: seconds = int(fields['second']) jpayne@68: if fields['fraction']: jpayne@68: seconds = seconds + float('0.%s' % fields['fraction']) jpayne@68: elif fields['fraction']: jpayne@68: seconds = 60.0 * float('0.%s' % fields['fraction']) jpayne@68: jpayne@68: if fields['hour_off']: jpayne@68: hour_off = int(fields['hour_off']) jpayne@68: if fields['signal'] == '-': jpayne@68: hour_off *= -1 jpayne@68: jpayne@68: if fields['min_off']: jpayne@68: min_off = int(fields['min_off']) jpayne@68: jpayne@68: if fields['signal'] or fields['Z']: jpayne@68: tznaive = False jpayne@68: else: jpayne@68: tznaive = True jpayne@68: jpayne@68: # Differ from the specification here. To preserve backwards jpayne@68: # compatibility assume a default timezone == UTC. jpayne@68: tz = 'GMT%+03d%02d' % (hour_off, min_off) jpayne@68: jpayne@68: return year, month, day, hour, minute, seconds, tz, tznaive jpayne@68: jpayne@68: def JulianDay(self): jpayne@68: """Return the Julian day. jpayne@68: jpayne@68: See: https://www.tondering.dk/claus/cal/julperiod.php#formula jpayne@68: """ jpayne@68: a = (14 - self._month) // 12 jpayne@68: y = self._year + 4800 - a jpayne@68: m = self._month + (12 * a) - 3 jpayne@68: return (self._day + (153 * m + 2) // 5 + 365 * y + jpayne@68: y // 4 - y // 100 + y // 400 - 32045) jpayne@68: jpayne@68: def week(self): jpayne@68: """Return the week number according to ISO. jpayne@68: jpayne@68: See: https://www.tondering.dk/claus/cal/week.php#weekno jpayne@68: """ jpayne@68: J = self.JulianDay() jpayne@68: d4 = (J + 31741 - (J % 7)) % 146097 % 36524 % 1461 jpayne@68: L = d4 // 1460 jpayne@68: d1 = ((d4 - L) % 365) + L jpayne@68: return d1 // 7 + 1 jpayne@68: jpayne@68: def encode(self, out): jpayne@68: """Encode value for XML-RPC.""" jpayne@68: out.write('') jpayne@68: out.write(self.ISO8601()) jpayne@68: out.write('\n') jpayne@68: jpayne@68: jpayne@68: # Provide the _dt_reconstructor function here, in case something jpayne@68: # accidentally creates a reference to this function jpayne@68: jpayne@68: orig_reconstructor = copy_reg._reconstructor jpayne@68: jpayne@68: jpayne@68: def _dt_reconstructor(cls, base, state): jpayne@68: if cls is DateTime: jpayne@68: return cls(state) jpayne@68: return orig_reconstructor(cls, base, state)