Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add only function #33129

Merged
merged 13 commits into from
Sep 15, 2019
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ New library functions
* The `tempname` function now takes an optional `parent::AbstractString` argument to give it a directory in which to attempt to produce a temporary path name ([#33090]).
* The `tempname` function now takes a `cleanup::Bool` keyword argument defaulting to `true`, which causes the process to try to ensure that any file or directory at the path returned by `tempname` is deleted upon process exit ([#33090]).
* The `readdir` function now takes a `join::Bool` keyword argument defaulting to `false`, which when set causes `readdir` to join its directory argument with each listed name ([#33113]).
* The new `only(x)` function returns the one-and-only element of a collection `x`, and throws an `ArgumentError` if `x` contains zero or multiple elements. ([#33129])


Standard library changes
------------------------
Expand Down
2 changes: 1 addition & 1 deletion base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ include("ntuple.jl")
include("abstractdict.jl")

include("iterators.jl")
using .Iterators: zip, enumerate
using .Iterators: zip, enumerate, only
using .Iterators: Flatten, Filter, product # for generators

include("namedtuple.jl")
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ export

enumerate, # re-exported from Iterators
zip,
only,

# object identity and equality
copy,
Expand Down
40 changes: 38 additions & 2 deletions base/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ using .Base:
@inline, Pair, AbstractDict, IndexLinear, IndexCartesian, IndexStyle, AbstractVector, Vector,
tail, tuple_type_head, tuple_type_tail, tuple_type_cons, SizeUnknown, HasLength, HasShape,
IsInfinite, EltypeUnknown, HasEltype, OneTo, @propagate_inbounds, Generator, AbstractRange,
LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any
LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any, @boundscheck, @inbounds

import .Base:
first, last,
Expand Down Expand Up @@ -929,7 +929,6 @@ julia> collect(Iterators.partition([1,2,3,4,5], 2))
"""
partition(c::T, n::Integer) where {T} = PartitionIterator{T}(c, Int(n))


struct PartitionIterator{T}
c::T
n::Int
Expand Down Expand Up @@ -1095,4 +1094,41 @@ eltype(::Type{Stateful{T, VS}} where VS) where {T} = eltype(T)
IteratorEltype(::Type{Stateful{T,VS}}) where {T,VS} = IteratorEltype(T)
length(s::Stateful) = length(s.itr) - s.taken

"""
only(x)

Returns the one and only element of collection `x`, and throws an `ArgumentError` if the
collection has zero or multiple elements.
nickrobinson251 marked this conversation as resolved.
Show resolved Hide resolved
nickrobinson251 marked this conversation as resolved.
Show resolved Hide resolved

See also: [`first`](@ref), [`last`](@ref).

!!! compat "Julia 1.4"
This method requires at least Julia 1.4.
"""
@propagate_inbounds function only(x)
i = iterate(x)
@boundscheck if i === nothing
throw(ArgumentError("Collection is empty, must contain exactly 1 element"))
end
(ret, state) = i
@boundscheck if iterate(x, state) !== nothing
throw(ArgumentError("Collection has multiple elements, must contain exactly 1 element"))
end
return ret
end

# Collections of known size
only(x::Ref) = x[]
only(x::Number) = x
only(x::Char) = x
only(x::Tuple{Any}) = x[1]
only(x::Tuple) = throw(
ArgumentError("Tuple contains $(length(x)) elements, must contain exactly 1 element")
)
only(a::AbstractArray{<:Any, 0}) = @inbounds return a[]
nickrobinson251 marked this conversation as resolved.
Show resolved Hide resolved
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x)
only(x::NamedTuple) = throw(
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
)

end
1 change: 1 addition & 0 deletions doc/src/base/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ Base.Iterators.flatten
Base.Iterators.partition
Base.Iterators.filter
Base.Iterators.reverse
Base.Iterators.only
```
35 changes: 33 additions & 2 deletions test/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ end
@test Base.IteratorEltype(repeated(0, 5)) == Base.HasEltype()
@test Base.IteratorSize(zip(repeated(0), repeated(0))) == Base.IsInfinite()


# product
# -------

Expand Down Expand Up @@ -411,7 +410,6 @@ for n in [5,6]
[(1,1),(2,2),(3,3),(4,4),(5,5)]
end


@test join(map(x->string(x...), partition("Hello World!", 5)), "|") ==
"Hello| Worl|d!"

Expand Down Expand Up @@ -647,3 +645,36 @@ end
@test length(collect(d)) == 2
@test length(collect(d)) == 0
end

@testset "only" begin
@test only([3]) === 3
@test_throws ArgumentError only([])
@test_throws ArgumentError only([3, 2])

@test @inferred(only((3,))) === 3
@test_throws ArgumentError only(())
@test_throws ArgumentError only((3, 2))

@test only(Dict(1=>3)) === (1=>3)
@test_throws ArgumentError only(Dict{Int,Int}())
@test_throws ArgumentError only(Dict(1=>3, 2=>2))

@test only(Set([3])) === 3
@test_throws ArgumentError only(Set(Int[]))
@test_throws ArgumentError only(Set([3,2]))

@test @inferred(only((;a=1))) === 1
@test_throws ArgumentError only(NamedTuple())
@test_throws ArgumentError only((a=3, b=2.0))

@test @inferred(only(1)) === 1
@test @inferred(only('a')) === 'a'
@test @inferred(only(Ref([1, 2]))) == [1, 2]
@test_throws ArgumentError only(Pair(10, 20))

@test only(1 for ii in 1:1) === 1
@test only(1 for ii in 1:10 if ii < 2) === 1
@test_throws ArgumentError only(1 for ii in 1:10)
@test_throws ArgumentError only(1 for ii in 1:10 if ii > 2)
@test_throws ArgumentError only(1 for ii in 1:10 if ii > 200)
end