[go: nahoru, domu]

Skip to content

Commit

Permalink
Merge pull request jmoiron#104 from hugovk/75-support-milli-microseconds
Browse files Browse the repository at this point in the history
Add micro- and millisecond units to naturaldelta and naturaltime
  • Loading branch information
hugovk authored Feb 13, 2020
2 parents d64655a + 0acf89e commit f19fc4e
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ repos:
- id: isort

- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.4.4
rev: v1.5.1
hooks:
- id: python-check-blanket-noqa

Expand Down
2 changes: 1 addition & 1 deletion LICENCE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2010 Jason Moiron and Contributors
Copyright (c) 2010-2020 Jason Moiron and Contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ readable size or throughput. It is localized to:

## Usage

Integer humanization:
### Integer humanization

```pycon
>>> import humanize
Expand All @@ -46,7 +46,7 @@ Integer humanization:
'41'
```

Date & time humanization:
### Date & time humanization

```pycon
>>> import humanize
Expand All @@ -67,7 +67,35 @@ Date & time humanization:
'an hour ago'
```

File size humanization:
#### Smaller units

If seconds are too large, set `minimum_unit` to milliseconds or microseconds:

```pycon
>>> import humanize
>>> import datetime as dt
>>> humanize.naturaldelta(dt.timedelta(seconds=2))
'2 seconds'
```
```pycon
>>> delta = dt.timedelta(milliseconds=4)
>>> humanize.naturaldelta(delta)
'a moment'
>>> humanize.naturaldelta(delta, minimum_unit="milliseconds")
'4 milliseconds'
>>> humanize.naturaldelta(delta, minimum_unit="microseconds")
'4000 microseconds'
```
```pycon
>>> humanize.naturaltime(delta)
'now'
>>> humanize.naturaltime(delta, minimum_unit="milliseconds")
'4 milliseconds ago'
>>> humanize.naturaltime(delta, minimum_unit="microseconds")
'4000 microseconds ago'
```

### File size humanization

```pycon
>>> import humanize
Expand All @@ -79,7 +107,7 @@ File size humanization:
'976.6K'
```

Human readable floating point numbers:
### Human-readable floating point numbers

