Commit | Line | Data |
---|---|---|
920dae64 AT |
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 | ||
14 | import re | |
15 | ||
16 | __all__ = ["fix","sci","NotANumber"] | |
17 | ||
18 | # Compiled regular expression to "decode" a number | |
19 | decoder = re.compile(r'^([-+]?)0*(\d*)((?:\.\d*)?)(([eE][-+]?\d+)?)$') | |
20 | # \0 the whole thing | |
21 | # \1 leading sign or empty | |
22 | # \2 digits left of decimal point | |
23 | # \3 fraction (empty or begins with point) | |
24 | # \4 exponent part (empty or begins with 'e' or 'E') | |
25 | ||
26 | try: | |
27 | class NotANumber(ValueError): | |
28 | pass | |
29 | except TypeError: | |
30 | NotANumber = 'fpformat.NotANumber' | |
31 | ||
32 | def extract(s): | |
33 | """Return (sign, intpart, fraction, expo) or raise an exception: | |
34 | sign is '+' or '-' | |
35 | intpart is 0 or more digits beginning with a nonzero | |
36 | fraction is 0 or more digits | |
37 | expo is an integer""" | |
38 | res = decoder.match(s) | |
39 | if res is None: raise NotANumber, s | |
40 | sign, intpart, fraction, exppart = res.group(1,2,3,4) | |
41 | if sign == '+': sign = '' | |
42 | if fraction: fraction = fraction[1:] | |
43 | if exppart: expo = int(exppart[1:]) | |
44 | else: expo = 0 | |
45 | return sign, intpart, fraction, expo | |
46 | ||
47 | def unexpo(intpart, fraction, expo): | |
48 | """Remove the exponent by changing intpart and fraction.""" | |
49 | if expo > 0: # Move the point left | |
50 | f = len(fraction) | |
51 | intpart, fraction = intpart + fraction[:expo], fraction[expo:] | |
52 | if expo > f: | |
53 | intpart = intpart + '0'*(expo-f) | |
54 | elif expo < 0: # Move the point right | |
55 | i = len(intpart) | |
56 | intpart, fraction = intpart[:expo], intpart[expo:] + fraction | |
57 | if expo < -i: | |
58 | fraction = '0'*(-expo-i) + fraction | |
59 | return intpart, fraction | |
60 | ||
61 | def roundfrac(intpart, fraction, digs): | |
62 | """Round or extend the fraction to size digs.""" | |
63 | f = len(fraction) | |
64 | if f <= digs: | |
65 | return intpart, fraction + '0'*(digs-f) | |
66 | i = len(intpart) | |
67 | if i+digs < 0: | |
68 | return '0'*-digs, '' | |
69 | total = intpart + fraction | |
70 | nextdigit = total[i+digs] | |
71 | if nextdigit >= '5': # Hard case: increment last digit, may have carry! | |
72 | n = i + digs - 1 | |
73 | while n >= 0: | |
74 | if total[n] != '9': break | |
75 | n = n-1 | |
76 | else: | |
77 | total = '0' + total | |
78 | i = i+1 | |
79 | n = 0 | |
80 | total = total[:n] + chr(ord(total[n]) + 1) + '0'*(len(total)-n-1) | |
81 | intpart, fraction = total[:i], total[i:] | |
82 | if digs >= 0: | |
83 | return intpart, fraction[:digs] | |
84 | else: | |
85 | return intpart[:digs] + '0'*-digs, '' | |
86 | ||
87 | def fix(x, digs): | |
88 | """Format x as [-]ddd.ddd with 'digs' digits after the point | |
89 | and at least one digit before. | |
90 | If digs <= 0, the point is suppressed.""" | |
91 | if type(x) != type(''): x = repr(x) | |
92 | try: | |
93 | sign, intpart, fraction, expo = extract(x) | |
94 | except NotANumber: | |
95 | return x | |
96 | intpart, fraction = unexpo(intpart, fraction, expo) | |
97 | intpart, fraction = roundfrac(intpart, fraction, digs) | |
98 | while intpart and intpart[0] == '0': intpart = intpart[1:] | |
99 | if intpart == '': intpart = '0' | |
100 | if digs > 0: return sign + intpart + '.' + fraction | |
101 | else: return sign + intpart | |
102 | ||
103 | def sci(x, digs): | |
104 | """Format x as [-]d.dddE[+-]ddd with 'digs' digits after the point | |
105 | and exactly one digit before. | |
106 | If digs is <= 0, one digit is kept and the point is suppressed.""" | |
107 | if type(x) != type(''): x = repr(x) | |
108 | sign, intpart, fraction, expo = extract(x) | |
109 | if not intpart: | |
110 | while fraction and fraction[0] == '0': | |
111 | fraction = fraction[1:] | |
112 | expo = expo - 1 | |
113 | if fraction: | |
114 | intpart, fraction = fraction[0], fraction[1:] | |
115 | expo = expo - 1 | |
116 | else: | |
117 | intpart = '0' | |
118 | else: | |
119 | expo = expo + len(intpart) - 1 | |
120 | intpart, fraction = intpart[0], intpart[1:] + fraction | |
121 | digs = max(0, digs) | |
122 | intpart, fraction = roundfrac(intpart, fraction, digs) | |
123 | if len(intpart) > 1: | |
124 | intpart, fraction, expo = \ | |
125 | intpart[0], intpart[1:] + fraction[:-1], \ | |
126 | expo + len(intpart) - 1 | |
127 | s = sign + intpart | |
128 | if digs > 0: s = s + '.' + fraction | |
129 | e = repr(abs(expo)) | |
130 | e = '0'*(3-len(e)) + e | |
131 | if expo < 0: e = '-' + e | |
132 | else: e = '+' + e | |
133 | return s + 'e' + e | |
134 | ||
135 | def test(): | |
136 | """Interactive test run.""" | |
137 | try: | |
138 | while 1: | |
139 | x, digs = input('Enter (x, digs): ') | |
140 | print x, fix(x, digs), sci(x, digs) | |
141 | except (EOFError, KeyboardInterrupt): | |
142 | pass |