Opened 39 hours ago

Last modified 36 hours ago

#35857 new Bug

django.utils.timesince.timesince incorrectly handles daylight saving time

Reported by: Frank Sauerburger Owned by:
Component: Utilities Version: 5.0
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

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

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.

Change History (3)

comment:1 by Sarah Boyce, 36 hours ago

Component: UncategorizedUtilities
Triage Stage: UnreviewedAccepted
Type: UncategorizedBug

If I understood correctly, I think the example was meant to show:

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 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 discussion, it appears timesince should convert the dates to UTC before subtracting them.

Linking #34483 as this is slightly related

comment:2 by Frank Sauerburger, 36 hours ago

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.

EDIT: Actually, if I'm not mistaken, in your example, there is no difference between

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

The overlapping folds are between 2 and 3 am.

Last edited 36 hours ago by Frank Sauerburger (previous) (diff)

comment:3 by Sarah Boyce, 36 hours ago

Ah I see, sorry Frank I misunderstood your example

Note: See TracTickets for help on using tickets.
Back to Top