Skip to content

Commit f1fb37c

Browse files
committed
Add the only(f, x) method, which allows users to provide a default value or customize the error message
1 parent 1e64682 commit f1fb37c

File tree

2 files changed

+184
-4
lines changed

2 files changed

+184
-4
lines changed

base/iterators.jl

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,19 +1413,68 @@ Stacktrace:
14131413
return ret
14141414
end
14151415

1416-
# Collections of known size
1416+
"""
1417+
only(f, x)
1418+
1419+
Return the one and only element of collection `x`, or return `f()` if the collection has zero
1420+
or multiple elements. `f()` will not be called if the collection has exactly one element.
1421+
1422+
!!! compat "Julia 1.9"
1423+
This method requires at least Julia 1.9.
1424+
1425+
# Examples
1426+
```jldoctest
1427+
julia> only(('a', 'b'))
1428+
ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element
1429+
Stacktrace:
1430+
[...]
1431+
1432+
julia> only(('a', 'b')) do
1433+
'c'
1434+
end
1435+
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
1436+
1437+
julia> only(('a', 'b')) do
1438+
throw(ErrorException("My custom error message"))
1439+
end
1440+
ERROR: My custom error message
1441+
```
1442+
"""
1443+
function only(f::F, x) where {F}
1444+
i = iterate(x)
1445+
if i === nothing
1446+
return f()
1447+
end
1448+
(ret, state) = i::NTuple{2,Any}
1449+
if iterate(x, state) !== nothing
1450+
return f()
1451+
end
1452+
return ret
1453+
end
1454+
1455+
# Collections of known size that have exactly one element
14171456
only(x::Ref) = x[]
14181457
only(x::Number) = x
14191458
only(x::Char) = x
14201459
only(x::Tuple{Any}) = x[1]
1460+
only(a::AbstractArray{<:Any, 0}) = @inbounds return a[]
1461+
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x)
1462+
only(f::F, x::Ref) where {F} = only(x)
1463+
only(f::F, x::Number) where {F} = only(x)
1464+
only(f::F, x::Char) where {F} = only(x)
1465+
only(f::F, x::Tuple{Any}) where {F} = only(x)
1466+
only(f::F, a::AbstractArray{<:Any, 0}) where {F} = only(a)
1467+
only(f::F, x::NamedTuple{<:Any, <:Tuple{Any}}) where {F} = only(x)
1468+
1469+
# Collections of known size that either have zero elements or more than one element
14211470
only(x::Tuple) = throw(
14221471
ArgumentError("Tuple contains $(length(x)) elements, must contain exactly 1 element")
14231472
)
1424-
only(a::AbstractArray{<:Any, 0}) = @inbounds return a[]
1425-
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x)
14261473
only(x::NamedTuple) = throw(
14271474
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
14281475
)
1476+
only(f::F, x::Tuple) where {F} = f()
1477+
only(f::F, x::NamedTuple) where {F} = f()
14291478

14301479

14311480
Base.intersect(a::ProductIterator, b::ProductIterator) = ProductIterator(intersect.(a.iterators, b.iterators))

test/iterators.jl

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ end
832832
@test length(collect(d)) == 0
833833
end
834834

835-
@testset "only" begin
835+
@testset "only(x)" begin
836836
@test only([3]) === 3
837837
@test_throws ArgumentError only([])
838838
@test_throws ArgumentError only([3, 2])
@@ -865,6 +865,137 @@ end
865865
@test_throws ArgumentError only(1 for ii in 1:10 if ii > 200)
866866
end
867867

868+
@testset "only(f, x)" begin
869+
my_exception = ErrorException("Hello world")
870+
my_throw = () -> throw(my_exception)
871+
@test 3 === only([3]) do
872+
my_throw()
873+
end
874+
@test_throws my_exception only([]) do
875+
my_throw()
876+
end
877+
@test 1 === only([]) do
878+
1
879+
end
880+
@test_throws my_exception only([3, 2]) do
881+
my_throw()
882+
end
883+
@test 1 === only([3, 2]) do
884+
1
885+
end
886+
887+
f = () -> only((3,)) do
888+
my_throw()
889+
end
890+
@test 3 === @inferred(f())
891+
@test_throws my_exception only(()) do
892+
my_throw()
893+
end
894+
@test 1 === only(()) do
895+
1
896+
end
897+
@test_throws my_exception only((3, 2)) do
898+
my_throw()
899+
end
900+
@test 1 === only((3, 2)) do
901+
1
902+
end
903+
904+
@test (1=>3) === only(Dict(1=>3)) do
905+
my_throw()
906+
end
907+
@test_throws my_exception only(Dict{Int,Int}()) do
908+
my_throw()
909+
end
910+
@test 1 === only(Dict{Int,Int}()) do
911+
1
912+
end
913+
@test_throws my_exception only(Dict(1=>3, 2=>2)) do
914+
my_throw()
915+
end
916+
@test 1 === only(Dict(1=>3, 2=>2)) do
917+
1
918+
end
919+
920+
@test 3 === only(Set([3])) do
921+
my_throw()
922+
end
923+
@test_throws my_exception only(Set(Int[])) do
924+
my_throw()
925+
end
926+
@test 1 === only(Set(Int[])) do
927+
1
928+
end
929+
@test_throws my_exception only(Set([3,2])) do
930+
my_throw()
931+
end
932+
@test 1 === only(Set([3,2])) do
933+
1
934+
end
935+
936+
f = () -> only((;a=1)) do
937+
my_throw()
938+
end
939+
@test 1 === @inferred(f())
940+
@test_throws my_exception only(NamedTuple()) do
941+
my_throw()
942+
end
943+
@test 1 === only(NamedTuple()) do
944+
1
945+
end
946+
@test_throws my_exception only((a=3, b=2.0)) do
947+
my_throw()
948+
end
949+
@test 1 === only((a=3, b=2.0)) do
950+
1
951+
end
952+
953+
f = () -> only(1) do
954+
my_throw()
955+
end
956+
@test 1 === @inferred(f())
957+
f = () -> only('a') do
958+
my_throw()
959+
end
960+
@test 'a' === @inferred(f())
961+
f = () -> only(Ref([1, 2])) do
962+
my_throw()
963+
964+
end
965+
@test [1, 2] == @inferred(f())
966+
@test_throws my_exception only(Pair(10, 20)) do
967+
my_throw()
968+
end
969+
@test 30 === only(Pair(10, 20)) do
970+
30
971+
end
972+
973+
@test 1 === only(1 for ii in 1:1) do
974+
my_throw()
975+
end
976+
@test 1 === only(1 for ii in 1:10 if ii < 2) do
977+
my_throw()
978+
end
979+
@test_throws my_exception only(1 for ii in 1:10) do
980+
my_throw()
981+
end
982+
@test 0 === only(1 for ii in 1:10) do
983+
0
984+
end
985+
@test_throws my_exception only(1 for ii in 1:10 if ii > 2) do
986+
my_throw()
987+
end
988+
@test 0 === only(1 for ii in 1:10 if ii > 2) do
989+
0
990+
end
991+
@test_throws my_exception only(1 for ii in 1:10 if ii > 200) do
992+
my_throw()
993+
end
994+
@test 0 === only(1 for ii in 1:10 if ii > 200) do
995+
0
996+
end
997+
end
998+
868999
@testset "flatten empty tuple" begin
8691000
@test isempty(collect(Iterators.flatten(())))
8701001
end

0 commit comments

Comments
 (0)