diff --git a/README.md b/README.md index 90fcabec2..316863687 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]) + * `mod` now accepts a unit range as the second argument ([#32628]). * `eachrow`, `eachcol`, and `eachslice` to iterate over first, second, or given dimension @@ -576,3 +578,5 @@ includes this fix. Find the minimum version from there. [#29259]: https://github.com/JuliaLang/julia/issues/29259 [#29674]: https://github.com/JuliaLang/julia/issues/29674 [#29749]: https://github.com/JuliaLang/julia/issues/29749 +[#33129]: https://github.com/JuliaLang/julia/issues/33129 +[#32628]: https://github.com/JuliaLang/julia/issues/32628 diff --git a/src/Compat.jl b/src/Compat.jl index 8fa251337..f0d99c7f9 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 + # https://github.com/JuliaLang/julia/pull/32628 if v"0.7.0" <= VERSION < v"1.3.0-alpha.8" Base.mod(i::Integer, r::Base.OneTo) = mod1(i, last(r)) diff --git a/test/runtests.jl b/test/runtests.jl index a4ab5412c..0765091e1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1499,6 +1499,43 @@ 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' + if VERSION >= v"1.0" + @test @inferred(only(Ref([1, 2]))) == [1, 2] # Fails on v0.7, depwarn "`Ref(x::AbstractArray)` is deprecated, use `Ref(x, 1)` instead." + end + @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 + # https://github.com/JuliaLang/julia/pull/32628 if VERSION >= v"0.7" @testset "mod with ranges" begin