Skip to content

Commit

Permalink
add replace(io, str, patterns...)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevengj committed Feb 10, 2023
1 parent ce292c1 commit 246f2d4
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 11 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ New library features
* The `initialized=true` keyword assignment for `sortperm!` and `partialsortperm!`
is now a no-op ([#47979]). It previously exposed unsafe behavior ([#47977]).
* `binomial(x, k)` now supports non-integer `x` ([#48124]).
* `replace(string, pattern...)` now supports an optional `IO` argument to
write the output to a stream rather than returning a string ([#48625]).
* A `CartesianIndex` is now treated as a "scalar" for broadcasting ([#47044]).

Standard library changes
Expand Down
65 changes: 55 additions & 10 deletions base/strings/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,8 @@ _free_pat_replacer(x) = nothing
_pat_replacer(x::AbstractChar) = isequal(x)
_pat_replacer(x::Union{Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}) = in(x)

function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(Int)) where N
count == 0 && return str
# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_init(str, pat_repl::NTuple{N, Pair}, count::Int) where N
count < 0 && throw(DomainError(count, "`count` must be non-negative."))
n = 1
e1 = nextind(str, lastindex(str)) # sizeof(str)
Expand All @@ -697,11 +697,12 @@ function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(I
r isa Int && (r = r:r) # findnext / performance fix
return r
end
if all(>(e1), map(first, rs))
foreach(_free_pat_replacer, patterns)
return str
end
out = IOBuffer(sizehint=floor(Int, 1.2sizeof(str)))
return patterns, replaces, rs, all(>(e1), map(first, rs))
end

# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_finish(out::IO, str, count::Int,
patterns::NTuple{N}, replaces::NTuple{N}, rs::NTuple{N}) where N
while true
p = argmin(map(first, rs)) # TODO: or argmin(rs), to pick the shortest first match ?
r = rs[p]
Expand Down Expand Up @@ -737,12 +738,38 @@ function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(I
end
foreach(_free_pat_replacer, patterns)
write(out, SubString(str, i))
return String(take!(out))
return out
end

# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_io(out::IO, retval, str, pat_repl::Pair...; count::Integer=typemax(Int))
if count == 0
write(out, str)
return out
end
patterns, replaces, rs, notfound = _replace_init(str, pat_repl, count)
if notfound
foreach(_free_pat_replacer, patterns)
write(out, str)
return out
end
return _replace_finish(out, str, count, patterns, replaces, rs)
end

# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_str(str, pat_repl::Pair...; count::Integer=typemax(Int))
count == 0 && return str
patterns, replaces, rs, notfound = _replace_init(str, pat_repl, count)
if notfound
foreach(_free_pat_replacer, patterns)
return str
end
out = IOBuffer(sizehint=floor(Int, 1.2sizeof(str)))
return String(take!(_replace_finish(out, str, count, patterns, replaces, rs)))
end

"""
replace(s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer])
replace([out::IO], s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer])
Search for the given pattern `pat` in `s`, and replace each occurrence with `r`.
If `count` is provided, replace at most `count` occurrences.
Expand All @@ -755,13 +782,21 @@ If `pat` is a regular expression and `r` is a [`SubstitutionString`](@ref), then
references in `r` are replaced with the corresponding matched text.
To remove instances of `pat` from `string`, set `r` to the empty `String` (`""`).
The return value is a new string after the replacements. If the `out::IO` argument
is supplied, the transformed string is instead written to `out` (returning `out`).
(For example, this can be used in conjunction with an [`IOBuffer`](@ref) to re-use
a pre-allocated buffer array in-place.)
Multiple patterns can be specified, and they will be applied left-to-right
simultaneously, so only one pattern will be applied to any character, and the
patterns will only be applied to the input text, not the replacements.
!!! compat "Julia 1.7"
Support for multiple patterns requires version 1.7.
!!! compat "Julia 1.10"
The `out::IO` argument requires version 1.10.
# Examples
```jldoctest
julia> replace("Python is a programming language.", "Python" => "Julia")
Expand All @@ -780,8 +815,18 @@ julia> replace("abcabc", "a" => "b", "b" => "c", r".+" => "a")
"bca"
```
"""
replace(out::IO, s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
_replace_io(out, String(s), pat_f..., count=count)

replace(s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
replace(String(s), pat_f..., count=count)
_replace_str(String(s), pat_f..., count=count)

# no copy needed for SubString{String}
replace(out::IO, s::SubString{String}, pat_f::Pair...; count=typemax(Int)) =
_replace_io(out, s, pat_f..., count=count)
replace(s::SubString{String}, pat_f::Pair...; count=typemax(Int)) =
_replace_str(s, pat_f..., count=count)


# TODO: allow transform as the first argument to replace?

Expand Down
2 changes: 1 addition & 1 deletion doc/src/base/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Base.findlast(::AbstractChar, ::AbstractString)
Base.findprev(::AbstractString, ::AbstractString, ::Integer)
Base.occursin
Base.reverse(::Union{String,SubString{String}})
Base.replace(s::AbstractString, ::Pair...)
Base.replace(::IO, s::AbstractString, ::Pair...)
Base.eachsplit
Base.split
Base.rsplit
Expand Down

0 comments on commit 246f2d4

Please sign in to comment.