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