Repository: numpy/numpy

Issue:
binary_repr: extra bit for most-negative number
`binary_repr` does not correctly represent maximally-negative numbers of a given width.  For example, an 8-bit two's complement can represent numbers in the range [-128, 127], inclusive.  That is, -128 as an 8-bit two's complement number is "10000000", and 127 is "01111111".  However, `binary_repr` mishandles -128:
```
>>> import numpy
>>> numpy.__version__
'1.12.0'
>>> numpy.binary_repr(-128, 8)
'110000000'
```

Notice, the result is a 9-bit string instead of the requested 8-bit string.

This is not limited to a width of 8:
```
>>> numpy.binary_repr(-8, 4)
'11000'
>>> numpy.binary_repr(-32768, 16)
'11000000000000000'
>>> numpy.binary_repr(-1, 1)
'11'
```



Discussion:
Hmm, works fine in 1.11.3:

```
[~]
|3> np.binary_repr(-128, width=8)
'10000000'

[~]
|4> np.__version__
'1.11.3'
```
@rkern Confirmed. Looks like a regression.
The only recent changes to the function came in with b03527de, #7178. @gfyoung Head up.
Here is why:
~~~python
>>> bin(127)
'0b1111111'
>>>
>>> bin(128)
'0b10000000'
~~~
My patch relied on computing the 2's complement using the length of the positive number.  Numbers at the boundary get one additional digit.  This should be relatively easy to patch with one `if` branch.

Resolving patch:
diff --git a/numpy/core/numeric.py b/numpy/core/numeric.py
--- a/numpy/core/numeric.py
+++ b/numpy/core/numeric.py
@@ -2206,7 +2206,7 @@ def binary_repr(num, width=None):
         designated form.
 
         If the `width` value is insufficient, it will be ignored, and `num` will
-        be returned in binary(`num` > 0) or two's complement (`num` < 0) form
+        be returned in binary (`num` > 0) or two's complement (`num` < 0) form
         with its width equal to the minimum number of bits needed to represent
         the number in the designated form. This behavior is deprecated and will
         later raise an error.
@@ -2276,10 +2276,16 @@ def warn_if_insufficient(width, binwdith):
 
         else:
             poswidth = len(bin(-num)[2:])
-            twocomp = 2**(poswidth + 1) + num
 
+            # See gh-8679: remove extra digit
+            # for numbers at boundaries.
+            if 2**(poswidth - 1) == -num:
+                poswidth -= 1
+
+            twocomp = 2**(poswidth + 1) + num
             binary = bin(twocomp)[2:]
             binwidth = len(binary)
+
             outwidth = max(binwidth, width)
             warn_if_insufficient(width, binwidth)
             return '1' * (outwidth - binwidth) + binary


Test patch:
diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py
--- a/numpy/core/tests/test_numeric.py
+++ b/numpy/core/tests/test_numeric.py
@@ -1149,6 +1149,18 @@ def test_sufficient_width(self):
         assert_equal(np.binary_repr(10, width=7), '0001010')
         assert_equal(np.binary_repr(-5, width=7), '1111011')
 
+    def test_neg_width_boundaries(self):
+        # see gh-8670
+
+        # Ensure that the example in the issue does not
+        # break before proceeding to a more thorough test.
+        assert_equal(np.binary_repr(-128, width=8), '10000000')
+
+        for width in range(1, 11):
+            num = -2**(width - 1)
+            exp = '1' + (width - 1) * '0'
+            assert_equal(np.binary_repr(num, width=width), exp)
+
 
 class TestBaseRepr(TestCase):
     def test_base3(self):