Skip to content

Commit

Permalink
Add methods of first and last accepting a predicate
Browse files Browse the repository at this point in the history
On zulip, there was a thread about the fact that there's not a good
way of finding the first item matching a predicate in a non-indexable
iterable without manually writing out the for-loop. One possibility is
using the existing first function composed with Iterators.filter:

    first(Iterators.filter(>(5), 1:10))

The problem with this approach is that it requires wrapping in a try
block if the predicate not being satisfied is a
possibility (Iterators.filter returns an empty collection). There
should be a way of expressing this without using exception-handling as
control flow or writing the for-loop manually.

Ideally, I think first and last should just return
Union{Some{T},Nothing} always, but that would be a breaking
change. So I see two obvious alternatives:

1. This commit adds an optional predicate argument to first and last
   which is mapped over the collection and the first/last value
   satisfying predicate is returned.
2. Add a Iterators.first/Iterators.last which are identical to the
   versions in Base except that when successful they return Some{T}
   and when unsuccesful return Nothing
  • Loading branch information
non-Jedi committed Aug 19, 2020
1 parent 0fd5833 commit 39a39e9
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 0 deletions.
65 changes: 65 additions & 0 deletions base/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,34 @@ function first(itr)
x[1]
end

"""
first(predicate, coll)
Get the first element of `coll` satisfying `predicate` wrapped in `Some`.
If no element of `coll` satisfies `predicate`, return `nothing`.
# Examples
```jldoctest
julia> first(>(5), 1:10)
Some(6)
julia> first(isodd, 2:2:10) |> isnothing
true
julia> first(iseven, [5; 3; 4; 2; 6; 8]) |> something
4
julia> something(first(>(10), 1:10), 0)
0
```
"""
function first(predicate, itr)
for x in itr
predicate(x) && return Some(x)
end
end

"""
first(itr, n::Integer)
Expand Down Expand Up @@ -388,6 +416,43 @@ julia> last([1; 2; 3; 4])
"""
last(a) = a[end]

"""
last(predicate, coll)
Get the last element of `coll` satisfying `predicate` wrapped in `Some`.
If no element of `coll` satisfies `predicate`, return `nothing`.
# Examples
```jldoctest
julia> last(<(5), 1:10)
Some(4)
julia> last(isodd, 2:2:10) |> isnothing
true
julia> last(iseven, [5; 3; 4; 2; 6; 9]) |> something
6
julia> something(last(>(10), 1:10), 0)
0
```
"""
function last(predicate, itr)
out = nothing
for x in itr
out = ifelse(predicate(x), Some(x), out)
end
out
end

# faster version for arrays
function last(predicate, a::AbstractArray)
@inbounds for i in reverse(eachindex(a))
predicate(a[i]) && return Some(a[i])
end
end

"""
last(itr, n::Integer)
Expand Down
10 changes: 10 additions & 0 deletions test/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1141,3 +1141,13 @@ end
@test last(itr, 1) == [itr[end]]
@test_throws ArgumentError last(itr, -6)
end

@testset "first/last element satisfying predicate of $(typeof(itr))" for itr in (1:9,
collect(1:9),
reshape(1:9, (3, 3)),
ntuple(identity, 9))
@test first(>(5), itr) |> something == 6
@test last(<(5), itr) |> something == 4
@test first(>(9), itr) == nothing
@test last(>(9), itr) == nothing
end

0 comments on commit 39a39e9

Please sign in to comment.