diff --git a/NEWS.md b/NEWS.md index 67de538932be1..2406a3ece88d0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ------------------------ diff --git a/base/Base.jl b/base/Base.jl index f64c061f9e9fb..9a19e02672a17 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -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") diff --git a/base/exports.jl b/base/exports.jl index ebd746392558a..49063720c14c4 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -628,6 +628,7 @@ export enumerate, # re-exported from Iterators zip, + only, # object identity and equality copy, diff --git a/base/iterators.jl b/base/iterators.jl index bebe8ac1701f1..829f0497c30f0 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -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, @@ -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 @@ -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. + +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[] +only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x) +only(x::NamedTuple) = throw( + ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") +) + end diff --git a/doc/src/base/iterators.md b/doc/src/base/iterators.md index 7f89b792117cc..0b63561ebdc32 100644 --- a/doc/src/base/iterators.md +++ b/doc/src/base/iterators.md @@ -15,4 +15,5 @@ Base.Iterators.flatten Base.Iterators.partition Base.Iterators.filter Base.Iterators.reverse +Base.Iterators.only ``` diff --git a/test/iterators.jl b/test/iterators.jl index 352a0ec8404d6..eabe5e0905b98 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -183,7 +183,6 @@ end @test Base.IteratorEltype(repeated(0, 5)) == Base.HasEltype() @test Base.IteratorSize(zip(repeated(0), repeated(0))) == Base.IsInfinite() - # product # ------- @@ -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!" @@ -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