Skip to content

Commit

Permalink
Add only function (#33129)
Browse files Browse the repository at this point in the history
The function `only(x)` returns the one-and-only element of a collection
`x`, or else throws an error.

Co-Authored-By: Andy Ferris <andy.ferris@roames.com.au>
Co-Authored-By: Nick Robinson <npr251@gmail.com>
Co-Authored-By: Lyndon White <oxinabox@ucc.asn.au>
  • Loading branch information
3 people authored and c42f committed Sep 15, 2019
1 parent 3f5d56a commit ed3aefe
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 5 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------
Expand Down
2 changes: 1 addition & 1 deletion base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ export

enumerate, # re-exported from Iterators
zip,
only,

# object identity and equality
copy,
Expand Down
40 changes: 38 additions & 2 deletions base/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions doc/src/base/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ Base.Iterators.flatten
Base.Iterators.partition
Base.Iterators.filter
Base.Iterators.reverse
Base.Iterators.only
```
35 changes: 33 additions & 2 deletions test/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ end
@test Base.IteratorEltype(repeated(0, 5)) == Base.HasEltype()
@test Base.IteratorSize(zip(repeated(0), repeated(0))) == Base.IsInfinite()


# product
# -------

Expand Down Expand Up @@ -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!"

Expand Down Expand Up @@ -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

2 comments on commit ed3aefe

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily benchmark build, I will reply here when finished:

@nanosoldier runbenchmarks(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan

Please sign in to comment.