Pretty-print dollar amounts using custom precision for large amounts Given a floating-point number, which is representing a dollar value, I'm aiming to return a rounded, human-readable string. Here are the rules for numbers equal to or greater than 1 million, equal to or greater than 1 but less than 1 million, and, finally, less than 1:

one decimal place for million, two places for billions and three places for trillions
numbers over 999 trillion are displayed in trillions, e.g. 1,000 trillion not 1 quadrillion
if less than 1 million, two decimal places (cents)
if less than 1, then as cents (multiplied by 100)

numbers which don't round up to at least 1 cent count as 0 cents



Also, for numbers greater than 1, there's no display of precision if mantissa is all zeros (e.g. 1.000 trillion becomes 1 trillion and 1.00 becomes 1.)
import math

def convert_float_to_value(number):
    if number == 0.0:
      return &quot;0 cents&quot;
    digits = 1 + math.floor(math.log10(number))
    if digits &gt; 12:
        return f&quot;${round(number / 10**12, 3):,.3f} trillion&quot;.replace(&quot;.000&quot;, &quot;&quot;)
    elif digits &gt; 9:
        return f&quot;${round(number / 10**9, 2):,.2f} billion&quot;.replace(&quot;.00&quot;, &quot;&quot;)
    elif digits &gt; 6:
        return f&quot;${round(number / 10**6, 1):,.1f} million&quot;.replace(&quot;.0&quot;, &quot;&quot;)
    elif digits &gt; 0:
        return f&quot;${round(number, 2):,.2f}&quot;.replace(&quot;.00&quot;,&quot;&quot;)
    else:
        return f&quot;{round(number * 100):,} cents&quot;.replace(&quot;1 cents&quot;, &quot;1 cent&quot;)

I already sense some improvements, like perhaps I should be checking this really is a floating-point value and not an integer, or just turning whatever I get into a float. But what do you think of this approach? There are a number of pretty-printing libraries but these are some very specific rules.
To me your code looks very WET. Everything is using the same core but with minor tweeks here and there. And so we can convert your code to a for loop.
TRANSFORMS = [
    (           12, &quot;$&quot;, 10**12,    3, &quot;.3f&quot;, &quot; trillion&quot;,    &quot;.000&quot;,        &quot;&quot;),
    (            9, &quot;$&quot;, 10** 9,    2, &quot;.2f&quot;,  &quot; billion&quot;,     &quot;.00&quot;,        &quot;&quot;),
    (            6, &quot;$&quot;, 10** 6,    1, &quot;.1f&quot;,  &quot; million&quot;,      &quot;.0&quot;,        &quot;&quot;),
    (            0, &quot;$&quot;, 10** 0,    2, &quot;.2f&quot;,          &quot;&quot;,     &quot;.00&quot;,        &quot;&quot;),
    (float(&quot;-inf&quot;),  &quot;&quot;, 10**-2, None,    &quot;&quot;,    &quot; cents&quot;, &quot;1 cents&quot;,  &quot;1 cent&quot;),
]


def convert_float_to_value(number):
    if number == 0.0:
        return &quot;0 cents&quot;
    digits = 1 + math.floor(math.log10(number))
    for d, *t in TRANSFORMS:
        if digits &gt; d:
            return f&quot;{t[0]}{round(number / t[1], t[2]):,{t[3]}}{t[4]}&quot;.replace(t[5], t[6])

From here we can start to see patterns and other improvements.

Excluding cents; t[2], t[3] and t[5] all have the same size.
This means we can build them all from one value.
v = ...
t[2] = v
t[3] = f&quot;.{v}f&quot;
t[4] = &quot;.&quot; + &quot;0&quot;*v


It makes little sense to use .2f for billions. I've never, until this day, seen &quot;$1.10 billion&quot;. It's almost like you've used the wrong word and meant to say &quot;dollars&quot;.
To fix this I'd stop using .2f. However this will break how .replace works, and can add some hard to fix bugs.
(Converting 10.01 to 101)
To fix this I'd convert integers to ints and keeps floats as floats.

