如何在Python中将浮点数格式化为最大固定宽度
我正在为一个有着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()
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()