Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to format a floating number to maximum fixed width in Python

I am writing an input file to a program with roots in the 60s, and it reads data from fixed-width data fields on text files. The format is:

  • field width 8 characters
  • floating point numbers must contain a '.' or be written on exponential format, e.g. '1.23e8'

The closest i've gotten is

print "{0:8.3g}".format(number)

which yields '1.23e+06' with 1234567, and ' 1234' with 1234.

I would like to tweak this however, to get

  • '1234567.' with 1234567 (i.e. not going to exponential format before it is required),
  • ' 1234.' with 1234 (i.e. ending with a dot so it is not interpreted as an integer),
  • '1.235e+7' with 12345678 (i.e. using only one digit for the exponent),
  • '-1.23e+7' with -1234567 (i.e. not violating the 8 digit maximum on negative numbers).

Since this is (as far as I recall) easily achievable with Fortran and the problem probably comes up now and then when interacting with legacy code I suspect that there must be some easy way to do this?

like image 904
FE-Analyst Avatar asked Oct 01 '22 01:10

FE-Analyst


1 Answers

I simply took the answer by @Harvey251 but split into test part and the part we need in production.

Usage would be:

# save the code at the end as formatfloat.py and then
import formatfloat

# do this first
width = 8
ff8 = formatfloat.FormatFloat(width)

# now use ff8 whenever you need
print(ff8(12345678901234))

And here is the solution. Save the code as formatfloat.py and import it to use FlotFormat class. As I said below, loop part of calculation better be moved to init part of the FormatFlot class.

import unittest

class FormatFloat:
    def __init__(self, width = 8):
        self.width = width
        self.maxnum = int('9'*(width - 1))  # 9999999
        self.minnum = -int('9'*(width - 2)) # -999999

    def __call__(self, x):

        # for small numbers
        # if -999,999 < given < 9,999,999:
        if x > self.minnum and x < self.maxnum:

            # o = f'{x:7}'
            o = f'{x:{self.width - 1}}'

            # converting int to float without adding zero
            if '.' not in o:
                o += '.'

            # float longer than 8 will need rounding to fit width
            elif len(o) > self.width:
                # output = str(round(x, 7 - str(x).index(".")))
                o = str(round(x, self.width-1 - str(x).index('.')))

        else:

            # for exponents
            # added a loop for super large numbers or negative as "-" is another char
            # Added max(max_char, 5) to account for max length of less 
            #     than 5, was having too much fun
            # TODO can i come up with a threshold value for these up front, 
            #     so that i dont have to do this calc for every value??
            for n in range(max(self.width, 5) - 5, 0, -1):
                fill = f'.{n}e'
                o = f'{x:{fill}}'.replace('+0', '+')

                # if all good stop looping
                if len(o) == self.width:
                    break
            else:
                raise ValueError(f"Number is too large to fit in {self.width} characters", x)
        return o


class TestFormatFloat(unittest.TestCase):
    def test_all(self):
        test = ( 
            ("1234567.", 1234567), 
            ("-123456.", -123456), 
            ("1.23e+13", 12345678901234), 
            ("123.4567", 123.4567), 
            ("123.4568", 123.45678), 
            ("1.234568", 1.2345678), 
            ("0.123457", 0.12345678), 
            ("   1234.", 1234), 
            ("1.235e+7", 12345678), 
            ("-1.23e+6", -1234567),
            )

        width = 8
        ff8 = FormatFloat(width)

        for expected, given in test:
            output = ff8(given)
            self.assertEqual(len(output), width, msg=output)
            self.assertEqual(output, expected, msg=given)

if __name__ == '__main__':
    unittest.main()
like image 54
yosukesabai Avatar answered Oct 13 '22 10:10

yosukesabai