Skip to content
This repository was archived by the owner on Mar 23, 2023. It is now read-only.

Commit dba35db

Browse files
m4ns0urtrotterdylan
authored andcommitted
Add fpformat module (#355)
1 parent 4c750a2 commit dba35db

3 files changed

Lines changed: 224 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ STDLIB_TESTS := \
9797
test/test_colorsys \
9898
test/test_datetime \
9999
test/test_dict \
100+
test/test_fpformat \
100101
test/test_list \
101102
test/test_md5 \
102103
test/test_mimetools \

third_party/stdlib/fpformat.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""General floating point formatting functions.
2+
3+
Functions:
4+
fix(x, digits_behind)
5+
sci(x, digits_behind)
6+
7+
Each takes a number or a string and a number of digits as arguments.
8+
9+
Parameters:
10+
x: number to be formatted; or a string resembling a number
11+
digits_behind: number of digits behind the decimal point
12+
"""
13+
from warnings import warnpy3k
14+
warnpy3k("the fpformat module has been removed in Python 3.0", stacklevel=2)
15+
del warnpy3k
16+
17+
import re
18+
19+
__all__ = ["fix","sci","NotANumber"]
20+
21+
# Compiled regular expression to "decode" a number
22+
decoder = re.compile(r'^([-+]?)0*(\d*)((?:\.\d*)?)(([eE][-+]?\d+)?)$')
23+
# \0 the whole thing
24+
# \1 leading sign or empty
25+
# \2 digits left of decimal point
26+
# \3 fraction (empty or begins with point)
27+
# \4 exponent part (empty or begins with 'e' or 'E')
28+
29+
try:
30+
class NotANumber(ValueError):
31+
pass
32+
except TypeError:
33+
NotANumber = 'fpformat.NotANumber'
34+
35+
def extract(s):
36+
"""Return (sign, intpart, fraction, expo) or raise an exception:
37+
sign is '+' or '-'
38+
intpart is 0 or more digits beginning with a nonzero
39+
fraction is 0 or more digits
40+
expo is an integer"""
41+
res = decoder.match(s)
42+
if res is None: raise NotANumber, s
43+
sign, intpart, fraction, exppart = res.group(1,2,3,4)
44+
if sign == '+': sign = ''
45+
if fraction: fraction = fraction[1:]
46+
if exppart: expo = int(exppart[1:])
47+
else: expo = 0
48+
return sign, intpart, fraction, expo
49+
50+
def unexpo(intpart, fraction, expo):
51+
"""Remove the exponent by changing intpart and fraction."""
52+
if expo > 0: # Move the point left
53+
f = len(fraction)
54+
intpart, fraction = intpart + fraction[:expo], fraction[expo:]
55+
if expo > f:
56+
intpart = intpart + '0'*(expo-f)
57+
elif expo < 0: # Move the point right
58+
i = len(intpart)
59+
intpart, fraction = intpart[:expo], intpart[expo:] + fraction
60+
if expo < -i:
61+
fraction = '0'*(-expo-i) + fraction
62+
return intpart, fraction
63+
64+
def roundfrac(intpart, fraction, digs):
65+
"""Round or extend the fraction to size digs."""
66+
f = len(fraction)
67+
if f <= digs:
68+
return intpart, fraction + '0'*(digs-f)
69+
i = len(intpart)
70+
if i+digs < 0:
71+
return '0'*-digs, ''
72+
total = intpart + fraction
73+
nextdigit = total[i+digs]
74+
if nextdigit >= '5': # Hard case: increment last digit, may have carry!
75+
n = i + digs - 1
76+
while n >= 0:
77+
if total[n] != '9': break
78+
n = n-1
79+
else:
80+
total = '0' + total
81+
i = i+1
82+
n = 0
83+
total = total[:n] + chr(ord(total[n]) + 1) + '0'*(len(total)-n-1)
84+
intpart, fraction = total[:i], total[i:]
85+
if digs >= 0:
86+
return intpart, fraction[:digs]
87+
else:
88+
return intpart[:digs] + '0'*-digs, ''
89+
90+
def fix(x, digs):
91+
"""Format x as [-]ddd.ddd with 'digs' digits after the point
92+
and at least one digit before.
93+
If digs <= 0, the point is suppressed."""
94+
if type(x) != type(''): x = repr(x)
95+
try:
96+
sign, intpart, fraction, expo = extract(x)
97+
except NotANumber:
98+
return x
99+
intpart, fraction = unexpo(intpart, fraction, expo)
100+
intpart, fraction = roundfrac(intpart, fraction, digs)
101+
while intpart and intpart[0] == '0': intpart = intpart[1:]
102+
if intpart == '': intpart = '0'
103+
if digs > 0: return sign + intpart + '.' + fraction
104+
else: return sign + intpart
105+
106+
def sci(x, digs):
107+
"""Format x as [-]d.dddE[+-]ddd with 'digs' digits after the point
108+
and exactly one digit before.
109+
If digs is <= 0, one digit is kept and the point is suppressed."""
110+
if type(x) != type(''): x = repr(x)
111+
sign, intpart, fraction, expo = extract(x)
112+
if not intpart:
113+
while fraction and fraction[0] == '0':
114+
fraction = fraction[1:]
115+
expo = expo - 1
116+
if fraction:
117+
intpart, fraction = fraction[0], fraction[1:]
118+
expo = expo - 1
119+
else:
120+
intpart = '0'
121+
else:
122+
expo = expo + len(intpart) - 1
123+
intpart, fraction = intpart[0], intpart[1:] + fraction
124+
digs = max(0, digs)
125+
intpart, fraction = roundfrac(intpart, fraction, digs)
126+
if len(intpart) > 1:
127+
intpart, fraction, expo = \
128+
intpart[0], intpart[1:] + fraction[:-1], \
129+
expo + len(intpart) - 1
130+
s = sign + intpart
131+
if digs > 0: s = s + '.' + fraction
132+
e = repr(abs(expo))
133+
e = '0'*(3-len(e)) + e
134+
if expo < 0: e = '-' + e
135+
else: e = '+' + e
136+
return s + 'e' + e
137+
138+
def test():
139+
"""Interactive test run."""
140+
try:
141+
while 1:
142+
x, digs = input('Enter (x, digs): ')
143+
print x, fix(x, digs), sci(x, digs)
144+
except (EOFError, KeyboardInterrupt):
145+
pass
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'''
2+
Tests for fpformat module
3+
Nick Mathewson
4+
'''
5+
from test.test_support import run_unittest #, import_module
6+
import unittest
7+
# fpformat = import_module('fpformat', deprecated=True)
8+
import fpformat
9+
fix, sci, NotANumber = fpformat.fix, fpformat.sci, fpformat.NotANumber
10+
11+
StringType = type('')
12+
13+
# Test the old and obsolescent fpformat module.
14+
#
15+
# (It's obsolescent because fix(n,d) == "%.*f"%(d,n) and
16+
# sci(n,d) == "%.*e"%(d,n)
17+
# for all reasonable numeric n and d, except that sci gives 3 exponent
18+
# digits instead of 2.
19+
#
20+
# Differences only occur for unreasonable n and d. <.2 wink>)
21+
22+
class FpformatTest(unittest.TestCase):
23+
24+
def checkFix(self, n, digits):
25+
result = fix(n, digits)
26+
if isinstance(n, StringType):
27+
n = repr(n)
28+
expected = "%.*f" % (digits, float(n))
29+
30+
self.assertEqual(result, expected)
31+
32+
def checkSci(self, n, digits):
33+
result = sci(n, digits)
34+
if isinstance(n, StringType):
35+
n = repr(n)
36+
expected = "%.*e" % (digits, float(n))
37+
# add the extra 0 if needed
38+
num, exp = expected.split("e")
39+
if len(exp) < 4:
40+
exp = exp[0] + "0" + exp[1:]
41+
expected = "%se%s" % (num, exp)
42+
43+
self.assertEqual(result, expected)
44+
45+
def test_basic_cases(self):
46+
self.assertEqual(fix(100.0/3, 3), '33.333')
47+
self.assertEqual(sci(100.0/3, 3), '3.333e+001')
48+
49+
@unittest.skip('grumpy')
50+
def test_reasonable_values(self):
51+
for d in range(7):
52+
for val in (1000.0/3, 1000, 1000.0, .002, 1.0/3, 1e10):
53+
for realVal in (val, 1.0/val, -val, -1.0/val):
54+
self.checkFix(realVal, d)
55+
self.checkSci(realVal, d)
56+
57+
def test_failing_values(self):
58+
# Now for 'unreasonable n and d'
59+
self.assertEqual(fix(1.0, 1000), '1.'+('0'*1000))
60+
self.assertEqual(sci("1"+('0'*1000), 0), '1e+1000')
61+
62+
# This behavior is inconsistent. sci raises an exception; fix doesn't.
63+
yacht = "Throatwobbler Mangrove"
64+
self.assertEqual(fix(yacht, 10), yacht)
65+
try:
66+
sci(yacht, 10)
67+
except NotANumber:
68+
pass
69+
else:
70+
self.fail("No exception on non-numeric sci")
71+
72+
73+
def test_main():
74+
run_unittest(FpformatTest)
75+
76+
77+
if __name__ == "__main__":
78+
test_main()

0 commit comments

Comments
 (0)