|
@@ -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)
|
|
|
|
|
|
|