summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Harris <charlesr.harris@gmail.com>2021-03-31 11:11:53 -0600
committerGitHub <noreply@github.com>2021-03-31 11:11:53 -0600
commit2afcdbf7b82c4196004e5fa60fffacd22d94b6d1 (patch)
tree5d2c1f83364b763e988f8413347f26fd00ae852d
parent2c9130e848c3ecc489ac1bba1d759b1ebc5d5c65 (diff)
parent179d62bc18174378e220c6babf3806b838f02229 (diff)
downloadnumpy-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.py68
-rw-r--r--numpy/core/arrayprint.pyi2
-rw-r--r--numpy/core/src/multiarray/dragon4.c131
-rw-r--r--numpy/core/src/multiarray/dragon4.h15
-rw-r--r--numpy/core/src/multiarray/multiarraymodule.c14
-rw-r--r--numpy/core/src/multiarray/scalartypes.c.src4
-rw-r--r--numpy/core/tests/test_arrayprint.py4
-rw-r--r--numpy/core/tests/test_scalarprint.py69
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]