Fixed #31623 -- Allowed specifying number of adjacent time units in timesince()/timeuntil().
This commit is contained in:
parent
bde33bdd51
commit
8fa9a6d29e
|
@ -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
|
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
|
formatted string, e.g. "10 minutes". If d occurs after now, return
|
||||||
"0 minutes".
|
"0 minutes".
|
||||||
|
|
||||||
Units used are years, months, weeks, days, hours, and 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
|
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.
|
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` is an optional dict of strings to replace the default
|
||||||
TIME_STRINGS dict.
|
TIME_STRINGS dict.
|
||||||
|
|
||||||
|
`depth` is an optional integer to control the number of adjacent time
|
||||||
|
units returned.
|
||||||
|
|
||||||
Adapted from
|
Adapted from
|
||||||
https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
|
https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
|
||||||
"""
|
"""
|
||||||
if time_strings is None:
|
if time_strings is None:
|
||||||
time_strings = TIME_STRINGS
|
time_strings = TIME_STRINGS
|
||||||
|
if depth <= 0:
|
||||||
|
raise ValueError('depth must be greater than 0.')
|
||||||
# Convert datetime.date to datetime.datetime for comparison.
|
# Convert datetime.date to datetime.datetime for comparison.
|
||||||
if not isinstance(d, datetime.datetime):
|
if not isinstance(d, datetime.datetime):
|
||||||
d = datetime.datetime(d.year, d.month, d.day)
|
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
|
count = since // seconds
|
||||||
if count != 0:
|
if count != 0:
|
||||||
break
|
break
|
||||||
result = avoid_wrapping(time_strings[name] % count)
|
else:
|
||||||
if i + 1 < len(TIMESINCE_CHUNKS):
|
return avoid_wrapping(time_strings['minute'] % 0)
|
||||||
# Now get the second item
|
result = []
|
||||||
seconds2, name2 = TIMESINCE_CHUNKS[i + 1]
|
current_depth = 0
|
||||||
count2 = (since - (seconds * count)) // seconds2
|
while i < len(TIMESINCE_CHUNKS) and current_depth < depth:
|
||||||
if count2 != 0:
|
seconds, name = TIMESINCE_CHUNKS[i]
|
||||||
result += gettext(', ') + avoid_wrapping(time_strings[name2] % count2)
|
count = since // seconds
|
||||||
return result
|
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.
|
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)
|
||||||
|
|
|
@ -298,7 +298,9 @@ URLs
|
||||||
Utilities
|
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
|
Validators
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
from django.test.utils import requires_tz_support
|
from django.test.utils import requires_tz_support
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.timesince import timesince, timeuntil
|
from django.utils.timesince import timesince, timeuntil
|
||||||
from django.utils.translation import npgettext_lazy
|
from django.utils.translation import npgettext_lazy
|
||||||
|
|
||||||
|
|
||||||
class TimesinceTests(unittest.TestCase):
|
class TimesinceTests(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.t = datetime.datetime(2007, 8, 14, 13, 46, 0)
|
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)
|
t = datetime.datetime(1007, 8, 14, 13, 46, 0)
|
||||||
self.assertEqual(timesince(t, self.t), '1000\xa0years')
|
self.assertEqual(timesince(t, self.t), '1000\xa0years')
|
||||||
self.assertEqual(timeuntil(self.t, 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)
|
||||||
|
|
Loading…
Reference in New Issue