diff options
author | Charles Harris <charlesr.harris@gmail.com> | 2021-03-31 11:11:53 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-31 11:11:53 -0600 |
commit | 2afcdbf7b82c4196004e5fa60fffacd22d94b6d1 (patch) | |
tree | 5d2c1f83364b763e988f8413347f26fd00ae852d | |
parent | 2c9130e848c3ecc489ac1bba1d759b1ebc5d5c65 (diff) | |
parent | 179d62bc18174378e220c6babf3806b838f02229 (diff) | |
download | numpy-2afcdbf7b82c4196004e5fa60fffacd22d94b6d1.tar.gz |
Merge pull request #18629 from ahaldane/min_digits
BUG, ENH: fix array2string rounding bug by adding min_digits option
-rw-r--r-- | numpy/core/arrayprint.py | 68 | ||||
-rw-r--r-- | numpy/core/arrayprint.pyi | 2 | ||||
-rw-r--r-- | numpy/core/src/multiarray/dragon4.c | 131 | ||||
-rw-r--r-- | numpy/core/src/multiarray/dragon4.h | 15 | ||||
-rw-r--r-- | numpy/core/src/multiarray/multiarraymodule.c | 14 | ||||
-rw-r--r-- | numpy/core/src/multiarray/scalartypes.c.src | 4 | ||||
-rw-r--r-- | numpy/core/tests/test_arrayprint.py | 4 | ||||
-rw-r--r-- | numpy/core/tests/test_scalarprint.py | 69 |
8 files changed, 219 insertions, 88 deletions
diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py index 3251c51e3..f16bcfd39 100644 --- a/numpy/core/arrayprint.py +++ b/numpy/core/arrayprint.py @@ -914,6 +914,7 @@ class FloatingFormat: self.trim = '.' self.exp_size = -1 self.unique = True + self.min_digits = None elif self.exp_format: trim, unique = '.', True if self.floatmode == 'fixed' or self._legacy == '1.13': @@ -927,6 +928,8 @@ class FloatingFormat: self.trim = 'k' self.precision = max(len(s) for s in frac_part) + self.min_digits = self.precision + self.unique = unique # for back-compat with np 1.13, use 2 spaces & sign and full prec if self._legacy == '1.13': @@ -936,10 +939,7 @@ class FloatingFormat: self.pad_left = max(len(s) for s in int_part) # pad_right is only needed for nan length calculation self.pad_right = self.exp_size + 2 + self.precision - - self.unique = False else: - # first pass printing to determine sizes trim, unique = '.', True if self.floatmode == 'fixed': trim, unique = 'k', False @@ -955,14 +955,14 @@ class FloatingFormat: self.pad_left = max(len(s) for s in int_part) self.pad_right = max(len(s) for s in frac_part) self.exp_size = -1 + self.unique = unique if self.floatmode in ['fixed', 'maxprec_equal']: - self.precision = self.pad_right - self.unique = False + self.precision = self.min_digits = self.pad_right self.trim = 'k' else: - self.unique = True self.trim = '.' + self.min_digits = 0 if self._legacy != '1.13': # account for sign = ' ' by adding one to pad_left @@ -991,6 +991,7 @@ class FloatingFormat: if self.exp_format: return dragon4_scientific(x, precision=self.precision, + min_digits=self.min_digits, unique=self.unique, trim=self.trim, sign=self.sign == '+', @@ -999,6 +1000,7 @@ class FloatingFormat: else: return dragon4_positional(x, precision=self.precision, + min_digits=self.min_digits, unique=self.unique, fractional=True, trim=self.trim, @@ -1009,7 +1011,8 @@ class FloatingFormat: @set_module('numpy') def format_float_scientific(x, precision=None, unique=True, trim='k', - sign=False, pad_left=None, exp_digits=None): + sign=False, pad_left=None, exp_digits=None, + min_digits=None): """ Format a floating-point scalar as a decimal string in scientific notation. @@ -1027,11 +1030,12 @@ def format_float_scientific(x, precision=None, unique=True, trim='k', If `True`, use a digit-generation strategy which gives the shortest representation which uniquely identifies the floating-point number from other values of the same type, by judicious rounding. If `precision` - was omitted, print all necessary digits, otherwise digit generation is - cut off after `precision` digits and the remaining value is rounded. + is given fewer digits than necessary can be printed. If `min_digits` + is given more can be printed, in which cases the last digit is rounded + with unbiased rounding. If `False`, digits are generated as if printing an infinite-precision value and stopping after `precision` digits, rounding the remaining - value. + value with unbiased rounding trim : one of 'k', '.', '0', '-', optional Controls post-processing trimming of trailing digits, as follows: @@ -1048,7 +1052,13 @@ def format_float_scientific(x, precision=None, unique=True, trim='k', exp_digits : non-negative integer, optional Pad the exponent with zeros until it contains at least this many digits. If omitted, the exponent will be at least 2 digits. + min_digits : non-negative integer or None, optional + Minimum number of digits to print. This only has an effect for + `unique=True`. In that case more digits than necessary to uniquely + identify the value may be printed and rounded unbiased. + -- versionadded:: 1.21.0 + Returns ------- rep : string @@ -1071,15 +1081,18 @@ def format_float_scientific(x, precision=None, unique=True, trim='k', precision = _none_or_positive_arg(precision, 'precision') pad_left = _none_or_positive_arg(pad_left, 'pad_left') exp_digits = _none_or_positive_arg(exp_digits, 'exp_digits') + min_digits = _none_or_positive_arg(min_digits, 'min_digits') + if min_digits > 0 and precision > 0 and min_digits > precision: + raise ValueError("min_digits must be less than or equal to precision") return dragon4_scientific(x, precision=precision, unique=unique, trim=trim, sign=sign, pad_left=pad_left, - exp_digits=exp_digits) + exp_digits=exp_digits, min_digits=min_digits) @set_module('numpy') def format_float_positional(x, precision=None, unique=True, fractional=True, trim='k', sign=False, - pad_left=None, pad_right=None): + pad_left=None, pad_right=None, min_digits=None): """ Format a floating-point scalar as a decimal string in positional notation. @@ -1097,16 +1110,19 @@ def format_float_positional(x, precision=None, unique=True, If `True`, use a digit-generation strategy which gives the shortest representation which uniquely identifies the floating-point number from other values of the same type, by judicious rounding. If `precision` - was omitted, print out all necessary digits, otherwise digit generation - is cut off after `precision` digits and the remaining value is rounded. + is given fewer digits than necessary can be printed, or if `min_digits` + is given more can be printed, in which cases the last digit is rounded + with unbiased rounding. If `False`, digits are generated as if printing an infinite-precision value and stopping after `precision` digits, rounding the remaining - value. + value with unbiased rounding fractional : boolean, optional - If `True`, the cutoff of `precision` digits refers to the total number - of digits after the decimal point, including leading zeros. - If `False`, `precision` refers to the total number of significant - digits, before or after the decimal point, ignoring leading zeros. + If `True`, the cutoffs of `precision` and `min_digits` refer to the + total number of digits after the decimal point, including leading + zeros. + If `False`, `precision` and `min_digits` refer to the total number of + significant digits, before or after the decimal point, ignoring leading + zeros. trim : one of 'k', '.', '0', '-', optional Controls post-processing trimming of trailing digits, as follows: @@ -1123,6 +1139,12 @@ def format_float_positional(x, precision=None, unique=True, pad_right : non-negative integer, optional Pad the right side of the string with whitespace until at least that many characters are to the right of the decimal point. + min_digits : non-negative integer or None, optional + Minimum number of digits to print. Only has an effect if `unique=True` + in which case additional digits past those necessary to uniquely + identify the value may be printed, rounding the last additional digit. + + -- versionadded:: 1.21.0 Returns ------- @@ -1147,10 +1169,16 @@ def format_float_positional(x, precision=None, unique=True, precision = _none_or_positive_arg(precision, 'precision') pad_left = _none_or_positive_arg(pad_left, 'pad_left') pad_right = _none_or_positive_arg(pad_right, 'pad_right') + min_digits = _none_or_positive_arg(min_digits, 'min_digits') + if not fractional and precision == 0: + raise ValueError("precision must be greater than 0 if " + "fractional=False") + if min_digits > 0 and precision > 0 and min_digits > precision: + raise ValueError("min_digits must be less than or equal to precision") return dragon4_positional(x, precision=precision, unique=unique, fractional=fractional, trim=trim, sign=sign, pad_left=pad_left, - pad_right=pad_right) + pad_right=pad_right, min_digits=min_digits) class IntegerFormat: diff --git a/numpy/core/arrayprint.pyi b/numpy/core/arrayprint.pyi index d2a5fdef9..ac2b6f5a8 100644 --- a/numpy/core/arrayprint.pyi +++ b/numpy/core/arrayprint.pyi @@ -103,6 +103,7 @@ def format_float_scientific( sign: bool = ..., pad_left: Optional[int] = ..., exp_digits: Optional[int] = ..., + min_digits: Optional[int] = ..., ) -> str: ... def format_float_positional( x: _FloatLike_co, @@ -113,6 +114,7 @@ def format_float_positional( sign: bool = ..., pad_left: Optional[int] = ..., pad_right: Optional[int] = ..., + min_digits: Optional[int] = ..., ) -> str: ... def array_repr( arr: ndarray[Any, Any], diff --git a/numpy/core/src/multiarray/dragon4.c b/numpy/core/src/multiarray/dragon4.c index a7b252a77..1d8c27570 100644 --- a/numpy/core/src/multiarray/dragon4.c +++ b/numpy/core/src/multiarray/dragon4.c @@ -1130,8 +1130,9 @@ BigInt_ShiftLeft(BigInt *result, npy_uint32 shift) * * exponent - value exponent in base 2 * * mantissaBit - index of the highest set mantissa bit * * hasUnequalMargins - is the high margin twice as large as the low margin - * * cutoffMode - how to interpret cutoffNumber: fractional or total digits? - * * cutoffNumber - cut off printing after this many digits. -1 for no cutoff + * * cutoffMode - how to interpret cutoff_*: fractional or total digits? + * * cutoff_max - cut off printing after this many digits. -1 for no cutoff + * * cutoff_min - print at least this many digits. -1 for no cutoff * * pOutBuffer - buffer to output into * * bufferSize - maximum characters that can be printed to pOutBuffer * * pOutExponent - the base 10 exponent of the first digit @@ -1142,7 +1143,7 @@ static npy_uint32 Dragon4(BigInt *bigints, const npy_int32 exponent, const npy_uint32 mantissaBit, const npy_bool hasUnequalMargins, const DigitMode digitMode, const CutoffMode cutoffMode, - npy_int32 cutoffNumber, char *pOutBuffer, + npy_int32 cutoff_max, npy_int32 cutoff_min, char *pOutBuffer, npy_uint32 bufferSize, npy_int32 *pOutExponent) { char *curDigit = pOutBuffer; @@ -1169,7 +1170,8 @@ Dragon4(BigInt *bigints, const npy_int32 exponent, BigInt *temp2 = &bigints[6]; const npy_float64 log10_2 = 0.30102999566398119521373889472449; - npy_int32 digitExponent, cutoffExponent, hiBlock; + npy_int32 digitExponent, hiBlock; + npy_int32 cutoff_max_Exponent, cutoff_min_Exponent; npy_uint32 outputDigit; /* current digit being output */ npy_uint32 outputLen; npy_bool isEven = BigInt_IsEven(mantissa); @@ -1294,9 +1296,9 @@ Dragon4(BigInt *bigints, const npy_int32 exponent, * increases the number. This will either correct digitExponent to an * accurate value or it will clamp it above the accurate value. */ - if (cutoffNumber >= 0 && cutoffMode == CutoffMode_FractionLength && - digitExponent <= -cutoffNumber) { - digitExponent = -cutoffNumber + 1; + if (cutoff_max >= 0 && cutoffMode == CutoffMode_FractionLength && + digitExponent <= -cutoff_max) { + digitExponent = -cutoff_max + 1; } @@ -1347,26 +1349,44 @@ Dragon4(BigInt *bigints, const npy_int32 exponent, } /* - * Compute the cutoff exponent (the exponent of the final digit to print). - * Default to the maximum size of the output buffer. + * Compute the cutoff_max exponent (the exponent of the final digit to + * print). Default to the maximum size of the output buffer. */ - cutoffExponent = digitExponent - bufferSize; - if (cutoffNumber >= 0) { + cutoff_max_Exponent = digitExponent - bufferSize; + if (cutoff_max >= 0) { npy_int32 desiredCutoffExponent; if (cutoffMode == CutoffMode_TotalLength) { - desiredCutoffExponent = digitExponent - cutoffNumber; - if (desiredCutoffExponent > cutoffExponent) { - cutoffExponent = desiredCutoffExponent; + desiredCutoffExponent = digitExponent - cutoff_max; + if (desiredCutoffExponent > cutoff_max_Exponent) { + cutoff_max_Exponent = desiredCutoffExponent; } } - /* Otherwise it's CutoffMode_FractionLength. Print cutoffNumber digits + /* Otherwise it's CutoffMode_FractionLength. Print cutoff_max digits * past the decimal point or until we reach the buffer size */ else { - desiredCutoffExponent = -cutoffNumber; - if (desiredCutoffExponent > cutoffExponent) { - cutoffExponent = desiredCutoffExponent; + desiredCutoffExponent = -cutoff_max; + if (desiredCutoffExponent > cutoff_max_Exponent) { + cutoff_max_Exponent = desiredCutoffExponent; + } + } + } + /* Also compute the cutoff_min exponent. */ + cutoff_min_Exponent = digitExponent; + if (cutoff_min >= 0) { + npy_int32 desiredCutoffExponent; + + if (cutoffMode == CutoffMode_TotalLength) { + desiredCutoffExponent = digitExponent - cutoff_min; + if (desiredCutoffExponent < cutoff_min_Exponent) { + cutoff_min_Exponent = desiredCutoffExponent; + } + } + else { + desiredCutoffExponent = -cutoff_min; + if (desiredCutoffExponent < cutoff_min_Exponent) { + cutoff_min_Exponent = desiredCutoffExponent; } } } @@ -1432,14 +1452,17 @@ Dragon4(BigInt *bigints, const npy_int32 exponent, /* * stop looping if we are far enough away from our neighboring - * values or if we have reached the cutoff digit + * values (and we have printed at least the requested minimum + * digits) or if we have reached the cutoff digit */ cmp = BigInt_Compare(scaledValue, scaledMarginLow); low = isEven ? (cmp <= 0) : (cmp < 0); cmp = BigInt_Compare(scaledValueHigh, scale); high = isEven ? (cmp >= 0) : (cmp > 0); - if (low | high | (digitExponent == cutoffExponent)) + if (((low | high) & (digitExponent <= cutoff_min_Exponent)) | + (digitExponent == cutoff_max_Exponent)) { break; + } /* store the output digit */ *curDigit = (char)('0' + outputDigit); @@ -1471,7 +1494,7 @@ Dragon4(BigInt *bigints, const npy_int32 exponent, DEBUG_ASSERT(outputDigit < 10); if ((scaledValue->length == 0) | - (digitExponent == cutoffExponent)) { + (digitExponent == cutoff_max_Exponent)) { break; } @@ -1589,6 +1612,7 @@ typedef struct Dragon4_Options { DigitMode digit_mode; CutoffMode cutoff_mode; npy_int32 precision; + npy_int32 min_digits; npy_bool sign; TrimMode trim_mode; npy_int32 digits_left; @@ -1617,11 +1641,12 @@ FormatPositional(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, npy_int32 exponent, char signbit, npy_uint32 mantissaBit, npy_bool hasUnequalMargins, DigitMode digit_mode, CutoffMode cutoff_mode, npy_int32 precision, - TrimMode trim_mode, npy_int32 digits_left, - npy_int32 digits_right) + npy_int32 min_digits, TrimMode trim_mode, + npy_int32 digits_left, npy_int32 digits_right) { npy_int32 printExponent; npy_int32 numDigits, numWholeDigits=0, has_sign=0; + npy_int32 add_digits; npy_int32 maxPrintLen = (npy_int32)bufferSize - 1, pos = 0; @@ -1644,8 +1669,9 @@ FormatPositional(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, } numDigits = Dragon4(mantissa, exponent, mantissaBit, hasUnequalMargins, - digit_mode, cutoff_mode, precision, buffer + has_sign, - maxPrintLen - has_sign, &printExponent); + digit_mode, cutoff_mode, precision, min_digits, + buffer + has_sign, maxPrintLen - has_sign, + &printExponent); DEBUG_ASSERT(numDigits > 0); DEBUG_ASSERT(numDigits <= bufferSize); @@ -1744,9 +1770,10 @@ FormatPositional(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, buffer[pos++] = '.'; } - desiredFractionalDigits = precision; - if (cutoff_mode == CutoffMode_TotalLength && precision >= 0) { - desiredFractionalDigits = precision - numWholeDigits; + add_digits = digit_mode == DigitMode_Unique ? min_digits : precision; + desiredFractionalDigits = add_digits < 0 ? 0 : add_digits; + if (cutoff_mode == CutoffMode_TotalLength) { + desiredFractionalDigits = add_digits - numWholeDigits; } if (trim_mode == TrimMode_LeaveOneZero) { @@ -1757,10 +1784,9 @@ FormatPositional(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, } } else if (trim_mode == TrimMode_None && - digit_mode != DigitMode_Unique && desiredFractionalDigits > numFractionDigits && pos < maxPrintLen) { - /* add trailing zeros up to precision length */ + /* add trailing zeros up to add_digits length */ /* compute the number of trailing zeros needed */ npy_int32 count = desiredFractionalDigits - numFractionDigits; if (pos + count > maxPrintLen) { @@ -1778,7 +1804,7 @@ FormatPositional(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, * when rounding, we may still end up with trailing zeros. Remove them * depending on trim settings. */ - if (precision >= 0 && trim_mode != TrimMode_None && numFractionDigits > 0) { + if (trim_mode != TrimMode_None && numFractionDigits > 0) { while (buffer[pos-1] == '0') { pos--; numFractionDigits--; @@ -1852,7 +1878,7 @@ static npy_uint32 FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, npy_int32 exponent, char signbit, npy_uint32 mantissaBit, npy_bool hasUnequalMargins, DigitMode digit_mode, - npy_int32 precision, TrimMode trim_mode, + npy_int32 precision, npy_int32 min_digits, TrimMode trim_mode, npy_int32 digits_left, npy_int32 exp_digits) { npy_int32 printExponent; @@ -1860,12 +1886,12 @@ FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, char *pCurOut; npy_int32 numFractionDigits; npy_int32 leftchars; + npy_int32 add_digits; if (digit_mode != DigitMode_Unique) { DEBUG_ASSERT(precision >= 0); } - DEBUG_ASSERT(bufferSize > 0); pCurOut = buffer; @@ -1893,7 +1919,9 @@ FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, } numDigits = Dragon4(mantissa, exponent, mantissaBit, hasUnequalMargins, - digit_mode, CutoffMode_TotalLength, precision + 1, + digit_mode, CutoffMode_TotalLength, + precision < 0 ? -1 : precision + 1, + min_digits < 0 ? -1 : min_digits + 1, pCurOut, bufferSize, &printExponent); DEBUG_ASSERT(numDigits > 0); @@ -1928,6 +1956,8 @@ FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, --bufferSize; } + add_digits = digit_mode == DigitMode_Unique ? min_digits : precision; + add_digits = add_digits < 0 ? 0 : add_digits; if (trim_mode == TrimMode_LeaveOneZero) { /* if we didn't print any fractional digits, add the 0 */ if (numFractionDigits == 0 && bufferSize > 1) { @@ -1937,13 +1967,12 @@ FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, ++numFractionDigits; } } - else if (trim_mode == TrimMode_None && - digit_mode != DigitMode_Unique) { - /* add trailing zeros up to precision length */ - if (precision > (npy_int32)numFractionDigits) { + else if (trim_mode == TrimMode_None) { + /* add trailing zeros up to add_digits length */ + if (add_digits > (npy_int32)numFractionDigits) { char *pEnd; /* compute the number of trailing zeros needed */ - npy_int32 numZeros = (precision - numFractionDigits); + npy_int32 numZeros = (add_digits - numFractionDigits); if (numZeros > (npy_int32)bufferSize - 1) { numZeros = (npy_int32)bufferSize - 1; @@ -1961,7 +1990,7 @@ FormatScientific (char *buffer, npy_uint32 bufferSize, BigInt *mantissa, * when rounding, we may still end up with trailing zeros. Remove them * depending on trim settings. */ - if (precision >= 0 && trim_mode != TrimMode_None && numFractionDigits > 0) { + if (trim_mode != TrimMode_None && numFractionDigits > 0) { --pCurOut; while (*pCurOut == '0') { --pCurOut; @@ -2153,14 +2182,14 @@ Format_floatbits(char *buffer, npy_uint32 bufferSize, BigInt *mantissa, return FormatScientific(buffer, bufferSize, mantissa, exponent, signbit, mantissaBit, hasUnequalMargins, opt->digit_mode, opt->precision, - opt->trim_mode, opt->digits_left, - opt->exp_digits); + opt->min_digits, opt->trim_mode, + opt->digits_left, opt->exp_digits); } else { return FormatPositional(buffer, bufferSize, mantissa, exponent, signbit, mantissaBit, hasUnequalMargins, opt->digit_mode, opt->cutoff_mode, - opt->precision, opt->trim_mode, + opt->precision, opt->min_digits, opt->trim_mode, opt->digits_left, opt->digits_right); } } @@ -3100,7 +3129,7 @@ Dragon4_Positional_##Type##_opt(npy_type *val, Dragon4_Options *opt)\ \ PyObject *\ Dragon4_Positional_##Type(npy_type *val, DigitMode digit_mode,\ - CutoffMode cutoff_mode, int precision,\ + CutoffMode cutoff_mode, int precision, int min_digits, \ int sign, TrimMode trim, int pad_left, int pad_right)\ {\ Dragon4_Options opt;\ @@ -3109,6 +3138,7 @@ Dragon4_Positional_##Type(npy_type *val, DigitMode digit_mode,\ opt.digit_mode = digit_mode;\ opt.cutoff_mode = cutoff_mode;\ opt.precision = precision;\ + opt.min_digits = min_digits;\ opt.sign = sign;\ opt.trim_mode = trim;\ opt.digits_left = pad_left;\ @@ -3136,7 +3166,8 @@ Dragon4_Scientific_##Type##_opt(npy_type *val, Dragon4_Options *opt)\ }\ PyObject *\ Dragon4_Scientific_##Type(npy_type *val, DigitMode digit_mode, int precision,\ - int sign, TrimMode trim, int pad_left, int exp_digits)\ + int min_digits, int sign, TrimMode trim, int pad_left, \ + int exp_digits)\ {\ Dragon4_Options opt;\ \ @@ -3144,6 +3175,7 @@ Dragon4_Scientific_##Type(npy_type *val, DigitMode digit_mode, int precision,\ opt.digit_mode = digit_mode;\ opt.cutoff_mode = CutoffMode_TotalLength;\ opt.precision = precision;\ + opt.min_digits = min_digits;\ opt.sign = sign;\ opt.trim_mode = trim;\ opt.digits_left = pad_left;\ @@ -3166,8 +3198,8 @@ make_dragon4_typefuncs(LongDouble, npy_longdouble, NPY_LONGDOUBLE_BINFMT_NAME) PyObject * Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, - int precision, int sign, TrimMode trim, int pad_left, - int pad_right) + int precision, int min_digits, int sign, TrimMode trim, + int pad_left, int pad_right) { npy_double val; Dragon4_Options opt; @@ -3176,6 +3208,7 @@ Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, opt.digit_mode = digit_mode; opt.cutoff_mode = cutoff_mode; opt.precision = precision; + opt.min_digits = min_digits; opt.sign = sign; opt.trim_mode = trim; opt.digits_left = pad_left; @@ -3208,7 +3241,8 @@ Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, PyObject * Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, - int sign, TrimMode trim, int pad_left, int exp_digits) + int min_digits, int sign, TrimMode trim, int pad_left, + int exp_digits) { npy_double val; Dragon4_Options opt; @@ -3217,6 +3251,7 @@ Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, opt.digit_mode = digit_mode; opt.cutoff_mode = CutoffMode_TotalLength; opt.precision = precision; + opt.min_digits = min_digits; opt.sign = sign; opt.trim_mode = trim; opt.digits_left = pad_left; diff --git a/numpy/core/src/multiarray/dragon4.h b/numpy/core/src/multiarray/dragon4.h index 3a99bde6c..4b76bf9e5 100644 --- a/numpy/core/src/multiarray/dragon4.h +++ b/numpy/core/src/multiarray/dragon4.h @@ -112,12 +112,12 @@ typedef enum TrimMode PyObject *\ Dragon4_Positional_##Type(npy_type *val, DigitMode digit_mode,\ CutoffMode cutoff_mode, int precision,\ - int sign, TrimMode trim, int pad_left,\ - int pad_right);\ + int min_digits, int sign, TrimMode trim, \ + int pad_left, int pad_right);\ PyObject *\ Dragon4_Scientific_##Type(npy_type *val, DigitMode digit_mode,\ - int precision, int sign, TrimMode trim,\ - int pad_left, int exp_digits); + int precision, int min_digits, int sign, \ + TrimMode trim, int pad_left, int exp_digits); make_dragon4_typedecl(Half, npy_half) make_dragon4_typedecl(Float, npy_float) @@ -128,12 +128,13 @@ make_dragon4_typedecl(LongDouble, npy_longdouble) PyObject * Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, - int precision, int sign, TrimMode trim, int pad_left, - int pad_right); + int precision, int min_digits, int sign, TrimMode trim, + int pad_left, int pad_right); PyObject * Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, - int sign, TrimMode trim, int pad_left, int exp_digits); + int min_digits, int sign, TrimMode trim, int pad_left, + int exp_digits); #endif diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 953e2b4cf..f3c1b7f98 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -3579,7 +3579,7 @@ dragon4_scientific(PyObject *NPY_UNUSED(dummy), PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames) { PyObject *obj; - int precision=-1, pad_left=-1, exp_digits=-1; + int precision=-1, pad_left=-1, exp_digits=-1, min_digits=-1; DigitMode digit_mode; TrimMode trim = TrimMode_None; int sign=0, unique=1; @@ -3593,6 +3593,7 @@ dragon4_scientific(PyObject *NPY_UNUSED(dummy), "|trim", &trimmode_converter, &trim, "|pad_left", &PyArray_PythonPyIntFromInt, &pad_left, "|exp_digits", &PyArray_PythonPyIntFromInt, &exp_digits, + "|min_digits", &PyArray_PythonPyIntFromInt, &min_digits, NULL, NULL, NULL) < 0) { return NULL; } @@ -3605,7 +3606,7 @@ dragon4_scientific(PyObject *NPY_UNUSED(dummy), return NULL; } - return Dragon4_Scientific(obj, digit_mode, precision, sign, trim, + return Dragon4_Scientific(obj, digit_mode, precision, min_digits, sign, trim, pad_left, exp_digits); } @@ -3620,7 +3621,7 @@ dragon4_positional(PyObject *NPY_UNUSED(dummy), PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames) { PyObject *obj; - int precision=-1, pad_left=-1, pad_right=-1; + int precision=-1, pad_left=-1, pad_right=-1, min_digits=-1; CutoffMode cutoff_mode; DigitMode digit_mode; TrimMode trim = TrimMode_None; @@ -3636,6 +3637,7 @@ dragon4_positional(PyObject *NPY_UNUSED(dummy), "|trim", &trimmode_converter, &trim, "|pad_left", &PyArray_PythonPyIntFromInt, &pad_left, "|pad_right", &PyArray_PythonPyIntFromInt, &pad_right, + "|min_digits", &PyArray_PythonPyIntFromInt, &min_digits, NULL, NULL, NULL) < 0) { return NULL; } @@ -3650,8 +3652,8 @@ dragon4_positional(PyObject *NPY_UNUSED(dummy), return NULL; } - return Dragon4_Positional(obj, digit_mode, cutoff_mode, precision, sign, - trim, pad_left, pad_right); + return Dragon4_Positional(obj, digit_mode, cutoff_mode, precision, + min_digits, sign, trim, pad_left, pad_right); } static PyObject * @@ -3670,7 +3672,7 @@ format_longfloat(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) "not a longfloat"); return NULL; } - return Dragon4_Scientific(obj, DigitMode_Unique, precision, 0, + return Dragon4_Scientific(obj, DigitMode_Unique, precision, -1, 0, TrimMode_LeaveOneZero, -1, -1); } diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 10f304fe7..a001500b0 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -331,13 +331,13 @@ format_@name@(@type@ val, npy_bool scientific, { if (scientific) { return Dragon4_Scientific_@Name@(&val, - DigitMode_Unique, precision, + DigitMode_Unique, precision, -1, sign, trim, pad_left, exp_digits); } else { return Dragon4_Positional_@Name@(&val, DigitMode_Unique, CutoffMode_TotalLength, precision, - sign, trim, pad_left, pad_right); + -1, sign, trim, pad_left, pad_right); } } diff --git a/numpy/core/tests/test_arrayprint.py b/numpy/core/tests/test_arrayprint.py index 8f63b5b70..09cc79f72 100644 --- a/numpy/core/tests/test_arrayprint.py +++ b/numpy/core/tests/test_arrayprint.py @@ -759,6 +759,10 @@ class TestPrintOptions: assert_equal(repr(c), "array([1.00000000+1.00000000j, 1.12345679+1.12345679j])") + # test unique special case (gh-18609) + a = np.float64.fromhex('-1p-97') + assert_equal(np.float64(np.array2string(a, floatmode='unique')), a) + def test_legacy_mode_scalars(self): # in legacy mode, str of floats get truncated, and complex scalars # use * for non-finite imaginary part diff --git a/numpy/core/tests/test_scalarprint.py b/numpy/core/tests/test_scalarprint.py index 6502ec4c1..620472683 100644 --- a/numpy/core/tests/test_scalarprint.py +++ b/numpy/core/tests/test_scalarprint.py @@ -9,7 +9,7 @@ import sys from tempfile import TemporaryFile import numpy as np -from numpy.testing import assert_, assert_equal +from numpy.testing import assert_, assert_equal, assert_raises class TestRealScalars: def test_str(self): @@ -176,7 +176,8 @@ class TestRealScalars: "87538682506419718265533447265625") # largest numbers - assert_equal(fpos32(np.finfo(np.float32).max, **preckwd(0)), + f32x = np.finfo(np.float32).max + assert_equal(fpos32(f32x, **preckwd(0)), "340282346638528859811704183484516925440.") assert_equal(fpos64(np.finfo(np.float64).max, **preckwd(0)), "1797693134862315708145274237317043567980705675258449965989" @@ -186,10 +187,66 @@ class TestRealScalars: "6580855933212334827479782620414472316873817718091929988125" "0404026184124858368.") # Warning: In unique mode only the integer digits necessary for - # uniqueness are computed, the rest are 0. Should we change this? - assert_equal(fpos32(np.finfo(np.float32).max, precision=0), + # uniqueness are computed, the rest are 0. + assert_equal(fpos32(f32x), "340282350000000000000000000000000000000.") + # Further tests of zero-padding vs rounding in different combinations + # of unique, fractional, precision, min_digits + # precision can only reduce digits, not add them. + # min_digits can only extend digits, not reduce them. + assert_equal(fpos32(f32x, unique=True, fractional=True, precision=0), + "340282350000000000000000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=True, precision=4), + "340282350000000000000000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=True, min_digits=0), + "340282346638528859811704183484516925440.") + assert_equal(fpos32(f32x, unique=True, fractional=True, min_digits=4), + "340282346638528859811704183484516925440.0000") + assert_equal(fpos32(f32x, unique=True, fractional=True, + min_digits=4, precision=4), + "340282346638528859811704183484516925440.0000") + assert_raises(ValueError, fpos32, f32x, unique=True, fractional=False, + precision=0) + assert_equal(fpos32(f32x, unique=True, fractional=False, precision=4), + "340300000000000000000000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=False, precision=20), + "340282350000000000000000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=False, min_digits=4), + "340282350000000000000000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=False, + min_digits=20), + "340282346638528859810000000000000000000.") + assert_equal(fpos32(f32x, unique=True, fractional=False, + min_digits=15), + "340282346638529000000000000000000000000.") + assert_equal(fpos32(f32x, unique=False, fractional=False, precision=4), + "340300000000000000000000000000000000000.") + # test that unique rounding is preserved when precision is supplied + # but no extra digits need to be printed (gh-18609) + a = np.float64.fromhex('-1p-97') + assert_equal(fsci64(a, unique=True), '-6.310887241768095e-30') + assert_equal(fsci64(a, unique=False, precision=15), + '-6.310887241768094e-30') + assert_equal(fsci64(a, unique=True, precision=15), + '-6.310887241768095e-30') + assert_equal(fsci64(a, unique=True, min_digits=15), + '-6.310887241768095e-30') + assert_equal(fsci64(a, unique=True, precision=15, min_digits=15), + '-6.310887241768095e-30') + # adds/remove digits in unique mode with unbiased rnding + assert_equal(fsci64(a, unique=True, precision=14), + '-6.31088724176809e-30') + assert_equal(fsci64(a, unique=True, min_digits=16), + '-6.3108872417680944e-30') + assert_equal(fsci64(a, unique=True, precision=16), + '-6.310887241768095e-30') + assert_equal(fsci64(a, unique=True, min_digits=14), + '-6.310887241768095e-30') + # test min_digits in unique mode with different rounding cases + assert_equal(fsci64('1e120', min_digits=3), '1.000e+120') + assert_equal(fsci64('1e100', min_digits=3), '1.000e+100') + # test trailing zeros assert_equal(fpos32('1.0', unique=False, precision=3), "1.000") assert_equal(fpos64('1.0', unique=False, precision=3), "1.000") @@ -200,7 +257,9 @@ class TestRealScalars: assert_equal(fsci32('1.5', unique=False, precision=3), "1.500e+00") assert_equal(fsci64('1.5', unique=False, precision=3), "1.500e+00") # gh-10713 - assert_equal(fpos64('324', unique=False, precision=5, fractional=False), "324.00") + assert_equal(fpos64('324', unique=False, precision=5, + fractional=False), "324.00") + def test_dragon4_interface(self): tps = [np.float16, np.float32, np.float64] |