diff options
| author | GianpaoloBranca <gianpaolo@protonmail.com> | 2023-01-04 11:14:06 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-04 11:14:06 +0100 |
| commit | 8d67e16493c903adc9d049141028bc0fff43f8c8 (patch) | |
| tree | 62d4b1215958ba12f8f538c9c6dcad9e30153eed /django/utils | |
| parent | 99bd5fb4c2d51f7bf8a19b2c12a603ab38b85ec9 (diff) | |
Fixed #33879 -- Improved timesince handling of long intervals.
Diffstat (limited to 'django/utils')
| -rw-r--r-- | django/utils/timesince.py | 100 |
1 files changed, 68 insertions, 32 deletions
diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 3a0d4afb1a..701c49bab9 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -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) |
