Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add replace(io, str, patterns...) #48625

Merged
merged 9 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ New library functions

New library features
--------------------
* `replace(string, pattern...)` now supports an optional `IO` argument to
write the output to a stream rather than returning a string ([#48625]).

Standard library changes
------------------------
Expand Down
72 changes: 56 additions & 16 deletions base/strings/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -700,12 +700,11 @@ _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)
i = a = firstindex(str)
e1 = nextind(str, lastindex(str)) # sizeof(str)+1
a = firstindex(str)
patterns = map(p -> _pat_replacer(first(p)), pat_repl)
replaces = map(last, pat_repl)
rs = map(patterns) do p
Expand All @@ -716,21 +715,24 @@ 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 e1, 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(io::IO, str, count::Int,
e1::Int, patterns::Tuple, replaces::Tuple, rs::Tuple)
n = 1
i = a = firstindex(str)
while true
p = argmin(map(first, rs)) # TODO: or argmin(rs), to pick the shortest first match ?
r = rs[p]
j, k = first(r), last(r)
j > e1 && break
if i == a || i <= k
# copy out preserved portion
GC.@preserve str unsafe_write(out, pointer(str, i), UInt(j-i))
GC.@preserve str unsafe_write(io, pointer(str, i), UInt(j-i))
# copy out replacement string
_replace(out, replaces[p], str, r, patterns[p])
_replace(io, replaces[p], str, r, patterns[p])
end
if k < j
i = j
Expand All @@ -755,13 +757,39 @@ function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(I
n += 1
end
foreach(_free_pat_replacer, patterns)
write(out, SubString(str, i))
return String(take!(out))
write(io, SubString(str, i))
return io
end

# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_(io::IO, str, pat_repl::NTuple{N, Pair}, count::Int) where N
if count == 0
write(io, str)
return io
end
e1, patterns, replaces, rs, notfound = _replace_init(str, pat_repl, count)
if notfound
foreach(_free_pat_replacer, patterns)
write(io, str)
return io
end
return _replace_finish(io, str, count, e1, patterns, replaces, rs)
end

# note: leave str untyped here to make it easier for packages like StringViews to hook in
function _replace_(str, pat_repl::NTuple{N, Pair}, count::Int) where N
count == 0 && return str
e1, 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, e1, patterns, replaces, rs)))
end

"""
replace(s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer])
replace([io::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 @@ -774,13 +802,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 `io::IO` argument
is supplied, the transformed string is instead written to `io` (returning `io`).
(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 `io::IO` argument requires version 1.10.

# Examples
```jldoctest
julia> replace("Python is a programming language.", "Python" => "Julia")
Expand All @@ -799,8 +835,12 @@ julia> replace("abcabc", "a" => "b", "b" => "c", r".+" => "a")
"bca"
```
"""
replace(io::IO, s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
_replace_(io, String(s), pat_f, Int(count))

replace(s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
replace(String(s), pat_f..., count=count)
_replace_(String(s), pat_f, Int(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
22 changes: 22 additions & 0 deletions test/strings/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,28 @@ end
# Issue 36953
@test replace("abc", "" => "_", count=1) == "_abc"

# tests for io::IO API (in addition to internals exercised above):
let buf = IOBuffer()
replace(buf, "aaa", 'a' => 'z', count=0)
replace(buf, "aaa", 'a' => 'z', count=1)
replace(buf, "bbb", 'a' => 'z')
replace(buf, "aaa", 'a' => 'z')
@test String(take!(buf)) == "aaazaabbbzzz"
end
let tempfile = tempname()
try
open(tempfile, "w") do f
replace(f, "aaa", 'a' => 'z', count=0)
replace(f, "aaa", 'a' => 'z', count=1)
replace(f, "bbb", 'a' => 'z')
replace(f, "aaa", 'a' => 'z')
print(f, "\n")
end
@test read(tempfile, String) == "aaazaabbbzzz\n"
finally
rm(tempfile, force=true)
end
end
end

@testset "replace many" begin
Expand Down