Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions expression/collections/seq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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",
Expand Down
48 changes: 45 additions & 3 deletions tests/test_seq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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")
Expand Down
Loading