From b08f798ea43ba667b73092e3d62bc46cfcfc1530 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch Date: Fri, 18 Sep 2026 02:52:42 +0000 Subject: [PATCH] Fix scientific() sign placement and naturaldelta() negative durations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scientific() was stripping "-" from the mantissa and applying it to the exponent instead, so scientific(-1000) became "1.00 x 10⁻³" (and scientific(-0.001) produced a double-minus exponent). Put the sign on the coefficient and return non-finite values unchanged. naturaldelta() used abs(delta.days) and abs(delta.seconds) separately. Python normalizes timedelta(seconds=-5) to (days=-1, seconds=86395), so negative short durations were reported as "a day". Normalize via negation before reading components. --- src/humanize/number.py | 24 ++++++++++++++---------- src/humanize/time.py | 9 +++++++-- tests/test_number.py | 12 +++++++++++- tests/test_time.py | 7 +++++++ 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/humanize/number.py b/src/humanize/number.py index 6611a21..b342dff 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -322,7 +322,7 @@ def scientific(value, precision=2): >>> scientific(int(500)) '5.00 x 10²' >>> scientific(-1000) - '1.00 x 10⁻³' + '-1.00 x 10³' >>> scientific(1000, 1) '1.0 x 10³' >>> scientific(1000, 3) @@ -357,18 +357,23 @@ def scientific(value, precision=2): "+": "⁺", "-": "⁻", } - negative = False try: - if "-" in str(value): - value = str(value).replace("-", "") - negative = True - if isinstance(value, str): value = float(value) + else: + value = float(value) + except (ValueError, TypeError): + return value + + if not math.isfinite(value): + return value + negative = value < 0 + value = abs(value) + + try: fmt = "{:.%se}" % str(int(precision)) n = fmt.format(value) - except (ValueError, TypeError): return value @@ -380,13 +385,12 @@ def scientific(value, precision=2): part2 = part2.replace("+0", "") new_part2 = [] - if negative: - new_part2.append(exponents["-"]) - for char in part2: new_part2.append(exponents[char]) final_str = part1 + " x 10" + "".join(new_part2) + if negative: + final_str = "-" + final_str return final_str diff --git a/src/humanize/time.py b/src/humanize/time.py index 3fbefed..709810b 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -135,8 +135,13 @@ def naturaldelta( use_months = months - seconds = abs(delta.seconds) - days = abs(delta.days) + # timedelta normalizes negatives as (days=-1, seconds=86395) for -5s; + # abs(days)/abs(seconds) separately would report that as "a day". + if delta.total_seconds() < 0: + delta = -delta + + seconds = delta.seconds + days = delta.days years = days // 365 days = days % 365 months = int(days // 30.5) diff --git a/tests/test_number.py b/tests/test_number.py index eec6217..e4ed93d 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -139,7 +139,7 @@ def test_fractional(test_input, expected): "test_args, expected", [ ([1000], "1.00 x 10³"), - ([-1000], "1.00 x 10⁻³"), + ([-1000], "-1.00 x 10³"), ([5.5], "5.50 x 10⁰"), ([5781651000], "5.78 x 10⁹"), (["1000"], "1.00 x 10³"), @@ -172,3 +172,13 @@ def test_scientific(test_args, expected): ) def test_clamp(test_args, expected): assert humanize.clamp(*test_args) == expected + + +def test_scientific_negative_and_nonfinite(): + assert humanize.scientific(-1000) == "-1.00 x 10³" + assert humanize.scientific(-0.001) == "-1.00 x 10⁻³" + # non-finite values are returned unchanged (no cryptic split errors) + assert humanize.scientific(float("inf")) == float("inf") + assert humanize.scientific(float("-inf")) == float("-inf") + import math + assert math.isnan(humanize.scientific(float("nan"))) diff --git a/tests/test_time.py b/tests/test_time.py index defb8e5..a4f9aeb 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -623,3 +623,10 @@ def test_time_unit(): with pytest.raises(TypeError): years < "foo" + + +def test_naturaldelta_negative_duration(): + """Negative durations must match the positive magnitude (not timedelta day borrow).""" + assert humanize.naturaldelta(-5) == humanize.naturaldelta(5) + assert humanize.naturaldelta(dt.timedelta(seconds=-5)) == "5 seconds" + assert humanize.naturaldelta(dt.timedelta(days=-1)) == "a day"