Browse Source

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

Tim Park 4 years ago
parent
commit
8fa9a6d29e
3 changed files with 56 additions and 16 deletions
  1. 23 13
      django/utils/timesince.py
  2. 3 1
      docs/releases/3.2.txt
  3. 30 2
      tests/utils_tests/test_timesince.py

+ 23 - 13
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)

+ 3 - 1
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
 ~~~~~~~~~~

+ 30 - 2
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)