[Django] #35857: django.utils.timesince.timesince incorrectly handles daylight saving time

21 views
Skip to first unread message

Django

unread,
Oct 22, 2024, 1:31:05 AM10/22/24
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-------------------------------------+-------------------------------------
Reporter: Frank Sauerburger | Type:
| Uncategorized
Status: new | Component:
| Uncategorized
Version: 5.0 | Severity: Normal
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
timesince computes the time elapsed between two datetimes (d and now) and
returns it as a human readable string. The function is intended to show
the elapsed time from a user perspective (sitting with a stopwatch in
front of the computer). timesince relies on Python's timezone arithmetic,
however, there are subtle implementation details for intra- and inter-
timezone calculations. See

* Discussion about the topic:
https://github.com/python/cpython/issues/116111
* Quiz to illustrate subtle examples: https://quiz.sauerburger.com/dxi7m/

Consider the following example around the daylight saving time transition
next weekend in Europe. We start at a point in time a, ten minutes later
we have a_10, and another 60 minutes later we have a_70.

{{{
from zoneinfo import ZoneInfo
from datetime import datetime
from django.utils.timesince import timesince

berlin = ZoneInfo("Europe/Berlin")

a = datetime(2024, 10, 27, 2, 55, tzinfo=berlin)
a_10 = datetime(2024, 10, 27, 2, 5, fold=1, tzinfo=berlin)
a_70 = datetime(2024, 10, 27, 3, 5, tzinfo=berlin)

assert a.isoformat() == '2024-10-27T02:55:00+02:00'
assert a_10.isoformat() == '2024-10-27T02:05:00+01:00'
assert a_70.isoformat() == '2024-10-27T03:05:00+01:00'

assert timesince(a, a_10) == '0\xa0minutes'
assert timesince(a, a_70) == '10\xa0minutes'
}}}

My expectation is that timesince(a, a_10) yields 10 minutes and
timesince(a, a_70) yields 70 minutes aligned with what a user with a
stopwatch would observe.

I think this can lead to a lot of unexpected behavior in web applications
around the DST transition and maybe even exploitable behavior.
--
Ticket URL: <https://code.djangoproject.com/ticket/35857>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Oct 22, 2024, 4:31:26 AM10/22/24
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-----------------------------------+------------------------------------
Reporter: Frank Sauerburger | Owner: (none)
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------------+------------------------------------
Changes (by Sarah Boyce):

* component: Uncategorized => Utilities
* stage: Unreviewed => Accepted
* type: Uncategorized => Bug

Comment:

If I understood correctly, I think the example was meant to show:
{{{#!python
a = datetime(2024, 10, 27, 2, 55, tzinfo=berlin)
a_10 = datetime(2024, 10, 27, 3, 5, fold=0, tzinfo=berlin)
a_70 = datetime(2024, 10, 27, 3, 5, fold=1, tzinfo=berlin)
assert timesince(a, a_10) == '10\xa0minutes'
assert timesince(a, a_70) == '10\xa0minutes' # expected 1 hour 10 minutes
}}}

For those not familiar with
[https://docs.python.org/3/library/datetime.html#datetime.datetime.fold
fold], this is used to disambiguate wall times during a repeated interval.
The values 0 and 1 represent, respectively, the earlier and later of the
two moments with the same wall time representation.

Looking at the
[https://github.com/python/cpython/issues/116111#issuecomment-2335918369
discussion], it appears timesince should convert the dates to UTC before
subtracting them.

Linking #34483 as this is slightly related
--
Ticket URL: <https://code.djangoproject.com/ticket/35857#comment:1>

Django

unread,
Oct 22, 2024, 4:43:22 AM10/22/24
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-----------------------------------+------------------------------------
Reporter: Frank Sauerburger | Owner: (none)
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------------+------------------------------------
Comment (by Frank Sauerburger):

Hi Sarah,

yes that's also a good way to demonstrate the issue. In my example, I also
wanted to demonstrate that timesince can confuse the order of events. a_10
happens 10 minutes after event a, but {{{timesince}}} returns '0 minutes'
as if the order was reversed.
--
Ticket URL: <https://code.djangoproject.com/ticket/35857#comment:2>

Django

unread,
Oct 22, 2024, 5:00:33 AM10/22/24
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-----------------------------------+------------------------------------
Reporter: Frank Sauerburger | Owner: (none)
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------------+------------------------------------
Comment (by Sarah Boyce):

Ah I see, sorry Frank I misunderstood your example
--
Ticket URL: <https://code.djangoproject.com/ticket/35857#comment:3>

Django

unread,
Nov 13, 2024, 11:52:20 AM11/13/24
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-----------------------------------+------------------------------------
Reporter: Frank Sauerburger | Owner: (none)
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------------+------------------------------------
Comment (by rohan yadav):

hey,
I've implemented several changes to improve handling of timezone-aware
datetimes, particularly around DST transitions. Key improvements include
ensuring naive datetimes are converted to aware ones using make_aware,
normalizing timezone info for both d and now, and adjusting the comparison
logic to account for DST transitions. Could you please review the code and
let me know if there are any further improvements or edge cases that I
might have missed?

Your feedback would be greatly appreciated!

{{{
# If d is naive (has no timezone info), make it aware using the
current time zone
if d.tzinfo is None:
d = make_aware(d) # Convert naive datetime to aware

if now and now.tzinfo is None: # If now is naive, make it aware
now = make_aware(now)


# Compared datetimes must be in the same time zone.
if not now:
now = datetime.datetime.now(d.tzinfo if is_aware(d) else None)
elif is_aware(now) and is_aware(d):
now = now.astimezone(d.tzinfo)

if reversed:
d, now = now, d

if d.tzinfo is not None and now.tzinfo is not None:
delta = now.astimezone(d.tzinfo) - d
else:
delta = now - d

# Normalize times to handle DST transitions
if d.tzinfo is not None:
d = d.tzinfo.normalize(d)
if now.tzinfo is not None:
now = now.tzinfo.normalize(now)

}}}
{{{
remaining_time = (now.astimezone(d.tzinfo) - pivot).total_seconds()
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/35857#comment:4>

Django

unread,
Feb 24, 2025, 7:35:58 PM2/24/25
to django-...@googlegroups.com
#35857: django.utils.timesince.timesince incorrectly handles daylight saving time
-------------------------------------+-------------------------------------
Reporter: Frank Sauerburger | Owner:
| haileyajohnson
Type: Bug | Status: assigned
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by haileyajohnson):

* has_patch: 0 => 1
* needs_better_patch: 0 => 1
* owner: (none) => haileyajohnson
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/35857#comment:5>
Reply all
Reply to author
Forward
0 new messages