diff --git a/deepdiff/docstrings/numbers.rst b/deepdiff/docstrings/numbers.rst index c14fe5ef..ae8d5694 100644 --- a/deepdiff/docstrings/numbers.rst +++ b/deepdiff/docstrings/numbers.rst @@ -83,6 +83,11 @@ Approximate number comparison (significant_digits after the decimal point in sci Number To String Function ------------------------- +The default formatter rounds numeric values only. Date, time, datetime, +timedelta, and NumPy datetime values pass through unchanged, including when +dictionary-key comparison uses numeric precision or ignores numeric types. +Numeric precision does not round or merge distinct temporal keys. + number_to_string_func : function, default=None In many cases DeepDiff converts numbers to strings in order to compare them. For example when ignore_order=True, when significant digits parameter is defined or when the ignore_numeric_type_changes=True. In its simplest form, the number_to_string_func is "{:.Xf}".format(Your Number) where X is the significant digits and the number_format_notation is left as the default of "f" meaning fixed point. diff --git a/deepdiff/helper.py b/deepdiff/helper.py index 5143a3b7..90fa2683 100644 --- a/deepdiff/helper.py +++ b/deepdiff/helper.py @@ -468,14 +468,17 @@ def get_doc(doc_filename: str) -> str: def number_to_string(number: Any, significant_digits: int, number_format_notation: Literal['f', 'e'] = 'f') -> Any: """ - Convert numbers to string considering significant digits. + Convert numeric values to strings considering significant digits. + + Temporal objects belong to the broader comparison ``numbers`` group, but + are not numeric values to round. Preserve them like other non-numeric input. """ try: 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 not isinstance(number, numbers): # type: ignore + if not isinstance(number, only_numbers): # type: ignore return number elif isinstance(number, Decimal): with localcontext() as ctx: diff --git a/tests/test_diff_datetime.py b/tests/test_diff_datetime.py index c3905291..728c89e7 100644 --- a/tests/test_diff_datetime.py +++ b/tests/test_diff_datetime.py @@ -1,5 +1,7 @@ import pytz -from datetime import date, datetime, time, timezone +import pytest +import numpy as np +from datetime import date, datetime, time, timedelta, timezone from deepdiff import DeepDiff @@ -123,3 +125,37 @@ def test_datetime_within_array_with_timezone_diff(self): assert not DeepDiff(d1, d2) assert not DeepDiff(d1, d2, ignore_order=True) assert not DeepDiff(d1, d2, truncate_datetime='second') + + +@pytest.mark.parametrize("key, other_key", [ + (datetime(2020, 5, 17, 22, 15), datetime(2020, 5, 17, 22, 15, 0, 1)), + (datetime(2020, 5, 17, tzinfo=timezone.utc), datetime(2020, 5, 17, microsecond=1, tzinfo=timezone.utc)), + (date(2020, 5, 17), date(2020, 5, 18)), + (time(22, 15), time(22, 15, microsecond=1)), + (timedelta(seconds=1), timedelta(seconds=1, microseconds=1)), + (np.datetime64('2020-05-17T22:15:00.000000'), np.datetime64('2020-05-17T22:15:00.000001')), +]) +@pytest.mark.parametrize("flag", [ + "ignore_numeric_type_changes", "ignore_string_case", "ignore_string_type_changes", +]) +@pytest.mark.parametrize("ignore_order", [False, True]) +def test_temporal_dict_keys_preserve_identity_with_numeric_precision(key, other_key, flag, ignore_order): + """Key cleaning must not round temporal keys or merge distinct instants.""" + t1 = {key: 1, other_key: 2} + t2 = {key: 1, other_key: 3} + if ignore_order: + t1, t2 = [t1], [t2] + result = DeepDiff(t1, t2, significant_digits=0, view='tree', ignore_order=ignore_order, **{flag: True}) + assert set(result) == {'values_changed'} + changes = list(result['values_changed']) + assert len(changes) == 1 + assert changes[0].t1 == 2 + assert changes[0].t2 == 3 + assert changes[0].up.t1 == {key: 1, other_key: 2} + assert changes[0].up.t2 == {key: 1, other_key: 3} + + +def test_datetime_key_with_ignored_numeric_value_types(): + """Reproduce issue 550 through the public comparison API.""" + key = datetime(2020, 5, 17, 22, 15) + assert DeepDiff({key: 10.0}, {key: 10}, ignore_numeric_type_changes=True) == {} diff --git a/tests/test_helper.py b/tests/test_helper.py index a42407bd..2f2046bd 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -304,3 +304,17 @@ def test_add_root_to_paths(self, test_num, value, expected): def test_get_semvar_as_integer(self, test_num, value, expected): result = get_semvar_as_integer(value) assert expected == result, f"test_get_semvar_as_integer #{test_num} failed." + + +@pytest.mark.parametrize("value", [ + datetime.datetime(2020, 5, 17, microsecond=123456), + datetime.date(2020, 5, 17), + datetime.time(22, 15, microsecond=123456), + datetime.timedelta(seconds=1, microseconds=123456), + np.datetime64('2020-05-17T22:15:00.123456'), +]) +@pytest.mark.parametrize("significant_digits", [0, 3, 55]) +@pytest.mark.parametrize("number_format_notation", ['f', 'e']) +def test_number_to_string_preserves_temporal_objects(value, significant_digits, number_format_notation): + """Non-numeric temporal values pass through the shared formatter unchanged.""" + assert number_to_string(value, significant_digits, number_format_notation) is value