diff --git a/doc/release/1.14.0-notes.rst b/doc/release/1.14.0-notes.rst index 0aeeadd40fae..0f119275d83a 100644 --- a/doc/release/1.14.0-notes.rst +++ b/doc/release/1.14.0-notes.rst @@ -396,3 +396,18 @@ Seeding ``RandomState`` using an array requires a 1-d array ``RandomState`` previously would accept empty arrays or arrays with 2 or more dimensions, which resulted in either a failure to seed (empty arrays) or for some of the passed values to be ignored when setting the seed. + +repr for longfloat rewritten, repr precision for longfoat,half now customizable +------------------------------------------------------------------------------- +The reprs for arrays of ``longfloat`` type has been changed to more closely +match the printing behavior of ``float`` arrays. Also, the precision printed +for ``longfloat`` and ``half`` arrays is now customizable using the +``longfloat_precision`` and ``halffloat_precision`` arguments of +``np.set_printoptions``, with defaults of 8 and 3 respectively. + +repr of floating-point arrays now trims 0s in scientific notation +----------------------------------------------------------------- +When an array's floating-point values are output in scientific notation +the trailing zeros in the fractional part are now dropped. In other words, +``1.1230000e+100`` now prints as ``1.123e+100``. This new behavior +is disable by setting the ``legacy=True`` option to ``np.set_printoptions``. diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py index 7ce6e0795605..3780dae2dd8b 100644 --- a/numpy/core/arrayprint.py +++ b/numpy/core/arrayprint.py @@ -59,6 +59,8 @@ 'edgeitems': 3, # repr N leading and trailing items of each dimension 'threshold': 1000, # total items > triggers array summarization 'precision': 8, # precision of floating point representations + 'longfloat_precision': 8, # precision of long float representations + 'halffloat_precision': 3, # precision of half float representations 'suppress': False, # suppress printing small floating values in exp format 'linewidth': 75, 'nanstr': 'nan', @@ -68,7 +70,8 @@ def _make_options_dict(precision=None, threshold=None, edgeitems=None, linewidth=None, suppress=None, nanstr=None, infstr=None, - sign=None, formatter=None): + sign=None, formatter=None, longfloat_precision=None, + halffloat_precision=None): """ make a dictionary out of the non-None arguments, plus sanity checks """ options = {k: v for k, v in locals().items() if v is not None} @@ -84,7 +87,8 @@ def _make_options_dict(precision=None, threshold=None, edgeitems=None, def set_printoptions(precision=None, threshold=None, edgeitems=None, linewidth=None, suppress=None, nanstr=None, infstr=None, - formatter=None, sign=None): + formatter=None, sign=None, longfloat_precision=None, + halffloat_precision=None): """ Set printing options. @@ -95,6 +99,10 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None, ---------- precision : int, optional Number of digits of precision for floating point output (default 8). + longfloat_precision : int, optional + Number of digits of precision for longfloat output (default 8). + halffloat_precision : int, optional + Number of digits of precision for half float output (default 8). threshold : int, optional Total number of array elements which trigger summarization rather than full repr (default 1000). @@ -196,7 +204,8 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None, ... suppress=False, threshold=1000, formatter=None) """ opt = _make_options_dict(precision, threshold, edgeitems, linewidth, - suppress, nanstr, infstr, sign, formatter) + suppress, nanstr, infstr, sign, formatter, + longfloat_precision, halffloat_precision) # formatter is always reset opt['formatter'] = formatter _format_options.update(opt) @@ -260,14 +269,17 @@ def repr_format(x): def _get_formatdict(data, **opt): prec, supp, sign = opt['precision'], opt['suppress'], opt['sign'] + lprec, hprec = opt['longfloat_precision'], opt['halffloat_precision'] # wrapped in lambdas to avoid taking a code path with the wrong type of data formatdict = {'bool': lambda: BoolFormat(data), 'int': lambda: IntegerFormat(data), 'float': lambda: FloatFormat(data, prec, supp, sign), - 'longfloat': lambda: LongFloatFormat(prec), + 'longfloat': lambda: LongFloatFormat(data, lprec, supp, sign), + 'halffloat': lambda: FloatFormat(data, hprec, supp, sign), 'complexfloat': lambda: ComplexFormat(data, prec, supp, sign), - 'longcomplexfloat': lambda: LongComplexFormat(prec), + 'longcomplexfloat': lambda: LongComplexFormat( + data, lprec, supp, sign), 'datetime': lambda: DatetimeFormat(data), 'timedelta': lambda: TimedeltaFormat(data), 'object': lambda: _object_format, @@ -323,6 +335,8 @@ def _get_format_function(data, **options): elif issubclass(dtypeobj, _nt.floating): if issubclass(dtypeobj, _nt.longfloat): return formatdict['longfloat']() + elif issubclass(dtypeobj, _nt.half): + return formatdict['halffloat']() else: return formatdict['float']() elif issubclass(dtypeobj, _nt.complexfloating): @@ -396,7 +410,8 @@ def _array2string(a, options, separator=' ', prefix=""): def array2string(a, max_line_width=None, precision=None, suppress_small=None, separator=' ', prefix="", style=np._NoValue, formatter=None, threshold=None, - edgeitems=None, sign=None): + edgeitems=None, sign=None, longfloat_precision=None, + halffloat_precision=None): """ Return a string representation of an array. @@ -410,6 +425,10 @@ def array2string(a, max_line_width=None, precision=None, precision : int, optional Floating point precision. Default is the current printing precision (usually 8), which can be altered using `set_printoptions`. + longfloat_precision : int, optional + Number of digits of precision for longfloat output (default 8). + halffloat_precision : int, optional + Number of digits of precision for half float output (default 8). suppress_small : bool, optional Represent very small numbers as zero. A number is "very small" if it is smaller than the current printing precision. @@ -510,7 +529,8 @@ def array2string(a, max_line_width=None, precision=None, overrides = _make_options_dict(precision, threshold, edgeitems, max_line_width, suppress_small, None, None, - sign, formatter) + sign, formatter, longfloat_precision, + halffloat_precision) options = _format_options.copy() options.update(overrides) @@ -638,28 +658,34 @@ def fillFormat(self, data): self.exp_format = True if self.exp_format: - self.large_exponent = 0 < min_val < 1e-99 or max_val >= 1e100 + # remove trailing zeros, compute exponent size + fmt = self._makeFormatter(True, '', 0, self.precision) + strs = (fmt(x) for x in abs_non_zero) + frac_strs, _, exp_strs = zip(*(s.partition('e') for s in strs)) + self.exp_size = max(len(s) for s in exp_strs) - 1 - signpos = self.sign != '-' or any(non_zero < 0) - # for back-compatibility with np 1.13, use two spaces + # for back-compatibility with np 1.13, use two spaces and full prec if self._legacy: signpos = 2 - max_str_len = signpos + 6 + self.precision + self.large_exponent - - conversion = '' if self.sign == '-' else self.sign - format = '%' + conversion + '%d.%de' % (max_str_len, self.precision) + sigfig = self.precision + else: + signpos = self.sign != '-' or any(non_zero < 0) + sigfig = max(self.precision - (len(s) - len(s.rstrip('0'))) + for s in frac_strs) + max_str_len = signpos + 2 + sigfig + 2 + self.exp_size else: if len(non_zero) and self.precision > 0: - precision = self.precision - trim_zero = lambda s: precision - (len(s) - len(s.rstrip('0'))) - fmt = '%%.%df' % (precision,) - precision = max(trim_zero(fmt % x) for x in abs_non_zero) + fmt = self._makeFormatter(False, '', 0, self.precision) + strs = (fmt(x) for x in abs_non_zero) + sigfig = max(self.precision - (len(s) - len(s.rstrip('0'))) + for s in strs) else: - precision = 0 + sigfig = 0 int_len = len(str(int(max_val))) signpos = self.sign != '-' or (len(str(int(min_val_sgn))) > int_len) - max_str_len = signpos + int_len + 1 + precision + self.exp_size = None + max_str_len = signpos + int_len + 1 + sigfig if any(special): neginf = self.sign != '-' or any(data[hasinf] < 0) @@ -667,45 +693,48 @@ def fillFormat(self, data): inflen = len(_format_options['infstr']) + neginf max_str_len = max(max_str_len, nanlen, inflen) - conversion = '' if self.sign == '-' else self.sign - format = '%#' + conversion + '%d.%df' % (max_str_len, precision) - + conversion = '' if self.sign == '-' else self.sign + self.elem_format = self._makeFormatter(self.exp_format, conversion, + max_str_len, sigfig) self.special_fmt = '%%%ds' % (max_str_len,) - self.format = format + + def _makeFormatter(self, exp_mode, conversion, max_str_len, sigfig): + fmt = "%#{}{}.{}{}".format(conversion, max_str_len, sigfig, + "e" if exp_mode else "f") + return lambda x: fmt % x def __call__(self, x, strip_zeros=True): with errstate(invalid='ignore'): if isnan(x): nan_str = _format_options['nanstr'] - if self.sign == '+': - return self.special_fmt % ('+' + nan_str,) - else: - return self.special_fmt % (nan_str,) + sign = '+' if self.sign == '+' else '' + return self.special_fmt % (sign + nan_str,) elif isinf(x): inf_str = _format_options['infstr'] - if x > 0: - if self.sign == '+': - return self.special_fmt % ('+' + inf_str,) - else: - return self.special_fmt % (inf_str,) - else: - return self.special_fmt % ('-' + inf_str,) - - s = self.format % x - if self.large_exponent: - # 3-digit exponent - expsign = s[-3] - if expsign == '+' or expsign == '-': - s = s[1:-2] + '0' + s[-2:] - elif self.exp_format: - # 2-digit exponent - if s[-3] == '0': - s = ' ' + s[:-3] + s[-2:] + sign = '-' if x < 0 else '+' if self.sign == '+' else '' + return self.special_fmt % (sign + inf_str,) + + s = self.elem_format(x) + + if self.exp_format: + # pad exponent with zeros, up to exp_size 0s. + frac_str, _, exp_str = s.partition('e') + expsign, expval = exp_str[0], exp_str[1:] + npad = self.exp_size -len(expval) + s = frac_str[npad:] + 'e' + expsign + '0'*npad + expval elif strip_zeros: z = s.rstrip('0') s = z + ' '*(len(s)-len(z)) return s + +class LongFloatFormat(FloatFormat): + def _makeFormatter(self, exp_mode, conversion, max_len, sigfig): + def formatter(x): + return format_longfloat(x, exp_mode, conversion, max_len, sigfig) + return formatter + + class IntegerFormat(object): def __init__(self, data): try: @@ -726,6 +755,7 @@ def __call__(self, x): else: return "%s" % x + class BoolFormat(object): def __init__(self, data, **kwargs): # add an extra space so " True" and "False" have the same length and @@ -736,53 +766,6 @@ def __call__(self, x): return self.truestr if x else "False" -class LongFloatFormat(object): - # XXX Have to add something to determine the width to use a la FloatFormat - # Right now, things won't line up properly - def __init__(self, precision, sign=False): - # for backcompatibility, accept bools - if isinstance(sign, bool): - sign = '+' if sign else '-' - - self.precision = precision - self.sign = sign - - def __call__(self, x): - if isnan(x): - nan_str = _format_options['nanstr'] - if self.sign == '+': - return '+' + nan_str - else: - return ' ' + nan_str - elif isinf(x): - inf_str = _format_options['infstr'] - if x > 0: - if self.sign == '+': - return '+' + inf_str - else: - return ' ' + inf_str - else: - return '-' + inf_str - elif x >= 0: - if self.sign == '+': - return '+' + format_longfloat(x, self.precision) - else: - return ' ' + format_longfloat(x, self.precision) - else: - return format_longfloat(x, self.precision) - - -class LongComplexFormat(object): - def __init__(self, precision): - self.real_format = LongFloatFormat(precision) - self.imag_format = LongFloatFormat(precision, sign='+') - - def __call__(self, x): - r = self.real_format(x.real) - i = self.imag_format(x.imag) - return r + i + 'j' - - class ComplexFormat(object): def __init__(self, x, precision, suppress_small, sign=False): # for backcompatibility, accept bools @@ -805,6 +788,19 @@ def __call__(self, x): return r + i +class LongComplexFormat(object): + def __init__(self, x, precision, suppress_small, sign=False): + self.real_format = LongFloatFormat(x.real, precision, suppress_small, + sign=sign) + self.imag_format = LongFloatFormat(x.imag, precision, suppress_small, + sign='+') + + def __call__(self, x): + r = self.real_format(x.real) + i = self.imag_format(x.imag) + return r + i + 'j' + + class DatetimeFormat(object): def __init__(self, x, unit=None, timezone=None, casting='same_kind'): # Get the unit from the dtype @@ -826,6 +822,7 @@ def __call__(self, x): timezone=self.timezone, casting=self.casting) + class TimedeltaFormat(object): def __init__(self, data): nat_value = array(['NaT'], dtype=data.dtype)[0] diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 499ec343c392..2d9aeb6c87ac 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -17,6 +17,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" #include "structmember.h" +#include #define NPY_NO_DEPRECATED_API NPY_API_VERSION #define _MULTIARRAYMODULE @@ -3586,26 +3587,67 @@ as_buffer(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) static PyObject * format_longfloat(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) { - PyObject *obj; - unsigned int precision; - npy_longdouble x; - static char *kwlist[] = {"x", "precision", NULL}; - static char repr[100]; + char format[64], repr[64]; + char *modechar, *conv; + int exp_mode; + int maxlen, precision; + int ret; + PyObject *val; + long double x; + static char *kwlist[] = {"x", "exp_mode", "conversion", "maxlen", + "precision", NULL}; + char *saved_locale=NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oisii", kwlist, + &val, &exp_mode, &conv, &maxlen, &precision)) { + return NULL; + } - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OI:format_longfloat", kwlist, - &obj, &precision)) { + if (!PyArray_IsScalar(val, LongDouble)) { + PyErr_SetString(PyExc_TypeError, "not a longfloat"); return NULL; } - if (!PyArray_IsScalar(obj, LongDouble)) { - PyErr_SetString(PyExc_TypeError, - "not a longfloat"); + x = ((PyLongDoubleScalarObject *)val)->obval; + + if (strcmp(conv, "+") && strcmp(conv, "-") && strcmp(conv, "")) { + PyErr_SetString(PyExc_ValueError, "conversion must be '', '+' or '-'"); + return NULL; + } + + modechar = exp_mode ? "Le" : "Lf"; + + ret = PyOS_snprintf(format, sizeof(format), + "%%#%s%d.%d%s", conv, maxlen, precision, modechar); + if (ret < 0 || ret >= sizeof(format)){ + PyErr_SetString(PyExc_RuntimeError, "printf failed"); return NULL; } - x = ((PyLongDoubleScalarObject *)obj)->obval; - if (precision > 70) { - precision = 70; + + // if we have a locale where the decimal point is not a "." character, + // temporarily switch to the C locale where it is. + // (This avoids all the juggling in ensure_decimal_point) + if (strcmp(localeconv()->decimal_point, ".") != 0) { + // save the current locale, copying the name as setlocale clobbers it + saved_locale = strdup(setlocale(LC_ALL, NULL)); + if (saved_locale == NULL){ + PyErr_SetString(PyExc_RuntimeError, "printf failed"); + return NULL; + } + setlocale (LC_ALL, "C"); + } + + ret = PyOS_snprintf(repr, sizeof(repr), format, x); + + // restore locale if necessary + if (saved_locale != NULL) { + setlocale(LC_ALL, saved_locale); + free(saved_locale); + } + + if (ret < 0 || ret >= sizeof(format)){ + PyErr_SetString(PyExc_RuntimeError, "printf failed"); + return NULL; } - format_longdouble(repr, 100, x, precision); return PyUString_FromString(repr); } diff --git a/numpy/core/tests/test_arrayprint.py b/numpy/core/tests/test_arrayprint.py index 26faabfb8855..c4ce0e0d55d9 100644 --- a/numpy/core/tests/test_arrayprint.py +++ b/numpy/core/tests/test_arrayprint.py @@ -72,42 +72,42 @@ def test_str(self): dtypes = [np.complex64, np.cdouble, np.clongdouble] actual = [str(np.array([c], dt)) for c in cvals for dt in dtypes] wanted = [ - '[0.+0.j]', '[0.+0.j]', '[ 0.0+0.0j]', - '[0.+1.j]', '[0.+1.j]', '[ 0.0+1.0j]', - '[0.-1.j]', '[0.-1.j]', '[ 0.0-1.0j]', - '[0.+infj]', '[0.+infj]', '[ 0.0+infj]', - '[0.-infj]', '[0.-infj]', '[ 0.0-infj]', - '[0.+nanj]', '[0.+nanj]', '[ 0.0+nanj]', - '[1.+0.j]', '[1.+0.j]', '[ 1.0+0.0j]', - '[1.+1.j]', '[1.+1.j]', '[ 1.0+1.0j]', - '[1.-1.j]', '[1.-1.j]', '[ 1.0-1.0j]', - '[1.+infj]', '[1.+infj]', '[ 1.0+infj]', - '[1.-infj]', '[1.-infj]', '[ 1.0-infj]', - '[1.+nanj]', '[1.+nanj]', '[ 1.0+nanj]', - '[-1.+0.j]', '[-1.+0.j]', '[-1.0+0.0j]', - '[-1.+1.j]', '[-1.+1.j]', '[-1.0+1.0j]', - '[-1.-1.j]', '[-1.-1.j]', '[-1.0-1.0j]', - '[-1.+infj]', '[-1.+infj]', '[-1.0+infj]', - '[-1.-infj]', '[-1.-infj]', '[-1.0-infj]', - '[-1.+nanj]', '[-1.+nanj]', '[-1.0+nanj]', - '[inf+0.j]', '[inf+0.j]', '[ inf+0.0j]', - '[inf+1.j]', '[inf+1.j]', '[ inf+1.0j]', - '[inf-1.j]', '[inf-1.j]', '[ inf-1.0j]', - '[inf+infj]', '[inf+infj]', '[ inf+infj]', - '[inf-infj]', '[inf-infj]', '[ inf-infj]', - '[inf+nanj]', '[inf+nanj]', '[ inf+nanj]', - '[-inf+0.j]', '[-inf+0.j]', '[-inf+0.0j]', - '[-inf+1.j]', '[-inf+1.j]', '[-inf+1.0j]', - '[-inf-1.j]', '[-inf-1.j]', '[-inf-1.0j]', - '[-inf+infj]', '[-inf+infj]', '[-inf+infj]', - '[-inf-infj]', '[-inf-infj]', '[-inf-infj]', - '[-inf+nanj]', '[-inf+nanj]', '[-inf+nanj]', - '[nan+0.j]', '[nan+0.j]', '[ nan+0.0j]', - '[nan+1.j]', '[nan+1.j]', '[ nan+1.0j]', - '[nan-1.j]', '[nan-1.j]', '[ nan-1.0j]', - '[nan+infj]', '[nan+infj]', '[ nan+infj]', - '[nan-infj]', '[nan-infj]', '[ nan-infj]', - '[nan+nanj]', '[nan+nanj]', '[ nan+nanj]'] + '[0.+0.j]', '[0.+0.j]', '[0.+0.j]', + '[0.+1.j]', '[0.+1.j]', '[0.+1.j]', + '[0.-1.j]', '[0.-1.j]', '[0.-1.j]', + '[0.+infj]', '[0.+infj]', '[0.+infj]', + '[0.-infj]', '[0.-infj]', '[0.-infj]', + '[0.+nanj]', '[0.+nanj]', '[0.+nanj]', + '[1.+0.j]', '[1.+0.j]', '[1.+0.j]', + '[1.+1.j]', '[1.+1.j]', '[1.+1.j]', + '[1.-1.j]', '[1.-1.j]', '[1.-1.j]', + '[1.+infj]', '[1.+infj]', '[1.+infj]', + '[1.-infj]', '[1.-infj]', '[1.-infj]', + '[1.+nanj]', '[1.+nanj]', '[1.+nanj]', + '[-1.+0.j]', '[-1.+0.j]', '[-1.+0.j]', + '[-1.+1.j]', '[-1.+1.j]', '[-1.+1.j]', + '[-1.-1.j]', '[-1.-1.j]', '[-1.-1.j]', + '[-1.+infj]', '[-1.+infj]', '[-1.+infj]', + '[-1.-infj]', '[-1.-infj]', '[-1.-infj]', + '[-1.+nanj]', '[-1.+nanj]', '[-1.+nanj]', + '[inf+0.j]', '[inf+0.j]', '[inf+0.j]', + '[inf+1.j]', '[inf+1.j]', '[inf+1.j]', + '[inf-1.j]', '[inf-1.j]', '[inf-1.j]', + '[inf+infj]', '[inf+infj]', '[inf+infj]', + '[inf-infj]', '[inf-infj]', '[inf-infj]', + '[inf+nanj]', '[inf+nanj]', '[inf+nanj]', + '[-inf+0.j]', '[-inf+0.j]', '[-inf+0.j]', + '[-inf+1.j]', '[-inf+1.j]', '[-inf+1.j]', + '[-inf-1.j]', '[-inf-1.j]', '[-inf-1.j]', + '[-inf+infj]', '[-inf+infj]', '[-inf+infj]', + '[-inf-infj]', '[-inf-infj]', '[-inf-infj]', + '[-inf+nanj]', '[-inf+nanj]', '[-inf+nanj]', + '[nan+0.j]', '[nan+0.j]', '[nan+0.j]', + '[nan+1.j]', '[nan+1.j]', '[nan+1.j]', + '[nan-1.j]', '[nan-1.j]', '[nan-1.j]', + '[nan+infj]', '[nan+infj]', '[nan+infj]', + '[nan-infj]', '[nan-infj]', '[nan-infj]', + '[nan+nanj]', '[nan+nanj]', '[nan+nanj]'] for res, val in zip(actual, wanted): assert_equal(res, val) @@ -290,17 +290,17 @@ def test_sign_spacing(self): assert_equal(repr(a), 'array([0., 1., 2., 3.])') assert_equal(repr(np.array(1.)), 'array(1.)') - assert_equal(repr(b), 'array([1.23400000e+09])') + assert_equal(repr(b), 'array([1.234e+09])') np.set_printoptions(sign=' ') assert_equal(repr(a), 'array([ 0., 1., 2., 3.])') assert_equal(repr(np.array(1.)), 'array( 1.)') - assert_equal(repr(b), 'array([ 1.23400000e+09])') + assert_equal(repr(b), 'array([ 1.234e+09])') np.set_printoptions(sign='+') assert_equal(repr(a), 'array([+0., +1., +2., +3.])') assert_equal(repr(np.array(1.)), 'array(+1.)') - assert_equal(repr(b), 'array([+1.23400000e+09])') + assert_equal(repr(b), 'array([+1.234e+09])') np.set_printoptions(sign='legacy') assert_equal(repr(a), 'array([ 0., 1., 2., 3.])')