diff --git a/millify/__init__.py b/millify/__init__.py index ed58dfa..51c5f0e 100644 --- a/millify/__init__.py +++ b/millify/__init__.py @@ -23,6 +23,13 @@ def millify(n, precision=0, drop_nulls=True, prefixes=[]): millidx = max(0, min(len(millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)))) result = '{:.{precision}f}'.format(n / 10**(3 * millidx), precision=precision) + # Rounding to ``precision`` digits can push the mantissa up to 1000 + # (e.g. 999999 -> "1000k"); carry it into the next unit so the result + # stays below 1000 ("1M") while a larger suffix is available. + if abs(float(result)) >= 1000 and millidx < len(millnames) - 1: + millidx += 1 + result = '{:.{precision}f}'.format(n / 10**(3 * millidx), + precision=precision) if drop_nulls: result = remove_exponent(Decimal(result)) return '{0}{dx}'.format(result, dx=millnames[millidx]) diff --git a/test_millify.py b/test_millify.py new file mode 100644 index 0000000..58f3b4d --- /dev/null +++ b/test_millify.py @@ -0,0 +1,32 @@ +"""Tests for millify().""" +import pytest + +from millify import millify + + +@pytest.mark.parametrize( + "n, kwargs, expected", + [ + (0, {}, "0"), + (999, {}, "999"), + (1000, {}, "1k"), + (1000000, {}, "1M"), + (1234567, {"precision": 2}, "1.23M"), + (-1500, {"precision": 1}, "-1.5k"), + # Rounding must carry into the next unit instead of producing + # an out-of-range mantissa like "1000k". + (999999, {}, "1M"), + (999500, {}, "1M"), + (999999, {"precision": 2}, "1M"), + (-999999, {}, "-1M"), + (999999999, {}, "1B"), + ], +) +def test_millify(n, kwargs, expected): + assert millify(n, **kwargs) == expected + + +def test_millify_no_overflow_at_largest_unit(): + # No larger suffix than "Y" exists, so the mantissa is left as-is + # rather than carrying past the table. + assert millify(10 ** 27, precision=0).endswith("Y")