```pycon
>>> import humanize
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
max_line_length = 88

[tool:isort]
known_third_party = freezegun,humanize,pkg_resources,setuptools
known_third_party = freezegun,humanize,pkg_resources,pytest,setuptools
force_grid_wrap = 0
include_trailing_comma = True
line_length = 88
Expand Down
74 changes: 57 additions & 17 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
``contrib.humanize``."""

import datetime as dt
from enum import Enum

from .i18n import gettext as _
from .i18n import ngettext

__all__ = ["naturaldelta", "naturaltime", "naturalday", "naturaldate"]


class Unit(Enum):
MILLISECONDS = 0
MICROSECONDS = 1
SECONDS = 2


def _now():
return dt.datetime.now()

Expand All @@ -24,10 +31,11 @@ def abs_timedelta(delta):
return delta


def date_and_delta(value):
def date_and_delta(value, *, now=None):
"""Turn a value into a date and a timedelta which represents how long ago
it was. If that's not possible, return (None, value)."""
now = _now()
if not now:
now = _now()
if isinstance(value, dt.datetime):
date = value
delta = now - value
Expand All @@ -44,12 +52,22 @@ def date_and_delta(value):
return date, abs_timedelta(delta)


def naturaldelta(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."""
def naturaldelta(value, months=True, minimum_unit="seconds"):
"""Return a natural representation of a timedelta or number of seconds.
This is similar to naturaltime, but does not add tense to the result.
Args:
value: A timedelta or a number of seconds.
months: If True, then a number of months (based on 30.5 days) will be used for
fuzziness between years.
minimum_unit: If microseconds or milliseconds, use those units for subsecond
deltas.
Returns:
str: A natural representation of the amount of time elapsed.
"""
minimum_unit = Unit[minimum_unit.upper()]
date, delta = date_and_delta(value)
if date is None:
return value
Expand All @@ -64,6 +82,17 @@ def naturaldelta(value, months=True):

if not years and days < 1:
if seconds == 0:
if minimum_unit == Unit.MICROSECONDS:
return (
ngettext("%d microsecond", "%d microseconds", delta.microseconds)
% delta.microseconds
)
elif minimum_unit == Unit.MILLISECONDS:
milliseconds = delta.microseconds / 1000
return (
ngettext("%d millisecond", "%d milliseconds", milliseconds)
% milliseconds
)
return _("a moment")
elif seconds == 1:
return _("a second")
Expand Down Expand Up @@ -109,23 +138,34 @@ def naturaldelta(value, months=True):
return ngettext("%d year", "%d years", years) % years


def naturaltime(value, future=False, months=True):
"""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
datetimes, where the tense is always figured out based on the current time.
If an integer is passed, the return value will be past tense by default,
unless ``future`` is set to True."""
def naturaltime(value, future=False, months=True, minimum_unit="seconds"):
"""Return a natural representation of a time in a resolution that makes sense.
This is more or less compatible with Django's naturaltime filter.
Args:
value: A timedate or a number of seconds.
future: Ignored for datetimes, where the tense is always figured out based on
the current time. For integers, the return value will be past tense by
default, unless future is True.
months: If True, then a number of months (based on 30.5 days) will be used for
fuzziness between years.
minimum_unit: If microseconds or milliseconds, use those units for subsecond
times.
Returns:
str: A natural representation of the input in a resolution that makes sense.
"""
now = _now()
date, delta = date_and_delta(value)
date, delta = date_and_delta(value, now=now)
if date is None:
return value
# determine tense by value only if datetime/timedelta were passed
if isinstance(value, (dt.datetime, dt.timedelta)):
future = date > now

ago = _("%s from now") if future else _("%s ago")
delta = naturaldelta(delta, months)
delta = naturaldelta(delta, months, minimum_unit)

if delta == _("a moment"):
return _("now")
Expand Down
134 changes: 129 additions & 5 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@
import datetime as dt
from unittest.mock import patch

import pytest
from freezegun import freeze_time
from humanize import time

from .base import HumanizeTestCase

ONE_DAY = dt.timedelta(days=1)
ONE_DAY_DELTA = dt.timedelta(days=1)

# In seconds
ONE_YEAR = 31556952
ONE_DAY = 86400
ONE_HOUR = 3600
FOUR_MILLISECONDS = 4 / 1000
ONE_MILLISECOND = 1 / 1000
FOUR_MICROSECONDS = 4 / 1000000
ONE_MICROSECOND = 1 / 1000000


class FakeDate:
Expand Down Expand Up @@ -268,8 +278,8 @@ def nt_nomonths(d):
def test_naturalday(self):
# Arrange
today = dt.date.today()
tomorrow = today + ONE_DAY
yesterday = today - ONE_DAY
tomorrow = today + ONE_DAY_DELTA
yesterday = today - ONE_DAY_DELTA

someday = dt.date(today.year, 3, 5)
someday_result = "Mar 05"
Expand Down Expand Up @@ -309,8 +319,8 @@ def test_naturalday(self):
def test_naturaldate(self):
# Arrange
today = dt.date.today()
tomorrow = today + ONE_DAY
yesterday = today - ONE_DAY
tomorrow = today + ONE_DAY_DELTA
yesterday = today - ONE_DAY_DELTA

someday = dt.date(today.year, 3, 5)
someday_result = "Mar 05"
Expand Down Expand Up @@ -340,3 +350,117 @@ def test_naturaldate(self):

# Act / Assert
self.assertManyResults(time.naturaldate, test_list, result_list)


@pytest.mark.parametrize(
"seconds, expected",
[
(ONE_MICROSECOND, "a moment"),
(FOUR_MICROSECONDS, "a moment"),
(ONE_MILLISECOND, "a moment"),
(FOUR_MILLISECONDS, "a moment"),
(2, "2 seconds"),
(4, "4 seconds"),
(ONE_HOUR + FOUR_MILLISECONDS, "an hour"),
(ONE_DAY + FOUR_MILLISECONDS, "a day"),
(ONE_YEAR + FOUR_MICROSECONDS, "a year"),
],
)
def test_naturaldelta_minimum_unit_default(seconds, expected):
# Arrange
delta = dt.timedelta(seconds=seconds)

# Act / Assert
assert time.naturaldelta(delta) == expected


@pytest.mark.parametrize(
"minimum_unit, seconds, expected",
[
("seconds", ONE_MICROSECOND, "a moment"),
("seconds", FOUR_MICROSECONDS, "a moment"),
("seconds", ONE_MILLISECOND, "a moment"),
("seconds", FOUR_MILLISECONDS, "a moment"),
("seconds", 2, "2 seconds"),
("seconds", 4, "4 seconds"),
("seconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour"),
("seconds", ONE_DAY + FOUR_MILLISECONDS, "a day"),
("seconds", ONE_YEAR + FOUR_MICROSECONDS, "a year"),
("microseconds", ONE_MICROSECOND, "1 microsecond"),
("microseconds", FOUR_MICROSECONDS, "4 microseconds"),
("microseconds", 2, "2 seconds"),
("microseconds", 4, "4 seconds"),
("microseconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour"),
("microseconds", ONE_DAY + FOUR_MILLISECONDS, "a day"),
("microseconds", ONE_YEAR + FOUR_MICROSECONDS, "a year"),
("milliseconds", ONE_MILLISECOND, "1 millisecond"),
("milliseconds", FOUR_MILLISECONDS, "4 milliseconds"),
("milliseconds", 2, "2 seconds"),
("milliseconds", 4, "4 seconds"),
("milliseconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour"),
("milliseconds", ONE_YEAR + FOUR_MICROSECONDS, "a year"),
],
)
def test_naturaldelta_minimum_unit_explicit(minimum_unit, seconds, expected):
# Arrange
delta = dt.timedelta(seconds=seconds)

# Act / Assert
assert time.naturaldelta(delta, minimum_unit=minimum_unit) == expected


@pytest.mark.parametrize(
"seconds, expected",
[
(ONE_MICROSECOND, "now"),
(FOUR_MICROSECONDS, "now"),
(ONE_MILLISECOND, "now"),
(FOUR_MILLISECONDS, "now"),
(2, "2 seconds ago"),
(4, "4 seconds ago"),
(ONE_HOUR + FOUR_MILLISECONDS, "an hour ago"),
(ONE_DAY + FOUR_MILLISECONDS, "a day ago"),
(ONE_YEAR + FOUR_MICROSECONDS, "a year ago"),
],
)
def test_naturaltime_minimum_unit_default(seconds, expected):
# Arrange
delta = dt.timedelta(seconds=seconds)

# Act / Assert
assert time.naturaltime(delta) == expected


@pytest.mark.parametrize(
"minimum_unit, seconds, expected",
[
("seconds", ONE_MICROSECOND, "now"),
("seconds", FOUR_MICROSECONDS, "now"),
("seconds", ONE_MILLISECOND, "now"),
("seconds", FOUR_MILLISECONDS, "now"),
("seconds", 2, "2 seconds ago"),
("seconds", 4, "4 seconds ago"),
("seconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour ago"),
("seconds", ONE_DAY + FOUR_MILLISECONDS, "a day ago"),
("seconds", ONE_YEAR + FOUR_MICROSECONDS, "a year ago"),
("microseconds", ONE_MICROSECOND, "1 microsecond ago"),
("microseconds", FOUR_MICROSECONDS, "4 microseconds ago"),
("microseconds", 2, "2 seconds ago"),
("microseconds", 4, "4 seconds ago"),
("microseconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour ago"),
("microseconds", ONE_DAY + FOUR_MILLISECONDS, "a day ago"),
("microseconds", ONE_YEAR + FOUR_MICROSECONDS, "a year ago"),
("milliseconds", ONE_MILLISECOND, "1 millisecond ago"),
("milliseconds", FOUR_MILLISECONDS, "4 milliseconds ago"),
("milliseconds", 2, "2 seconds ago"),
("milliseconds", 4, "4 seconds ago"),
("milliseconds", ONE_HOUR + FOUR_MILLISECONDS, "an hour ago"),
("milliseconds", ONE_YEAR + FOUR_MICROSECONDS, "a year ago"),
],
)
def test_naturaltime_minimum_unit_explicit(minimum_unit, seconds, expected):
# Arrange
delta = dt.timedelta(seconds=seconds)

# Act / Assert
assert time.naturaltime(delta, minimum_unit=minimum_unit) == expected

0 comments on commit f19fc4e

Please sign in to comment.