Repository: numpy/numpy

Issue:
Legacy printing mode gives not-legacy result for array with negative value, value > 9
Numpy 1.13:

```
$ python -c 'import numpy; print(numpy.array([  -1.  ,    10]))'
[ -1.  10.]
```

There is whitespace before the`-1`.

Numpy 1.14:

```
$ python -c 'import numpy; print(numpy.array([  -1.  ,    10]))'
[-1. 10.]
$ python -c 'import numpy; numpy.set_printoptions(legacy="1.13"); print(numpy.array([  -1.  ,    10]))'
[-1. 10.]
```

No whitespace before the `-1`.


Discussion:
This is breaking a fair bunch of my doctests: https://travis-ci.org/colour-science/colour/builds/328061368
And mine :) - and we now can't fix them by adding something like this:

https://github.com/matthew-brett/nb2plots/commit/b98975f4e724684650547a9f40c91fcec9add007

Resolving patch:
diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py
--- a/numpy/core/arrayprint.py
+++ b/numpy/core/arrayprint.py
@@ -99,8 +99,10 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None,
 
     Parameters
     ----------
-    precision : int, optional
+    precision : int or None, optional
         Number of digits of precision for floating point output (default 8).
+        May be `None` if `floatmode` is not `fixed`, to print as many digits as
+        necessary to uniquely specify the value.
     threshold : int, optional
         Total number of array elements which trigger summarization
         rather than full repr (default 1000).
@@ -240,6 +242,8 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None,
     # set the C variable for legacy mode
     if _format_options['legacy'] == '1.13':
         set_legacy_print_mode(113)
+        # reset the sign option in legacy mode to avoid confusion
+        _format_options['sign'] = '-'
     elif _format_options['legacy'] is False:
         set_legacy_print_mode(0)
 
@@ -502,7 +506,7 @@ def array2string(a, max_line_width=None, precision=None,
     max_line_width : int, optional
         The maximum number of columns the string should span. Newline
         characters splits the string appropriately after array elements.
-    precision : int, optional
+    precision : int or None, optional
         Floating point precision. Default is the current printing
         precision (usually 8), which can be altered using `set_printoptions`.
     suppress_small : bool, optional
@@ -782,6 +786,13 @@ def recurser(index, hanging_indent, curr_width):
         curr_width=line_width)
 
 
+def _none_or_positive_arg(x, name):
+    if x is None:
+        return -1
+    if x < 0:
+        raise ValueError("{} must be >= 0".format(name))
+    return x
+
 class FloatingFormat(object):
     """ Formatter for subtypes of np.floating """
     def __init__(self, data, precision, floatmode, suppress_small, sign=False,
@@ -792,17 +803,18 @@ def __init__(self, data, precision, floatmode, suppress_small, sign=False,
 
         self._legacy = kwarg.get('legacy', False)
         if self._legacy == '1.13':
-            sign = '-' if data.shape == () else ' '
+            # when not 0d, legacy does not support '-'
+            if data.shape != () and sign == '-':
+                sign = ' '
 
         self.floatmode = floatmode
         if floatmode == 'unique':
-            self.precision = -1
+            self.precision = None
         else:
-            if precision < 0:
-                raise ValueError(
-                    "precision must be >= 0 in {} mode".format(floatmode))
             self.precision = precision
 
+        self.precision = _none_or_positive_arg(self.precision, 'precision')
+
         self.suppress_small = suppress_small
         self.sign = sign
         self.exp_format = False
@@ -845,11 +857,9 @@ def fillFormat(self, data):
             self.trim = 'k'
             self.precision = max(len(s) for s in frac_part)
 
-            # for back-compatibility with np 1.13, use two spaces and full prec
+            # for back-compat with np 1.13, use 2 spaces & sign and full prec
             if self._legacy == '1.13':
-                # undo addition of sign pos below
-                will_add_sign = all(finite_vals > 0) and self.sign == ' '
-                self.pad_left = 3 - will_add_sign
+                self.pad_left = 3
             else:
                 # this should be only 1 or 2. Can be calculated from sign.
                 self.pad_left = max(len(s) for s in int_part)
@@ -868,7 +878,10 @@ def fillFormat(self, data):
                                        sign=self.sign == '+')
                     for x in finite_vals)
             int_part, frac_part = zip(*(s.split('.') for s in strs))
-            self.pad_left = max(len(s) for s in int_part)
+            if self._legacy == '1.13':
+                self.pad_left = 1 + max(len(s.lstrip('-+')) for s in int_part)
+            else:
+                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
 
@@ -880,9 +893,10 @@ def fillFormat(self, data):
                 self.unique = True
                 self.trim = '.'
 
