diff --git a/src/humanize/number.py b/src/humanize/number.py index 2fb22c6..d6f7e9e 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -416,7 +416,8 @@ def scientific(value: NumberOrString, precision: int = 2) -> str: return _format_not_finite(value) except (ValueError, TypeError): return str(value) - fmt = f"{{:.{int(precision)}e}}" + # max(0): a negative precision builds an invalid format spec ("{:.-1e}"). + fmt = f"{{:.{max(0, int(precision))}e}}" n = fmt.format(value) part1, part2 = n.split("e") # Normalise exponent: int() strips the "+" sign and leading zeros, diff --git a/tests/test_number.py b/tests/test_number.py index 78639c3..a9ac79d 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -218,6 +218,10 @@ def test_fractional(test_input: float | str, expected: str) -> None: ([-math.inf], "-Inf"), (["nan"], "NaN"), (["-inf"], "-Inf"), + # Negative precision clamps to zero instead of building an invalid spec. + ([1e300, -1], "1 x 10³⁰⁰"), + ([1e300, -5], "1 x 10³⁰⁰"), + ([-1000, -1], "-1 x 10³"), ], ) def test_scientific(test_args: list[typing.Any], expected: str) -> None: @@ -310,6 +314,11 @@ def test_clamp(test_args: list[typing.Any], expected: str) -> None: ([math.nan, "m"], "NaN"), ([math.inf], "+Inf"), ([-math.inf], "-Inf"), + # precision=0 on a scientific-fallback magnitude passed -1 to scientific(); see #159. + ([1e300, "m", 0], "1 x 10³⁰⁰m"), + ([-1e300, "m", 0], "-1 x 10³⁰⁰m"), + ([1e-300, "m", 0], "1 x 10⁻³⁰⁰m"), + ([1e40, "", 0], "1 x 10⁴⁰"), ], ids=str, )