Fixed #31623 -- Allowed specifying number of adjacent time units in timesince()/timeuntil().

This commit is contained in:
Tim Park 2020-07-02 23:01:45 -07:00 committed by Mariusz Felisiak
parent bde33bdd51
commit 8fa9a6d29e
3 changed files with 56 additions and 16 deletions

View File

@ -24,26 +24,30 @@ TIMESINCE_CHUNKS = (
)
def timesince(d, now=None, reversed=False, time_strings=None):
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. Up to two adjacent units will be
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.
`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.
Adapted from
https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
"""
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)
@ -74,18 +78,24 @@ def timesince(d, now=None, reversed=False, time_strings=None):
count = since // seconds
if count != 0:
break
result = avoid_wrapping(time_strings[name] % count)
if i + 1 < len(TIMESINCE_CHUNKS):
# Now get the second item
seconds2, name2 = TIMESINCE_CHUNKS[i + 1]
count2 = (since - (seconds * count)) // seconds2
if count2 != 0:
result += gettext(', ') + avoid_wrapping(time_strings[name2] % count2)
return result
else:
return avoid_wrapping(time_strings['minute'] % 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:
break
result.append(avoid_wrapping(time_strings[name] % count))
since -= seconds * count
current_depth += 1
i += 1
return gettext(', ').join(result)
def timeuntil(d, now=None, time_strings=None):
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)
return timesince(d, now, reversed=True, time_strings=time_strings, depth=depth)

View File

@ -298,7 +298,9 @@ URLs
Utilities
~~~~~~~~~
* ...
* The new ``depth`` parameter of ``django.utils.timesince.timesince()`` and
``django.utils.timesince.timeuntil()`` functions allows specifying the number
of adjacent time units to return.
Validators
~~~~~~~~~~

View File

@ -1,13 +1,13 @@
import datetime
import unittest
from django.test import TestCase
from django.test.utils import requires_tz_support
from django.utils import timezone, translation
from django.utils.timesince import timesince, timeuntil
from django.utils.translation import npgettext_lazy
class TimesinceTests(unittest.TestCase):
class TimesinceTests(TestCase):
def setUp(self):
self.t = datetime.datetime(2007, 8, 14, 13, 46, 0)
@ -140,3 +140,31 @@ class TimesinceTests(unittest.TestCase):
t = datetime.datetime(1007, 8, 14, 13, 46, 0)
self.assertEqual(timesince(t, self.t), '1000\xa0years')
self.assertEqual(timeuntil(self.t, t), '1000\xa0years')
def test_depth(self):
t = self.t + self.oneyear + self.onemonth + self.oneweek + self.oneday + self.onehour
tests = [
(t, 1, '1\xa0year'),
(t, 2, '1\xa0year, 1\xa0month'),
(t, 3, '1\xa0year, 1\xa0month, 1\xa0week'),
(t, 4, '1\xa0year, 1\xa0month, 1\xa0week, 1\xa0day'),
(t, 5, '1\xa0year, 1\xa0month, 1\xa0week, 1\xa0day, 1\xa0hour'),
(t, 6, '1\xa0year, 1\xa0month, 1\xa0week, 1\xa0day, 1\xa0hour'),
(self.t + self.onehour, 5, '1\xa0hour'),
(self.t + (4 * self.oneminute), 3, '4\xa0minutes'),
(self.t + self.onehour + self.oneminute, 1, '1\xa0hour'),
(self.t + self.oneday + self.onehour, 1, '1\xa0day'),
(self.t + self.oneweek + self.oneday, 1, '1\xa0week'),
(self.t + self.onemonth + self.oneweek, 1, '1\xa0month'),
(self.t + self.oneyear + self.onemonth, 1, '1\xa0year'),
(self.t + self.oneyear + self.oneweek + self.oneday, 3, '1\xa0year'),
]
for value, depth, expected in tests:
with self.subTest():
self.assertEqual(timesince(self.t, value, depth=depth), expected)
self.assertEqual(timeuntil(value, self.t, depth=depth), expected)
def test_depth_invalid(self):
msg = 'depth must be greater than 0.'
with self.assertRaisesMessage(ValueError, msg):
timesince(self.t, self.t, depth=0)