From 80475bb4d21d1e5ddbb9eb0042adb1113052b38a Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Wed, 25 Dec 2002 07:40:55 +0000 Subject: [PATCH] Implemented datetime.astimezone() and datetimetz.astimezone(). --- Doc/lib/libdatetime.tex | 22 ++++++++ Lib/test/test_datetime.py | 53 ++++++++++++++++++++ Modules/datetimemodule.c | 102 +++++++++++++++++++++++++++++++------- 3 files changed, 159 insertions(+), 18 deletions(-) diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex index f29db5b7b3f..1adbc8e863c 100644 --- a/Doc/lib/libdatetime.tex +++ b/Doc/lib/libdatetime.tex @@ -601,6 +601,11 @@ Instance methods: Return a datetime with the same value, except for those fields given new values by whichever keyword arguments are specified. + - astimezone(tz) + Return a \class{datetimetz} with the same date and time fields, and + with \member{tzinfo} member \var{tz}. \var{tz} must be an instance + of a \class{tzinfo} subclass. + - timetuple() Return a 9-element tuple of the form returned by \function{time.localtime()}. @@ -1083,6 +1088,23 @@ Instance methods: \code{tzinfo=None} can be specified to create a naive datetimetz from an aware datetimetz. + - astimezone(tz) + Return a \class{datetimetz} with new tzinfo member \var{tz}. \var{tz} + must be an instance of a \class{tzinfo} subclass. If self is naive, or + if \code(tz.utcoffset(self)} returns \code{None}, + \code{self.astimezone(tz)} is equivalent to + \code{self.replace(tzinfo=tz)}: a new timezone object is attached + without any conversion of date or time fields. If self is aware and + \code{tz.utcoffset(self)} does not return \code{None}, the date and + time fields are adjusted so that the result is local time in timezone + tz, representing the same UTC time as self. \code{self.astimezone(tz)} + is then equivalent to + \begin{verbatim} + (self - (self.utcoffset() - tz.utcoffset(self)).replace(tzinfo=tz) + \end{verbatim} + where the result of \code{tz.uctcoffset(self)} is converted to a + \class{timedelta} if it's an integer. + - utcoffset() If \member{tzinfo} is \code{None}, returns \code{None}, else \code{tzinfo.utcoffset(self)} converted to a \class{timedelta} diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index 25c77162250..de0d17ab096 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -1295,6 +1295,21 @@ class TestDateTime(TestDate): base = cls(2000, 2, 29) self.assertRaises(ValueError, base.replace, year=2001) + def test_astimezone(self): + # Pretty boring for a datetime! datetimetz is more interesting here. + dt = self.theclass.now() + f = FixedOffset(44, "") + for dtz in dt.astimezone(f), dt.astimezone(tz=f): + self.failUnless(isinstance(dtz, datetimetz)) + self.assertEqual(dt.date(), dtz.date()) + self.assertEqual(dt.time(), dtz.time()) + self.failUnless(dtz.tzinfo is f) + self.assertEqual(dtz.utcoffset(), timedelta(minutes=44)) + + self.assertRaises(TypeError, dt.astimezone) # not enough args + self.assertRaises(TypeError, dt.astimezone, f, f) # too many args + self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type + class TestTime(unittest.TestCase): @@ -2308,6 +2323,44 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase): base = cls(2000, 2, 29) self.assertRaises(ValueError, base.replace, year=2001) + def test_more_astimezone(self): + # The inherited test_astimezone covered some trivial and error cases. + fnone = FixedOffset(None, "None") + f44m = FixedOffset(44, "44") + fm5h = FixedOffset(-timedelta(hours=5), "m300") + + dt = self.theclass.now(tzinfo=f44m) + self.failUnless(dt.tzinfo is f44m) + # Replacing with degenerate tzinfo doesn't do any adjustment. + for x in dt.astimezone(fnone), dt.astimezone(tz=fnone): + self.failUnless(x.tzinfo is fnone) + self.assertEqual(x.date(), dt.date()) + self.assertEqual(x.time(), dt.time()) + # Ditt with None tz. + x = dt.astimezone(tz=None) + self.failUnless(x.tzinfo is None) + self.assertEqual(x.date(), dt.date()) + self.assertEqual(x.time(), dt.time()) + # Ditto replacing with same tzinfo. + x = dt.astimezone(dt.tzinfo) + self.failUnless(x.tzinfo is f44m) + self.assertEqual(x.date(), dt.date()) + self.assertEqual(x.time(), dt.time()) + + # Replacing with different tzinfo does adjust. + got = dt.astimezone(fm5h) + self.failUnless(got.tzinfo is fm5h) + self.assertEqual(got.utcoffset(), timedelta(hours=-5)) + expected = dt - dt.utcoffset() # in effect, convert to UTC + expected += fm5h.utcoffset(dt) # and from there to local time + expected = expected.replace(tzinfo=fm5h) # and attach new tzinfo + self.assertEqual(got.date(), expected.date()) + self.assertEqual(got.time(), expected.time()) + self.assertEqual(got.timetz(), expected.timetz()) + self.failUnless(got.tzinfo is expected.tzinfo) + self.assertEqual(got, expected) + + def test_suite(): allsuites = [unittest.makeSuite(klass, 'test') for klass in (TestModule, diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 6fd10ed558d..d7c6005acd1 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -600,6 +600,18 @@ get_tzinfo_member(PyObject *self) return tzinfo; } +/* self is a datetimetz. Replace its tzinfo member. */ +void +replace_tzinfo(PyObject *self, PyObject *newtzinfo) +{ + assert(self != NULL); + assert(PyDateTimeTZ_Check(self)); + assert(check_tzinfo_subclass(newtzinfo) >= 0); + Py_INCREF(newtzinfo); + Py_DECREF(((PyDateTime_DateTimeTZ *)self)->tzinfo); + ((PyDateTime_DateTimeTZ *)self)->tzinfo = newtzinfo; +} + /* Internal helper. * Call getattr(tzinfo, name)(tzinfoarg), and extract an int from the * result. tzinfo must be an instance of the tzinfo class. If the method @@ -2915,10 +2927,7 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw) TIME_GET_MICROSECOND(time)); if (result && PyTimeTZ_Check(time) && PyDateTimeTZ_Check(result)) { /* Copy the tzinfo field. */ - PyObject *tzinfo = ((PyDateTime_TimeTZ *)time)->tzinfo; - Py_INCREF(tzinfo); - Py_DECREF(((PyDateTime_DateTimeTZ *)result)->tzinfo); - ((PyDateTime_DateTimeTZ *)result)->tzinfo = tzinfo; + replace_tzinfo(result, ((PyDateTime_TimeTZ *)time)->tzinfo); } return result; } @@ -3246,6 +3255,24 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) return clone; } +static PyObject * +datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) +{ + PyObject *tzinfo; + static char *keywords[] = {"tz", NULL}; + + if (! PyArg_ParseTupleAndKeywords(args, kw, "O:astimezone", keywords, + &tzinfo)) + return NULL; + if (check_tzinfo_subclass(tzinfo) < 0) + return NULL; + return new_datetimetz(GET_YEAR(self), GET_MONTH(self), GET_DAY(self), + DATE_GET_HOUR(self), DATE_GET_MINUTE(self), + DATE_GET_SECOND(self), + DATE_GET_MICROSECOND(self), + tzinfo); +} + static PyObject * datetime_timetuple(PyDateTime_DateTime *self) { @@ -3397,6 +3424,9 @@ static PyMethodDef datetime_methods[] = { {"replace", (PyCFunction)datetime_replace, METH_KEYWORDS, PyDoc_STR("Return datetime with new specified fields.")}, + {"astimezone", (PyCFunction)datetime_astimezone, METH_KEYWORDS, + PyDoc_STR("tz -> datetimetz with same date & time, and tzinfo=tz\n")}, + {"__setstate__", (PyCFunction)datetime_setstate, METH_O, PyDoc_STR("__setstate__(state)")}, @@ -4398,20 +4428,6 @@ static PyGetSetDef datetimetz_getset[] = { * optional tzinfo argument. */ -/* Internal helper. - * self is a datetimetz. Replace its tzinfo member. - */ -void -replace_tzinfo(PyObject *self, PyObject *newtzinfo) -{ - assert(self != NULL); - assert(newtzinfo != NULL); - assert(PyDateTimeTZ_Check(self)); - Py_INCREF(newtzinfo); - Py_DECREF(((PyDateTime_DateTimeTZ *)self)->tzinfo); - ((PyDateTime_DateTimeTZ *)self)->tzinfo = newtzinfo; -} - static char *datetimetz_kws[] = { "year", "month", "day", "hour", "minute", "second", "microsecond", "tzinfo", NULL @@ -4696,6 +4712,53 @@ datetimetz_replace(PyDateTime_DateTimeTZ *self, PyObject *args, PyObject *kw) return clone; } +static PyObject * +datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args, + PyObject *kw) +{ + int y = GET_YEAR(self); + int m = GET_MONTH(self); + int d = GET_DAY(self); + int hh = DATE_GET_HOUR(self); + int mm = DATE_GET_MINUTE(self); + int ss = DATE_GET_SECOND(self); + int us = DATE_GET_MICROSECOND(self); + + PyObject *tzinfo; + static char *keywords[] = {"tz", NULL}; + + if (! PyArg_ParseTupleAndKeywords(args, kw, "O:astimezone", keywords, + &tzinfo)) + return NULL; + if (check_tzinfo_subclass(tzinfo) < 0) + return NULL; + + if (tzinfo != Py_None && self->tzinfo != Py_None) { + int none; + int selfoffset; + selfoffset = call_utcoffset(self->tzinfo, + (PyObject *)self, + &none); + if (selfoffset == -1 && PyErr_Occurred()) + return NULL; + if (! none) { + int tzoffset; + tzoffset = call_utcoffset(tzinfo, + (PyObject *)self, + &none); + if (tzoffset == -1 && PyErr_Occurred()) + return NULL; + if (! none) { + mm -= selfoffset - tzoffset; + if (normalize_datetime(&y, &m, &d, + &hh, &mm, &ss, &us) < 0) + return NULL; + } + } + } + return new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); +} + static PyObject * datetimetz_timetuple(PyDateTime_DateTimeTZ *self) { @@ -4908,6 +4971,9 @@ static PyMethodDef datetimetz_methods[] = { {"replace", (PyCFunction)datetimetz_replace, METH_KEYWORDS, PyDoc_STR("Return datetimetz with new specified fields.")}, + {"astimezone", (PyCFunction)datetimetz_astimezone, METH_KEYWORDS, + PyDoc_STR("tz -> convert to local time in new timezone tz\n")}, + {"__setstate__", (PyCFunction)datetimetz_setstate, METH_O, PyDoc_STR("__setstate__(state)")},