#10912 closed defect (fixed)
Wrong arithmetic datetime with LocalTimezone if across a DST boundary
Reported by: | Jun Omae | Owned by: | Jun Omae |
---|---|---|---|
Priority: | normal | Milestone: | 0.12.5 |
Component: | general | Version: | |
Severity: | normal | Keywords: | datetime pytz timezone |
Cc: | Branch: | ||
Release Notes: |
Fix of datetime arithmetic with |
||
API Changes: |
|
||
Internal Changes: |
Description
If arithmetic datetime with LocalTimezone
and timedelta across a DST boundary, the result have the lack of consistency.
$ TZ=Europe/Paris ~/venv/trac/0.12.4/bin/python Python 2.4.3 (#1, Jun 18 2012, 08:55:31) [GCC 4.1.2 20080704 (Red Hat 4.1.2-52)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from datetime import datetime, timedelta >>> from trac.util.datefmt import to_datetime, localtz, to_timestamp, utc >>> localtz <LocalTimezone "CET" 1:00:00 "CEST" 2:00:00> >>> t1 = datetime(2012, 3, 25, 1, 15, tzinfo=localtz) >>> t2 = t1 + timedelta(hours=1) >>> t1.isoformat() '2012-03-25T01:15:00+01:00' >>> t2.isoformat() '2012-03-25T02:15:00+02:00' # Expects 2012-03-25T02:15:00+01:00 # or 2012-03-25T03:15:00+02:00 >>> t1 == t2 False >>> t1 - t1.utcoffset() == t2 - t2.utcoffset() True # Expects `False` >>> to_timestamp(t2) - to_timestamp(t1) 0 # Expects 3600
If arithmetic datetime with pytz
across a DST boundary, the value of datetime.utcoffset()
remains unchanged. tzinfo.normalize()
method changes the utcoffset()
value.
>>> from trac.util.datefmt import timezone >>> tz = timezone('Europe/Paris') >>> t1 = tz.normalize(tz.localize(datetime(2012, 3, 25, 1, 15))) >>> t1 datetime.datetime(2012, 3, 25, 1, 15, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>) >>> t2 = t1 + timedelta(hours=1) >>> t1.isoformat() '2012-03-25T01:15:00+01:00' >>> t2.isoformat() '2012-03-25T02:15:00+01:00' >>> t3 = t2 + timedelta(hours=1) >>> t3.isoformat() '2012-03-25T03:15:00+01:00' >>> tz.normalize(t2).isoformat() '2012-03-25T03:15:00+02:00'
Attachments (0)
Change History (10)
comment:1 by , 12 years ago
comment:2 by , 12 years ago
In paris, Daylight Saving Time started on 25th March 2012 at 2.00am.
in case t1 == '2012-03-25T01:15:00+01:00'
t1 + 1 hours == '2012-03-25T03:15:00+02:00'
comment:3 by , 12 years ago
Keywords: | pytz added |
---|
comment:4 by , 12 years ago
I worked in repos:jomae.git:ticket10912/localtz. After the changes, LocalTimezone
behaves like pytz.tzinfo
. I confirmed on CentOS 5.8 and Windows XP sp3 with timezone in Paris. It needs more testing….
comment:5 by , 12 years ago
[2ec1cf3e/jomae.git] could perhaps benefit from a LocalTimezone._localtime(dt)
→ localized time or None
helper method, but other than that looks great! I'll test and let you know.
follow-up: 7 comment:6 by , 12 years ago
I just tested [63b4287d/jomae.git], redoing the steps from comment:1:
>>> localtz.normalize(t1 + timedelta(hours=0)).isoformat() '2012-03-25T01:15:00+01:00' >>> localtz.normalize(t1 + timedelta(hours=1)).isoformat() '2012-03-25T03:15:00+02:00' >>> localtz.normalize(t1 + timedelta(hours=2)).isoformat() '2012-03-25T03:15:00+02:00' >>> localtz.normalize(t1 + timedelta(hours=3)).isoformat() '2012-03-25T04:15:00+02:00'
… so while the result for +1 hour is now fine, there's no differences between +1 and +2 in the above? I don't think that's correct.
And indeed, pytz gives:
>>> tz.normalize(t1 + timedelta(hours=0)).isoformat() '2012-03-25T01:15:00+01:00' >>> tz.normalize(t1 + timedelta(hours=1)).isoformat() '2012-03-25T03:15:00+02:00' >>> tz.normalize(t1 + timedelta(hours=2)).isoformat() '2012-03-25T04:15:00+02:00' >>> tz.normalize(t1 + timedelta(hours=3)).isoformat() '2012-03-25T05:15:00+02:00'
But other than that, the new code looks nice, so I think we're on the right track ;-)
follow-up: 8 comment:7 by , 12 years ago
Thanks for the comments.
Replying to cboos:
I just tested [63b4287d/jomae.git], redoing the steps from comment:1:
>>> localtz.normalize(t1 + timedelta(hours=0)).isoformat() '2012-03-25T01:15:00+01:00' ... >>> localtz.normalize(t1 + timedelta(hours=2)).isoformat() '2012-03-25T03:15:00+02:00' ...… so while the result for +1 hour is now fine, there's no differences between +1 and +2 in the above? I don't think that's correct.
You're right. Ah, it seems difficult to fix without being localize
d, however, I'll try.
Also, time.mktime()
is inconsistent with ambiguous times. The problem is caused by glibc. See time/mktime.c in glibc.git. I have yet to investigate the other C runtime.
$ PYTHONPATH=$PWD TZ=Europe/Paris ~/venv/py24/bin/python Python 2.4.3 (#1, Jun 18 2012, 08:55:31) [GCC 4.1.2 20080704 (Red Hat 4.1.2-52)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import time >>> time.localtime(time.mktime((2001, 1, 1, 0, 0, 0, 0, 0, 0))) (2001, 1, 1, 0, 0, 0, 0, 1, 0) >>> time.localtime(time.mktime((2011, 10, 30, 2, 15, 0, 0, 0, -1))) (2011, 10, 30, 2, 15, 0, 6, 303, 0) >>> time.localtime(time.mktime((2001, 1, 1, 0, 0, 0, 0, 0, 1))) (2000, 12, 31, 23, 0, 0, 6, 366, 0) >>> time.localtime(time.mktime((2011, 10, 30, 2, 15, 0, 0, 0, -1))) (2011, 10, 30, 2, 15, 0, 6, 303, 1) >>>
comment:8 by , 12 years ago
Replying to jomae:
… so while the result for +1 hour is now fine, there's no differences between +1 and +2 in the above? I don't think that's correct.
You're right. Ah, it seems difficult to fix without being
localize
d, however, I'll try.
I worked in [04eaf8aa/jomae.git]. After the changes, if the datetime object is not localize
d, LocalTimezone.normalize
do nothing.
>>> t1 = datetime(2012, 3, 25, 1, 15, tzinfo=localtz) >>> localtz.normalize(t1 + timedelta(hours=0)).isoformat() '2012-03-25T01:15:00+01:00' >>> localtz.normalize(t1 + timedelta(hours=1)).isoformat() '2012-03-25T02:15:00+01:00' >>> localtz.normalize(t1 + timedelta(hours=2)).isoformat() '2012-03-25T03:15:00+02:00' >>> localtz.normalize(t1 + timedelta(hours=3)).isoformat() '2012-03-25T04:15:00+02:00'
Use to_datetime
(call internally tz.localize()
and tz.normalize()
) to retrieve normalized datetime objects, in the same manner as pytz.tzinfo
.
>>> t1 = to_datetime(datetime(2012, 3, 25, 1, 15), localtz) >>> to_datetime(t1 + timedelta(hours=0), localtz).isoformat() '2012-03-25T01:15:00+01:00' >>> to_datetime(t1 + timedelta(hours=1), localtz).isoformat() '2012-03-25T03:15:00+02:00' >>> to_datetime(t1 + timedelta(hours=2), localtz).isoformat() '2012-03-25T04:15:00+02:00' >>> to_datetime(t1 + timedelta(hours=3), localtz).isoformat() '2012-03-25T05:15:00+02:00'
Also,
time.mktime()
is inconsistent with ambiguous times. The problem is caused by glibc. See time/mktime.c in glibc.git. I have yet to investigate the other C runtime....
That issue is fixed in [c2ac69c7/jomae.git]. I confirmed with Python 2.5-2.7 on Linux 64-bits, Python 2.7 on FreeBSD 64-bits and Python 2.4-2.7 on Windows XP (32-bits).
comment:9 by , 12 years ago
API Changes: | modified (diff) |
---|---|
Release Notes: | modified (diff) |
Resolution: | → fixed |
Status: | new → closed |
The patch is applied in [11419-11421].
comment:10 by , 12 years ago
Owner: | set to |
---|
On Windows, the first part works the way you expected:
However:
So it also doesn't fully match what we get via
pytz
.And it's therefore "buggy" as well, I think the criterion for correctness should definitely be
localtz.normalize(t1 + timedelta(hours=1)).isoformat()
→'2012-03-25T03:15:00+02:00'
.