Skip to content

Commit 0a77256

Browse files
committed
add max_by() and min_by()
this is similar to maxima_by, but according to .net 6 doc this function only returns one value
1 parent 12eccc4 commit 0a77256

File tree

6 files changed

+206
-0
lines changed

6 files changed

+206
-0
lines changed

doc/api/types_linq.enumerable.rst

+69
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,43 @@ Example
12231223

12241224
----
12251225

1226+
instancemethod ``max_by[TSupportsLessThan](key_selector)``
1227+
------------------------------------------------------------
1228+
1229+
Parameters
1230+
- `key_selector` (``Callable[[TSource_co], TSupportsLessThan]``)
1231+
1232+
Returns
1233+
- ``TSource_co``
1234+
1235+
Returns the maximal element of the sequence based on the given key selector. Raises
1236+
`InvalidOperationError` if there is no value.
1237+
1238+
Example
1239+
>>> strs = ['aaa', 'bb', 'c', 'dddd']
1240+
>>> Enumerable(strs).max_by(len)
1241+
'dddd'
1242+
1243+
----
1244+
1245+
instancemethod ``max_by[TKey](key_selector, __comparer)``
1246+
-----------------------------------------------------------
1247+
1248+
Parameters
1249+
- `key_selector` (``Callable[[TSource_co], TKey]``)
1250+
- `__comparer` (``Callable[[TKey, TKey], int]``)
1251+
1252+
Returns
1253+
- ``TSource_co``
1254+
1255+
Returns the maximal element of the sequence based on the given key selector and the comparer.
1256+
Raises `InvalidOperationError` if there is no value.
1257+
1258+
Such comparer takes two values and return positive ints when lhs > rhs, negative ints
1259+
if lhs < rhs, and 0 if they are equal.
1260+
1261+
----
1262+
12261263
instancemethod ``min[TSupportsLessThan]()``
12271264
---------------------------------------------
12281265

@@ -1280,6 +1317,38 @@ resulting values. Returns the default one if there is no value.
12801317

12811318
----
12821319

1320+
instancemethod ``min_by[TSupportsLessThan](key_selector)``
1321+
------------------------------------------------------------
1322+
1323+
Parameters
1324+
- `key_selector` (``Callable[[TSource_co], TSupportsLessThan]``)
1325+
1326+
Returns
1327+
- ``TSource_co``
1328+
1329+
Returns the minimal element of the sequence based on the given key selector. Raises
1330+
`InvalidOperationError` if there is no value.
1331+
1332+
----
1333+
1334+
instancemethod ``min_by[TKey](key_selector, __comparer)``
1335+
-----------------------------------------------------------
1336+
1337+
Parameters
1338+
- `key_selector` (``Callable[[TSource_co], TKey]``)
1339+
- `__comparer` (``Callable[[TKey, TKey], int]``)
1340+
1341+
Returns
1342+
- ``TSource_co``
1343+
1344+
Returns the minimal element of the sequence based on the given key selector and the comparer.
1345+
Raises `InvalidOperationError` if there is no value.
1346+
1347+
Such comparer takes two values and return positive ints when lhs > rhs, negative ints
1348+
if lhs < rhs, and 0 if they are equal.
1349+
1350+
----
1351+
12831352
instancemethod ``of_type[TResult](t_result)``
12841353
-----------------------------------------------
12851354

doc/api_spec.py

+2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ class ClassSpec(TypedDict):
7171
'last2',
7272
'max',
7373
'max2',
74+
'max_by',
7475
'min',
7576
'min2',
77+
'min_by',
7678
'of_type',
7779
'order_by',
7880
'order_by_descending',

