diff --git a/expression/collections/seq.py b/expression/collections/seq.py index 8ab3695..c4e079b 100644 --- a/expression/collections/seq.py +++ b/expression/collections/seq.py @@ -341,6 +341,9 @@ def try_find_index(self, predicate: Callable[[_TSource], bool]) -> Option[int]: """Return the index of the first element matching the predicate, if any.""" return pipe(self, try_find_index(predicate)) + def try_find(self, predicate: Callable[[_TSource], bool]) -> Option[_TSource]: + """Return the first element matching the predicate, if any.""" + return pipe(self, try_find(predicate)) def try_pick(self, chooser: Callable[[_TSource], Option[_TResult]]) -> Option[_TResult]: """Return the first value produced by the chooser, if any.""" return pipe(self, try_pick(chooser)) @@ -1018,6 +1021,26 @@ def try_find_index(source: Iterable[_TSource], predicate: Callable[[_TSource], b @curry_flip(1) +def try_find(source: Iterable[_TSource], predicate: Callable[[_TSource], bool]) -> Option[_TSource]: + """Return the first element matching the predicate, if any. + + Evaluation stops as soon as the predicate returns `True`. + + Args: + source: The input sequence. + predicate: A function to test each element. + + Returns: + The first matching element wrapped in `Some`, or `Nothing` when no + element matches. + + Example: + >>> pipe([1, 2, 3], try_find(lambda value: value % 2 == 0)) + Some 2 + """ + for value in source: + if predicate(value): + return Some(value) def try_pick(source: Iterable[_TSource], chooser: Callable[[_TSource], Option[_TResult]]) -> Option[_TResult]: """Return the first value produced by the chooser, if any. @@ -1137,6 +1160,7 @@ def _zip(source2: Iterable[_TResult]) -> Iterable[tuple[_TSource, _TResult]]: "sum_by", "tail", "take", + "try_find", "take_while", "try_find_index", "try_pick", diff --git a/tests/test_seq.py b/tests/test_seq.py index 7499e57..e0ab784 100644 --- a/tests/test_seq.py +++ b/tests/test_seq.py @@ -407,6 +407,13 @@ def test_seq_try_find_index_pipe_returns_first_matching_index(): assert result == Some(2) +def test_seq_try_find_pipe_returns_first_match(): + is_even: Callable[[int], bool] = lambda value: value % 2 == 0 + result = pipe([1, 2, 4], seq.try_find(is_even)) + + assert result == Some(2) + + def test_seq_try_find_index_fluent(): source = Seq[int].of_iterable([1, 3, 4, 6]) @@ -428,6 +435,21 @@ def test_seq_try_find_index_returns_nothing_without_match(): assert pipe([1, 3], seq.try_find_index(is_even)) is Nothing +def test_seq_try_find_fluent(): + source = Seq[int].of_iterable([1, 2, 4]) + + assert source.try_find(lambda value: value % 2 == 0) == Some(2) + + +def test_seq_try_find_returns_nothing_without_match(): + empty: list[int] = [] + always_true: Callable[[int], bool] = lambda _: True + is_even: Callable[[int], bool] = lambda value: value % 2 == 0 + + assert pipe(empty, seq.try_find(always_true)) is Nothing + assert pipe([1, 3], seq.try_find(is_even)) is Nothing + + def test_seq_try_find_index_stops_after_first_match(): consumed: list[int] = [] is_even: Callable[[int], bool] = lambda value: value % 2 == 0 @@ -451,6 +473,27 @@ def predicate(_: int) -> bool: pipe([1], seq.try_find_index(predicate)) +def test_seq_try_find_can_match_none(): + source: list[int | None] = [1, None, 2] + is_none: Callable[[int | None], bool] = lambda value: value is None + result = pipe(source, seq.try_find(is_none)) + +def test_seq_try_find_stops_after_first_match(): + consumed: list[int] = [] + is_two: Callable[[int], bool] = lambda value: value == 2 + + +def test_seq_try_find_propagates_predicate_exceptions(): + def predicate(_: int) -> bool: + raise ValueError("predicate failed") + + with pytest.raises(ValueError, match="predicate failed"): + pipe([1], seq.try_find(predicate)) + result = pipe(source(), seq.try_pick(choose_two)) + + assert result == Some(20) + assert consumed == [1, 2] + def test_seq_try_pick_pipe_returns_first_transformed_value(): choose_even: Callable[[int], Option[str]] = lambda value: Some(str(value)) if value % 2 == 0 else Nothing result = pipe([1, 2, 4], seq.try_pick(choose_even)) @@ -489,12 +532,11 @@ def source() -> Iterable[int]: consumed.append(value) yield value - result = pipe(source(), seq.try_pick(choose_two)) + result = pipe(source(), seq.try_find(is_two)) - assert result == Some(20) + assert result == Some(2) assert consumed == [1, 2] - def test_seq_try_pick_propagates_chooser_exceptions(): def chooser(_: int) -> Option[str]: raise ValueError("chooser failed")