From cf2a55daf7e74d177c95149da623172b1b6d93ae Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 26 Feb 2023 23:21:37 -0800 Subject: Ensure None, NaN, and Infinity are sorted consistently Internally, these may be translated to the same value, so they will be output in the same order they were input, which could lead to suprise. This commit ensures the order is always consistent. --- natsort/utils.py | 12 +++++++++++- tests/test_natsorted.py | 29 ++++++++++++++++++++--------- tests/test_parse_number_function.py | 20 +++++++++++++++----- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/natsort/utils.py b/natsort/utils.py index b86225e..2a3745f 100644 --- a/natsort/utils.py +++ b/natsort/utils.py @@ -420,7 +420,17 @@ def parse_number_or_none_factory( val: Any, _nan_replace: float = nan_replace, _sep: StrOrBytes = sep ) -> BasicTuple: """Given a number, place it in a tuple with a leading null string.""" - return _sep, (_nan_replace if val != val or val is None else val) + # Add a trailing string numbers equaling _nan_replace. This will make + # the ordering between None NaN, and the NaN replacement value... + # None comes first, then NaN, then the replacement value. + if val is None: + return _sep, _nan_replace, "1" + elif val != val: + return _sep, _nan_replace, "2" + elif val == _nan_replace: + return _sep, _nan_replace, "3" + else: + return _sep, val # Return the function, possibly wrapping in tuple if PATH is selected. if alg & ns.PATH and alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA: diff --git a/tests/test_natsorted.py b/tests/test_natsorted.py index eccb9d2..3d6375c 100644 --- a/tests/test_natsorted.py +++ b/tests/test_natsorted.py @@ -4,6 +4,7 @@ Here are a collection of examples of how this module can be used. See the README or the natsort homepage for more details. """ +import math from operator import itemgetter from pathlib import PurePosixPath from typing import List, Tuple, Union @@ -110,19 +111,29 @@ def test_natsorted_handles_mixed_types( @pytest.mark.parametrize( - "alg, expected, slc", + "alg, expected", [ - (ns.DEFAULT, [float("nan"), 5, "25", 1e40], slice(1, None)), - (ns.NANLAST, [5, "25", 1e40, float("nan")], slice(None, 3)), + (ns.DEFAULT, [None, float("nan"), float("-inf"), 5, "25", 1e40, float("inf")]), + (ns.NANLAST, [float("-inf"), 5, "25", 1e40, None, float("nan"), float("inf")]), ], ) -def test_natsorted_handles_nan( - alg: NSType, expected: List[Union[str, float, int]], slc: slice +def test_natsorted_consistent_ordering_with_nan_and_friends( + alg: NSType, expected: List[Union[str, float, None, int]] ) -> None: - given: List[Union[str, float, int]] = ["25", 5, float("nan"), 1e40] - # The slice is because NaN != NaN - # noinspection PyUnresolvedReferences - assert natsorted(given, alg=alg)[slc] == expected[slc] + sentinel = math.pi + expected = [sentinel if x != x else x for x in expected] + given: List[Union[str, float, None, int]] = [ + float("inf"), + float("-inf"), + "25", + 5, + float("nan"), + 1e40, + None, + ] + result = natsorted(given, alg=alg) + result = [sentinel if x != x else x for x in result] + assert result == expected def test_natsorted_with_mixed_bytes_and_str_input_raises_type_error() -> None: diff --git a/tests/test_parse_number_function.py b/tests/test_parse_number_function.py index 85d6b96..5ac5700 100644 --- a/tests/test_parse_number_function.py +++ b/tests/test_parse_number_function.py @@ -20,7 +20,7 @@ from natsort.utils import NumTransformer, parse_number_or_none_factory (ns.PATH | ns.UNGROUPLETTERS | ns.LOCALE, lambda x: ((("xx",), ("", x)),)), ], ) -@given(x=floats(allow_nan=False) | integers()) +@given(x=floats(allow_nan=False, allow_infinity=False) | integers()) def test_parse_number_factory_makes_function_that_returns_tuple( x: Union[float, int], alg: NSType, example_func: NumTransformer ) -> None: @@ -32,10 +32,20 @@ def test_parse_number_factory_makes_function_that_returns_tuple( "alg, x, result", [ (ns.DEFAULT, 57, ("", 57)), - (ns.DEFAULT, float("nan"), ("", float("-inf"))), # NaN transformed to -infinity - (ns.NANLAST, float("nan"), ("", float("+inf"))), # NANLAST makes it +infinity - (ns.DEFAULT, None, ("", float("-inf"))), # None transformed to -infinity - (ns.NANLAST, None, ("", float("+inf"))), # NANLAST makes it +infinity + ( + ns.DEFAULT, + float("nan"), + ("", float("-inf"), "2"), + ), # NaN transformed to -infinity + ( + ns.NANLAST, + float("nan"), + ("", float("+inf"), "2"), + ), # NANLAST makes it +infinity + (ns.DEFAULT, None, ("", float("-inf"), "1")), # None transformed to -infinity + (ns.NANLAST, None, ("", float("+inf"), "1")), # NANLAST makes it +infinity + (ns.DEFAULT, float("-inf"), ("", float("-inf"), "3")), + (ns.NANLAST, float("+inf"), ("", float("+inf"), "3")), ], ) def test_parse_number_factory_treats_nan_and_none_special( -- cgit v1.2.1