Skip to content

Commit 4138a11

Browse files
feat(stdlib): make sorted stable (#22)
Make the `sorted` function available in the `polyfills.stdlib.functions` module behave like the original one respect to sort stability (preserving the order of equal elements).
1 parent fa8a5df commit 4138a11

1 file changed

Lines changed: 122 additions & 30 deletions

File tree

src/polyfills/stdlib/functions.py

Lines changed: 122 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -90,51 +90,96 @@ def sorted(__iterable, key=None, reverse=False):
9090
"""
9191
item_type = type(__iterable)
9292
_dict, _list, _tuple, _str = type({}), type([]), type(()), type("")
93-
94-
def key_to_cmp(key):
95-
""" Convert a key function to a cmp function.
93+
94+
class _ReverseCompare:
95+
"""Wrapper class to reverse comparison for a single sort key component.
9696
97-
Info: https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter
97+
This allows us to reverse only the primary sort key while keeping
98+
the secondary index-based key in normal order for stability.
9899
"""
99-
def cmp(a, b):
100-
""" The `cmp` function does not exist on Python 3.x.
101-
102-
Source: https://stackoverflow.com/a/22490617/8965861
103-
"""
104-
return (a > b) - (a < b)
105-
106-
# def cmp_func(x, y):
107-
# if key(x) < key(y):
108-
# return -1
109-
# elif key(x) > key(y):
110-
# return 1
111-
# else:
112-
# return 0
113-
# def cmp_func(x, y):
114-
# return cmp(key(x), key(y))
115-
# return cmp_func
116-
return lambda x, y: cmp(key(x), key(y))
100+
def __init__(self, obj):
101+
self.obj = obj
102+
103+
def __lt__(self, other):
104+
return self.obj > other.obj
105+
106+
def __gt__(self, other):
107+
return self.obj < other.obj
108+
109+
def __eq__(self, other):
110+
return self.obj == other.obj
111+
112+
def __le__(self, other):
113+
return self.obj >= other.obj
114+
115+
def __ge__(self, other):
116+
return self.obj <= other.obj
117+
118+
def __ne__(self, other):
119+
return self.obj != other.obj
117120

118121
# ---> List
119122
#
120123
# When passed a list, the original `sorted` function
121124
# sorts its elements as expected.
122125
if item_type == _list:
123-
elements = [elem for elem in __iterable] # Make a copy of the original iterable
126+
# Make a copy and add original indices for stable sorting
127+
elements_with_index = [(i, elem) for i, elem in enumerate(__iterable)]
124128

125129
if key is None:
126130
# If no key function is passed, sort the elements as they are
127-
elements.sort()
131+
# Add index as secondary sort key to ensure stability
132+
def stable_key(x):
133+
if reverse:
134+
return (_ReverseCompare(x[1]), x[0])
135+
return (x[1], x[0])
136+
137+
# For very old Python versions without key support
138+
def stable_cmp(a, b):
139+
result = (a[1] > b[1]) - (a[1] < b[1])
140+
141+
if reverse:
142+
result = -result
143+
144+
if result == 0:
145+
# If keys are equal, compare indices to maintain stability (never reverse index order)
146+
result = (a[0] > b[0]) - (a[0] < b[0])
147+
148+
return result
149+
150+
try:
151+
elements_with_index.sort(key=stable_key)
152+
except TypeError:
153+
elements_with_index.sort(stable_cmp)
128154
else:
129155
# The `key` argument was introduced starting from Python 2.4
156+
# Wrap the key function to include the index for stability
157+
def stable_key_wrapper(x):
158+
if reverse:
159+
return (_ReverseCompare(key(x[1])), x[0])
160+
return (key(x[1]), x[0])
161+
162+
# Convert the key function to a `cmp` function for older Python versions (<3.0)
163+
# Info: https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter
164+
#
165+
# Include index comparison for stability
166+
def stable_key_to_cmp(a, b):
167+
ka, kb = key(a[1]), key(b[1])
168+
result = (ka > kb) - (ka < kb)
169+
if reverse:
170+
result = -result
171+
if result == 0:
172+
# If keys are equal, compare indices to maintain stability (never reverse index order)
173+
result = (a[0] > b[0]) - (a[0] < b[0])
174+
return result
175+
130176
try:
131-
elements.sort(key=key)
177+
elements_with_index.sort(key=stable_key_wrapper)
132178
except TypeError:
133-
# Convert the key function to a cmp function, since the `key` argument is not available.
134-
elements.sort(key_to_cmp(key))
179+
elements_with_index.sort(stable_key_to_cmp)
135180

136-
if reverse:
137-
elements.reverse()
181+
# Extract just the elements (without indices)
182+
elements = [elem for _, elem in elements_with_index]
138183

139184
value = []
140185
for element in elements:
@@ -289,6 +334,53 @@ def test_arg_reverse(self):
289334
[_TestClass(2), _TestClass(1), _TestClass(0)],
290335
)
291336

337+
def test_stability(self):
338+
"""Test sorting a list multiple times to ensure stability.
339+
340+
Demonstrates the idiom of sorting by secondary key first, then primary key.
341+
This leverages sort stability to achieve multi-key sorting.
342+
"""
343+
# List of (name, section)
344+
data = [
345+
("Dave", "A"),
346+
("Alice", "B"),
347+
("Ken", "A"),
348+
("Eric", "B"),
349+
("Carol", "A"),
350+
]
351+
352+
# First, sort by name (secondary key)
353+
data_sorted_by_name = sorted(data, key=lambda x: x[0])
354+
355+
self.assertEqual(
356+
data_sorted_by_name,
357+
[
358+
("Alice", "B"),
359+
("Carol", "A"),
360+
("Dave", "A"),
361+
("Eric", "B"),
362+
("Ken", "A"),
363+
],
364+
"First sort by name should produce alphabetical order"
365+
)
366+
367+
# Then, sort by section (primary key)
368+
# Since sort is stable, within each section the name order is preserved
369+
data_sorted_by_section = sorted(data_sorted_by_name, key=lambda x: x[1])
370+
371+
self.assertEqual(
372+
data_sorted_by_section,
373+
[
374+
("Carol", "A"),
375+
("Dave", "A"),
376+
("Ken", "A"),
377+
("Alice", "B"),
378+
("Eric", "B"),
379+
],
380+
"Second sort by section should group by section while preserving name order within each section"
381+
)
382+
383+
292384
if __name__ == "__main__":
293385
_globals = globals()
294386

@@ -308,7 +400,7 @@ def test_arg_reverse(self):
308400
for test_case in test_cases
309401
for test_method in dir(test_case)
310402
if test_method.startswith("test_")
311-
# and "showall" in test_method
403+
# and test_case.__name__.lower().find("sorted") != -1
312404
]
313405

314406
suite = _unittest.TestSuite(tests)

0 commit comments

Comments
 (0)