doc/to-start/differences.rst

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ and its .NET counterpart.
3232
objects, construct hashmaps, etc. While in Python such identities are often solely determined by object's
3333
magic methods such as ``__hash__()``, ``__eq__()``, ``__lt__()``, etc. So method overloads that involve such
3434
comparer interfaces are omitted in this library, or implemented in another form.
35+
* In C#, there are nullable types and default values for a type. For example, ``default(int) == 0`` and ``default(int?) == null``.
36+
Some C# methods return such default values if the source sequence is empty, or skip ``null``'s if the source sequence contains
37+
concrete data too. There are no such notions in Python and the C#-like default semantics are non-existent. So, this usage is
38+
not supported by this library (Can ``None`` be considered a default value for all cases? Hmm..).
3539
* All classes in this library are concrete. There are no interfaces like what are usually done in C#.
3640

3741
Limitations:

tests/test_usage.py

+50
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,30 @@ def test_max2_overload2(self):
712712
en = Enumerable(lst)
713713
assert en.max2(lambda x: x[0], object) == 4
714714

715+
716+
class TestMaxByMethod:
717+
def test_overload1(self):
718+
strings = ['aaa', 'bb', 'c', 'dddd']
719+
en = Enumerable(strings)
720+
assert en.max_by(len) == 'dddd'
721+
722+
def test_overload1_empty(self):
723+
strings: List[str] = []
724+
en = Enumerable(strings)
725+
with pytest.raises(InvalidOperationError):
726+
en.max_by(len)
727+
728+
def test_dup_choose_first(self):
729+
strings = ['foo', 'cheese', 'baz', 'spam', 'orange', 'string']
730+
en = Enumerable(strings)
731+
assert en.max_by(len) == 'cheese'
732+
733+
def test_overload2(self):
734+
lst = [['foo'], ['cheese'], ['baz'], ['spam'], ['string']]
735+
en = Enumerable(lst)
736+
assert en.max_by(lambda x: x[0], lambda x, y: len(x) - len(y)) == ['cheese']
737+
738+
715739
class TestMinMethod:
716740
def test_min_overload1(self):
717741
nums = (1, 0.4, 2.2, 5, 1, 2)
@@ -757,6 +781,32 @@ def test_min2_overload2(self):
757781
assert en.min2(lambda x: x[0], object) == -11
758782

759783

784+
class TestMinByMethod:
785+
def test_overload1(self):
786+
strings = ['aaa', 'bb', 'c', 'dddd']
787+
en = Enumerable(strings)
788+
assert en.min_by(len) == 'c'
789+
790+
def test_overload1_empty(self):
791+
strings: List[str] = []
792+
en = Enumerable(strings)
793+
with pytest.raises(InvalidOperationError):
794+
en.min_by(len)
795+
796+
def test_dup_choose_first(self):
797+
class MyType:
798+
def __init__(self, x: int): self.x = x
799+
lst = [MyType(2), MyType(7), MyType(19), MyType(1), MyType(7), MyType(1)]
800+
en = Enumerable(lst)
801+
assert en.min_by(lambda x: x.x) == lst[3]
802+
assert en.min_by(lambda x: x.x) != lst[-1]
803+
804+
def test_overload2(self):
805+
lst = [['foo'], ['cheese'], ['baz'], ['spam'], ['string']]
806+
en = Enumerable(lst)
807+
assert en.min_by(lambda x: x[0], lambda x, y: len(x) - len(y)) == ['foo']
808+
809+
760810
class TestOfTypeMethod:
761811
def test_of_type(self):
762812
lst = [1, 5, 4.4, object(), 5.6, -12.2, [], 2.2, False]

types_linq/enumerable.py

+36
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,20 @@ def _minmax_helper(self, result_selector, op, when_empty) -> Any:
447447
curr = mapped if op(curr, mapped) else curr
448448
return curr
449449

450+
def _minmax_by_helper(self, key_selector, op) -> Any:
451+
iterator = iter(self)
452+
try:
453+
curr = next(iterator)
454+
except StopIteration:
455+
self._raise_empty_sequence()
456+
curr_key = key_selector(curr)
457+
for elem in iterator:
458+
elem_key = key_selector(elem)
459+
if op(curr_key, elem_key):
460+
curr = elem
461+
curr_key = elem_key
462+
return curr
463+
450464
def max(self, *args: Callable[[TSource_co], Any]) -> Any:
451465
if len(args) == 0:
452466
result_selector: Any = lambda x: x
@@ -469,6 +483,17 @@ def max2(self, *args) -> Any:
469483
lambda: default,
470484
)
471485

