From 8fa46c0e61f3bd47a971679181c7e1c5f8584478 Mon Sep 17 00:00:00 2001 From: David Hanak Date: Mon, 6 May 2024 07:35:14 +0200 Subject: [PATCH] Add `chopprefix`, `chopsuffix` (#825) --- Project.toml | 2 +- README.md | 7 +++- src/Compat.jl | 101 +++++++++++++++++++++++++++++++++++++++++++++-- test/runtests.jl | 62 +++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index fd1d17c61..7b1fb1673 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Compat" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.14.0" +version = "4.15.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/README.md b/README.md index b935b68f9..bd385e38c 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,12 @@ changes in `julia`. ## Supported features -* `allequal(f, itr)` and `allunique(f, itr)` methods. ([#47679]) (since Compat 4.13.0) +* `chopprefix(s, prefix)` and `chopsuffix(s, suffix)` ([#40995]) (since Compat 4.15.0) * `logrange(lo, hi; length)` is like `range` but with a constant ratio, not difference. ([#39071]) (since Compat 4.14.0) Note that on Julia 1.8 and earlier, the version from Compat has slightly lower floating-point accuracy than the one in Base (Julia 1.11 and later). +* `allequal(f, itr)` and `allunique(f, itr)` methods. ([#47679]) (since Compat 4.13.0) + * `Iterators.cycle(itr, n)` is the lazy version of `repeat(vector, n)`. ([#47354]) (since Compat 4.13.0) * `@compat public foo, bar` marks `foo` and `bar` as public in Julia 1.11+ and is a no-op in Julia 1.10 and earlier. ([#50105]) (since Compat 3.47.0, 4.10.0) @@ -171,6 +173,7 @@ Note that you should specify the correct minimum version for `Compat` in the [#39794]: https://github.com/JuliaLang/julia/issues/39794 [#40729]: https://github.com/JuliaLang/julia/issues/40729 [#40803]: https://github.com/JuliaLang/julia/issues/40803 +[#40995]: https://github.com/JuliaLang/julia/pull/40995 [#41007]: https://github.com/JuliaLang/julia/issues/41007 [#41032]: https://github.com/JuliaLang/julia/issues/41032 [#41076]: https://github.com/JuliaLang/julia/issues/41076 @@ -184,6 +187,6 @@ Note that you should specify the correct minimum version for `Compat` in the [#45052]: https://github.com/JuliaLang/julia/issues/45052 [#45607]: https://github.com/JuliaLang/julia/issues/45607 [#47354]: https://github.com/JuliaLang/julia/issues/47354 +[#47679]: https://github.com/JuliaLang/julia/pull/47679 [#48038]: https://github.com/JuliaLang/julia/issues/48038 [#50105]: https://github.com/JuliaLang/julia/issues/50105 -[#47679]: https://github.com/JuliaLang/julia/pull/47679 diff --git a/src/Compat.jl b/src/Compat.jl index 8543d9160..4321523e3 100644 --- a/src/Compat.jl +++ b/src/Compat.jl @@ -784,7 +784,7 @@ if VERSION < v"1.11.0-DEV.1562" length(t) < 32 || return Base._hashed_allunique(Base.Generator(f, t)) return allunique(map(f, t)) end - + # allequal is either imported or defined above allequal(f, xs) = allequal(Base.Generator(f, xs)) function allequal(f, xs::Tuple) @@ -1001,9 +1001,9 @@ if !isdefined(Base, :logrange) # VERSION < v"1.12.0-DEV.2" or appropriate 1.11. _exp_allowing_twice64(x::Number) = exp(x) if VERSION >= v"1.9.0-DEV.318" # Julia PR #44717 allows this high-precision path: - + _exp_allowing_twice64(x::Base.TwicePrecision{Float64}) = Base.Math.exp_impl(x.hi, x.lo, Val(:ℯ)) - + function _log_twice64_unchecked(x::Float64) xu = reinterpret(UInt64, x) if xu < (UInt64(1)<<52) # x is subnormal @@ -1027,6 +1027,101 @@ else using Base: LogRange end +# https://github.com/JuliaLang/julia/pull/40995: add chopprefix, chopsuffix +if VERSION < v"1.8.0-DEV.1016" + function chopprefix(s::AbstractString, prefix::Regex) + m = match(prefix, s, firstindex(s), Base.PCRE.ANCHORED) + m === nothing && return SubString(s) + return SubString(s, ncodeunits(m.match) + 1) + end + + function chopsuffix(s::AbstractString, suffix::Regex) + m = match(suffix, s, firstindex(s), Base.PCRE.ENDANCHORED) + m === nothing && return SubString(s) + isempty(m.match) && return SubString(s) + return SubString(s, firstindex(s), prevind(s, m.offset)) + end + + """ + chopprefix(s::AbstractString, prefix::Union{AbstractString,Regex}) -> SubString + + Remove the prefix `prefix` from `s`. If `s` does not start with `prefix`, a string equal to `s` is returned. + + See also [`chopsuffix`](@ref). + + # Examples + ```jldoctest + julia> chopprefix("Hamburger", "Ham") + "burger" + + julia> chopprefix("Hamburger", "hotdog") + "Hamburger" + ``` + """ + function chopprefix(s::AbstractString, prefix::AbstractString) + k = firstindex(s) + i, j = iterate(s), iterate(prefix) + while true + j === nothing && i === nothing && return SubString(s, 1, 0) # s == prefix: empty result + j === nothing && return @inbounds SubString(s, k) # ran out of prefix: success! + i === nothing && return SubString(s) # ran out of source: failure + i[1] == j[1] || return SubString(s) # mismatch: failure + k = i[2] + i, j = iterate(s, k), iterate(prefix, j[2]) + end + end + + function chopprefix(s::Union{String, SubString{String}}, + prefix::Union{String, SubString{String}}) + if startswith(s, prefix) + SubString(s, 1 + ncodeunits(prefix)) + else + SubString(s) + end + end + + """ + chopsuffix(s::AbstractString, suffix::Union{AbstractString,Regex}) -> SubString + + Remove the suffix `suffix` from `s`. If `s` does not end with `suffix`, a string equal to `s` is returned. + + See also [`chopprefix`](@ref). + + # Examples + ```jldoctest + julia> chopsuffix("Hamburger", "er") + "Hamburg" + julia> chopsuffix("Hamburger", "hotdog") + "Hamburger" + ``` + """ + function chopsuffix(s::AbstractString, suffix::AbstractString) + a, b = Iterators.Reverse(s), Iterators.Reverse(suffix) + k = lastindex(s) + i, j = iterate(a), iterate(b) + while true + j === nothing && i === nothing && return SubString(s, 1, 0) # s == suffix: empty result + j === nothing && return @inbounds SubString(s, firstindex(s), k) # ran out of suffix: success! + i === nothing && return SubString(s) # ran out of source: failure + i[1] == j[1] || return SubString(s) # mismatch: failure + k = i[2] + i, j = iterate(a, k), iterate(b, j[2]) + end + end + + function chopsuffix(s::Union{String, SubString{String}}, + suffix::Union{String, SubString{String}}) + if !isempty(suffix) && endswith(s, suffix) + astart = ncodeunits(s) - ncodeunits(suffix) + 1 + @inbounds SubString(s, firstindex(s), prevind(s, astart)) + else + SubString(s) + end + end + + export chopprefix, chopsuffix +end + include("deprecated.jl") end # module Compat diff --git a/test/runtests.jl b/test/runtests.jl index f036601fb..718014df7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -845,3 +845,65 @@ end @test_skip repr("text/plain", Compat.LogRange(1,2,3)) == "3-element Compat.LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" @test_skip repr("text/plain", Compat.LogRange(1,2,0)) == "LogRange{Float64}(1.0, 2.0, 0)" # empty case end + +# https://github.com/JuliaLang/julia/pull/40995 +@testset "chopprefix, chopsuffix" begin + SubStr(s) = SubString("abc$(s)de", firstindex(s) + 3, lastindex(s) + 3) + + for S in (String, SubStr, Test.GenericString) + for T in (String, SubStr, Test.GenericString, Regex) + S === Test.GenericString && T === Regex && continue # not supported + @test chopprefix(S("fo∀\n"), T("bog")) == "fo∀\n" + @test chopprefix(S("fo∀\n"), T("\n∀foΔ")) == "fo∀\n" + @test chopprefix(S("fo∀\n"), T("∀foΔ")) == "fo∀\n" + @test chopprefix(S("fo∀\n"), T("f")) == "o∀\n" + @test chopprefix(S("fo∀\n"), T("fo")) == "∀\n" + @test chopprefix(S("fo∀\n"), T("fo∀")) == "\n" + @test chopprefix(S("fo∀\n"), T("fo∀\n")) == "" + @test chopprefix(S("\nfo∀"), T("bog")) == "\nfo∀" + @test chopprefix(S("\nfo∀"), T("\n∀foΔ")) == "\nfo∀" + @test chopprefix(S("\nfo∀"), T("\nfo∀")) == "" + @test chopprefix(S("\nfo∀"), T("\n")) == "fo∀" + @test chopprefix(S("\nfo∀"), T("\nf")) == "o∀" + @test chopprefix(S("\nfo∀"), T("\nfo")) == "∀" + @test chopprefix(S("\nfo∀"), T("\nfo∀")) == "" + @test chopprefix(S(""), T("")) == "" + @test chopprefix(S(""), T("asdf")) == "" + @test chopprefix(S(""), T("∃∃∃")) == "" + @test chopprefix(S("εfoo"), T("ε")) == "foo" + @test chopprefix(S("ofoε"), T("o")) == "foε" + @test chopprefix(S("∃∃∃∃"), T("∃")) == "∃∃∃" + @test chopprefix(S("∃∃∃∃"), T("")) == "∃∃∃∃" + + @test chopsuffix(S("fo∀\n"), T("bog")) == "fo∀\n" + @test chopsuffix(S("fo∀\n"), T("\n∀foΔ")) == "fo∀\n" + @test chopsuffix(S("fo∀\n"), T("∀foΔ")) == "fo∀\n" + @test chopsuffix(S("fo∀\n"), T("\n")) == "fo∀" + @test chopsuffix(S("fo∀\n"), T("∀\n")) == "fo" + @test chopsuffix(S("fo∀\n"), T("o∀\n")) == "f" + @test chopsuffix(S("fo∀\n"), T("fo∀\n")) == "" + @test chopsuffix(S("\nfo∀"), T("bog")) == "\nfo∀" + @test chopsuffix(S("\nfo∀"), T("\n∀foΔ")) == "\nfo∀" + @test chopsuffix(S("\nfo∀"), T("\nfo∀")) == "" + @test chopsuffix(S("\nfo∀"), T("∀")) == "\nfo" + @test chopsuffix(S("\nfo∀"), T("o∀")) == "\nf" + @test chopsuffix(S("\nfo∀"), T("fo∀")) == "\n" + @test chopsuffix(S("\nfo∀"), T("\nfo∀")) == "" + @test chopsuffix(S(""), T("")) == "" + @test chopsuffix(S(""), T("asdf")) == "" + @test chopsuffix(S(""), T("∃∃∃")) == "" + @test chopsuffix(S("fooε"), T("ε")) == "foo" + @test chopsuffix(S("εofo"), T("o")) == "εof" + @test chopsuffix(S("∃∃∃∃"), T("∃")) == "∃∃∃" + @test chopsuffix(S("∃∃∃∃"), T("")) == "∃∃∃∃" + end + + if S !== Test.GenericString + @test chopprefix(S("∃∃∃b∃"), r"∃+") == "b∃" + @test chopsuffix(S("∃b∃∃∃"), r"∃+") == "∃b" + end + + @test isa(chopprefix(S("foo"), "fo"), SubString) + @test isa(chopsuffix(S("foo"), "oo"), SubString) + end +end