Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deepdiff/docstrings/numbers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions deepdiff/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
38 changes: 37 additions & 1 deletion tests/test_diff_datetime.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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) == {}
14 changes: 14 additions & 0 deletions tests/test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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