4

I don't care about the placement of the decimal point, and would like to print in scientific notation when appropriate to display with maximum accuracy. However, I would like to round appropriately to maintain a fixed width, ragged left or ragged right is fine.

e.g.

>>> my_format("{:10f}", 0.0000000456)
"  4.56e-08"
>>> my_format("{:10f}", 12.345678987654321)
" 12.345679"
#or "12.34567890" because significant digits
#but not "1.2346e+01", since it is less accurate than the above representations
>>> my_format("{:10f}", 12345678987654321)
"1.2345e+16"

EDIT to clarify the examples, the width formatting specifier does not provide a fixed width. It provides a minimum width. How do I obtain a fixed width representation?

5 Answers 5

3

the g format specifier is generally good if you want scientific notation, i.e:

my_format = "{:.10g}".format

should do the right thing:

>>> list(my_format(v) for v in (0.0000000456, 12.345678987654321, 12345678987654321))
['4.56e-08', '12.34567899', '1.234567899e+16']

I realised later that the above function doesn't do what the OP wanted

Based on helpful comments from @a_guest I've come up with the following:

def my_format(v, length=10):
    n = length
    while n > 0:
        i = len('%#.*g' % (n, v))
        s = '%.*g' % (n + n - i, v)
        if len(s) <= length:
            return s
        n -= 1
    return s

I now get ['4.56e-08', '12.345679', '1.2346e+16'] back which is closer to what was wanted.

I've tested this by generating a lot of random numbers using:

from random import uniform
def rnd_float():
    return uniform(-10, 10) * 10 ** (uniform(-1.6, 1.6) ** 11)

pretty arbitrary, but generates numbers reasonably close to the distribution I care about. I.e. mostly around 1, but some very small and large with decent probability.

I've passed numbers from this to my_format 100k times and I get appropriately formatted numbers back.

Sign up to request clarification or add additional context in comments.

7 Comments

huh, just realised this doesn't do the right thing, you want to limit output to 10 characters rather than having 10 digits of precision which is what I'm displaying. will update
@Scott It's Python 3.6 syntax. If you are using <= 3.5 then you can replace it with '{{v:.{length}g}}'.format(length=length).format(v=v) (similarly for the following one).
if you want compatibility I'd probably go with % formatting; something like: '%.*g' % (length, v) should work
@SamMason What about 0.123456789? It is returned unchanged, with total length 12 (> 10).
@a_guest how annoying, have put back some of my earlier iterative solution… code is getting annoyingly messy!
|
1

what you need is a way to maintain the number of characters displayed. So create a function to do that.

import decimal

# create a new context for this task
ctx = decimal.Context()

# 20 digits should be enough for everyone :D
ctx.prec = 20

def 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')

print float_to_str(0.0000000456)
print float_to_str(12.345678987654321)

1 Comment

This is a clever way to do it although it misses out on scientific notation. For anyone interested but doesn't want to copy the code the results are: # 0.0000000456 # 12.34567898765
1

You can test to format the number in both {:f} and {:e} and then parse the resulting strings to see which one fits better:

import re


def format(spec, number):
    def _decimal(s):
        return re.search(r'^\s*-?([0-9]+(\.[0-9]+)?)', s).group(1)

    def _significant_digits(s):
        return _decimal(s).rstrip('0')

    def _fit_to_width(s):
        decimal, significant = _decimal(s), _significant_digits(s)
        stripped = s.replace(decimal, significant)
        excess = len(stripped) - spec
        if excess > 0:
            # Replace excess digits from the right.
            significant = significant[::-1].replace(
                re.match(
                    r'[0-9]{{,{}}}'.format(excess),
                    significant[::-1]
                ).group(0), ''
            )[::-1]
        return s.replace(decimal, significant)

    formats = [
        _fit_to_width('{{:{}f}}'.format(spec).format(number)),
        _fit_to_width('{{:{}e}}'.format(spec).format(number)),
    ]
    return max(
        filter(
            lambda x: len(x[0]) <= spec,
            [(f, len(_significant_digits(f))) for f in formats]
        ),
        key=lambda x: x[-1]
    )[0].rjust(spec)


print(format(10, 0.0000000456))
print(format(10, 12.345678987654321))
print(format(10, 12345678987654321))

#   4.56e-08
#  12.345679
# 1.2345e+16

Comments

0

This seems to work. It is possible to get rid of numpy, but the rounding would require a little more work.

