Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding "precise" flag for naturaltime/delta which outputs tenths of integers #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ tags
docs/_build*
build
dist
.tox
.eggs
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 84 additions & 4 deletions humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
151 changes: 151 additions & 0 deletions tests/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist = py27,py33,pypy
envlist = py27,py33,py36,pypy

[testenv]
commands = python setup.py test
Expand Down