Skip to content

Commit ed3aefe

Browse files
nickrobinson251Andy Ferrisoxinabox
authored andcommitted
Add only function (#33129)
The function `only(x)` returns the one-and-only element of a collection `x`, or else throws an error. Co-Authored-By: Andy Ferris <[email protected]> Co-Authored-By: Nick Robinson <[email protected]> Co-Authored-By: Lyndon White <[email protected]>
1 parent 3f5d56a commit ed3aefe

File tree

6 files changed

+76
-5
lines changed

6 files changed

+76
-5
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ New library functions
2828
* 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]).
2929
* 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]).
3030
* 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]).
31+
* 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])
32+
3133

3234
Standard library changes
3335
------------------------

base/Base.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ include("ntuple.jl")
138138
include("abstractdict.jl")
139139

140140
include("iterators.jl")
141-
using .Iterators: zip, enumerate
141+
using .Iterators: zip, enumerate, only
142142
using .Iterators: Flatten, Filter, product # for generators
143143

144144
include("namedtuple.jl")

base/exports.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ export
628628

629629
enumerate, # re-exported from Iterators
630630
zip,
631+
only,
631632

632633
# object identity and equality
633634
copy,

base/iterators.jl

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ using .Base:
1212
@inline, Pair, AbstractDict, IndexLinear, IndexCartesian, IndexStyle, AbstractVector, Vector,
1313
tail, tuple_type_head, tuple_type_tail, tuple_type_cons, SizeUnknown, HasLength, HasShape,
1414
IsInfinite, EltypeUnknown, HasEltype, OneTo, @propagate_inbounds, Generator, AbstractRange,
15-
LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any
15+
LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any, @boundscheck, @inbounds
1616

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

932-
933932
struct PartitionIterator{T}
934933
c::T
935934
n::Int
@@ -1095,4 +1094,41 @@ eltype(::Type{Stateful{T, VS}} where VS) where {T} = eltype(T)
10951094
IteratorEltype(::Type{Stateful{T,VS}}) where {T,VS} = IteratorEltype(T)
10961095
length(s::Stateful) = length(s.itr) - s.taken
10971096

1097+
"""
1098+
only(x)
1099+
1100+
Returns the one and only element of collection `x`, and throws an `ArgumentError` if the
1101+
collection has zero or multiple elements.
1102+
1103+
See also: [`first`](@ref), [`last`](@ref).
1104+
1105+
!!! compat "Julia 1.4"
1106+
This method requires at least Julia 1.4.
1107+
"""
1108+
@propagate_inbounds function only(x)
1109+
i = iterate(x)
1110+
@boundscheck if i === nothing
1111+
throw(ArgumentError("Collection is empty, must contain exactly 1 element"))
1112+
end
1113+
(ret, state) = i
1114+
@boundscheck if iterate(x, state) !== nothing
1115+
throw(ArgumentError("Collection has multiple elements, must contain exactly 1 element"))
1116+
end
1117+
return ret
1118+
end
1119+
1120+
# Collections of known size
1121+
only(x::Ref) = x[]
1122+
only(x::Number) = x
1123+
only(x::Char) = x
1124+
only(x::Tuple{Any}) = x[1]
1125+
only(x::Tuple) = throw(
1126+
ArgumentError("Tuple contains $(length(x)) elements, must contain exactly 1 element")
1127+
)
1128+
only(a::AbstractArray{<:Any, 0}) = @inbounds return a[]
1129+
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x)
1130+
only(x::NamedTuple) = throw(
1131+
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
1132+
)
1133+
10981134
end

doc/src/base/iterators.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ Base.Iterators.flatten
1515
Base.Iterators.partition
1616
Base.Iterators.filter
1717
Base.Iterators.reverse
18+
Base.Iterators.only
1819
```

test/iterators.jl

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ end
183183
@test Base.IteratorEltype(repeated(0, 5)) == Base.HasEltype()
184184
@test Base.IteratorSize(zip(repeated(0), repeated(0))) == Base.IsInfinite()
185185

186-
187186
# product
188187
# -------
189188

@@ -411,7 +410,6 @@ for n in [5,6]
411410
[(1,1),(2,2),(3,3),(4,4),(5,5)]
412411
end
413412

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

@@ -647,3 +645,36 @@ end
647645
@test length(collect(d)) == 2
648646
@test length(collect(d)) == 0
649647
end
648+
649+
@testset "only" begin
650+
@test only([3]) === 3
651+
@test_throws ArgumentError only([])
652+
@test_throws ArgumentError only([3, 2])
653+
654+
@test @inferred(only((3,))) === 3
655+
@test_throws ArgumentError only(())
656+
@test_throws ArgumentError only((3, 2))
657+
658+
@test only(Dict(1=>3)) === (1=>3)
659+
@test_throws ArgumentError only(Dict{Int,Int}())
660+
@test_throws ArgumentError only(Dict(1=>3, 2=>2))
661+
662+
@test only(Set([3])) === 3
663+
@test_throws ArgumentError only(Set(Int[]))
664+
@test_throws ArgumentError only(Set([3,2]))
665+
666+
@test @inferred(only((;a=1))) === 1
667+
@test_throws ArgumentError only(NamedTuple())
668+
@test_throws ArgumentError only((a=3, b=2.0))
669+
670+
@test @inferred(only(1)) === 1
671+
@test @inferred(only('a')) === 'a'
672+
@test @inferred(only(Ref([1, 2]))) == [1, 2]
673+
@test_throws ArgumentError only(Pair(10, 20))
674+
675+
@test only(1 for ii in 1:1) === 1
676+
@test only(1 for ii in 1:10 if ii < 2) === 1
677+
@test_throws ArgumentError only(1 for ii in 1:10)
678+
@test_throws ArgumentError only(1 for ii in 1:10 if ii > 2)
679+
@test_throws ArgumentError only(1 for ii in 1:10 if ii > 200)
680+
end

0 commit comments

Comments
 (0)