Skip to content

Commit

Permalink
Add humanize.metric() for converting big/small numbers to SI units.
Browse files Browse the repository at this point in the history
  • Loading branch information
bwoodsend committed Jun 17, 2022
1 parent 43ebe21 commit 2408ac8
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/humanize/__init__.py
Expand Up @@ -10,6 +10,7 @@
intword,
ordinal,
scientific,
metric,
)
from humanize.time import (
naturaldate,
Expand Down
52 changes: 52 additions & 0 deletions src/humanize/number.py
Expand Up @@ -458,3 +458,55 @@ def clamp(
"Invalid format. Must be either a valid formatting string, or a function "
"that accepts value and returns a string."
)


def metric(value, unit="", precision=3):
"""Return a value with a metric SI unit-prefix appended.
Examples:
```pycon
>>> metric(1500, "V")
'1.50kV'
>>> metric(2e8, "W")
'200MW'
>>> metric(220e-6, "F")
'220μF'
>>> metric(1e-14, precision=4)
'10.00f'
```
The unit prefix is always chosen so that non-significant zero digits are required.
i.e. `123,000` will become `123k` instead of `0.123M` and `1,230,000` will become
`1.23M` instead of `1230K`. For numbers that are either too huge or too tiny to
represent without resorting to either leading or trailing zeroes, it falls back to
`scientific()`.
```pycon
>>> metric(1e40)
'1.00 x 10⁴⁰'
```
Args:
value (int, float): Input number.
unit (str): Optional base unit.
precision (int): The number of digits the output should contain.
Returns:
str:
"""
exponent = int(math.floor(math.log10(abs(value))))

if exponent >= 27 or exponent < -24:
return scientific(value, precision - 1) + unit
else:
value /= 10 ** (exponent // 3 * 3)
if exponent >= 3:
ordinal = "kMGTPEZY"[exponent // 3 - 1]
elif exponent < 0:
ordinal = "mμnpfazy"[(-exponent - 1) // 3]
else:
ordinal = ""
value = format(value, ".%if" % (precision - (exponent % 3) - 1))

return f"{value}{ordinal}{unit}"
33 changes: 33 additions & 0 deletions tests/test_number.py
Expand Up @@ -181,3 +181,36 @@ def test_scientific(test_args: list[typing.Any], expected: str) -> None:
)
def test_clamp(test_args: list[typing.Any], expected: str) -> None:
assert humanize.clamp(*test_args) == expected


@pytest.mark.parametrize(
"test_args, expected",
[
([1, "Hz"], "1.00Hz"),
([1.0, "W"], "1.00W"),
([3, "C"], "3.00C"),
([3, "W", 5], "3.0000W"),
([1.23456], "1.23"),
([12.3456], "12.3"),
([123.456], "123"),
([1234.56], "1.23k"),
([12345, "", 6], "12.3450k"),
([200_000], "200k"),
([1e25, "m"], "10.0Ym"),
([1e26, "m"], "100Ym"),
([1e27, "A"], "1.00 x 10²⁷A"),
([1.234e28, "A"], "1.23 x 10²⁸A"),
([-1500, "V"], "-1.50kV"),
([0.12], "120m"),
([0.012], "12.0m"),
([0.0012], "1.20m"),
([0.00012], "120μ"),
([1e-23], "10.0y"),
([1e-24], "1.00y"),
([1e-25], "1.00 x 10⁻²⁵"),
([1e-26], "1.00 x 10⁻²⁶"),
],
ids=str,
)
def test_metric(test_args, expected):
assert humanize.metric(*test_args) == expected

0 comments on commit 2408ac8

Please sign in to comment.