summaryrefslogtreecommitdiff
path: root/django/utils
diff options
context:
space:
mode:
Diffstat (limited to 'django/utils')
-rw-r--r--django/utils/timesince.py100
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)