123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- import datetime
- from django.utils.html import avoid_wrapping
- from django.utils.timezone import is_aware
- from django.utils.translation import gettext, ngettext_lazy
- TIME_STRINGS = {
- "year": ngettext_lazy("%(num)d year", "%(num)d years", "num"),
- "month": ngettext_lazy("%(num)d month", "%(num)d months", "num"),
- "week": ngettext_lazy("%(num)d week", "%(num)d weeks", "num"),
- "day": ngettext_lazy("%(num)d day", "%(num)d days", "num"),
- "hour": ngettext_lazy("%(num)d hour", "%(num)d hours", "num"),
- "minute": ngettext_lazy("%(num)d minute", "%(num)d minutes", "num"),
- }
- 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):
- """
- Take two datetime objects and return the time between d and now as a nicely
- formatted string, e.g. "10 minutes". If d occurs after now, return
- "0 minutes".
- Units used are years, months, weeks, days, hours, and minutes.
- 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.
- `depth` is an optional integer to control the number of adjacent time
- units returned.
- 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
- if depth <= 0:
- raise ValueError("depth must be greater than 0.")
- # Convert datetime.date to datetime.datetime for comparison.
- if not isinstance(d, datetime.datetime):
- d = datetime.datetime(d.year, d.month, d.day)
- if now and not isinstance(now, datetime.datetime):
- now = datetime.datetime(now.year, now.month, now.day)
- # Compared datetimes must be in the same time zone.
- if not now:
- now = datetime.datetime.now(d.tzinfo if is_aware(d) else None)
- elif is_aware(now) and is_aware(d):
- now = now.astimezone(d.tzinfo)
- if reversed:
- d, now = now, d
- delta = now - d
- # 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})
- # 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,
- tzinfo=d.tzinfo,
- )
- else:
- pivot = d
- remaining_time = (now - pivot).total_seconds()
- partials = [years, months]
- for chunk in TIME_CHUNKS:
- count = int(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(TIME_STRINGS_KEYS) and current_depth < depth:
- value = partials[i]
- if value == 0:
- break
- name = TIME_STRINGS_KEYS[i]
- result.append(avoid_wrapping(time_strings[name] % {"num": value}))
- current_depth += 1
- i += 1
- return gettext(", ").join(result)
- def timeuntil(d, now=None, time_strings=None, depth=2):
- """
- Like timesince, but return a string measuring the time until the given time.
- """
- return timesince(d, now, reversed=True, time_strings=time_strings, depth=depth)
|