Skip to content
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
14 changes: 12 additions & 2 deletions src/humanize/filesize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

from __future__ import annotations

__lazy_modules__ = {"humanize.i18n", "math"}
__lazy_modules__ = {"humanize.i18n", "humanize.number", "math"}

from math import log
from math import isfinite, log

# Import isfinite so we can check whether a number is
# a normal finite number or NaN / positive infinity / negative infinity.
# Reuse the existing helper from number.py so that
# naturalsize() behaves consistently with other numeric
# humanization functions in the library.
from humanize.i18n import _gettext as _
from humanize.number import _format_not_finite

suffixes = {
"decimal": (
Expand Down Expand Up @@ -92,6 +98,10 @@ def naturalsize(
bytes_ = float(value)
abs_bytes = abs(bytes_)

# Handle NaN and infinity before filesize formatting.
if not isfinite(bytes_):
return _format_not_finite(bytes_)

if abs_bytes == 1 and not gnu:
return _("%d Byte") % int(bytes_)

Expand Down
23 changes: 18 additions & 5 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ def precisedelta(
>>> precisedelta(delta)
'2 days, 1 hour and 33.12 seconds'

>>> precisedelta(dt.timedelta(seconds=-90))
'-1 minute and 30 seconds'

```

A custom `format` can be specified to control how the fractional part
Expand Down Expand Up @@ -546,6 +549,12 @@ def precisedelta(

```
"""
import datetime as dt

is_negative = (isinstance(value, dt.timedelta) and value.total_seconds() < 0) or (
isinstance(value, (int, float)) and value < 0
)

date, delta = _date_and_delta(value, precise=True)
if date is None:
return str(value)
Expand Down Expand Up @@ -652,10 +661,13 @@ def precisedelta(

import math

has_nonzero_value = False

texts: list[str] = []
for unit, fmt in zip(reversed(Unit), fmts):
singular_txt, plural_txt, fmt_value = fmt
if fmt_value > 0 or (not texts and unit == min_unit):
has_nonzero_value = has_nonzero_value or fmt_value != 0
_fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)
fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)
if unit == min_unit and math.modf(fmt_value)[0] > 0:
Expand All @@ -673,12 +685,13 @@ def precisedelta(
break

if len(texts) == 1:
return texts[0]

head = ", ".join(texts[:-1])
tail = texts[-1]
result = texts[0]
else:
head = ", ".join(texts[:-1])
tail = texts[-1]
result = _("%s and %s") % (head, tail)

return _("%s and %s") % (head, tail)
return f"-{result}" if is_negative and has_nonzero_value else result


def _rounding_by_fmt(format: str, value: float) -> float | int:
Expand Down
17 changes: 16 additions & 1 deletion tests/test_filesize.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from __future__ import annotations

import math

import pytest

import humanize
Expand Down Expand Up @@ -95,11 +97,24 @@
)
def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> None:
assert humanize.naturalsize(*test_args) == expected

# Retest with negative input
if isinstance(test_args[0], int):
test_args[0] *= -1
else:
test_args[0] = f"-{test_args[0]}"

assert humanize.naturalsize(*test_args) == "-" + expected


@pytest.mark.parametrize(
"test_input, expected",
[
(math.nan, "NaN"),
(math.inf, "+Inf"),
(-math.inf, "-Inf"),
],
)
def test_naturalsize_not_finite(test_input: float, expected: str) -> None:
assert humanize.naturalsize(test_input) == expected
assert humanize.naturalsize(test_input, binary=True) == expected
assert humanize.naturalsize(test_input, gnu=True) == expected
28 changes: 28 additions & 0 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ def test_naturaldelta_nomonths(test_input: dt.timedelta, expected: str) -> None:
(dt.timedelta(days=365 * 2 + 35), "2 years"),
(dt.timedelta(seconds=1), "a second"),
(dt.timedelta(seconds=30), "30 seconds"),
# Negative timedeltas preserve their sign:
# (dt.timedelta(seconds=-1), "-1 second"),
# (dt.timedelta(seconds=-30), "-30 seconds"),
(dt.timedelta(days=364), "a year"),
(dt.timedelta(days=365 + 364), "2 years"),
# regression tests for bugs in post-release humanize
Expand Down Expand Up @@ -589,6 +592,31 @@ def test_precisedelta_one_unit_enough(
assert humanize.precisedelta(val, minimum_unit=min_unit) == expected


@pytest.mark.parametrize(
"val, min_unit, suppress, expected",
[
(dt.timedelta(seconds=-1), "seconds", [], "-1 second"),
(-1, "seconds", [], "-1 second"),
(
dt.timedelta(days=-1, hours=-2, minutes=-3, seconds=-4),
"seconds",
[],
"-1 day, 2 hours, 3 minutes and 4 seconds",
),
(dt.timedelta(hours=-1, minutes=-30), "hours", [], "-1.50 hours"),
(-0.1, "minutes", [], "0 minutes"),
(-0.1, "minutes", ["seconds"], "0 minutes"),
(dt.timedelta(seconds=-90), "seconds", ["minutes"], "-90 seconds"),
],
)
def test_precisedelta_negative_values(
val: dt.timedelta | float, min_unit: str, suppress: list[str], expected: str
) -> None:
assert (
humanize.precisedelta(val, minimum_unit=min_unit, suppress=suppress) == expected
)


@pytest.mark.parametrize(
"val, min_unit, expected",
[
Expand Down