''' Provides a class FPFormat that formats floating point numbers in a variety of ways: Fixed fix() Significant sig() Scientific sci() Engineering eng(), engsi() How to use: Create an object: fp = fpformat.FPFormat(num_digits=3) Set number of digits: fp.digits(num_digits) Get formatted string: sig(x), fix(x), sci(x), eng(x), engsi(x), engsic(x) Set the following global variables to control some of the ouput characteristics: decimal_point exponent_character imaginary_unit ''' ###################################################################### # Copyright (C) 2008 Don Peterson # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public # License along with this program; if not, write to the Free # Software Foundation, Inc., 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA or visit http://www.gnu.org/. ###################################################################### # Since I am ignorant of how locale-specific formatting works, these # globals can be used to control the characters that appear in output. decimal_point = "." exponent_character = "e" imaginary_unit = "i" class FPFormatException(Exception): pass class FPFormat: '''Formats a floating point number in a variety of ways: Fixed Fixed number of digits after the decimal point (may spill over to scientific notation at a platform- dependent value). Significant Fixed number of significant digits; switches to scientific at user-specified values. Scientific Scientific with a specified number of significant digits. Engineering Engineering with a specified number of significant digits. EngineeringSI Engineering with an SI suffix if appropriate. Setting the number of digits controls the number of digits after the decimal point for Fixed mode and the number of significant digits for the other modes. You can set as many significant digits as you want; your python implementation may or may not choke on your chosen value. On my platform, I was able to get fixed outputs such as 314159265358979330000000000000000000000000000000000000000 and 0.0000000000000000000000000000000000000000000000000000000314 The actual number of significant figures in a python float is typically the size of your platform's C double, so if you specify more significant figures than that, the resulting excess number of digits will almost certainly be wrong. ''' def __init__(self, num_digits=3): self.num_digits = 0 self.digits_min = 0 self.digits(num_digits) self.low = 1e-4 # Below this, use scientific for sig self.high = 1e6 # Above this, use scientific for sig self.suffixes = { -8:"y", -7:"z", -6:"a", -5:"f", -4:"p", -3:"n", -2:"u", -1:"m", 0:"", 1:"k", 2:"M", 3:"G", 4:"T", 5:"P", 6:"E", 7:"Z", 8:"Y"} # SI suffixes def digits(self, num_digits): if num_digits < self.digits_min: raise FPFormatException("must be >= %d" % self.digits_min) self.num_digits = num_digits def fix(self, number): f = "%%.%df" % self.num_digits f1 = "%%+.%df" % self.num_digits # Note: we have to use 'j' here because that's what python's # represention uses. if "j" in str(number): if number.real == 0: return (f % number.imag) % imaginary_unit elif number.imag == 0: return f % number.real else: return (f % number.real) + (f1 % number.imag) + imaginary_unit else: return f % number def sci(self, number): if "j" in str(number): if number.real == 0: return self._sci(number.imag) + imaginary_unit elif number.imag == 0: return self._sci(number.real) else: im_sign = "+" if number.imag > 0 else "" return self._sci(number.real) + im_sign + \ self._sci(number.imag) + imaginary_unit else: return self._sci(number) def _sci(self, number): "Scientific format for a floating point number" from math import log10, floor efmt = "e%+04d" try: exponent = 0 if number == 0 else abs(int(floor(log10(abs(number))))) except ValueError: return str(number) except OverflowError: return str(number) n = max(self.num_digits - 1, 0) s = ("%%+.%d%s" % (n, exponent_character)) % (number/10**exponent) mantissa = s[1:s.find(exponent_character)] # Also chop off sign mantissa = mantissa.replace(decimal_point, "") while len(mantissa) < self.num_digits: mantissa += "0" if self.num_digits: if len(mantissa) == 1: mantissa += decimal_point else: mantissa = mantissa[:1] + decimal_point + mantissa[1:] e = efmt % exponent if abs(number) < 1: e = efmt % (-exponent) sign = "" if number >= 0 else "-" return sign + mantissa + e def eng(self, number): if "j" in str(number): if number.real == 0: return self._eng(number.imag) + imaginary_unit elif number.imag == 0: return self._eng(number.real) else: im_sign = "+" if number.imag > 0 else "" return self._eng(number.real) + im_sign + \ self._eng(number.imag) + imaginary_unit else: return self._eng(number) def engsic(self, number): return self.engsi(number, cuddle=True) def engsi(self, number, cuddle=False): '''Same as eng(), but decorate with SI suffix. Will throw exception for a complex number. If cuddle is true, do not put a space between the number and the SI suffix. ''' if "j" in str(number): raise FPFormatException("Complex numbers not supported") s = self.eng(number) pos = s.find(exponent_character) try: exponent = int(s[pos + 1:])//3 except ValueError: # Can't format it (e.g., it's like '-1.#IND' return s except OverflowError: return str(number) if exponent in self.suffixes: if cuddle: return s[:pos] + self.suffixes[exponent] else: return s[:pos] + " " + self.suffixes[exponent] return s def _eng(self, number): "Engineering format for a floating point number" from math import log10, floor efmt = "e%+04d" try: exponent = 0 if number == 0 else abs(int(floor(log10(abs(number))))) except ValueError: return str(number) except OverflowError: return str(number) n = max(self.num_digits - 1, 0) s = ("%%+.%d%s" % (n, exponent_character)) % (number/10.**exponent) mantissa = s[1:s.find(exponent_character)] # Also chop off sign mantissa = mantissa.replace(decimal_point, "") if abs(number) < 1: num_3, remainder = divmod(exponent-1, 3) e = efmt % (-3*(num_3 + 1)) dp = 3 - remainder # Decimal point postion while len(mantissa) < dp: mantissa += "0" if self.num_digits == 0: m = mantissa else: if len(mantissa) == dp: m = mantissa + decimal_point else: m = mantissa[:dp] + decimal_point + mantissa[dp:] else: num_3, remainder = divmod(exponent, 3) e = efmt % (3*num_3) while len(mantissa) < remainder+1: mantissa += "0" if self.num_digits == 0: m = mantissa else: # Insert decimal point if len(mantissa) == remainder+1: m = mantissa + decimal_point else: m = mantissa[:remainder+1] + decimal_point + \ mantissa[remainder+1:] if number < 0: m = "-" + m return m + e def sig(self, number): if number and (abs(number) < self.low or abs(number) > self.high): return self.sci(number) if "j" in str(number): if number.real == 0: return self._sig(number.imag) + imaginary_unit elif number.imag == 0: return self._sig(number.real) else: re = self.sig(number.real) im = self.sig(number.imag) im_sign = "+" if number.imag > 0 else "" return re + im_sign + im + imaginary_unit else: return self._sig(number) def _sig(self, number): "Handle the real number case" from math import log10, floor sign = "-" if number < 0 else "" if str(number) == "1.0": # This handles case where number is around 1-1e-16, which # is 1 for all intents and purposes, but will of course # test less than 1, causing the value to be sent back 10 # times too small. number = 1 try: exponent = 0 if number == 0 else abs(int(floor(log10(abs(number))))) except ValueError: return str(number) except OverflowError: return str(number) n = max(self.num_digits - 1, 0) mantissa = ("%%.%de" % n) % abs(number) mantissa = mantissa[:mantissa.find("e")].replace(decimal_point, "") if abs(number) < 1: num_zeros_needed = exponent - 1 while num_zeros_needed > 0: mantissa = "0" + mantissa num_zeros_needed -= 1 mantissa = "0" + decimal_point + mantissa else: if len(mantissa) > exponent + 1: # Need to position decimal point in mantissa mantissa = mantissa[:exponent + 1] + decimal_point + \ mantissa[exponent + 1:] else: # Need to add zeros num_zeros_needed = exponent - n while num_zeros_needed > 0: mantissa += "0" num_zeros_needed -= 1 if self.num_digits: mantissa += decimal_point return sign + mantissa if __name__ == "__main__": manual = '''Instructions for use of fpformat.py: Create an object: fp = fpformat.FPFormat(num_digits=3) Set number of digits: fp.digits(num_digits) Get formatted string: sig(x), fix(x), sci(x), eng(x), engsi(x), engsic(x)''' print manual # Test the FPFormat class. Lines that are likely to be platform # dependent are conditional upon the os.name attribute. If you # test on an alternate platform or find some missing test cases # that expose bugs, please submit your changes to the xfmpy # project at SourceForge. from os import name nt = "nt" e = RuntimeError("Potential platform dependency -- please check") imaginary_unit = "j" def Test_sig(): f = FPFormat() x = 1.2345678901234567890 f.digits( 0); assert(f.sig(x) == "1") f.digits( 0); assert(f.sig(-x) == "-1") f.digits( 1); assert(f.sig(x) == "1.") f.digits( 1); assert(f.sig(-x) == "-1.") f.digits( 2); assert(f.sig(x) == "1.2") f.digits( 3); assert(f.sig(x) == "1.23") f.digits( 4); assert(f.sig(x) == "1.235") f.digits( 5); assert(f.sig(x) == "1.2346") if name == nt: f.digits(20); assert(f.sig(x) == "1.2345678901234567000") f.digits(20); assert(f.sig(-x) == "-1.2345678901234567000") else: raise e f.high = 2e6 x *= 1e6 f.digits( 0); assert(f.sig(x) == "1000000") f.digits( 0); assert(f.sig(-x) == "-1000000") f.digits( 1); assert(f.sig(x) == "1000000.") f.digits( 2); assert(f.sig(x) == "1200000.") f.digits( 3); assert(f.sig(x) == "1230000.") f.digits( 7); assert(f.sig(x) == "1234568.") f.digits( 8); assert(f.sig(x) == "1234567.9") if name == nt: f.digits(20); assert(f.sig(x) == "1234567.8901234567000") f.digits(20); assert(f.sig(-x) == "-1234567.8901234567000") else: raise e x /= 1e12 f.low = 1e-7 f.digits( 0); assert(f.sig(x) == "0.000001") f.digits( 0); assert(f.sig(-x) == "-0.000001") f.digits( 1); assert(f.sig(x) == "0.000001") f.digits( 2); assert(f.sig(x) == "0.0000012") f.digits( 6); assert(f.sig(x) == "0.00000123457") if name == nt: f.digits(20); assert(f.sig(x) == "0.0000012345678901234567000") f.digits(20); assert(f.sig(-x) == "-0.0000012345678901234567000") else: raise e f.digits(2) x = 1.23456e3-1.23456e3j; assert(f.sig(x) == "1200.-1200.j") x = -1.23456e3-1.23456e3j; assert(f.sig(x) == "-1200.-1200.j") x = 1.23456e3+1.23456e3j; assert(f.sig(x) == "1200.+1200.j") x = -1.23456e3+1.23456e3j; assert(f.sig(x) == "-1200.+1200.j") x = 1.23456e-3-1.23456e-3j; assert(f.sig(x) == "0.0012-0.0012j") x = -1.23456e-3-1.23456e-3j; assert(f.sig(x) == "-0.0012-0.0012j") x = 1.23456e-3+1.23456e-3j; assert(f.sig(x) == "0.0012+0.0012j") x = -1.23456e-3+1.23456e-3j; assert(f.sig(x) == "-0.0012+0.0012j") def Test_fix(): f = FPFormat() x = 1.2345678901234567890 f.digits( 0); assert(f.fix(x) == "1") f.digits( 1); assert(f.fix(x) == "1.2") f.digits( 2); assert(f.fix(x) == "1.23") f.digits( 3); assert(f.fix(x) == "1.235") if name == nt: f.digits(20); assert(f.fix(x) == "1.23456789012345670000") f.digits(20); assert(f.fix(-x) == "-1.23456789012345670000") else: raise e x *= 1e9 f.digits( 0); assert(f.fix(x) == "1234567890") f.digits( 1); assert(f.fix(x) == "1234567890.1") f.digits( 2); assert(f.fix(x) == "1234567890.12") f.digits( 3); assert(f.fix(x) == "1234567890.123") f.digits( 4); assert(f.fix(x) == "1234567890.1235") f.digits(10); assert(f.fix(x) == "1234567890.1234567000") x /= 1e12 f.digits( 0); assert(f.fix(x) == "0") f.digits( 0); assert(f.fix(-x) == "-0") f.digits( 1); assert(f.fix(x) == "0.0") f.digits( 1); assert(f.fix(-x) == "-0.0") f.digits( 2); assert(f.fix(x) == "0.00") f.digits( 2); assert(f.fix(-x) == "-0.00") f.digits( 3); assert(f.fix(x) == "0.001") f.digits( 3); assert(f.fix(-x) == "-0.001") f.digits( 4); assert(f.fix(x) == "0.0012") f.digits( 5); assert(f.fix(x) == "0.00123") f.digits( 7); assert(f.fix(x) == "0.0012346") if name == nt: f.digits(25); assert(f.fix(x) == "0.0012345678901234567000000") f.digits(25); assert(f.fix(-x) == "-0.0012345678901234567000000") else: raise e f.digits(2) x = 1.23456e3-1.23456e3j; assert(f.fix(x) == "1234.56-1234.56j") x = -1.23456e3-1.23456e3j; assert(f.fix(x) == "-1234.56-1234.56j") x = 1.23456e3+1.23456e3j; assert(f.fix(x) == "1234.56+1234.56j") x = -1.23456e3+1.23456e3j; assert(f.fix(x) == "-1234.56+1234.56j") f.digits(5) x = 1.23456e-3-1.23456e-3j; assert(f.fix(x) == "0.00123-0.00123j") x = -1.23456e-3-1.23456e-3j; assert(f.fix(x) == "-0.00123-0.00123j") x = 1.23456e-3+1.23456e-3j; assert(f.fix(x) == "0.00123+0.00123j") x = -1.23456e-3+1.23456e-3j; assert(f.fix(x) == "-0.00123+0.00123j") def Test_sci(): f = FPFormat() x = 1.2345678901234567890 f.digits( 0); assert(f.sci( x) == "1e+000") f.digits( 0); assert(f.sci(-x) == "-1e+000") f.digits( 1); assert(f.sci(x) == "1.e+000") f.digits( 1); assert(f.sci(-x) == "-1.e+000") f.digits( 2); assert(f.sci(x) == "1.2e+000") f.digits( 3); assert(f.sci(x) == "1.23e+000") f.digits( 3); assert(f.sci(-x) == "-1.23e+000") if name == nt: f.digits(20); assert(f.sci(x) == "1.2345678901234567000e+000") else: raise e x *= 1e9 f.digits( 0); assert(f.sci(x) == "1e+009") f.digits( 0); assert(f.sci(-x) == "-1e+009") f.digits( 1); assert(f.sci(x) == "1.e+009") f.digits( 2); assert(f.sci(x) == "1.2e+009") f.digits( 2); assert(f.sci(-x) == "-1.2e+009") x /= 1e18 f.digits( 0); assert(f.sci(x) == "1e-009") f.digits( 1); assert(f.sci(x) == "1.e-009") f.digits( 2); assert(f.sci(x) == "1.2e-009") if name == nt: f.digits(20); assert(f.sci( x) == "1.2345678901234566000e-009") f.digits(20); assert(f.sci(-x) == "-1.2345678901234566000e-009") else: raise e f.digits(2) x = 1.23456e6-1.23456e6j; assert(f.sci(x) == "1.2e+006-1.2e+006j") x = -1.23456e6-1.23456e6j; assert(f.sci(x) == "-1.2e+006-1.2e+006j") x = 1.23456e6+1.23456e6j; assert(f.sci(x) == "1.2e+006+1.2e+006j") x = -1.23456e6+1.23456e6j; assert(f.sci(x) == "-1.2e+006+1.2e+006j") def Test_eng(): f = FPFormat() x = 1.2345678901234567890 f.digits( 0); assert(f.eng( x) == "1e+000") f.digits( 0); assert(f.eng(-x) == "-1e+000") f.digits( 1); assert(f.eng(x) == "1.e+000") f.digits( 1); assert(f.eng(-x) == "-1.e+000") f.digits( 2); assert(f.eng(x) == "1.2e+000") f.digits( 3); assert(f.eng(x) == "1.23e+000") f.digits( 3); assert(f.eng(-x) == "-1.23e+000") x = 1.2345678901234567890e1 f.digits( 0); assert(f.eng( x) == "10e+000") f.digits( 0); assert(f.eng(-x) == "-10e+000") f.digits( 1); assert(f.eng(x) == "10.e+000") f.digits( 1); assert(f.eng(-x) == "-10.e+000") f.digits( 2); assert(f.eng(x) == "12.e+000") f.digits( 3); assert(f.eng(x) == "12.3e+000") f.digits( 3); assert(f.eng(-x) == "-12.3e+000") x = 1.2345678901234567890e-1 f.digits( 0); assert(f.eng( x) == "100e-003") f.digits( 0); assert(f.eng(-x) == "-100e-003") f.digits( 1); assert(f.eng(x) == "100.e-003") f.digits( 1); assert(f.eng(-x) == "-100.e-003") f.digits( 2); assert(f.eng(x) == "120.e-003") f.digits( 2); assert(f.eng(-x) == "-120.e-003") f.digits( 3); assert(f.eng(x) == "123.e-003") f.digits( 3); assert(f.eng(-x) == "-123.e-003") x = 1.2345678901234567890e2 f.digits( 0); assert(f.eng( x) == "100e+000") f.digits( 0); assert(f.eng(-x) == "-100e+000") f.digits( 1); assert(f.eng(x) == "100.e+000") f.digits( 1); assert(f.eng(-x) == "-100.e+000") f.digits( 2); assert(f.eng(x) == "120.e+000") f.digits( 2); assert(f.eng(-x) == "-120.e+000") f.digits( 3); assert(f.eng(x) == "123.e+000") f.digits( 3); assert(f.eng(-x) == "-123.e+000") x = 1.2345678901234567890e-2 f.digits( 0); assert(f.eng( x) == "10e-003") f.digits( 0); assert(f.eng(-x) == "-10e-003") f.digits( 1); assert(f.eng(x) == "10.e-003") f.digits( 1); assert(f.eng(-x) == "-10.e-003") f.digits( 2); assert(f.eng(x) == "12.e-003") f.digits( 3); assert(f.eng(x) == "12.3e-003") f.digits( 3); assert(f.eng(-x) == "-12.3e-003") x = 1.2345678901234567890e3 f.digits( 0); assert(f.eng( x) == "1e+003") f.digits( 0); assert(f.eng(-x) == "-1e+003") f.digits( 1); assert(f.eng(x) == "1.e+003") f.digits( 1); assert(f.eng(-x) == "-1.e+003") f.digits( 2); assert(f.eng(x) == "1.2e+003") f.digits( 3); assert(f.eng(x) == "1.23e+003") f.digits( 3); assert(f.eng(-x) == "-1.23e+003") x = 1.2345678901234567890e-3 f.digits( 0); assert(f.eng( x) == "1e-003") f.digits( 0); assert(f.eng(-x) == "-1e-003") f.digits( 1); assert(f.eng(x) == "1.e-003") f.digits( 1); assert(f.eng(-x) == "-1.e-003") f.digits( 2); assert(f.eng(x) == "1.2e-003") f.digits( 3); assert(f.eng(x) == "1.23e-003") f.digits( 3); assert(f.eng(-x) == "-1.23e-003") f.digits(2) x = 1.23456e6-1.23456e6j; assert(f.eng(x) == "1.2e+006-1.2e+006j") x = -1.23456e6-1.23456e6j; assert(f.eng(x) == "-1.2e+006-1.2e+006j") x = 1.23456e6+1.23456e6j; assert(f.eng(x) == "1.2e+006+1.2e+006j") x = -1.23456e6+1.23456e6j; assert(f.eng(x) == "-1.2e+006+1.2e+006j") def Test_engsi(): f = FPFormat() f.digits(2) x = 1.23e-27; assert(f.engsi(x) == "1.2e-027") x = 1.23e-26; assert(f.engsi(x) == "12.e-027") x = 1.23e-25; assert(f.engsi(x) == "120.e-027") x = 1.23e-24; assert(f.engsi(x) == "1.2 y") x = 1.23e-23; assert(f.engsi(x) == "12. y") x = 1.23e-22; assert(f.engsi(x) == "120. y") x = 1.23e-21; assert(f.engsi(x) == "1.2 z") x = 1.23e-20; assert(f.engsi(x) == "12. z") x = 1.23e-19; assert(f.engsi(x) == "120. z") x = 1.23e-18; assert(f.engsi(x) == "1.2 a") x = 1.23e-17; assert(f.engsi(x) == "12. a") x = 1.23e-16; assert(f.engsi(x) == "120. a") x = 1.23e-15; assert(f.engsi(x) == "1.2 f") x = 1.23e-14; assert(f.engsi(x) == "12. f") x = 1.23e-13; assert(f.engsi(x) == "120. f") x = 1.23e-12; assert(f.engsi(x) == "1.2 p") x = 1.23e-11; assert(f.engsi(x) == "12. p") x = 1.23e-10; assert(f.engsi(x) == "120. p") x = 1.23e-09; assert(f.engsi(x) == "1.2 n") x = 1.23e-08; assert(f.engsi(x) == "12. n") x = 1.23e-07; assert(f.engsi(x) == "120. n") x = 1.23e-06; assert(f.engsi(x) == "1.2 u") x = 1.23e-05; assert(f.engsi(x) == "12. u") x = 1.23e-04; assert(f.engsi(x) == "120. u") x = 1.23e-03; assert(f.engsi(x) == "1.2 m") x = 1.23e-02; assert(f.engsi(x) == "12. m") x = 1.23e-01; assert(f.engsi(x) == "120. m") x = 1.23e+00; assert(f.engsi(x) == "1.2 ") x = 1.23e+01; assert(f.engsi(x) == "12. ") x = 1.23e+02; assert(f.engsi(x) == "120. ") x = 1.23e+03; assert(f.engsi(x) == "1.2 k") x = 1.23e+04; assert(f.engsi(x) == "12. k") x = 1.23e+05; assert(f.engsi(x) == "120. k") x = 1.23e+06; assert(f.engsi(x) == "1.2 M") x = 1.23e+07; assert(f.engsi(x) == "12. M") x = 1.23e+08; assert(f.engsi(x) == "120. M") x = 1.23e+09; assert(f.engsi(x) == "1.2 G") x = 1.23e+10; assert(f.engsi(x) == "12. G") x = 1.23e+11; assert(f.engsi(x) == "120. G") x = 1.23e+12; assert(f.engsi(x) == "1.2 T") x = 1.23e+13; assert(f.engsi(x) == "12. T") x = 1.23e+14; assert(f.engsi(x) == "120. T") x = 1.23e+15; assert(f.engsi(x) == "1.2 P") x = 1.23e+16; assert(f.engsi(x) == "12. P") x = 1.23e+17; assert(f.engsi(x) == "120. P") x = 1.23e+18; assert(f.engsi(x) == "1.2 E") x = 1.23e+19; assert(f.engsi(x) == "12. E") x = 1.23e+20; assert(f.engsi(x) == "120. E") x = 1.23e+21; assert(f.engsi(x) == "1.2 Z") x = 1.23e+22; assert(f.engsi(x) == "12. Z") x = 1.23e+23; assert(f.engsi(x) == "120. Z") x = 1.23e+24; assert(f.engsi(x) == "1.2 Y") x = 1.23e+25; assert(f.engsi(x) == "12. Y") x = 1.23e+26; assert(f.engsi(x) == "120. Y") x = 1.23e+27; assert(f.engsi(x) == "1.2e+027") # Run the unit tests Test_sig() Test_fix() Test_sci() Test_eng() Test_engsi()