We haven't built t[3] and t[5] from t[2] for plain dollars as it still requires .2f to make sense.
To deal with this we can make a function pretty_round that takes a number, a number of digets to round to, an option for the output to be a fixed amount of decimal places and any additional formatting. The only thing is that the output has return a string and a float.
def pretty_round(number, ndigits=None, fixed=False, format_spec=&quot;&quot;):
    number = round(number, ndigits)
    if number % 1 == 0:
        return number, format(int(number), format_spec)
    return number, format(number, format_spec + (f&quot;.{ndigits}f&quot; if fixed else &quot;&quot;))


With the above changes we can see t[5] and t[6] are only there to convert cents from plural to singular.
And so why not just check if the output is singular or plural and append the correct term?

By changing the method of finding digits to use len we can replace float('-inf') with -2 and build t[1] from d.

I am going to reorder the table so that they're grouped better. t[0] should be near t[5] and t[6].
And we can now give them easy to understand names.


TRANSFORMS = [
    (12,    3, False, &quot; trillion&quot;, &quot;$&quot;,       &quot;&quot;,       &quot;&quot;),
    ( 9,    2, False,  &quot; billion&quot;, &quot;$&quot;,       &quot;&quot;,       &quot;&quot;),
    ( 6,    1, False,  &quot; million&quot;, &quot;$&quot;,       &quot;&quot;,       &quot;&quot;),
    ( 0,    2,  True,          &quot;&quot;, &quot;$&quot;,       &quot;&quot;,       &quot;&quot;),
    (-2, None, False,          &quot;&quot;,  &quot;&quot;, &quot; cents&quot;,  &quot; cent&quot;),
]


def pretty_round(number, ndigits=None, fixed=False, format_spec=&quot;&quot;):
    number = round(number, ndigits)
    if number % 1 == 0:
        return number, format(int(number), format_spec)
    return number, format(number, format_spec + (f&quot;.{ndigits}f&quot; if fixed else &quot;&quot;))


def convert_float_to_value(number):
    digits = len(str(int(number)).lstrip('0-'))
    for exp, dp, fixed, magnitude, prefix, plural, singular in TRANSFORMS:
        if digits &gt; exp:
            number_f, number_s = pretty_round(number / 10**exp, dp, fixed, &quot;,&quot;)
            name = singular if number_f == 1 and exp &lt;= 0 else plural
            return f&quot;{prefix}{number_s}{magnitude}{name}&quot;


We can see that TRANSFORMS is not normalized.

All of prefix, plural and singular are refering to dollers and cents.
This can be extracted out into it's own table.
Not only does it reduce how WET your code is it allows us to replace dollars with other monetary notations.
Additionally it allows making changes easier if you want to change from using &quot;$&quot; to &quot;dollars&quot;

The magnitude can change even in English. This is as 1 trillion can equal 1 billion, depending on which scale you use.
Having this baked into the TRANSFORMS table can be problematic in the future.



With your values fixed is the same as not exp.


SHORT_SCALE = [
    &quot;&quot;,
    &quot; thousand&quot;,
    &quot; million&quot;,
    &quot; billion&quot;,
    &quot; trillion&quot;,
]
USD = [
    (&quot;$&quot;, &quot;&quot;, &quot;&quot;),
    (&quot;&quot;, &quot; cents&quot;, &quot; cent&quot;),
]
TRANSFORMS = [
    (12, 3),
    ( 9, 2),
    ( 6, 1),
    ( 0, 2),
    (-2, 0),
]


def pretty_round(number, ndigits=None, fixed=False, format_spec=&quot;&quot;):
    number = round(number, ndigits)
    if number % 1 == 0:
        return number, format(int(number), format_spec)
    return number, format(number, format_spec + (f&quot;.{ndigits}f&quot; if fixed else &quot;&quot;))


def convert_float_to_value(number, *, scale=SHORT_SCALE, currency=USD):
    digits = len(str(int(number)).lstrip('0-'))
    for exp, dp in TRANSFORMS:
        if digits &gt; exp:
            number_f, number_s = pretty_round(number / 10**exp, dp, not exp, &quot;,&quot;)
            prefix, plural, singular = currency[exp &lt; 0]
            name = singular if number_f == 1 and exp &lt;= 0 else plural
            return f&quot;{prefix}{number_s}{scale[int(exp / 3)]}{name}&quot;