486+
def max_by(self,
487+
key_selector: Callable[[TSource_co], Any],
488+
*args: Callable[[Any, Any], int],
489+
) -> Any:
490+
if len(args) == 0:
491+
op = lambda l, r: l < r
492+
else: # len(args) == 1
493+
comp = args[0]
494+
op = lambda l, r: comp(l, r) < 0
495+
return self._minmax_by_helper(key_selector, op)
496+
472497
def min(self, *args: Callable[[TSource_co], Any]) -> Any:
473498
if len(args) == 0:
474499
result_selector: Any = lambda x: x
@@ -491,6 +516,17 @@ def min2(self, *args) -> Any:
491516
lambda: default,
492517
)
493518

519+
def min_by(self,
520+
key_selector: Callable[[TSource_co], Any],
521+
*args: Callable[[Any, Any], int],
522+
) -> Any:
523+
if len(args) == 0:
524+
op = lambda l, r: r < l
525+
else: # len(args) == 1
526+
comp = args[0]
527+
op = lambda l, r: comp(l, r) > 0
528+
return self._minmax_by_helper(key_selector, op)
529+
494530
def of_type(self, t_result: Type[TResult]) -> Enumerable[TResult]:
495531
return self.where(lambda e: isinstance(e, t_result)).cast(t_result)
496532

types_linq/enumerable.pyi

+45
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,31 @@ class Enumerable(Sequence[TSource_co], Generic[TSource_co]):
924924
1
925925
'''
926926

927+
@overload
928+
def max_by(self, key_selector: Callable[[TSource_co], TSupportsLessThan]) -> TSource_co:
929+
'''
930+
Returns the maximal element of the sequence based on the given key selector. Raises
931+
`InvalidOperationError` if there is no value.
932+
933+
Example
934+
>>> strs = ['aaa', 'bb', 'c', 'dddd']
935+
>>> Enumerable(strs).max_by(len)
936+
'dddd'
937+
'''
938+
939+
@overload
940+
def max_by(self,
941+
key_selector: Callable[[TSource_co], TKey],
942+
__comparer: Callable[[TKey, TKey], int],
943+
) -> TSource_co:
944+
'''
945+
Returns the maximal element of the sequence based on the given key selector and the comparer.
946+
Raises `InvalidOperationError` if there is no value.
947+
948+
Such comparer takes two values and return positive ints when lhs > rhs, negative ints
949+
if lhs < rhs, and 0 if they are equal.
950+
'''
951+
927952
@overload
928953
def min(self: Enumerable[TSupportsLessThan]) -> TSupportsLessThan:
929954
'''
@@ -955,6 +980,26 @@ class Enumerable(Sequence[TSource_co], Generic[TSource_co]):
955980
resulting values. Returns the default one if there is no value.
956981
'''
957982

983+
@overload
984+
def min_by(self, key_selector: Callable[[TSource_co], TSupportsLessThan]) -> TSource_co:
985+
'''
986+
Returns the minimal element of the sequence based on the given key selector. Raises
987+
`InvalidOperationError` if there is no value.
988+
'''
989+
990+
@overload
991+
def min_by(self,
992+
key_selector: Callable[[TSource_co], TKey],
993+
__comparer: Callable[[TKey, TKey], int],
994+
) -> TSource_co:
995+
'''
996+
Returns the minimal element of the sequence based on the given key selector and the comparer.
997+
Raises `InvalidOperationError` if there is no value.
998+
999+
Such comparer takes two values and return positive ints when lhs > rhs, negative ints
1000+
if lhs < rhs, and 0 if they are equal.
1001+
'''
1002+
9581003
def of_type(self, t_result: Type[TResult]) -> Enumerable[TResult]:
9591004
'''
9601005
Filters elements based on the specified type.

0 commit comments

Comments
 (0)