Convert float to string in positional format (without scientific notation and false precision) Convert float to string in positional format (without scientific notation and false precision) python-3.x python-3.x

Convert float to string in positional format (without scientific notation and false precision)


Unfortunately it seems that not even the new-style formatting with float.__format__ supports this. The default formatting of floats is the same as with repr; and with f flag there are 6 fractional digits by default:

>>> format(0.0000000005, 'f')'0.000000'

However there is a hack to get the desired result - not the fastest one, but relatively simple:

  • first the float is converted to a string using str() or repr()
  • then a new Decimal instance is created from that string.
  • Decimal.__format__ supports f flag which gives the desired result, and, unlike floats it prints the actual precision instead of default precision.

Thus we can make a simple utility function float_to_str:

import decimal# create a new context for this taskctx = decimal.Context()# 20 digits should be enough for everyone :Dctx.prec = 20def float_to_str(f):    """    Convert the given float to a string,    without resorting to scientific notation    """    d1 = ctx.create_decimal(repr(f))    return format(d1, 'f')

Care must be taken to not use the global decimal context, so a new context is constructed for this function. This is the fastest way; another way would be to use decimal.local_context but it would be slower, creating a new thread-local context and a context manager for each conversion.

This function now returns the string with all possible digits from mantissa, rounded to the shortest equivalent representation:

>>> float_to_str(0.1)'0.1'>>> float_to_str(0.00000005)'0.00000005'>>> float_to_str(420000000000000000.0)'420000000000000000'>>> float_to_str(0.000000000123123123123123123123)'0.00000000012312312312312313'

The last result is rounded at the last digit

As @Karin noted, float_to_str(420000000000000000.0) does not strictly match the format expected; it returns 420000000000000000 without trailing .0.


If you are satisfied with the precision in scientific notation, then could we just take a simple string manipulation approach? Maybe it's not terribly clever, but it seems to work (passes all of the use cases you've presented), and I think it's fairly understandable:

def float_to_str(f):    float_string = repr(f)    if 'e' in float_string:  # detect scientific notation        digits, exp = float_string.split('e')        digits = digits.replace('.', '').replace('-', '')        exp = int(exp)        zero_padding = '0' * (abs(int(exp)) - 1)  # minus 1 for decimal point in the sci notation        sign = '-' if f < 0 else ''        if exp > 0:            float_string = '{}{}{}.0'.format(sign, digits, zero_padding)        else:            float_string = '{}0.{}{}'.format(sign, zero_padding, digits)    return float_stringn = 0.000000054321654321assert(float_to_str(n) == '0.000000054321654321')n = 0.00000005assert(float_to_str(n) == '0.00000005')n = 420000000000000000.0assert(float_to_str(n) == '420000000000000000.0')n = 4.5678e-5assert(float_to_str(n) == '0.000045678')n = 1.1assert(float_to_str(n) == '1.1')n = -4.5678e-5assert(float_to_str(n) == '-0.000045678')

Performance:

I was worried this approach may be too slow, so I ran timeit and compared with the OP's solution of decimal contexts. It appears the string manipulation is actually quite a bit faster. Edit: It appears to only be much faster in Python 2. In Python 3, the results were similar, but with the decimal approach slightly faster.

Result:

  • Python 2: using ctx.create_decimal(): 2.43655490875

  • Python 2: using string manipulation: 0.305557966232

  • Python 3: using ctx.create_decimal(): 0.19519368198234588

  • Python 3: using string manipulation: 0.2661344590014778

Here is the timing code:

from timeit import timeitCODE_TO_TIME = '''float_to_str(0.000000054321654321)float_to_str(0.00000005)float_to_str(420000000000000000.0)float_to_str(4.5678e-5)float_to_str(1.1)float_to_str(-0.000045678)'''SETUP_1 = '''import decimal# create a new context for this taskctx = decimal.Context()# 20 digits should be enough for everyone :Dctx.prec = 20def float_to_str(f):    """    Convert the given float to a string,    without resorting to scientific notation    """    d1 = ctx.create_decimal(repr(f))    return format(d1, 'f')'''SETUP_2 = '''def float_to_str(f):    float_string = repr(f)    if 'e' in float_string:  # detect scientific notation        digits, exp = float_string.split('e')        digits = digits.replace('.', '').replace('-', '')        exp = int(exp)        zero_padding = '0' * (abs(int(exp)) - 1)  # minus 1 for decimal point in the sci notation        sign = '-' if f < 0 else ''        if exp > 0:            float_string = '{}{}{}.0'.format(sign, digits, zero_padding)        else:            float_string = '{}0.{}{}'.format(sign, zero_padding, digits)    return float_string'''print(timeit(CODE_TO_TIME, setup=SETUP_1, number=10000))print(timeit(CODE_TO_TIME, setup=SETUP_2, number=10000))


As of NumPy 1.14.0, you can just use numpy.format_float_positional. For example, running against the inputs from your question:

>>> numpy.format_float_positional(0.000000054321654321)'0.000000054321654321'>>> numpy.format_float_positional(0.00000005)'0.00000005'>>> numpy.format_float_positional(0.1)'0.1'>>> numpy.format_float_positional(4.5678e-20)'0.000000000000000000045678'

numpy.format_float_positional uses the Dragon4 algorithm to produce the shortest decimal representation in positional format that round-trips back to the original float input. There's also numpy.format_float_scientific for scientific notation, and both functions offer optional arguments to customize things like rounding and trimming of zeros.