diff --git a/src/humanize/__init__.py b/src/humanize/__init__.py index af593d7..4ec6440 100644 --- a/src/humanize/__init__.py +++ b/src/humanize/__init__.py @@ -8,6 +8,7 @@ fractional, intcomma, intword, + intsuffix, ordinal, scientific, ) @@ -38,6 +39,7 @@ "fractional", "intcomma", "intword", + "intsuffix", "naturaldate", "naturalday", "naturaldelta", diff --git a/src/humanize/number.py b/src/humanize/number.py index 6611a21..cb45c01 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -211,6 +211,75 @@ def intword(value, format="%.1f"): return str(value) +suffix_powers = [10**x for x in (3, 6, 9, 12, 15, 18, 21, 24, 27)] +suffix_human_powers = ( + NS_("k", "k"), + NS_("M", "M"), + NS_("G", "G"), + NS_("T", "T"), + NS_("P", "P"), + NS_("E", "E"), + NS_("Z", "Z"), + NS_("Y", "Y"), +) + + +def intsuffix(value, format="%.1f"): + """Converts a large integer to a friendly text representation with only suffix. + + Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 M", + 1200000 becomes "1.2 M" and "1_200_000_000" becomes "1.2 T". Supports up + to decillion (33 digits) and googol (100 digits). + + Examples: + ```pycon + >>> intsuffix("100") + '100' + >>> intsuffix("12400") + '12.4 k' + >>> intsuffix("1000000") + '1.0 M' + >>> intsuffix(1_200_000_000) + '1.2 G' + >>> intsuffix(None) is None + True + >>> intsuffix("1234000", "%0.3f") + '1.234 M' + + ``` + Args: + value (int, float, str): Integer to convert. + format (str): To change the number of decimal or general format of the number + portion. + + Returns: + str: Friendly text representation as a string, unless the value passed could not + be coaxed into an `int`. + """ + try: + value = int(value) + except (TypeError, ValueError): + return value + + if value < suffix_powers[0]: + return str(value) + for ordinal, power in enumerate(suffix_powers[1:], 1): + if value < power: + chopped = value / float(suffix_powers[ordinal - 1]) + if float(format % chopped) == float(10**3): + chopped = value / float(suffix_powers[ordinal]) + singular, plural = suffix_human_powers[ordinal] + return ( + " ".join([format, _ngettext(singular, plural, math.ceil(chopped))]) + ) % chopped + else: + singular, plural = suffix_human_powers[ordinal - 1] + return ( + " ".join([format, _ngettext(singular, plural, math.ceil(chopped))]) + ) % chopped + return str(value) + + def apnumber(value): """Converts an integer to Associated Press style. diff --git a/tests/test_number.py b/tests/test_number.py index eec6217..059d622 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -96,6 +96,42 @@ def test_intword(test_args, expected): assert humanize.intword(*test_args) == expected +def test_intsuffix_powers(): + # make sure that suffix_powers & suffix_human_powers have the same number of items + assert len(number.powers) == len(number.human_powers) + + +@pytest.mark.parametrize( + "test_args, expected", + [ + (["100"], "100"), + (["1000"], "1.0 k"), + (["12400"], "12.4 k"), + (["12490"], "12.5 k"), + (["1000000"], "1.0 M"), + (["1200000"], "1.2 M"), + (["1290000"], "1.3 M"), + (["999999999"], "1.0 G"), + (["1000000000"], "1.0 G"), + (["2000000000"], "2.0 G"), + (["999999999999"], "1.0 T"), + (["1000000000000"], "1.0 T"), + (["6000000000000"], "6.0 T"), + (["999999999999999"], "1.0 P"), + (["1000000000000000"], "1.0 P"), + (["1300000000000000"], "1.3 P"), + (["1400000000000000000"], "1.4 E"), + (["3500000000000000000000"], "3.5 Z"), + (["3600000000000000000000000"], "3.6 Y"), + ([None], None), + (["1230000", "%0.2f"], "1.23 M"), + ([10**101], "1" + "0" * 101), + ], +) +def test_intsuffix(test_args, expected): + assert humanize.intsuffix(*test_args) == expected + + @pytest.mark.parametrize( "test_input, expected", [