如何在Python中将浮点数格式化为最大固定宽度

6 投票
4 回答
3488 浏览
提问于 2025-04-18 02:12

我正在为一个有着60年代历史的程序写输入文件,这个程序从文本文件中读取固定宽度的数据字段。格式要求如下:

  • 每个字段宽度为8个字符
  • 浮点数必须包含一个'.',或者以科学计数法的形式写,比如'1.23e8'

我目前得到的结果是

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

这段代码的输出是'1.23e+06',对应输入1234567,还有' 1234',对应输入1234

不过我想对这个结果进行一些调整,目标是:

  • 对于1234567,我希望输出'1234567.'(也就是说,不要在需要之前就转换为科学计数法),
  • 对于1234,我希望输出' 1234.'(也就是说,结尾要有一个点,这样它就不会被当作整数处理),
  • 对于12345678,我希望输出'1.235e+7'(也就是说,指数部分只用一个数字),
  • 对于-1234567,我希望输出'-1.23e+7'(也就是说,负数的最大位数不能超过8位)。

因为我记得用Fortran可以很容易做到这一点,而且在处理旧代码时可能会经常遇到这个问题,我怀疑应该有简单的方法可以实现这些要求?

4 个回答

1

你可以这样做,虽然有点晚了,我花了太多时间在这上面,但我在尝试解决类似问题时发现了这个方法。

import unittest


class TestStringMethods(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),
        )

        max_char = 8
        max_number = int("9" * (max_char - 1))  # 9,999,999
        min_number = -int("9" * (max_char - 2))  # -999,999
        for expected, given in test:
            # for small numbers
            # if -999,999 < given < 9,999,999:
            if min_number < given < max_number:

                # output = f"{given:7}"
                output = f"{given:{max_char - 1}}"

                # converting ints to floats without adding zero
                if '.' not in output:
                    output += '.'

                # floats longer than 8 will need rounding to fit max length
                elif len(output) > max_char:
                    # output = str(round(given, 7 - str(given).index(".")))
                    output = str(round(given, max_char - 1 - str(given).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
                for n in range(max(max_char, 5) - 5, 0, -1):
                    fill = f".{n}e"
                    output = f"{given:{fill}}".replace('+0', '+')
                    # if all good stop looping
                    if len(output) == max_char:
                        break
                else:
                    raise ValueError(f"Number is too large to fit in {max_char} characters", given)

            self.assertEqual(len(output), max_char, msg=output)
            self.assertEqual(output, expected, msg=given)


if __name__ == '__main__':
    unittest.main()
1

你已经接近解决方案了,但我觉得你最后的解决办法可能需要写一个自定义的格式化器。比如,我认为迷你格式语言不能像你想的那样控制指数的宽度。

(顺便说一下,在你的第一个例子中,“e”后面没有“+”,而在其他例子中有。明确你想要哪个可能会帮助其他回答者。)

如果我是来写这个格式化函数,第一件事就是为它写一套全面的测试。可以使用doctest或者unittest,这两者都很合适。

然后你就可以继续完善你的格式化函数,直到所有测试都通过为止。

3

我对yosukesabai的贡献做了一点小修改,以处理一个很少见的情况:在四舍五入的时候,字符串的宽度会变成7个字符,而不是8个字符!

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('.')))
            if len(o) < self.width:
                o+=(self.width-len(o))*'0'

    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
3

我只是把@Harvey251的答案分成了测试部分和我们在实际使用中需要的部分。

使用方法如下:

# 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))

这是解决方案。把代码保存为formatfloat.py,然后导入它以使用FlotFormat类。正如我下面所说的,计算的循环部分最好移到FormatFlot类的初始化部分。

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()

撰写回答