From 8fa9a6d29efe2622872b4788190ea7c1bcb92019 Mon Sep 17 00:00:00 2001 From: Tim Park Date: Thu, 2 Jul 2020 23:01:45 -0700 Subject: [PATCH] Fixed #31623 -- Allowed specifying number of adjacent time units in timesince()/timeuntil(). --- django/utils/timesince.py | 36 ++++++++++++++++++----------- docs/releases/3.2.txt | 4 +++- tests/utils_tests/test_timesince.py | 32 +++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 3ee70ead21..a9e6b61959 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -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) diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index aa25b0f511..2f3045f971 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -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 ~~~~~~~~~~ diff --git a/tests/utils_tests/test_timesince.py b/tests/utils_tests/test_timesince.py index e373b83f43..d30bf00ae9 100644 --- a/tests/utils_tests/test_timesince.py +++ b/tests/utils_tests/test_timesince.py @@ -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)