From 4a1e5b3ab18da75e5ac6574e2aa434a5d5d5efef Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Wed, 17 Aug 2022 12:39:59 +0200 Subject: [PATCH] Bugfix: ValueError when using Decimal 0.x --- deepdiff/helper.py | 60 ++++++++++++--- tests/test_hash.py | 4 +- tests/test_helper.py | 174 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 202 insertions(+), 36 deletions(-) diff --git a/deepdiff/helper.py b/deepdiff/helper.py index f5a6bc88..a1241957 100644 --- a/deepdiff/helper.py +++ b/deepdiff/helper.py @@ -41,6 +41,7 @@ class np_type: np_complex64 = np_type # pragma: no cover. np_complex128 = np_type # pragma: no cover. np_complex_ = np_type # pragma: no cover. + np_complexfloating = np_type # pragma: no cover. else: np_array_factory = np.array np_ndarray = np.ndarray @@ -61,6 +62,7 @@ class np_type: np_complex64 = np.complex64 np_complex128 = np.complex128 np_complex_ = np.complex_ + np_complexfloating = np.complexfloating numpy_numbers = ( np_int8, np_int16, np_int32, np_int64, np_uint8, @@ -68,6 +70,10 @@ class np_type: np_float32, np_float64, np_float_, np_complex64, np_complex128, np_complex_,) +numpy_complex_numbers = ( + np_complexfloating, np_complex64, np_complex128, np_complex_, +) + numpy_dtypes = set(numpy_numbers) numpy_dtypes.add(np_bool_) @@ -102,6 +108,7 @@ class np_type: strings = (str, bytes) # which are both basestring unicode_type = str bytes_type = bytes +only_complex_number = (complex,) + numpy_complex_numbers only_numbers = (int, float, complex, Decimal) + numpy_numbers datetimes = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time) uuids = (uuid.UUID) @@ -115,8 +122,6 @@ class np_type: ID_PREFIX = '!>*id' -ZERO_DECIMAL_CHARACTERS = set("-0.") - KEY_TO_VAL_STR = "{}:{}" TREE_VIEW = 'tree' @@ -323,20 +328,51 @@ def number_to_string(number, significant_digits, number_format_notation="f"): using = number_formatting[number_format_notation] except KeyError: raise ValueError("number_format_notation got invalid value of {}. The valid values are 'f' and 'e'".format(number_format_notation)) from None - if isinstance(number, Decimal): - tup = number.as_tuple() + + if not isinstance(number, numbers): + return number + elif isinstance(number, Decimal): with localcontext() as ctx: - ctx.prec = len(tup.digits) + tup.exponent + significant_digits + # Precision = number of integer digits + significant_digits + # Using number//1 to get the integer part of the number + ctx.prec = len(str(abs(number // 1))) + significant_digits number = number.quantize(Decimal('0.' + '0' * significant_digits)) - elif not isinstance(number, numbers): - return number + elif isinstance(number, only_complex_number): + # Case for complex numbers. + number = number.__class__( + "{real}+{imag}j".format( + real=number_to_string( + number=number.real, + significant_digits=significant_digits, + number_format_notation=number_format_notation + ), + imag=number_to_string( + number=number.imag, + significant_digits=significant_digits, + number_format_notation=number_format_notation + ) + ) + ) + else: + number = round(number=number, ndigits=significant_digits) + + if significant_digits == 0: + number = int(number) + + if number == 0.0: + # Special case for 0: "-0.xx" should compare equal to "0.xx" + number = abs(number) + + # Cast number to string result = (using % significant_digits).format(number) - # Special case for 0: "-0.00" should compare equal to "0.00" - if set(result) <= ZERO_DECIMAL_CHARACTERS: - result = "0.00" # https://bugs.python.org/issue36622 - if number_format_notation == 'e' and isinstance(number, float): - result = result.replace('+0', '+') + if number_format_notation == 'e': + # Removing leading 0 for exponential part. + result = re.sub( + pattern=r'(?<=e(\+|\-))0(?=\d)+', + repl=r'', + string=result + ) return result diff --git a/tests/test_hash.py b/tests/test_hash.py index c5a90905..54c6e4a9 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -357,8 +357,8 @@ def test_same_sets_same_hash(self): assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, result", [ - ({0.012, 0.98}, {0.013, 0.99}, 1, "f", 'set:float:0.00,float:1.0'), - (100000, 100021, 3, "e", 'int:1.000e+05'), + ({0.012, 0.98}, {0.013, 0.99}, 1, "f", 'set:float:0.0,float:1.0'), + (100000, 100021, 3, "e", 'int:1.000e+5'), ]) def test_similar_significant_hash(self, t1, t2, significant_digits, number_format_notation, result): diff --git a/tests/test_helper.py b/tests/test_helper.py index b0b0b628..955117e7 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -33,30 +33,160 @@ def test_short_repr_when_long(self): output = short_repr(item) assert output == "{'Eat more':...}" - @pytest.mark.parametrize("t1, t2, significant_digits, expected_result", + @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, expected_result", [ - (10, 10.0, 5, True), - (10, 10.2, 5, ('10.00000', '10.20000')), - (10, 10.2, 0, True), - (Decimal(10), 10, 0, True), - (Decimal(10), 10, 10, True), - (Decimal(10), 10.0, 0, True), - (Decimal(10), 10.0, 10, True), - (Decimal('10.0'), 10.0, 5, True), - (Decimal('10.01'), 10.01, 1, True), - (Decimal('10.01'), 10.01, 2, True), - (Decimal('10.01'), 10.01, 5, True), - (Decimal('10.01'), 10.01, 8, True), - (Decimal('10.010'), 10.01, 3, True), - (Decimal('100000.1'), 100000.1, 0, True), - (Decimal('100000.1'), 100000.1, 1, True), - (Decimal('100000.1'), 100000.1, 5, True), - (Decimal('100000'), 100000.1, 0, True), - (Decimal('100000'), 100000.1, 1, ('100000.0', '100000.1')), + (10, 10.0, 5, "f", True), + (10, 10.0, 5, "e", True), + (10, 10.2, 5, "f", ('10.00000', '10.20000')), + (10, 10.2, 5, "e", ('1.00000e+1', '1.02000e+1')), + (10, 10.2, 0, "f", True), + (10, 10.2, 0, "e", True), + (Decimal(10), 10, 0, "f", True), + (Decimal(10), 10, 0, "e", True), + (Decimal(10), 10, 10, "f", True), + (Decimal(10), 10, 10, "e", True), + (Decimal(10), 10.0, 0, "f", True), + (Decimal(10), 10.0, 0, "e", True), + (Decimal(10), 10.0, 10, "f", True), + (Decimal(10), 10.0, 10, "e", True), + (Decimal('10.0'), 10.0, 5, "f", True), + (Decimal('10.0'), 10.0, 5, "e", True), + (Decimal('10.01'), 10.01, 1, "f", True), + (Decimal('10.01'), 10.01, 1, "e", True), + (Decimal('10.01'), 10.01, 2, "f", True), + (Decimal('10.01'), 10.01, 2, "e", True), + (Decimal('10.01'), 10.01, 5, "f", True), + (Decimal('10.01'), 10.01, 5, "e", True), + (Decimal('10.01'), 10.01, 8, "f", True), + (Decimal('10.01'), 10.01, 8, "e", True), + (Decimal('10.010'), 10.01, 3, "f", True), + (Decimal('10.010'), 10.01, 3, "e", True), + (Decimal('100000.1'), 100000.1, 0, "f", True), + (Decimal('100000.1'), 100000.1, 0, "e", True), + (Decimal('100000.1'), 100000.1, 1, "f", True), + (Decimal('100000.1'), 100000.1, 1, "e", True), + (Decimal('100000.1'), 100000.1, 5, "f", True), + (Decimal('100000.1'), 100000.1, 5, "e", True), + (Decimal('100000'), 100000.1, 0, "f", True), + (Decimal('100000'), 100000.1, 0, "e", True), + (Decimal('100000'), 100000.1, 1, "f", ('100000.0', '100000.1')), + (Decimal('100000'), 100000.1, 1, "e", True), + (Decimal('-100000'), 100000.1, 1, "f", ('-100000.0', '100000.1')), + (Decimal('-100000'), 100000.1, 1, "e", ("-1.0e+5","1.0e+5")), + (0, 0.0, 5, "f", True), + (0, 0.0, 5, "e", True), + (0, 0.2, 5, "f", ('0.00000', '0.20000')), + (0, 0.2, 5, "e", ('0.00000e+0', '2.00000e-1')), + (0, 0.2, 0, "f", True), + (0, 0.2, 0, "e", True), + (Decimal(0), 0, 0, "f", True), + (Decimal(0), 0, 0, "e", True), + (Decimal(0), 0, 10, "f", True), + (Decimal(0), 0, 10, "e", True), + (Decimal(0), 0.0, 0, "f", True), + (Decimal(0), 0.0, 0, "e", True), + (Decimal(0), 0.0, 10, "f", True), + (Decimal(0), 0.0, 10, "e", True), + (Decimal('0.0'), 0.0, 5, "f", True), + (Decimal('0.0'), 0.0, 5, "e", True), + (Decimal('0.01'), 0.01, 1, "f", True), + (Decimal('0.01'), 0.01, 1, "e", True), + (Decimal('0.01'), 0.01, 2, "f", True), + (Decimal('0.01'), 0.01, 2, "e", True), + (Decimal('0.01'), 0.01, 5, "f", True), + (Decimal('0.01'), 0.01, 5, "e", True), + (Decimal('0.01'), 0.01, 8, "f", True), + (Decimal('0.01'), 0.01, 8, "e", True), + (Decimal('0.010'), 0.01, 3, "f", True), + (Decimal('0.010'), 0.01, 3, "e", True), + (Decimal('0.00002'), 0.00001, 0, "f", True), + (Decimal('0.00002'), 0.00001, 0, "e", True), + (Decimal('0.00002'), 0.00001, 1, "f", True), + (Decimal('0.00002'), 0.00001, 1, "e", True), + (Decimal('0.00002'), 0.00001, 5, "f", ('0.00002', '0.00001')), + (Decimal('0.00002'), 0.00001, 5, "e", ('2.00000e-5', '1.00000e-5')), + (Decimal('0.00002'), 0.00001, 6, "f", ('0.000020', '0.000010')), + (Decimal('0.00002'), 0.00001, 6, "e", ('2.000000e-5', '1.000000e-5')), + (Decimal('0'), 0.1, 0, "f", True), + (Decimal('0'), 0.1, 0, "e", True), + (Decimal('0'), 0.1, 1, "f", ('0.0', '0.1')), + (Decimal('0'), 0.1, 1, "e", ('0.0e+0', '1.0e-1')), + (-0, 0.0, 5, "f", True), + (-0, 0.0, 5, "e", True), + (-0, 0.2, 5, "f", ('0.00000', '0.20000')), + (-0, 0.2, 5, "e", ('0.00000e+0', '2.00000e-1')), + (-0, 0.2, 0, "f", True), + (-0, 0.2, 0, "e", True), + (Decimal(-0), 0, 0, "f", True), + (Decimal(-0), 0, 0, "e", True), + (Decimal(-0), 0, 10, "f", True), + (Decimal(-0), 0, 10, "e", True), + (Decimal(-0), 0.0, 0, "f", True), + (Decimal(-0), 0.0, 0, "e", True), + (Decimal(-0), 0.0, 10, "f", True), + (Decimal(-0), 0.0, 10, "e", True), + (Decimal('-0.0'), 0.0, 5, "f", True), + (Decimal('-0.0'), 0.0, 5, "e", True), + (Decimal('-0.01'), 0.01, 1, "f", True), + (Decimal('-0.01'), 0.01, 1, "e", True), + (Decimal('-0.01'), 0.01, 2, "f", ('-0.01', '0.01')), + (Decimal('-0.01'), 0.01, 2, "e", ('-1.00e-2', '1.00e-2')), + (Decimal('-0.00002'), 0.00001, 0, "f", True), + (Decimal('-0.00002'), 0.00001, 0, "e", True), + (Decimal('-0.00002'), 0.00001, 1, "f", True), + (Decimal('-0.00002'), 0.00001, 1, "e", True), + (Decimal('-0.00002'), 0.00001, 5, "f", ('-0.00002', '0.00001')), + (Decimal('-0.00002'), 0.00001, 5, "e", ('-2.00000e-5', '1.00000e-5')), + (Decimal('-0.00002'), 0.00001, 6, "f", ('-0.000020', '0.000010')), + (Decimal('-0.00002'), 0.00001, 6, "e", ('-2.000000e-5', '1.000000e-5')), + (Decimal('-0'), 0.1, 0, "f", True), + (Decimal('-0'), 0.1, 0, "e", True), + (Decimal('-0'), 0.1, 1, "f", ('0.0', '0.1')), + (Decimal('-0'), 0.1, 1, "e", ('0.0e+0', '1.0e-1')), ]) - def test_number_to_string_decimal_digits(self, t1, t2, significant_digits, expected_result): - st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation="f") - st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation="f") + def test_number_to_string_decimal_digits(self, t1, t2, significant_digits, number_format_notation, expected_result): + st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) + st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) + if expected_result is True: + assert st1 == st2 + else: + assert st1 == expected_result[0] + assert st2 == expected_result[1] + + @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, expected_result", + [ + (10j, 10.0j, 5, "f", True), + (10j, 10.0j, 5, "e", True), + (4+10j, 4.0000002+10.0000002j, 5, "f", True), + (4+10j, 4.0000002+10.0000002j, 5, "e", True), + (4+10j, 4.0000002+10.0000002j, 7, "f", ('4.0000000+10.0000000j', '4.0000002+10.0000002j')), + (4+10j, 4.0000002+10.0000002j, 7, "e", ('4.0000000e+0+1.0000000e+1j', '4.0000002e+0+1.0000000e+1j')), + (0.00002+0.00002j, 0.00001+0.00001j, 0, "f", True), + (0.00002+0.00002j, 0.00001+0.00001j, 0, "e", True), + (0.00002+0.00002j, 0.00001+0.00001j, 5, "f", ('0.00002+0.00002j', '0.00001+0.00001j')), + (0.00002+0.00002j, 0.00001+0.00001j, 5, "e", ('2.00000e-5+2.00000e-5j', '1.00000e-5+1.00000e-5j')), + (-0.00002-0.00002j, 0.00001+0.00001j, 0, "f", True), + (-0.00002-0.00002j, 0.00001+0.00001j, 0, "e", True), + (10j, 10.2j, 5, "f", ('0.00000+10.00000j', '0.00000+10.20000j')), + (10j, 10.2j, 5, "e", ('0.00000e+0+1.00000e+1j', '0.00000e+0+1.02000e+1j')), + (10j, 10.2j, 0, "f", True), + (10j, 10.2j, 0, "e", True), + (0j, 0.0j, 5, "f", True), + (0j, 0.0j, 5, "e", True), + (0j, 0.2j, 5, "f", ('0.00000', '0.00000+0.20000j')), + (0j, 0.2j, 5, "e", ('0.00000e+0', '0.00000e+0+2.00000e-1j')), + (0j, 0.2j, 0, "f", True), + (0j, 0.2j, 0, "e", True), + (-0j, 0.0j, 5, "f", True), + (-0j, 0.0j, 5, "e", True), + (-0j, 0.2j, 5, "f", ('0.00000', '0.00000+0.20000j')), + (-0j, 0.2j, 5, "e", ('0.00000e+0', '0.00000e+0+2.00000e-1j')), + (-0j, 0.2j, 0, "f", True), + (-0j, 0.2j, 0, "e", True), + ]) + def test_number_to_string_complex_digits(self, t1, t2, significant_digits, number_format_notation, expected_result): + st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) + st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) if expected_result is True: assert st1 == st2 else: