From 10bc45a6da36c1e35769dc9416e6d507d44f2d2d Mon Sep 17 00:00:00 2001 From: Leo Dirac Date: Tue, 8 Oct 2019 08:25:06 -0700 Subject: [PATCH] Adding "precise" flag for naturaltime/delta which outputs tenths of integers. --- .gitignore | 2 + README.md | 4 ++ humanize/time.py | 88 +++++++++++++++++++++++++-- tests/time.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 5 files changed, 242 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index b38abcbd..012080ff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ tags docs/_build* build dist +.tox +.eggs diff --git a/README.md b/README.md index c3a97dce..82c7581a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ usage 'a second ago' >>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=3600)) 'an hour ago' +>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=7000)) +'an hour ago' +>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=7000), precise=True) +'1.9 hours ago' ``` #### Filesize humanization diff --git a/humanize/time.py b/humanize/time.py index 1a9daa2d..503936ff 100644 --- a/humanize/time.py +++ b/humanize/time.py @@ -40,12 +40,16 @@ def date_and_delta(value): return (None, value) return date, abs_timedelta(delta) -def naturaldelta(value, months=True): +def naturaldelta_approx(value, months=True): """Given a timedelta or a number of seconds, return a natural representation of the amount of time elapsed. This is similar to ``naturaltime``, but does not add tense to the result. If ``months`` is True, then a number of months (based on 30.5 days) will be used - for fuzziness between years.""" + for fuzziness between years. + + This _approx version outputs only integers, and rounds everything + from 1.000 to 1.999 to 1 in output. This is traditional and provides + the simplest shortest output.""" now = _now() date, delta = date_and_delta(value) if date is None: @@ -104,8 +108,84 @@ def naturaldelta(value, months=True): else: return ngettext("%d year", "%d years", years) % years +def naturaldelta_precise(value, months=True): + """Given a timedelta or a number of seconds, return a natural + representation of the amount of time elapsed. This is similar to + ``naturaltime``, but does not add tense to the result. If ``months`` + is True, then a number of months (based on 30.5 days) will be used + for fuzziness between years. + + This _precise version returns times with a tenth like "1.7 hours". + """ + now = _now() + date, delta = date_and_delta(value) + if date is None: + return value + + use_months = months + + seconds = abs(delta.seconds) + days = abs(delta.days) + years = days // 365 + years_float = days / 365 + days = days % 365 + months = int(days // 30.5) + + if not years and days < 1: + if seconds == 0: + #TODO: add milliseconds + return _("<1 second") + elif seconds < 60: + return _("%.1f seconds") % seconds + elif 60 <= seconds < 3600: + minutes = seconds / 60 + return _("%.1f minutes") % minutes + elif 3600 <= seconds: + hours = seconds / 3600 + return _("%.1f hours") % hours + elif years == 0: + if not use_months: + return _("%.1f days") % days + else: + if not months: + return _("%.1f days") % days + else: + return _("%.1f months") % months + elif years == 1: + if not months and not days: + return _("%.1f years") % years_float + elif not months: + # Leaving these in the old format, as they are already pretty precise. + return ngettext("1 year, %d day", "1 year, %d days", days) % days + elif use_months: + if months == 1: + return _("1 year, 1 month") + else: + return ngettext("1 year, %d month", + "1 year, %d months", months) % months + else: + return ngettext("1 year, %d day", "1 year, %d days", days) % days + else: + return _("%.1f years") % years_float + +def naturaldelta(value, months=True, precise=False): + """Given a timedelta or a number of seconds, return a natural + representation of the amount of time elapsed. This is similar to + ``naturaltime``, but does not add tense to the result. If ``months`` + is True, then a number of months (based on 30.5 days) will be used + for fuzziness between years. + + By default, it outputs only integers, and rounds everything + from 1.000 to 1.999 to 1 (or "a" or "an") in output. This is + traditional and provides the simplest shortest output. + + If precise=True, then it will return tenths like "1.7 hours".""" + if precise: + return naturaldelta_precise(value, months) + else: + return naturaldelta_approx(value, months) -def naturaltime(value, future=False, months=True): +def naturaltime(value, future=False, months=True, precise=False): """Given a datetime or a number of seconds, return a natural representation of that time in a resolution that makes sense. This is more or less compatible with Django's ``naturaltime`` filter. ``future`` is ignored for @@ -121,7 +201,7 @@ def naturaltime(value, future=False, months=True): future = date > now ago = _('%s from now') if future else _('%s ago') - delta = naturaldelta(delta, months) + delta = naturaldelta(delta, months, precise) if delta == _("a moment"): return _("now") diff --git a/tests/time.py b/tests/time.py index 7747c872..e5a57090 100644 --- a/tests/time.py +++ b/tests/time.py @@ -54,6 +54,25 @@ def test_naturaldelta_nomonths(self): nd_nomonths = lambda d: time.naturaldelta(d, months=False) self.assertManyResults(nd_nomonths, test_list, result_list) + def test_naturaldelta_nomonths_precise(self): + now = datetime.now() + test_list = [ + timedelta(days=7), + timedelta(days=31), + timedelta(days=230), + timedelta(days=400), + ] + result_list = [ + '7.0 days', + '31.0 days', + '230.0 days', + '1 year, 35 days', + ] + with patch('humanize.time._now') as mocked: + mocked.return_value = now + nd_nomonths_prec = lambda d: time.naturaldelta(d, months=False, precise=True) + self.assertManyResults(nd_nomonths_prec, test_list, result_list) + def test_naturaldelta(self): now = datetime.now() test_list = [ @@ -123,6 +142,76 @@ def test_naturaldelta(self): mocked.return_value = now self.assertManyResults(time.naturaldelta, test_list, result_list) + def test_naturaldelta_precise(self): + now = datetime.now() + test_list = [ + 0, + 1, + 30, + timedelta(minutes=1, seconds=30), + timedelta(minutes=2), + timedelta(hours=1, minutes=30, seconds=30), + timedelta(hours=23, minutes=50, seconds=50), + timedelta(days=1), + timedelta(days=500), + timedelta(days=365*2 + 35), + timedelta(seconds=1), + timedelta(seconds=30), + timedelta(minutes=1, seconds=30), + timedelta(minutes=2), + timedelta(hours=1, minutes=30, seconds=30), + timedelta(hours=23, minutes=50, seconds=50), + timedelta(days=1), + timedelta(days=500), + timedelta(days=365*2 + 35), + # regression tests for bugs in post-release humanize + timedelta(days=10000), + timedelta(days=365+35), + 30, + timedelta(days=365*2 + 65), + timedelta(days=365 + 4), + timedelta(days=35), + timedelta(days=65), + timedelta(days=9), + timedelta(days=365), + "NaN", + ] + result_list = [ + '<1 second', + '1.0 seconds', + '30.0 seconds', + '1.5 minutes', + '2.0 minutes', + '1.5 hours', + '23.8 hours', + '1.0 days', + '1 year, 4 months', + '2.1 years', + '1.0 seconds', + '30.0 seconds', + '1.5 minutes', + '2.0 minutes', + '1.5 hours', + '23.8 hours', + '1.0 days', + '1 year, 4 months', + '2.1 years', + '27.4 years', + '1 year, 1 month', + '30.0 seconds', + '2.2 years', + '1 year, 4 days', + '1.0 months', + '2.0 months', + '9.0 days', + '1.0 years', + "NaN", + ] + with patch('humanize.time._now') as mocked: + mocked.return_value = now + nd_prec = lambda d: time.naturaldelta(d, precise=True) + self.assertManyResults(nd_prec, test_list, result_list) + def test_naturaltime(self): now = datetime.now() test_list = [ @@ -184,6 +273,68 @@ def test_naturaltime(self): mocked.return_value = now self.assertManyResults(time.naturaltime, test_list, result_list) + def test_naturaltime_precise(self): + now = datetime.now() + test_list = [ + now, + now - timedelta(seconds=1), + now - timedelta(seconds=30), + now - timedelta(minutes=1, seconds=30), + now - timedelta(minutes=2), + now - timedelta(hours=1, minutes=30, seconds=30), + now - timedelta(hours=23, minutes=50, seconds=50), + now - timedelta(days=1), + now - timedelta(days=500), + now - timedelta(days=365*2 + 35), + now + timedelta(seconds=1), + now + timedelta(seconds=30), + now + timedelta(minutes=1, seconds=30), + now + timedelta(minutes=2), + now + timedelta(hours=1, minutes=30, seconds=30), + now + timedelta(hours=23, minutes=50, seconds=50), + now + timedelta(days=1), + now + timedelta(days=500), + now + timedelta(days=365*2 + 35), + # regression tests for bugs in post-release humanize + now + timedelta(days=10000), + now - timedelta(days=365+35), + 30, + now - timedelta(days=365*2 + 65), + now - timedelta(days=365 + 4), + "NaN", + ] + result_list = [ + '<1 second ago', + '1.0 seconds ago', + '30.0 seconds ago', + '1.5 minutes ago', + '2.0 minutes ago', + '1.5 hours ago', + '23.8 hours ago', + '1.0 days ago', + '1 year, 4 months ago', + '2.1 years ago', + '1.0 seconds from now', + '30.0 seconds from now', + '1.5 minutes from now', + '2.0 minutes from now', + '1.5 hours from now', + '23.8 hours from now', + '1.0 days from now', + '1 year, 4 months from now', + '2.1 years from now', + '27.4 years from now', + '1 year, 1 month ago', + '30.0 seconds ago', + '2.2 years ago', + '1 year, 4 days ago', + "NaN", + ] + with patch('humanize.time._now') as mocked: + mocked.return_value = now + nt_prec = lambda d: time.naturaltime(d, precise=True) + self.assertManyResults(nt_prec, test_list, result_list) + def test_naturaltime_nomonths(self): now = datetime.now() test_list = [ diff --git a/tox.ini b/tox.ini index cbcb26ef..3d1c075a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py27,py33,pypy +envlist = py27,py33,py36,pypy [testenv] commands = python setup.py test