Index: Doc/lib/libtime.tex =================================================================== --- Doc/lib/libtime.tex (revision 56710) +++ Doc/lib/libtime.tex (working copy) @@ -80,9 +80,9 @@ The time value as returned by \function{gmtime()}, \function{localtime()}, and \function{strptime()}, and accepted by \function{asctime()}, \function{mktime()} and \function{strftime()}, -is a sequence of 9 integers. The return values of \function{gmtime()}, -\function{localtime()}, and \function{strptime()} also offer attribute -names for individual fields. +may be considered as a sequence of 9 integers. The return values of +\function{gmtime()}, \function{localtime()}, and \function{strptime()} +also offer attribute names for individual fields. \begin{tableiii}{c|l|l}{textrm}{Index}{Attribute}{Values} \lineiii{0}{\member{tm_year}}{(for example, 1993)} @@ -94,6 +94,8 @@ \lineiii{6}{\member{tm_wday}}{range [0,6], Monday is 0} \lineiii{7}{\member{tm_yday}}{range [1,366]} \lineiii{8}{\member{tm_isdst}}{0, 1 or -1; see below} + \lineiii{-}{\member{tm_gmtoff}}{offset from UTC/GMT in seconds east of the Prime Meridian; see below} + \lineiii{-}{\member{tm_zone}}{time zone name; see below} \end{tableiii} Note that unlike the C structure, the month value is a @@ -102,6 +104,19 @@ daylight savings flag, passed to \function{mktime()} will usually result in the correct daylight savings state to be filled in. +To maintain backwards compatibility, the \member{tm_gmtoff} and +\member{tm_zone} attributes are not +included in the visual representation of each \class{struct_time}, but +they can be inspected as named, read-only attributes. Since functions +such as \function{mktime()} accept either a 9-tuple or a +\class{struct_time}, to manually construct a time with specific values +of these attributes, first construct an 11-tuple using the normal +9-tuple extended to include values for \member{tm_gmtoff} and +\member{tm_zone} as the last two elements, then pass this tuple to the +\class{struct_time} constructor. It is also possible to omit +\member{tm_zone} and to provide a 10-tuple to the \class{struct_time} +constructor. + When a tuple with an incorrect length is passed to a function expecting a \class{struct_time}, or having elements of the wrong type, a \exception{TypeError} is raised. @@ -109,6 +124,9 @@ \versionchanged[The time value sequence was changed from a tuple to a \class{struct_time}, with the addition of attribute names for the fields]{2.2} +\versionchanged[The time value sequence was extended to provide the + \member{tm_gmtoff} and \member{tm_zone} attributes as + described above]{2.6} \end{itemize} The module defines the following functions and data items: @@ -206,6 +224,24 @@ earliest date for which it can generate a time is platform-dependent. \end{funcdesc} +\begin{funcdesc}{mktimetz}{t} +This function interprets times in arbitrary time zones, producing a +value indicating the number of seconds since the epoch for each time. +It combines the ability of \function{mktime()} to convert local times +with the ability of \function{timegm()} to convert GMT/UTC times, but +is not constrained by the system's current time zone, instead +obtaining time zone information directly from its argument where +possible. Its argument is the \class{struct_time} (which may provide +time zone information) or a 9-tuple (which does not, causing the +argument to be interpreted as a time in the GMT/UTC zone). It returns +a floating point number, for compatibility with \function{time()}. If +the input value cannot be represented as a valid time, either +\exception{OverflowError} or \exception{ValueError} will be raised +(which depends on whether the invalid value is caught by Python or the +underlying C libraries). The earliest date for which it can generate +a time is platform-dependent. \versionadded{2.6} +\end{funcdesc} + \begin{funcdesc}{sleep}{secs} Suspend execution for the given number of seconds. The argument may be a floating point number to indicate a more precise sleep time. @@ -260,6 +296,10 @@ \lineiii{\%X}{Locale's appropriate time representation.}{} \lineiii{\%y}{Year without century as a decimal number [00,99].}{} \lineiii{\%Y}{Year with century as a decimal number.}{} + \lineiii{\%z}{Time zone offset indicating a positive or negative + time difference from UTC/GMT of the form +HHMM or -HHMM, + where H represents decimal hour digits and M represents + decimal minute digits [00,59].}{} \lineiii{\%Z}{Time zone name (no characters if no time zone exists).}{} \lineiii{\%\%}{A literal \character{\%} character.}{} \end{tableiii} @@ -355,6 +395,19 @@ call if the system clock has been set back between the two calls. \end{funcdesc} +\begin{funcdesc}{timegm}{t} +This is the inverse function of \function{gmtime()}. Its argument +is a \class{struct_time} or full 9-tuple which expresses the time in +\emph{GMT/UTC} time, not local time as expected by +\function{mktime()}. It returns a floating point number, for +compatibility with \function{time()}. If the input value cannot be +represented as a valid time, either \exception{OverflowError} or +\exception{ValueError} will be raised (which depends on whether the +invalid value is caught by Python or the underlying C libraries). The +earliest date for which it can generate a time is platform-dependent. +\versionadded{2.6} +\end{funcdesc} + \begin{datadesc}{timezone} The offset of the local (non-DST) timezone, in seconds west of UTC (negative in most of Western Europe, positive in the US, zero in the @@ -471,5 +524,7 @@ the functions in the \module{time} module.} \seemodule{calendar}{General calendar-related functions. \function{timegm()} is the inverse of - \function{gmtime()} from this module.} + \function{gmtime()} from this module, offering an + alternative implementation of \function{timegm()} + from this module.} \end{seealso} Index: Lib/_strptime.py =================================================================== --- Lib/_strptime.py (revision 56710) +++ Lib/_strptime.py (working copy) @@ -203,6 +203,7 @@ #XXX: Does 'Y' need to worry about having less or more than # 4 digits? 'Y': r"(?P\d\d\d\d)", + 'z': r"(?P[+-]\d\d[0-5]\d)", 'A': self.__seqToRE(self.locale_time.f_weekday, 'A'), 'a': self.__seqToRE(self.locale_time.a_weekday, 'a'), 'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'), @@ -329,6 +330,7 @@ month = day = 1 hour = minute = second = 0 tz = -1 + tzoffset = None # Default to -1 to signify that values not known; not critical to have, # though week_of_year = -1 @@ -404,6 +406,10 @@ else: # W starts week on Monday. week_of_year_start = 0 + elif group_key == 'z': + tzoffset = int(found_dict['z'][1:3]) * 60 + int(found_dict['z'][3:5]) + if found_dict['z'][0] == "-": + tzoffset = -tzoffset elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. @@ -440,6 +446,19 @@ day = datetime_result.day if weekday == -1: weekday = datetime_date(year, month, day).weekday() + # Add extra time structure named attributes. + if tz != -1: + tzname = found_dict.get("Z") + if found_zone in ("utc", "gmt"): + gmtoff = 0 + else: + gmtoff = [-time.timezone, -time.altzone][tz] + elif tzoffset is not None: + gmtoff = tzoffset * 60 + tzname = None + else: + gmtoff = 0 + tzname = None return time.struct_time((year, month, day, hour, minute, second, - weekday, julian, tz)) + weekday, julian, tz, gmtoff, tzname)) Index: Lib/test/test_time.py =================================================================== --- Lib/test/test_time.py (revision 56710) +++ Lib/test/test_time.py (working copy) @@ -218,6 +218,102 @@ t1 = time.mktime(lt1) self.assert_(0 <= (t1-t0) < 0.2) + def test_localtime_timezone(self): + # Get the localtime and examine it for the offset and zone. + lt = time.localtime() + self.assert_(hasattr(lt, "tm_gmtoff")) + self.assert_(hasattr(lt, "tm_zone")) + # See if the offset and zone are similar to the module + # attributes. + self.assert_(lt.tm_gmtoff is None and not hasattr(time, "timezone") or lt.tm_gmtoff == -[time.timezone, time.altzone][lt.tm_isdst]) + self.assert_(lt.tm_zone is None and not hasattr(time, "tzname") or lt.tm_zone == time.tzname[lt.tm_isdst]) + # Try and make UNIX times from the localtime and a 9-tuple + # created from the localtime. Test to see that the times are + # the same. + t = time.mktime(lt); t9 = time.mktime(lt[:9]) + self.assert_(t == t9) + # Make localtimes from the UNIX times and compare them to + # the original localtime, thus making a round trip. + new_lt = time.localtime(t); new_lt9 = time.localtime(t9) + self.assert_(new_lt == lt) + self.assert_(new_lt.tm_gmtoff is None and not hasattr(time, "timezone") or new_lt.tm_gmtoff == lt.tm_gmtoff) + self.assert_(new_lt.tm_zone is None and not hasattr(time, "tzname") or new_lt.tm_zone == lt.tm_zone) + self.assert_(new_lt9 == lt) + self.assert_(new_lt9.tm_gmtoff is None and not hasattr(time, "timezone") or new_lt.tm_gmtoff == lt.tm_gmtoff) + self.assert_(new_lt9.tm_zone is None and not hasattr(time, "tzname") or new_lt9.tm_zone == lt.tm_zone) + + def test_timegm(self): + # Check round trip using GMT/UTC times. + self.assert_(time.timegm(time.gmtime(0)) == 0) + # Check equivalence in reinterpreting times. + self.assert_(time.timegm(time.localtime(0)) == -time.mktime(time.gmtime(0))) + # Check round trip using GMT/UTC times for the current time. + t = time.time() + dt = abs(time.timegm(time.gmtime(t)) - t) + self.assert_(0 <= dt < 1) + # Check that the local time, reinterpreted as a GMT/UTC time, + # differs from the original UNIX time by only the timezone + # offset. + lt = time.localtime(t) + dt = abs(time.timegm(lt) + [time.timezone, time.altzone][lt.tm_isdst] - t) + self.assert_(0 <= dt < 1) + + def test_mktimetz(self): + # Check basic properties. + self.assert_(time.mktimetz(time.gmtime(0)) == 0) + self.assert_(time.mktimetz(time.localtime(0)) == 0) + self.assert_(time.mktimetz(time.gmtime(0)[:9]) == 0) + # Check the properties for the current time. + t = time.time() + lt = time.localtime(t) + gt = time.gmtime(t) + self.assert_(time.mktimetz(lt) == time.mktimetz(lt)) + self.assert_(time.mktimetz(gt) == time.mktimetz(gt)) + self.assert_(time.mktimetz(lt) == time.mktimetz(gt)) + self.assert_(0 <= abs(time.mktimetz(lt) - t) < 1) + self.assert_(0 <= abs(time.mktimetz(gt) - t) < 1) + # Adjust time zones and measure differences. + # Get the gmtime from above, retain the "clock time" but modify the time + # zone so that the real time is either past the original gmtime or + # before it. + fmt = "%Y-%m-%d %H:%M:%S " + # Try for other times of the year (possible DST differences). + # In order to check that various corrections work, attempt to provoke + # different DST adjustments. + month_plus_5 = ((gt[1] + 4) % 12) + 1 + month_minus_5 = ((gt[1] - 4) % 12) + 1 + # Reset the tm_isdst field in order to provoke a DST lookup. + # One or the other of these other times should employ a different DST + # status for many time zones. + gt_plus_5 = time.struct_time(gt[0:1] + (month_plus_5,) + gt[2:8] + (-1, gt.tm_gmtoff, gt.tm_zone)) + gt_minus_5 = time.struct_time(gt[0:1] + (month_minus_5,) + gt[2:8] + (-1, gt.tm_gmtoff, gt.tm_zone)) + # Try each of the times. + for gt_test in (gt, gt_plus_5, gt_minus_5): + # Make the basis of each revised time (this is the "clock time" + # string). + timestr = time.strftime(fmt, gt_test) + # Remember the UNIX time value for the time. + t_test = time.mktimetz(gt_test) + # For adjustments of -15 to +15 hours... + for i in range(-15, 16): + # And 0 to 50 minutes (in 10 minute intervals)... + for j in range(0, 60, 10): + # Times with offsets such as -1500, -0715 will be later than + # the original time. + if i < 0: + sign = "-" + difference = -i * 3600 + j * 60 + # Times with offsets such as +0715, +1500 will be earlier + # than the original time. + else: + sign = "+" + difference = -(i * 3600 + j * 60) + # Make a new time string and interpret it. + pt = time.strptime(timestr + sign + ("%02d%02d" % (abs(i), j)), fmt + "%z") + # Measure the difference. + dt = abs(time.mktimetz(pt) - t_test - difference) + self.assert_(0 <= dt < 1) + def test_main(): test_support.run_unittest(TimeTestCase) Index: Modules/timemodule.c =================================================================== --- Modules/timemodule.c (revision 56710) +++ Modules/timemodule.c (working copy) @@ -225,6 +225,8 @@ {"tm_wday", NULL}, {"tm_yday", NULL}, {"tm_isdst", NULL}, + {"tm_gmtoff", NULL}, + {"tm_zone", NULL}, {0} }; @@ -238,8 +240,45 @@ static int initialized; static PyTypeObject StructTimeType; +#ifndef HAVE_STRUCT_TM_TM_ZONE +static void +_tmtotuple_tz_helper(struct tm *p, PyObject *v) +{ + PyObject *timezone, *altzone, *tzname; + timezone = PyDict_GetItemString(moddict, "timezone"); + altzone = PyDict_GetItemString(moddict, "altzone"); + if ((timezone != NULL) && PyInt_Check(timezone) && + (altzone != NULL) && PyInt_Check(altzone)) { + long tz = 0; + if (p->tm_isdst == 0) + tz = - PyInt_AsLong(timezone); + else if (p->tm_isdst > 0) + tz = - PyInt_AsLong(altzone); + PyStructSequence_SET_ITEM(v, 9, PyInt_FromLong(tz)); + } else { + PyStructSequence_SET_ITEM(v, 9, Py_None); + Py_INCREF(Py_None); + } + + tzname = PyDict_GetItemString(moddict, "tzname"); + if ((tzname != NULL) && PyTuple_Check(tzname) && + PyTuple_Size(tzname) >= 2 && (p->tm_isdst >= 0)) { + PyObject *tzstrobj = PyTuple_GET_ITEM(tzname, + (p->tm_isdst > 0)); + if (!PyString_Check(tzstrobj)) + goto notzname; + Py_INCREF(tzstrobj); + PyStructSequence_SET_ITEM(v, 10, tzstrobj); + } else { + notzname: + PyStructSequence_SET_ITEM(v, 10, Py_None); + Py_INCREF(Py_None); + } +} +#endif + static PyObject * -tmtotuple(struct tm *p) +tmtotuple(struct tm *p, int isgmt) { PyObject *v = PyStructSequence_New(&StructTimeType); if (v == NULL) @@ -256,6 +295,17 @@ SET(6, (p->tm_wday + 6) % 7); /* Want Monday == 0 */ SET(7, p->tm_yday + 1); /* Want January, 1 == 1 */ SET(8, p->tm_isdst); +#ifdef HAVE_STRUCT_TM_TM_ZONE + SET(9, p->tm_gmtoff); + PyStructSequence_SET_ITEM(v, 10, PyString_FromString(p->tm_zone)); +#else + if (isgmt) { + SET(9, 0); + PyStructSequence_SET_ITEM(v, 10, PyString_FromString("GMT")); + } else { + _tmtotuple_tz_helper(p, v); + } +#endif #undef SET if (PyErr_Occurred()) { Py_XDECREF(v); @@ -266,7 +316,7 @@ } static PyObject * -time_convert(double when, struct tm * (*function)(const time_t *)) +time_convert(double when, struct tm * (*function)(const time_t *), int isgmt) { struct tm *p; time_t whent = _PyTime_DoubleToTimet(when); @@ -282,7 +332,7 @@ #endif return PyErr_SetFromErrno(PyExc_ValueError); } - return tmtotuple(p); + return tmtotuple(p, isgmt); } /* Parse arg tuple that can contain an optional float-or-None value; @@ -313,7 +363,7 @@ double when; if (!parse_time_double_args(args, "|O:gmtime", &when)) return NULL; - return time_convert(when, gmtime); + return time_convert(when, gmtime, 1); } PyDoc_STRVAR(gmtime_doc, @@ -329,14 +379,23 @@ double when; if (!parse_time_double_args(args, "|O:localtime", &when)) return NULL; - return time_convert(when, localtime); + return time_convert(when, localtime, 0); } PyDoc_STRVAR(localtime_doc, "localtime([seconds]) -> (tm_year,tm_mon,tm_day,tm_hour,tm_min,tm_sec,tm_wday,tm_yday,tm_isdst)\n\ \n\ Convert seconds since the Epoch to a time tuple expressing local time.\n\ -When 'seconds' is not passed in, convert the current time instead."); +When 'seconds' is not passed in, convert the current time instead.\n\ +\n\ +If time zone information is available, the tm_gmtoff and tm_zone\n\ +attributes may be consulted on the time tuple to obtain, respectively,\n\ +the location of the associated time zone in seconds east of GMT/UTC\n\ +and the name of the zone. Note that the value of tm_gmtoff may be\n\ +negative for time zones west of the prime meridian, for example.\n\ +\n\ +Where no time zone information is available, tm_gmtoff and tm_zone\n\ +will both be None."); static int gettmarg(PyObject *args, struct tm *p) @@ -355,6 +414,19 @@ &p->tm_yday, &p->tm_isdst)) return 0; +#ifdef HAVE_STRUCT_TM_TM_ZONE + /* Add GMT/UTC offset where a time structure is provided. */ + if (PyType_IsSubtype(args->ob_type, &StructTimeType)) { + PyObject *gmtoff = ((PyStructSequence *)(args))->ob_item[9]; + PyObject *zone = ((PyStructSequence *)(args))->ob_item[10]; + if (gmtoff != Py_None) + p->tm_gmtoff = PyInt_AsLong(gmtoff); + if (zone != Py_None) + p->tm_zone = PyString_AsString(zone); + if (PyErr_Occurred()) + return 0; + } +#endif if (y < 1900) { PyObject *accept = PyDict_GetItemString(moddict, "accept2dyear"); @@ -530,7 +602,6 @@ Parse a string to a time tuple according to a format specification.\n\ See the library reference manual for formatting codes (same as strftime())."); - static PyObject * time_asctime(PyObject *self, PyObject *args) { @@ -599,6 +670,7 @@ { struct tm buf; time_t tt; + /* Seemingly useless calls to make things "NeXT robust" (rev. 6061). */ tt = time(&tt); buf = *localtime(&tt); if (!gettmarg(tup, &buf)) @@ -615,7 +687,114 @@ PyDoc_STRVAR(mktime_doc, "mktime(tuple) -> floating point number\n\ \n\ -Convert a time tuple in local time to seconds since the Epoch."); +Convert a time tuple in local time to seconds since the Epoch.\n\ +Note that mktime(gmtime(0)) will not generally return zero for most\n\ +time zones; instead the returned value will either be equal to that\n\ +of the timezone or altzone attributes on the time module."); + +/* A time adjustment function employing the module's timezone and + * altzone attributes. This reverts any local time correction done by + * mktime. + */ + +static void +adjusttime(int isdst, time_t *tt) +{ + PyObject *timezone, *altzone; + timezone = PyDict_GetItemString(moddict, "timezone"); + altzone = PyDict_GetItemString(moddict, "altzone"); + if ((timezone != NULL) && PyInt_Check(timezone) && + (altzone != NULL) && PyInt_Check(altzone)) { + if (isdst == 0) + *tt -= PyInt_AsLong(timezone); + else if (isdst > 0) + *tt -= PyInt_AsLong(altzone); + } +} + +/* A function producing UNIX times, optionally consulting time tuple + * or structure information in order to handle local times. + */ + +static PyObject * +unixtime(PyObject *self, PyObject *args, int toutc) +{ + PyObject *tup; + struct tm buf; + time_t tt; + long gmtoff, isdst; + if (!PyArg_ParseTuple(args, "O:time", &tup)) + return NULL; + + /* Seemingly useless calls to make things "NeXT robust" (rev. 6061). */ + tt = time(&tt); + buf = *localtime(&tt); + if (!gettmarg(tup, &buf)) + return NULL; + + /* Remember things before mktime changes them. */ +#ifdef HAVE_STRUCT_TM_TM_ZONE + gmtoff = buf.tm_gmtoff; + isdst = buf.tm_isdst; +#else + gmtoff = 0; + isdst = 0; + if (PyType_IsSubtype(tup->ob_type, &StructTimeType)) { + PyObject *isdst_element = ((PyStructSequence *)(tup))->ob_item[8]; + PyObject *gmtoff_element = ((PyStructSequence *)(tup))->ob_item[9]; + if (isdst_element != Py_None) + isdst = PyInt_AsLong(isdst_element); + if (gmtoff_element != Py_None) + gmtoff = PyInt_AsLong(gmtoff_element); + } +#endif + + tt = mktime(&buf); + if (tt == (time_t)(-1)) { + PyErr_SetString(PyExc_OverflowError, + "timegm or mktimetz argument out of range"); + return NULL; + } + + /* Adjust the result using tm_gmtoff, if possible. */ + if (toutc) { + tt -= gmtoff; + } + + /* Revert any mktime corrections. + * Use the original isdst value if appropriate; otherwise, get the value + * produced by mktime. + */ + adjusttime(isdst != -1 ? isdst : buf.tm_isdst, &tt); + return PyFloat_FromDouble((double)tt); +} + +static PyObject * +time_timegm(PyObject *self, PyObject *args) +{ + return unixtime(self, args, 0); +} + +PyDoc_STRVAR(timegm_doc, +"timegm(tuple) -> floating point number\n\ +\n\ +Convert a time tuple considered to be in the GMT/UTC time zone to\n\ +seconds since the Epoch. Note that timegm(gmtime(0)) should always\n\ +return zero."); + +static PyObject * +time_mktimetz(PyObject *self, PyObject *args) +{ + return unixtime(self, args, 1); +} + +PyDoc_STRVAR(mktimetz_doc, +"mktimetz(tuple) -> floating point number\n\ +\n\ +Convert a time tuple to seconds since the Epoch using time zone\n\ +information available on the time tuple if possible. Note that\n\ +mktimetz(gmtime(0)) and mktimetz(localtime(0)) should both always\n\ +return zero."); #endif /* HAVE_MKTIME */ #ifdef HAVE_WORKING_TZSET @@ -672,7 +851,9 @@ And I'm lazy and hate C so nyer. */ -#if defined(HAVE_TZNAME) && !defined(__GLIBC__) && !defined(__CYGWIN__) +#if defined(HAVE_TZNAME) /* && !defined(__GLIBC__) && !defined(__CYGWIN__) */ + extern int daylight; + extern long timezone; tzset(); #ifdef PYOS_OS2 PyModule_AddIntConstant(m, "timezone", _timezone); @@ -755,6 +936,8 @@ {"ctime", time_ctime, METH_VARARGS, ctime_doc}, #ifdef HAVE_MKTIME {"mktime", time_mktime, METH_O, mktime_doc}, + {"timegm", time_timegm, METH_VARARGS, timegm_doc}, + {"mktimetz", time_mktimetz, METH_VARARGS, mktimetz_doc}, #endif #ifdef HAVE_STRFTIME {"strftime", time_strftime, METH_VARARGS, strftime_doc}, @@ -808,8 +991,10 @@ asctime() -- convert time tuple to string\n\ ctime() -- convert time in seconds to string\n\ mktime() -- convert local time tuple to seconds since Epoch\n\ +mktimetz() -- convert time structure with time zone to seconds since Epoch\n\ strftime() -- convert time tuple to string according to format specification\n\ strptime() -- parse string to time tuple according to format specification\n\ +timegm() -- convert UTC time tuple to seconds since Epoch\n\ tzset() -- change the local timezone");