From b03527ded99dd7f6e2c6dd85ca3fdd043398d8bc Mon Sep 17 00:00:00 2001 From: gfyoung Date: Wed, 3 Feb 2016 08:55:34 +0000 Subject: [PATCH] BUG: Fix binary_repr for negative numbers Completely rewrote binary_repr method to use the Python's built-in 'bin' function to generate binary representations quickly. Fixed the behaviour for negative numbers in which insufficient widths resulted in outputs of all zero's for the two's complement. Now, it will return the two's complement with width equal to the minimum number of bits needed to represent the complement. Closes gh-7168. --- doc/release/1.12.0-notes.rst | 12 +++- numpy/core/numeric.py | 90 ++++++++++++++------------- numpy/core/tests/test_deprecations.py | 34 ++++++++-- numpy/core/tests/test_numeric.py | 19 +++++- 4 files changed, 102 insertions(+), 53 deletions(-) diff --git a/doc/release/1.12.0-notes.rst b/doc/release/1.12.0-notes.rst index 946a6bfdfbdb..38e5e4338ef3 100644 --- a/doc/release/1.12.0-notes.rst +++ b/doc/release/1.12.0-notes.rst @@ -154,10 +154,18 @@ Deprecations Assignment of ndarray object's ``data`` attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Assigning the 'data' attribute is an inherently unsafe operation as pointed +Assigning the 'data' attribute is an inherently unsafe operation as pointed out in gh-7083. Such a capability will be removed in the future. Unsafe int casting of the num attribute in linspace ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``np.linspace`` now raises DeprecationWarning when num cannot be safely +``np.linspace`` now raises DeprecationWarning when num cannot be safely interpreted as an integer. + +Insufficient bit width parameter to ``binary_repr`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If a 'width' parameter is passed into ``binary_repr`` that is insufficient to +represent the number in base 2 (positive) or 2's complement (negative) form, +the function used to silently ignore the parameter and return a representation +using the minimal number of bits needed for the form in question. Such behavior +is now considered unsafe from a user perspective and will raise an error in the future. diff --git a/numpy/core/numeric.py b/numpy/core/numeric.py index 9ddc3c546d33..551d63a01685 100644 --- a/numpy/core/numeric.py +++ b/numpy/core/numeric.py @@ -2090,31 +2090,6 @@ def isscalar(num): else: return type(num) in ScalarType -_lkup = { - '0':'0000', - '1':'0001', - '2':'0010', - '3':'0011', - '4':'0100', - '5':'0101', - '6':'0110', - '7':'0111', - '8':'1000', - '9':'1001', - 'a':'1010', - 'b':'1011', - 'c':'1100', - 'd':'1101', - 'e':'1110', - 'f':'1111', - 'A':'1010', - 'B':'1011', - 'C':'1100', - 'D':'1101', - 'E':'1110', - 'F':'1111', - 'L':''} - def binary_repr(num, width=None): """ Return the binary representation of the input number as a string. @@ -2134,8 +2109,18 @@ def binary_repr(num, width=None): num : int Only an integer decimal number can be used. width : int, optional - The length of the returned string if `num` is positive, the length of - the two's complement if `num` is negative. + The length of the returned string if `num` is positive, or the length + of the two's complement if `num` is negative, provided that `width` is + at least a sufficient number of bits for `num` to be represented in the + 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 + 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. + + .. deprecated:: 1.12.0 Returns ------- @@ -2146,6 +2131,7 @@ def binary_repr(num, width=None): -------- base_repr: Return a string representation of a number in the given base system. + bin: Python's built-in binary representation generator of an integer. Notes ----- @@ -2169,27 +2155,43 @@ def binary_repr(num, width=None): The two's complement is returned when the input number is negative and width is specified: - >>> np.binary_repr(-3, width=4) - '1101' + >>> np.binary_repr(-3, width=3) + '101' + >>> np.binary_repr(-3, width=5) + '11101' """ - # ' <-- unbreak Emacs fontification - sign = '' - if num < 0: + def warn_if_insufficient(width, binwdith): + if width is not None and width < binwidth: + warnings.warn( + "Insufficient bit width provided. This behavior " + "will raise an error in the future.", DeprecationWarning + ) + + if num == 0: + return '0' * (width or 1) + + elif num > 0: + binary = bin(num)[2:] + binwidth = len(binary) + outwidth = (binwidth if width is None + else max(binwidth, width)) + warn_if_insufficient(width, binwidth) + return binary.zfill(outwidth) + + else: if width is None: - sign = '-' - num = -num + return '-' + bin(-num)[2:] + else: - # replace num with its 2-complement - num = 2**width + num - elif num == 0: - return '0'*(width or 1) - ostr = hex(num) - bin = ''.join([_lkup[ch] for ch in ostr[2:]]) - bin = bin.lstrip('0') - if width is not None: - bin = bin.zfill(width) - return sign + bin + poswidth = len(bin(-num)[2:]) + 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 def base_repr(number, base=2, padding=0): """ diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index 3a4c051365ef..7d12ed4665f3 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -579,14 +579,14 @@ def test_data_attr_assignment(self): class TestLinspaceInvalidNumParameter(_DeprecationTestCase): - """Argument to the num parameter in linspace that cannot be + """Argument to the num parameter in linspace that cannot be safely interpreted as an integer is deprecated in 1.12.0. - Argument to the num parameter in linspace that cannot be - safely interpreted as an integer should not be allowed. + Argument to the num parameter in linspace that cannot be + safely interpreted as an integer should not be allowed. In the interest of not breaking code that passes an argument that could still be interpreted as an integer, a - DeprecationWarning will be issued for the time being to give + DeprecationWarning will be issued for the time being to give developers time to refactor relevant code. """ def test_float_arg(self): @@ -594,6 +594,32 @@ def test_float_arg(self): self.assert_deprecated(np.linspace, args=(0, 10, 2.5)) +class TestBinaryReprInsufficientWidthParameterForRepresentation(_DeprecationTestCase): + """ + If a 'width' parameter is passed into ``binary_repr`` that is insufficient to + represent the number in base 2 (positive) or 2's complement (negative) form, + the function used to silently ignore the parameter and return a representation + using the minimal number of bits needed for the form in question. Such behavior + is now considered unsafe from a user perspective and will raise an error in the future. + """ + + def test_insufficient_width_positive(self): + args = (10,) + kwargs = {'width': 2} + + self.message = ("Insufficient bit width provided. This behavior " + "will raise an error in the future.") + self.assert_deprecated(np.binary_repr, args=args, kwargs=kwargs) + + def test_insufficient_width_negative(self): + args = (-5,) + kwargs = {'width': 2} + + self.message = ("Insufficient bit width provided. This behavior " + "will raise an error in the future.") + self.assert_deprecated(np.binary_repr, args=args, kwargs=kwargs) + + class TestTestDeprecated(object): def test_assert_deprecated(self): test_case_instance = _DeprecationTestCase() diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py index e22a5e193434..a43ad96b7729 100644 --- a/numpy/core/tests/test_numeric.py +++ b/numpy/core/tests/test_numeric.py @@ -1021,12 +1021,25 @@ class TestBinaryRepr(TestCase): def test_zero(self): assert_equal(np.binary_repr(0), '0') - def test_large(self): - assert_equal(np.binary_repr(10736848), '101000111101010011010000') + def test_positive(self): + assert_equal(np.binary_repr(10), '1010') + assert_equal(np.binary_repr(12522), + '11000011101010') + assert_equal(np.binary_repr(10736848), + '101000111101010011010000') def test_negative(self): assert_equal(np.binary_repr(-1), '-1') - assert_equal(np.binary_repr(-1, width=8), '11111111') + assert_equal(np.binary_repr(-10), '-1010') + assert_equal(np.binary_repr(-12522), + '-11000011101010') + assert_equal(np.binary_repr(-10736848), + '-101000111101010011010000') + + def test_sufficient_width(self): + assert_equal(np.binary_repr(0, width=5), '00000') + assert_equal(np.binary_repr(10, width=7), '0001010') + assert_equal(np.binary_repr(-5, width=7), '1111011') class TestBaseRepr(TestCase):