diff --git a/README.md b/README.md index 2751b9d14..3eaa4a352 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,8 @@ Currently, the `@compat` macro supports the following syntaxes: ## New functions, macros, and methods +* `only(x)` returns the one-and-only element of a collection `x`. ([#33129]) + * `eachrow`, `eachcol`, and `eachslice` to iterate over first, second, or given dimension of an array ([#29749]). diff --git a/src/Compat.jl b/src/Compat.jl index 1be0250ec..ef5dface7 100644 --- a/src/Compat.jl +++ b/src/Compat.jl @@ -1866,6 +1866,37 @@ if v"0.7.0" <= VERSION < v"1.1.0-DEV.594" Base.merge(a::NamedTuple) = a end +# https://github.com/JuliaLang/julia/pull/33129 +if v"0.7.0" <= VERSION < v"1.4.0-DEV.142" + export only + + Base.@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 + include("deprecated.jl") end # module Compat diff --git a/test/runtests.jl b/test/runtests.jl index b6a873768..2945cab01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -38,7 +38,7 @@ if VERSION >= v"0.7" @test collect(eachrow(M)) == collect(eachslice(M, dims = 1)) == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] @test collect(eachcol(M)) == collect(eachslice(M, dims = 2)) == [[1, 4, 7], [2, 5, 8], [3, 6, 9]] @test_throws DimensionMismatch eachslice(M, dims = 4) - + # Higher-dimensional case M = reshape([(1:16)...], 2, 2, 2, 2) @test_throws MethodError collect(eachrow(M)) @@ -1499,4 +1499,39 @@ end @test merge((a=1,), (b=2,), (c=3,)) == (a=1,b=2,c=3) end +@static if VERSION >= v"0.7.0" + @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 +end + nothing