Context

I am receiving numbers as strings in Python 3 and want to transmit them as strings in order to preserve accuracy so that the upstream server can convert the numbers-as-strings to decimal.Decimal without loss of accuracy.

[Admins: I have repeatedly searched Stack Overflow and other resources can cannot find anyone asking or answering this specific question.]

Question

How do I use str.format to provide the given floating point format (below)? Showing that it's impossible is an acceptable answer.

Succinctly put, the format is just to provide at least one decimal place at all times even if that is just a trailing zero, but never to perform any rounding.

I suppose there may be a Formatter class solution, which would be acceptable if available, but a str.format solution is preferred.

I would be interested in knowing if f-strings can achieve this as well. Using f-strings is not preferred as an answer in this context, but it would be interesting to know if f-strings can provide this format (without just packing the demonstrated expression inside the f-string for execution: that's just moving the required code into a secondary evaluation environment).

Required Format

To distinguish the numbers from int the format must suffix ".0" to whole numbers only if they are presented without any decimal places, but preserve exactly any decimal places that are presented, even if that includes trailing zeroes.

There is an alternative solution under consideration which is to strip all trailing decimal places that are zeroes but leave one and only one trailing decimal zero for whole numbers (and of course preserve all non-zero decimal places without rounding).

Incoming strings can be expected to be valid int or Decimal. The special case of receiving a whole number with just the decimal point and no decimal places is invalid and will not occur (no need to handle "42."). The empty string ("") will not occur.

It's not acceptable just to configure to a large number of decimal places ("{:.28f}".format(1)).

Demonstration

AFAIK this should be the required behaviour. Looking for this behaviour using format:

for number in ("42", "4200", "42.0000", "42.34", "42.3400", "42.3456"):
    string = "." in number and number or number + ".0"
    print(number, "=>", string)

42 => 42.0
4200 => 4200.0
42.0000 => 42.0000
42.34 => 42.34
42.3400 => 42.3400
42.3456 => 42.3456

Alternative

This alternative behaviour is also acceptable.

for number in ("42", "4200", "42.0000", "42.34", "42.3400", "42.3456"):
    string = (
        number.rstrip("0")[-1] == "." and number.rstrip("0") + "0"
        or "." not in number and number + ".0"
        or number.rstrip("0")
    )
    print(number, "=>", string)
42 => 42.0
4200 => 4200.0
42.0000 => 42.0
42.34 => 42.34
42.3400 => 42.34
42.3456 => 42.3456
3

There are 3 best solutions below

7
blhsing On BEST ANSWER

While your existing approaches using string manipulations are perfectly fine, if you have to use str.format, you would have to calculate the precision you need from the number of decimal places from the given number, which can be obtained from the exponent of the number after a conversion to decimal.Decimal. Use the max function to ensure a precision of at least 1:

from decimal import Decimal

def format(number):
    n = Decimal(number)
    return '{:.{}f}'.format(n, max(1, -n.as_tuple().exponent))

or with an f-string:

def format(number):
    n = Decimal(number)
    return f'{n:.{max(1, -n.as_tuple().exponent)}f}'

so that:

for number in "42", "42.0000", "42.34", "42.3400", "42.3456":
    print(number, "=>", format(number))

outputs:

42 => 42.0
42.0000 => 42.0000
42.34 => 42.34
42.3400 => 42.3400
42.3456 => 42.3456

Demo: https://ideone.com/9JbLA0

To produce the alternative output in the question, you can normalize the Decimal object first:

def format(number):
    n = Decimal(number)
    return '{:.{}f}'.format(n, max(1, -n.normalize().as_tuple().exponent))

This would make the test code output:

42 => 42.0
42.0000 => 42.0
42.34 => 42.34
42.3400 => 42.34
42.3456 => 42.3456

Demo: https://ideone.com/hbx3Q0

1
SimpleMiant On

f-string may help:

# For whole numbers add .0 if they are presented without any decimal places
fmt_string = "{{:.{}f}}".format(1 if number % format_string = "{{:.{}f}}".format(1 if number % 1 == 0 else 0)
# For Decimal, preserve all decimal places 
fmt_string = "{:." + str(number_str[::-1].find('.') + 1) + "f}"
2
Anastasiya-Romanova 秀 On

How about something like this?

for number in ("42", "4200", "42.0000", "42.34", "42.3400", "42.3456"):
    string = number if "." in number else "{:.1f}".format(float(number)) # int(number) also works
    print(number, "=>", string)