import numpy as np

SCIENTIFIC_NOTATION_WIDTH = 4

def my_format(number, n):
    places = np.log10(np.abs(number))
    if abs(places) == np.inf:
        places = 0
    highest_place = -int(places)
    if 1 <= highest_place < 3:
        rounded = np.round(number, n - highest_place - 1)
    elif highest_place >= 3:
        rounded = np.round(number, highest_place + n - 5)
    elif -n < highest_place < 1:
        rounded = np.round(number, n + highest_place - 2)
    else:
        rounded = np.round(number, highest_place + n - 6)

    return "{{:{}.{}g}}".format(n,n).format(rounded)
print(my_format(12345678987654321, 10))
print(my_format(12.345678987654321,10))
print(my_format(0.0000000456,10))

#1.2346e+16
# 12.345679
#  4.56e-08

Comments

0

The following code does the job and produces longer outputs if provided length is too small for the given digit, e.g. it is impossible to represent 4e100 using only four characters.

import math

def float_fmt(x, length=10):
  reduce_fp_by = 0

  if abs(x) > 0.0:
    abs_power = abs(math.floor(math.log10(abs(x))))
    if abs_power > 0:
      power_len = max(1, math.floor(math.log10(abs_power)))
      if abs_power > 4:
        reduce_fp_by = power_len + 3

  fp_n = max(0, length - reduce_fp_by - 1)
  fmt = ('%#.'+str(fp_n)+'g')
  return fmt % x

for l in [6, 8, 10, 13]:
  for bf in [0, 4.2, 4.222, 4.22222, 4.2222222]:
    for p in [-500, -100, -50, -5, 50, 100]:
      f = bf * (10.0 ** p)
      print(float_fmt(f, l), len(float_fmt(f, l)))

produces

0.0000 6
0.0000 6
0.0000 6
0.0000 6
0.0000 6
0.0000 6
0.0000 6
4.e-100 7
4.e-50 6
4.e-05 6
4.e+50 6
4.e+100 7
0.0000 6
4.e-100 7
4.e-50 6
4.e-05 6
4.e+50 6
4.e+100 7
0.0000 6
4.e-100 7
4.e-50 6
4.e-05 6
4.e+50 6
4.e+100 7
0.0000 6
4.e-100 7
4.e-50 6
4.e-05 6
4.e+50 6
4.e+100 7
0.000000 8
0.000000 8
0.000000 8
0.000000 8
0.000000 8
0.000000 8
0.000000 8
4.2e-100 8
4.20e-50 8
4.20e-05 8
4.20e+50 8
4.2e+100 8
0.000000 8
4.2e-100 8
4.22e-50 8
4.22e-05 8
4.22e+50 8
4.2e+100 8
0.000000 8
4.2e-100 8
4.22e-50 8
4.22e-05 8
4.22e+50 8
4.2e+100 8
0.000000 8
4.2e-100 8
4.22e-50 8
4.22e-05 8
4.22e+50 8
4.2e+100 8
0.00000000 10
0.00000000 10
0.00000000 10
0.00000000 10
0.00000000 10
0.00000000 10
0.00000000 10
4.200e-100 10
4.2000e-50 10
4.2000e-05 10
4.2000e+50 10
4.200e+100 10
0.00000000 10
4.222e-100 10
4.2220e-50 10
4.2220e-05 10
4.2220e+50 10
4.222e+100 10
0.00000000 10
4.222e-100 10
4.2222e-50 10
4.2222e-05 10
4.2222e+50 10
4.222e+100 10
0.00000000 10
4.222e-100 10
4.2222e-50 10
4.2222e-05 10
4.2222e+50 10
4.222e+100 10
0.00000000000 13
0.00000000000 13
0.00000000000 13
0.00000000000 13
0.00000000000 13
0.00000000000 13
0.00000000000 13
4.200000e-100 13
4.2000000e-50 13
4.2000000e-05 13
4.2000000e+50 13
4.200000e+100 13
0.00000000000 13
4.222000e-100 13
4.2220000e-50 13
4.2220000e-05 13
4.2220000e+50 13
4.222000e+100 13
0.00000000000 13
4.222220e-100 13
4.2222200e-50 13
4.2222200e-05 13
4.2222200e+50 13
4.222220e+100 13
0.00000000000 13
4.222222e-100 13
4.2222222e-50 13
4.2222222e-05 13
4.2222222e+50 13
4.222222e+100 13

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.