Handle time zones with seconds in the UTC offset.

The offset is rounded to the nearest minute.
This commit is contained in:
Daniele Varrazzo 2010-05-20 02:07:50 +01:00
parent e3d489f701
commit 305d86f38a
4 changed files with 66 additions and 54 deletions

View File

@ -1,3 +1,8 @@
2010-05-20 Daniele Varrazzo <daniele.varrazzo@gmail.com>
* psycopg/typecast_datetime.c: Round seconds in historical timezones to
the nearest minute.
2010-05-17 Federico Di Gregorio <fog@initd.org>
* Release 2.2.1.

View File

@ -365,6 +365,38 @@ the connection or globally: see the function
and then forget about this story.
.. index::
single: Time Zones
.. _tz-handling:
Time zones handling
^^^^^^^^^^^^^^^^^^^
The PostgreSQL type :sql:`timestamp with time zone` is converted into Python
`!datetime` objects with a `!tzinfo` attribute set to a
`~psycopg2.tz.FixedOffsetTimezone` instance.
>>> cur.execute("SET TIME ZONE 'Europe/Rome';") # UTC + 1 hour
>>> cur.execute("SELECT '2010-01-01 10:30:45'::timestamptz;")
>>> cur.fetchone()[0].tzinfo
psycopg2.tz.FixedOffsetTimezone(offset=60, name=None)
Notice that only time zones with an integer number of minutes are supported:
this is a limitation of the Python `!datetime` module. A few historical time
zones had seconds in the UTC offset: these time zones will have the offset
rounded to the nearest minute, with an error of up to 30 seconds.
>>> cur.execute("SET TIME ZONE 'Asia/Calcutta';") # offset was +5:53:20
>>> cur.execute("SELECT '1930-01-01 10:30:45'::timestamptz;")
>>> cur.fetchone()[0].tzinfo
psycopg2.tz.FixedOffsetTimezone(offset=353, name=None)
.. versionchanged:: 2.2.2
timezones with seconds are supported (with rounding). Previously such
timezones raised an error. In order to deal with them in previous
versions use `psycopg2.extras.register_tstz_w_secs`.
.. index:: Transaction, Begin, Commit, Rollback, Autocommit

View File

@ -134,15 +134,11 @@ typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
Dprintf("typecast_PYDATETIME_cast: UTC offset = %ds", tz);
/* The datetime module requires that time zone offsets be
a whole number of minutes, so fail if we have a time
zone with a seconds offset.
*/
if (tz % 60 != 0) {
PyErr_Format(PyExc_ValueError, "time zone offset %d is not "
"a whole number of minutes", tz);
return NULL;
}
tzinfo = PyObject_CallFunction(tzinfo_factory, "i", tz / 60);
a whole number of minutes, so truncate the seconds to the
closest minute. */
// printf("%d %d %d\n", tz, tzmin, round(tz / 60.0));
tzinfo = PyObject_CallFunction(tzinfo_factory, "i",
(int)round(tz / 60.0));
} else {
Py_INCREF(Py_None);
tzinfo = Py_None;
@ -192,15 +188,10 @@ typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
Dprintf("typecast_PYTIME_cast: UTC offset = %ds", tz);
/* The datetime module requires that time zone offsets be
a whole number of minutes, so fail if we have a time
zone with a seconds offset.
*/
if (tz % 60 != 0) {
PyErr_Format(PyExc_ValueError, "time zone offset %d is not "
"a whole number of minutes", tz);
return NULL;
}
tzinfo = PyObject_CallFunction(tzinfo_factory, "i", tz / 60);
a whole number of minutes, so truncate the seconds to the
closest minute. */
tzinfo = PyObject_CallFunction(tzinfo_factory, "i",
(int)round(tz / 60.0));
} else {
Py_INCREF(Py_None);
tzinfo = Py_None;

View File

@ -124,24 +124,16 @@ class DatetimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
self.check_time_tz("+01:15", 4500)
self.check_time_tz("-01:15", -4500)
# The Python datetime module does not support time zone
# offsets that are not a whole number of minutes, so we get an
# error here. Check that we are generating an understandable
# error message.
try:
self.check_time_tz("+01:15:42", 4542)
except ValueError, exc:
self.assertEqual(str(exc), "time zone offset 4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
try:
self.check_time_tz("-01:15:42", -4542)
except ValueError, exc:
self.assertEqual(str(exc), "time zone offset -4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
# offsets that are not a whole number of minutes.
# We round the offset to the nearest minute.
self.check_time_tz("+01:15:00", 60 * (60 + 15))
self.check_time_tz("+01:15:29", 60 * (60 + 15))
self.check_time_tz("+01:15:30", 60 * (60 + 16))
self.check_time_tz("+01:15:59", 60 * (60 + 16))
self.check_time_tz("-01:15:00", -60 * (60 + 15))
self.check_time_tz("-01:15:29", -60 * (60 + 15))
self.check_time_tz("-01:15:30", -60 * (60 + 16))
self.check_time_tz("-01:15:59", -60 * (60 + 16))
def check_datetime_tz(self, str_offset, offset):
from datetime import datetime, timedelta
@ -168,24 +160,16 @@ class DatetimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
self.check_datetime_tz("+01:15", 4500)
self.check_datetime_tz("-01:15", -4500)
# The Python datetime module does not support time zone
# offsets that are not a whole number of minutes, so we get an
# error here. Check that we are generating an understandable
# error message.
try:
self.check_datetime_tz("+01:15:42", 4542)
except ValueError, exc:
self.assertEqual(str(exc), "time zone offset 4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
try:
self.check_datetime_tz("-01:15:42", -4542)
except ValueError, exc:
self.assertEqual(str(exc), "time zone offset -4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
# offsets that are not a whole number of minutes.
# We round the offset to the nearest minute.
self.check_datetime_tz("+01:15:00", 60 * (60 + 15))
self.check_datetime_tz("+01:15:29", 60 * (60 + 15))
self.check_datetime_tz("+01:15:30", 60 * (60 + 16))
self.check_datetime_tz("+01:15:59", 60 * (60 + 16))
self.check_datetime_tz("-01:15:00", -60 * (60 + 15))
self.check_datetime_tz("-01:15:29", -60 * (60 + 15))
self.check_datetime_tz("-01:15:30", -60 * (60 + 16))
self.check_datetime_tz("-01:15:59", -60 * (60 + 16))
def test_parse_time_no_timezone(self):
self.assertEqual(self.TIME("13:30:29", self.curs).tzinfo, None)