Source code for pylablib.core.gui.formatter
import math
import re
SI_prefixes={"Y":1E+24,
"Z":1E+21,
"E":1E+18,
"P":1E+15,
"T":1E+12,
"G":1E+9,
"M":1E+6,
"k":1E+3,
"h":1E+2,
"da":1E+1,
"":1E+0,
"d":1E-1,
"c":1E-2,
"m":1E-3,
"u":1E-6,
"n":1E-9,
"p":1E-12,
"f":1E-15,
"a":1E-18,
"z":1E-21,
"y":1E-24}
SI_prefixes_print=dict([ (int(math.log10(o)),p) for p,o in SI_prefixes.items() if int(round(math.log10(o)))%3==0 ])
_SI_prefix_re_str="({0})".format( "|".join(SI_prefixes.keys()) )
_SI_float_re=re.compile(r'^\s*([+-]?)(\d*)(\.?)(\d*)((?:[Ee][+-]?\d+)?){0}\s*$'.format(_SI_prefix_re_str))
[docs]
def parse_float(s):
"""
Parse string as a float, with metric prefixes recognition.
Return tuple ``(sign, integer, dot, fractional, exponent, prefix)``, where each entry has structure ``(begin, end, text)``.
Return ``None`` if string is unrecognizable.
"""
m=re.match(_SI_float_re,s)
if m==None:
return None
g=m.groups()
if g[1]=="" and g[3]=="":
return None
return tuple([(b,e,t) for (b,e),t in zip(m.regs[1:],g)])
[docs]
def pos_to_order(s,pos):
"""
For a given string representation of a float and position in the string, get the decimal order for this position.
Return ``None`` if string is un-parsable or position is out of range (not in mantissa section of the number).
"""
parsed_value=parse_float(s)
if parsed_value==None:
return None
sign_position=parsed_value[0][1]
exponent_order=0
if parsed_value[4][2]!="":
exponent_order=int(parsed_value[4][2][1:])
prefix_order=int(round( math.log10(SI_prefixes[parsed_value[5][2]]) ))
order=exponent_order+prefix_order
if pos<=sign_position: #before the number
affected_digit=max(len(parsed_value[1][2])-1,0)
elif pos<=parsed_value[1][1]: #inside integer part
affected_digit=parsed_value[1][1]-pos
elif pos==parsed_value[2][1]: #right after the dot (if it's not there, then this condition is never true)
affected_digit=-1
elif pos<=parsed_value[3][1]: #inside fractional part
affected_digit=parsed_value[3][0]-pos
else:
return None
return order+affected_digit
[docs]
def order_to_pos(s,order):
"""
For a given string representation of float and decimal order, get the position in the string corresponding to this order.
If order is out of range for a given representation, truncates to most/least significant digit position.
Return ``None`` if string is un-parsable.
"""
parsed_value=parse_float(s)
if parsed_value==None:
return None
exponent_order=0
if parsed_value[4][2]!="":
exponent_order=int(parsed_value[4][2][1:])
prefix_order=int(round( math.log10(SI_prefixes[parsed_value[5][2]]) ))
rel_order=order-(exponent_order+prefix_order)
if rel_order>=0:
if rel_order>=len(parsed_value[1][2]):
pos=parsed_value[1][0]
else:
pos=parsed_value[1][1]-rel_order
else:
if (-rel_order)>=len(parsed_value[3][2]):
pos=parsed_value[3][1]
else:
pos=parsed_value[3][0]+(-rel_order)
return pos
[docs]
def str_to_float(s):
"""
Return float value of a string, with metric prefixes recognition.
Raise ``ValueError`` if string is unrecognizable.
"""
if len(s)==0:
raise ValueError()
if len(s)>2 and s[-2:] in SI_prefixes:
prefix=SI_prefixes[s[-2:]]
s=s[:-2]
elif s[-1:] in SI_prefixes:
prefix=SI_prefixes[s[-1:]]
s=s[:-1]
else:
prefix=1.
return float(s)*prefix
[docs]
def is_integer(n, tolerance=0.):
"""
Check if `n` is less than `tolerance` away from the nearest integer.
"""
return abs(n-round(n))<=tolerance
[docs]
def float_to_str_SI(n, digits=3, trailing_zeros=False):
"""
Represent float using SI metric prefixes.
For orders ``>=27`` and ``<-24`` use usual scientific notation with order being multiple of 3.
If ``trailing_zeros==True``, then digits define precision, rather than number significant digits
"""
if n==0:
if trailing_zeros:
return "0."+"0"*digits
else:
return "0"
order=int(math.floor(math.log10(abs(n))))
order_SI=(order//3)*3
n=n*10**(-order_SI)
if order_SI in SI_prefixes_print:
prefix=SI_prefixes_print[order_SI]
else:
prefix="E{0:+d}".format(order_SI)
if trailing_zeros:
nstr="{{:.{:d}F}}".format(digits).format(n)
else:
nstr="{{:.{:d}G}}".format(digits).format(n)
return nstr+prefix
[docs]
class FloatFormatter:
"""
Floating point number formatter.
Callable object with takes a number as an argument and returns is string representation.
Args:
output_format(str): can be ``"auto"`` (use standard Python conversion), ``"SI"`` (use SI prefixes if possible), or ``"sci"`` (scientific "E" notation).
digits (int): if ``add_trailing_zeros==False``, determines the number of significant digits; otherwise, determines precision (number of digits after decimal point).
add_trailing_zeros (bool): if ``True``, always show fixed number of digits after the decimal point, with zero padding if necessary.
leading_zeros (bool): determines the minimal size of the integer part (before the decimal point) of the number; pads with zeros if necessary.
explicit_sign (bool): if ``True``, always add explicit plus sign.
"""
def __init__(self, output_format="auto", digits=3, add_trailing_zeros=True, leading_zeros=0, explicit_sign=False):
"If trailing_zeros==True, then digits define precision, rather than number significant digits"
if not output_format in ["auto", "SI", "sci"]:
raise ValueError("unrecognized output format: {0}".format(output_format))
self.output_format=output_format
self.explicit_sign=explicit_sign
self.digits=digits
self.add_trailing_zeros=add_trailing_zeros
self.leading_zeros=leading_zeros
def __call__(self, value):
if self.output_format=="auto":
if self.add_trailing_zeros:
dig="{{:.{:d}F}}".format(self.digits).format(abs(value))
else:
dig="{{:.{:d}G}}".format(self.digits).format(abs(value))
elif self.output_format=="sci":
if self.add_trailing_zeros:
dig="{{:.0{:d}E}}".format(self.digits).format(abs(value))
else:
dig="{{:.{:d}E}}".format(self.digits).format(abs(value))
else:
dig=float_to_str_SI(abs(value),self.digits,self.add_trailing_zeros)
int_part_len=dig.find(".")
if int_part_len<0:
int_part_len=len(dig)
if self.leading_zeros>int_part_len:
dig="0"*(self.leading_zeros-int_part_len)+dig
if self.explicit_sign and value>=0:
dig="+"+dig
elif value<0:
dig="-"+dig
return dig
[docs]
class IntegerFormatter:
"""
Simple integer number formatter.
Callable object with takes a number as an argument and returns is string representation.
For more flexibility (e.g., adding leading zeros) it is possible to use :class:`FloatFormatter` with ``digits=0`` and ``add_trailing_zeros=True``.
"""
def __call__(self, value):
return "{:d}".format(int(value))
[docs]
class FmtStringFormatter:
"""
Formatter based on format string.
Callable object with takes a number as an argument and returns is string representation.
"""
def __init__(self, fmt):
self.fmt=fmt
def __call__(self, value):
return ("{:"+self.fmt+"}").format(value)
[docs]
def as_formatter(formatter):
"""
Turn an object into a formatter.
Can be a callable object turning value into a string, a string (``"float"``, ``"int"``, or a format string, e.g., ``".5f"``),
or a tuple starting with ``"float"`` which contains arguments to the :class:`FloatFormatter`.
"""
if hasattr(formatter,"__call__"):
return formatter
if formatter=="float" or formatter is None:
return FloatFormatter()
if formatter=="int":
return IntegerFormatter()
try:
("{:"+formatter+"}").format(0)
return FmtStringFormatter(formatter)
except (TypeError,ValueError):
pass
if isinstance(formatter,tuple):
if formatter[0]=="float":
return FloatFormatter(*formatter[1:])
raise ValueError("unknown formatter: {}".format(formatter))