-        # account for sign = ' ' by adding one to pad_left
-        if all(finite_vals >= 0) and self.sign == ' ':
-            self.pad_left += 1
+        if self._legacy != '1.13':
+            # account for sign = ' ' by adding one to pad_left
+            if self.sign == ' ' and not any(np.signbit(finite_vals)):
+                self.pad_left += 1
 
         # if there are non-finite values, may need to increase pad_left
         if data.size != finite_vals.size:
@@ -935,7 +949,6 @@ def __init__(self, *args, **kwargs):
                       DeprecationWarning, stacklevel=2)
         super(LongFloatFormat, self).__init__(*args, **kwargs)
 
-
 def format_float_scientific(x, precision=None, unique=True, trim='k',
                             sign=False, pad_left=None, exp_digits=None):
     """
@@ -948,9 +961,9 @@ def format_float_scientific(x, precision=None, unique=True, trim='k',
     ----------
     x : python float or numpy floating scalar
         Value to format.
-    precision : non-negative integer, optional
-        Maximum number of fractional digits to print. May be omitted if
-        `unique` is `True`, but is required if unique is `False`.
+    precision : non-negative integer or None, optional
+        Maximum number of digits to print. May be None if `unique` is
+        `True`, but must be an integer if unique is `False`.
     unique : boolean, optional
         If `True`, use a digit-generation strategy which gives the shortest
         representation which uniquely identifies the floating-point number from
@@ -995,9 +1008,9 @@ def format_float_scientific(x, precision=None, unique=True, trim='k',
     >>> np.format_float_scientific(s, exp_digits=4)
     '1.23e+0024'
     """
-    precision = -1 if precision is None else precision
-    pad_left = -1 if pad_left is None else pad_left
-    exp_digits = -1 if exp_digits is None else exp_digits
+    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')
     return dragon4_scientific(x, precision=precision, unique=unique,
                               trim=trim, sign=sign, pad_left=pad_left,
                               exp_digits=exp_digits)
