summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSeth Morton <seth.m.morton@gmail.com>2023-02-26 23:21:37 -0800
committerSeth Morton <seth.m.morton@gmail.com>2023-02-26 23:47:50 -0800
commitcf2a55daf7e74d177c95149da623172b1b6d93ae (patch)
tree6db55e4ddf210e4c7fe4824079ecc16bcd8abc62
parente778c1742fc94766b42110580809795605ca3c88 (diff)
downloadnatsort-cf2a55daf7e74d177c95149da623172b1b6d93ae.tar.gz
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.
-rw-r--r--natsort/utils.py12
-rw-r--r--tests/test_natsorted.py29
-rw-r--r--tests/test_parse_number_function.py20
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(