diff --git a/base/strings/util.jl b/base/strings/util.jl index bd4da03ce15715..a1fa41c75570b9 100644 --- a/base/strings/util.jl +++ b/base/strings/util.jl @@ -1023,3 +1023,38 @@ function Base.rest(s::AbstractString, st...) end return String(take!(io)) end + +""" + StringPairs{T}(x::AbstractString) + +This internal type is an iterator over (key => value) pairs of strings. +""" +struct StringPairs{T <: AbstractString} + x::T +end + +StringPairs(x) = StringPairs{typeof(x)}(x) +IteratorSize(::Type{StringPairs{T}}) where T = IteratorSize(T) +length(x::StringPairs) = length(x.x) +pairs(x::AbstractString) = StringPairs(x) + +# Generic fallback +function iterate(x::StringPairs, i=firstindex(x.x)) + i > ncodeunits(x.x) && return nothing + (i => x.x[i], nextind(x.x, i)) +end + +# In this method, exploit that string iteration's state is the index +function iterate( + x::StringPairs{<:Union{String, SubString{String}}}, + state::Int=firstindex(x.x) +) + (char, i) = @something iterate(x.x, state) return nothing + (state => char, i) +end + +# At this moment, Reverse{<:AbstractString} is inefficient, so this simple +# implementation is not easily optimised +function iterate(x::Iterators.Reverse{<:StringPairs}, i=lastindex(x.itr.x)) + i < firstindex(x.itr.x) ? nothing : (i => x.itr.x[i], prevind(x.itr.x, i)) +end diff --git a/test/strings/util.jl b/test/strings/util.jl index 0ad958eebb7f9d..390db2e9589985 100644 --- a/test/strings/util.jl +++ b/test/strings/util.jl @@ -702,3 +702,14 @@ end @test endswith(A, split(B, ' ')[end]) @test endswith(A, 'g') end + +@testset "pairs" begin + for s in ["", "a", "abcde", "γ", "∋γa"] + for T in (String, SubString, GenericString) + sT = T(s) + @test collect(pairs(sT)) == [k=>v for (k,v) in zip(keys(sT), sT)] + rv = Iterators.reverse(pairs(sT)) + @test collect(rv) == reverse([k=>v for (k,v) in zip(keys(sT), sT)]) + end + end +end