@@ -1015,9 +1028,9 @@ def format_float_positional(x, precision=None, unique=True,
     ----------
     x : python float or numpy floating scalar
         Value to format.
-    precision : non-negative integer, optional
-        Maximum number of digits to print. May be omitted if `unique` is
-        `True`, but is required if unique is `False`.
+    precision : non-negative integer or None, optional
+        Maximum number of digits to print. May be None if `unique` is
+        `True`, but must be an integer if unique is `False`.
     unique : boolean, optional
         If `True`, use a digit-generation strategy which gives the shortest
         representation which uniquely identifies the floating-point number from
@@ -1068,9 +1081,9 @@ def format_float_positional(x, precision=None, unique=True,
     >>> np.format_float_positional(np.float16(0.3), unique=False, precision=10)
     '0.3000488281'
     """
-    precision = -1 if precision is None else precision
-    pad_left = -1 if pad_left is None else pad_left
-    pad_right = -1 if pad_right is None else pad_right
+    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')
     return dragon4_positional(x, precision=precision, unique=unique,
                               fractional=fractional, trim=trim,
                               sign=sign, pad_left=pad_left,
@@ -1108,15 +1121,25 @@ def __init__(self, x, precision, floatmode, suppress_small,
         if isinstance(sign, bool):
             sign = '+' if sign else '-'
 
-        self.real_format = FloatingFormat(x.real, precision, floatmode,
+        floatmode_real = floatmode_imag = floatmode
+        if kwarg.get('legacy', False) == '1.13':
+            floatmode_real = 'maxprec_equal'
+            floatmode_imag = 'maxprec'
+
+        self.real_format = FloatingFormat(x.real, precision, floatmode_real,
                                           suppress_small, sign=sign, **kwarg)
-        self.imag_format = FloatingFormat(x.imag, precision, floatmode,
+        self.imag_format = FloatingFormat(x.imag, precision, floatmode_imag,
                                           suppress_small, sign='+', **kwarg)
 
     def __call__(self, x):
         r = self.real_format(x.real)
         i = self.imag_format(x.imag)
-        return r + i + 'j'
+
+        # add the 'j' before the terminal whitespace in i
+        sp = len(i.rstrip())
+        i = i[:sp] + 'j' + i[sp:]
+
+        return r + i
 
 # for back-compatibility, we keep the classes for each complex type too
 class ComplexFormat(ComplexFloatingFormat):


Test patch:
diff --git a/numpy/core/tests/test_arrayprint.py b/numpy/core/tests/test_arrayprint.py
--- a/numpy/core/tests/test_arrayprint.py
+++ b/numpy/core/tests/test_arrayprint.py
@@ -433,21 +433,30 @@ def test_bool_spacing(self):
     def test_sign_spacing(self):
         a = np.arange(4.)
         b = np.array([1.234e9])
+        c = np.array([1.0 + 1.0j, 1.123456789 + 1.123456789j], dtype='c16')
 
         assert_equal(repr(a), 'array([0., 1., 2., 3.])')
         assert_equal(repr(np.array(1.)), 'array(1.)')
         assert_equal(repr(b), 'array([1.234e+09])')
         assert_equal(repr(np.array([0.])), 'array([0.])')
+        assert_equal(repr(c),
+            "array([1.        +1.j        , 1.12345679+1.12345679j])")
+        assert_equal(repr(np.array([0., -0.])), 'array([ 0., -0.])')
 
         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.234e+09])')
+        assert_equal(repr(c),
+            "array([ 1.        +1.j        ,  1.12345679+1.12345679j])")
+        assert_equal(repr(np.array([0., -0.])), 'array([ 0., -0.])')
 
         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.234e+09])')
+        assert_equal(repr(c),
+            "array([+1.        +1.j        , +1.12345679+1.12345679j])")
 
         np.set_printoptions(legacy='1.13')
         assert_equal(repr(a), 'array([ 0.,  1.,  2.,  3.])')
@@ -455,6 +464,10 @@ def test_sign_spacing(self):
         assert_equal(repr(-b), 'array([ -1.23400000e+09])')
         assert_equal(repr(np.array(1.)), 'array(1.0)')
         assert_equal(repr(np.array([0.])), 'array([ 0.])')
+        assert_equal(repr(c),
+            "array([ 1.00000000+1.j        ,  1.12345679+1.12345679j])")
+        # gh-10383
+        assert_equal(str(np.array([-1., 10])), "[ -1.  10.]")
 
         assert_raises(TypeError, np.set_printoptions, wrongarg=True)
 
@@ -478,6 +491,7 @@ def test_floatmode(self):
                       0.0862072768214508, 0.39112753029631175],
                       dtype=np.float64)
         z = np.arange(6, dtype=np.float16)/10
+        c = np.array([1.0 + 1.0j, 1.123456789 + 1.123456789j], dtype='c16')
 
         # also make sure 1e23 is right (is between two fp numbers)
         w = np.array(['1e{}'.format(i) for i in range(25)], dtype=np.float64)
@@ -503,6 +517,8 @@ def test_floatmode(self):
             "       1.e+16, 1.e+17, 1.e+18, 1.e+19, 1.e+20, 1.e+21, 1.e+22, 1.e+23,\n"
             "       1.e+24])")
         assert_equal(repr(wp), "array([1.234e+001, 1.000e+002, 1.000e+123])")
+        assert_equal(repr(c),
+            "array([1.         +1.j         , 1.123456789+1.123456789j])")
 
         # maxprec mode, precision=8
         np.set_printoptions(floatmode='maxprec', precision=8)
@@ -517,6 +533,8 @@ def test_floatmode(self):
         assert_equal(repr(w[::5]),
             "array([1.e+00, 1.e+05, 1.e+10, 1.e+15, 1.e+20])")
         assert_equal(repr(wp), "array([1.234e+001, 1.000e+002, 1.000e+123])")
+        assert_equal(repr(c),
+            "array([1.        +1.j        , 1.12345679+1.12345679j])")
 
         # fixed mode, precision=4
         np.set_printoptions(floatmode='fixed', precision=4)
@@ -531,6 +549,8 @@ def test_floatmode(self):
             "array([1.0000e+00, 1.0000e+05, 1.0000e+10, 1.0000e+15, 1.0000e+20])")
         assert_equal(repr(wp), "array([1.2340e+001, 1.0000e+002, 1.0000e+123])")
         assert_equal(repr(np.zeros(3)), "array([0.0000, 0.0000, 0.0000])")
+        assert_equal(repr(c),
+            "array([1.0000+1.0000j, 1.1235+1.1235j])")
         # for larger precision, representation error becomes more apparent:
         np.set_printoptions(floatmode='fixed', precision=8)
         assert_equal(repr(z),
@@ -550,6 +570,8 @@ def test_floatmode(self):
         assert_equal(repr(w[::5]),
             "array([1.e+00, 1.e+05, 1.e+10, 1.e+15, 1.e+20])")
         assert_equal(repr(wp), "array([1.234e+001, 1.000e+002, 1.000e+123])")
+        assert_equal(repr(c),
+            "array([1.00000000+1.00000000j, 1.12345679+1.12345679j])")
 
     def test_legacy_mode_scalars(self):
         # in legacy mode, str of floats get truncated, and complex scalars