fpformat.py 4.46 KB
Newer Older
1
"""General floating point formatting functions.
Guido van Rossum's avatar
Guido van Rossum committed
2

3 4 5
Functions:
fix(x, digits_behind)
sci(x, digits_behind)
Guido van Rossum's avatar
Guido van Rossum committed
6

7
Each takes a number or a string and a number of digits as arguments.
Guido van Rossum's avatar
Guido van Rossum committed
8

9 10 11 12
Parameters:
x:             number to be formatted; or a string resembling a number
digits_behind: number of digits behind the decimal point
"""
Guido van Rossum's avatar
Guido van Rossum committed
13

14
import re
Guido van Rossum's avatar
Guido van Rossum committed
15

Skip Montanaro's avatar
Skip Montanaro committed
16 17
__all__ = ["fix","sci","NotANumber"]

Guido van Rossum's avatar
Guido van Rossum committed
18
# Compiled regular expression to "decode" a number
19
decoder = re.compile(r'^([-+]?)0*(\d*)((?:\.\d*)?)(([eE][-+]?\d+)?)$')
Guido van Rossum's avatar
Guido van Rossum committed
20 21 22 23
# \0 the whole thing
# \1 leading sign or empty
# \2 digits left of decimal point
# \3 fraction (empty or begins with point)
24
# \4 exponent part (empty or begins with 'e' or 'E')
Guido van Rossum's avatar
Guido van Rossum committed
25

26
try:
27 28
    class NotANumber(ValueError):
        pass
29
except TypeError:
30
    NotANumber = 'fpformat.NotANumber'
Guido van Rossum's avatar
Guido van Rossum committed
31 32

def extract(s):
33 34 35 36 37 38 39 40 41 42 43 44 45
    """Return (sign, intpart, fraction, expo) or raise an exception:
    sign is '+' or '-'
    intpart is 0 or more digits beginning with a nonzero
    fraction is 0 or more digits
    expo is an integer"""
    res = decoder.match(s)
    if res is None: raise NotANumber, s
    sign, intpart, fraction, exppart = res.group(1,2,3,4)
    if sign == '+': sign = ''
    if fraction: fraction = fraction[1:]
    if exppart: expo = int(exppart[1:])
    else: expo = 0
    return sign, intpart, fraction, expo
Guido van Rossum's avatar
Guido van Rossum committed
46 47

def unexpo(intpart, fraction, expo):
48 49 50 51 52 53 54 55 56 57 58 59
    """Remove the exponent by changing intpart and fraction."""
    if expo > 0: # Move the point left
        f = len(fraction)
        intpart, fraction = intpart + fraction[:expo], fraction[expo:]
        if expo > f:
            intpart = intpart + '0'*(expo-f)
    elif expo < 0: # Move the point right
        i = len(intpart)
        intpart, fraction = intpart[:expo], intpart[expo:] + fraction
        if expo < -i:
            fraction = '0'*(-expo-i) + fraction
    return intpart, fraction
Guido van Rossum's avatar
Guido van Rossum committed
60 61

def roundfrac(intpart, fraction, digs):
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
    """Round or extend the fraction to size digs."""
    f = len(fraction)
    if f <= digs:
        return intpart, fraction + '0'*(digs-f)
    i = len(intpart)
    if i+digs < 0:
        return '0'*-digs, ''
    total = intpart + fraction
    nextdigit = total[i+digs]
    if nextdigit >= '5': # Hard case: increment last digit, may have carry!
        n = i + digs - 1
        while n >= 0:
            if total[n] != '9': break
            n = n-1
        else:
            total = '0' + total
            i = i+1
            n = 0
        total = total[:n] + chr(ord(total[n]) + 1) + '0'*(len(total)-n-1)
        intpart, fraction = total[:i], total[i:]
    if digs >= 0:
        return intpart, fraction[:digs]
    else:
        return intpart[:digs] + '0'*-digs, ''
Guido van Rossum's avatar
Guido van Rossum committed
86 87

def fix(x, digs):
88 89 90 91 92 93 94 95 96 97 98 99 100 101
    """Format x as [-]ddd.ddd with 'digs' digits after the point
    and at least one digit before.
    If digs <= 0, the point is suppressed."""
    if type(x) != type(''): x = `x`
    try:
        sign, intpart, fraction, expo = extract(x)
    except NotANumber:
        return x
    intpart, fraction = unexpo(intpart, fraction, expo)
    intpart, fraction = roundfrac(intpart, fraction, digs)
    while intpart and intpart[0] == '0': intpart = intpart[1:]
    if intpart == '': intpart = '0'
    if digs > 0: return sign + intpart + '.' + fraction
    else: return sign + intpart
Guido van Rossum's avatar
Guido van Rossum committed
102 103

def sci(x, digs):
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    """Format x as [-]d.dddE[+-]ddd with 'digs' digits after the point
    and exactly one digit before.
    If digs is <= 0, one digit is kept and the point is suppressed."""
    if type(x) != type(''): x = `x`
    sign, intpart, fraction, expo = extract(x)
    if not intpart:
        while fraction and fraction[0] == '0':
            fraction = fraction[1:]
            expo = expo - 1
        if fraction:
            intpart, fraction = fraction[0], fraction[1:]
            expo = expo - 1
        else:
            intpart = '0'
    else:
        expo = expo + len(intpart) - 1
        intpart, fraction = intpart[0], intpart[1:] + fraction
    digs = max(0, digs)
    intpart, fraction = roundfrac(intpart, fraction, digs)
    if len(intpart) > 1:
        intpart, fraction, expo = \
            intpart[0], intpart[1:] + fraction[:-1], \
            expo + len(intpart) - 1
    s = sign + intpart
    if digs > 0: s = s + '.' + fraction
    e = `abs(expo)`
    e = '0'*(3-len(e)) + e
    if expo < 0: e = '-' + e
    else: e = '+' + e
    return s + 'e' + e
Guido van Rossum's avatar
Guido van Rossum committed
134 135

def test():
136 137 138 139 140 141 142
    """Interactive test run."""
    try:
        while 1:
            x, digs = input('Enter (x, digs): ')
            print x, fix(x, digs), sci(x, digs)
    except (EOFError, KeyboardInterrupt):
        pass