1
0
mirror of https://github.com/django/django.git synced 2025-10-31 01:25:32 +00:00

Fixed #33879 -- Improved timesince handling of long intervals.

This commit is contained in:
GianpaoloBranca
2023-01-04 11:14:06 +01:00
committed by GitHub
parent 99bd5fb4c2
commit 8d67e16493
4 changed files with 120 additions and 36 deletions

View File

@@ -1,4 +1,3 @@
import calendar
import datetime
from django.utils.html import avoid_wrapping
@@ -14,14 +13,16 @@ TIME_STRINGS = {
"minute": ngettext_lazy("%(num)d minute", "%(num)d minutes", "num"),
}
TIMESINCE_CHUNKS = (
(60 * 60 * 24 * 365, "year"),
(60 * 60 * 24 * 30, "month"),
(60 * 60 * 24 * 7, "week"),
(60 * 60 * 24, "day"),
(60 * 60, "hour"),
(60, "minute"),
)
TIME_STRINGS_KEYS = list(TIME_STRINGS.keys())
TIME_CHUNKS = [
60 * 60 * 24 * 7, # week
60 * 60 * 24, # day
60 * 60, # hour
60, # minute
]
MONTHS_DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
@@ -31,9 +32,16 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
"0 minutes".
Units used are years, months, weeks, days, hours, and minutes.
Seconds and microseconds are ignored. Up to `depth` adjacent units will be
displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are
possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not.
Seconds and microseconds are ignored.
The algorithm takes into account the varying duration of years and months.
There is exactly "1 year, 1 month" between 2013/02/10 and 2014/03/10,
but also between 2007/08/10 and 2008/09/10 despite the delta being 393 days
in the former case and 397 in the latter.
Up to `depth` adjacent units will be displayed. For example,
"2 weeks, 3 days" and "1 year, 3 months" are possible outputs, but
"2 weeks, 3 hours" and "1 year, 5 days" are not.
`time_strings` is an optional dict of strings to replace the default
TIME_STRINGS dict.
@@ -41,8 +49,9 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
`depth` is an optional integer to control the number of adjacent time
units returned.
Adapted from
Originally adapted from
https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
Modified to improve results for years and months.
"""
if time_strings is None:
time_strings = TIME_STRINGS
@@ -60,37 +69,64 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
d, now = now, d
delta = now - d
# Deal with leapyears by subtracing the number of leapdays
leapdays = calendar.leapdays(d.year, now.year)
if leapdays != 0:
if calendar.isleap(d.year):
leapdays -= 1
elif calendar.isleap(now.year):
leapdays += 1
delta -= datetime.timedelta(leapdays)
# ignore microseconds
# Ignore microseconds.
since = delta.days * 24 * 60 * 60 + delta.seconds
if since <= 0:
# d is in the future compared to now, stop processing.
return avoid_wrapping(time_strings["minute"] % {"num": 0})
for i, (seconds, name) in enumerate(TIMESINCE_CHUNKS):
count = since // seconds
if count != 0:
# Get years and months.
total_months = (now.year - d.year) * 12 + (now.month - d.month)
if d.day > now.day or (d.day == now.day and d.time() > now.time()):
total_months -= 1
years, months = divmod(total_months, 12)
# Calculate the remaining time.
# Create a "pivot" datetime shifted from d by years and months, then use
# that to determine the other parts.
if years or months:
pivot_year = d.year + years
pivot_month = d.month + months
if pivot_month > 12:
pivot_month -= 12
pivot_year += 1
pivot = datetime.datetime(
pivot_year,
pivot_month,
min(MONTHS_DAYS[pivot_month - 1], d.day),
d.hour,
d.minute,
d.second,
)
else:
pivot = d
remaining_time = (now - pivot).total_seconds()
partials = [years, months]
for chunk in TIME_CHUNKS:
count = remaining_time // chunk
partials.append(count)
remaining_time -= chunk * count
# Find the first non-zero part (if any) and then build the result, until
# depth.
i = 0
for i, value in enumerate(partials):
if value != 0:
break
else:
return avoid_wrapping(time_strings["minute"] % {"num": 0})
result = []
current_depth = 0
while i < len(TIMESINCE_CHUNKS) and current_depth < depth:
seconds, name = TIMESINCE_CHUNKS[i]
count = since // seconds
if count == 0:
while i < len(TIME_STRINGS_KEYS) and current_depth < depth:
value = partials[i]
if value == 0:
break
result.append(avoid_wrapping(time_strings[name] % {"num": count}))
since -= seconds * count
name = TIME_STRINGS_KEYS[i]
result.append(avoid_wrapping(time_strings[name] % {"num": value}))
current_depth += 1
i += 1
return gettext(", ").join(result)