Skip to content

Commit 388c734

Browse files
stevengjKristofferC
authored andcommitted
add replace(io, str, patterns...) (#48625)
(cherry picked from commit ce1b420)
1 parent 208e928 commit 388c734

File tree

4 files changed

+81
-17
lines changed

4 files changed

+81
-17
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ New library features
6767
* A `CartesianIndex` is now treated as a "scalar" for broadcasting ([#47044]).
6868
* `printstyled` now supports italic output ([#45164]).
6969
* `parent` and `parentindices` support `SubString`s
70+
* `replace(string, pattern...)` now supports an optional `IO` argument to
71+
write the output to a stream rather than returning a string ([#48625]).
7072

7173
Standard library changes
7274
------------------------

base/strings/util.jl

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -700,12 +700,11 @@ _free_pat_replacer(x) = nothing
700700
_pat_replacer(x::AbstractChar) = isequal(x)
701701
_pat_replacer(x::Union{Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}) = in(x)
702702

703-
function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(Int)) where N
704-
count == 0 && return str
703+
# note: leave str untyped here to make it easier for packages like StringViews to hook in
704+
function _replace_init(str, pat_repl::NTuple{N, Pair}, count::Int) where N
705705
count < 0 && throw(DomainError(count, "`count` must be non-negative."))
706-
n = 1
707-
e1 = nextind(str, lastindex(str)) # sizeof(str)
708-
i = a = firstindex(str)
706+
e1 = nextind(str, lastindex(str)) # sizeof(str)+1
707+
a = firstindex(str)
709708
patterns = map(p -> _pat_replacer(first(p)), pat_repl)
710709
replaces = map(last, pat_repl)
711710
rs = map(patterns) do p
@@ -716,21 +715,24 @@ function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(I
716715
r isa Int && (r = r:r) # findnext / performance fix
717716
return r
718717
end
719-
if all(>(e1), map(first, rs))
720-
foreach(_free_pat_replacer, patterns)
721-
return str
722-
end
723-
out = IOBuffer(sizehint=floor(Int, 1.2sizeof(str)))
718+
return e1, patterns, replaces, rs, all(>(e1), map(first, rs))
719+
end
720+
721+
# note: leave str untyped here to make it easier for packages like StringViews to hook in
722+
function _replace_finish(io::IO, str, count::Int,
723+
e1::Int, patterns::Tuple, replaces::Tuple, rs::Tuple)
724+
n = 1
725+
i = a = firstindex(str)
724726
while true
725727
p = argmin(map(first, rs)) # TODO: or argmin(rs), to pick the shortest first match ?
726728
r = rs[p]
727729
j, k = first(r), last(r)
728730
j > e1 && break
729731
if i == a || i <= k
730732
# copy out preserved portion
731-
GC.@preserve str unsafe_write(out, pointer(str, i), UInt(j-i))
733+
GC.@preserve str unsafe_write(io, pointer(str, i), UInt(j-i))
732734
# copy out replacement string
733-
_replace(out, replaces[p], str, r, patterns[p])
735+
_replace(io, replaces[p], str, r, patterns[p])
734736
end
735737
if k < j
736738
i = j
@@ -755,13 +757,39 @@ function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(I
755757
n += 1
756758
end
757759
foreach(_free_pat_replacer, patterns)
758-
write(out, SubString(str, i))
759-
return String(take!(out))
760+
write(io, SubString(str, i))
761+
return io
762+
end
763+
764+
# note: leave str untyped here to make it easier for packages like StringViews to hook in
765+
function _replace_(io::IO, str, pat_repl::NTuple{N, Pair}, count::Int) where N
766+
if count == 0
767+
write(io, str)
768+
return io
769+
end
770+
e1, patterns, replaces, rs, notfound = _replace_init(str, pat_repl, count)
771+
if notfound
772+
foreach(_free_pat_replacer, patterns)
773+
write(io, str)
774+
return io
775+
end
776+
return _replace_finish(io, str, count, e1, patterns, replaces, rs)
760777
end
761778

779+
# note: leave str untyped here to make it easier for packages like StringViews to hook in
780+
function _replace_(str, pat_repl::NTuple{N, Pair}, count::Int) where N
781+
count == 0 && return str
782+
e1, patterns, replaces, rs, notfound = _replace_init(str, pat_repl, count)
783+
if notfound
784+
foreach(_free_pat_replacer, patterns)
785+
return str
786+
end
787+
out = IOBuffer(sizehint=floor(Int, 1.2sizeof(str)))
788+
return String(take!(_replace_finish(out, str, count, e1, patterns, replaces, rs)))
789+
end
762790

763791
"""
764-
replace(s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer])
792+
replace([io::IO], s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer])
765793
766794
Search for the given pattern `pat` in `s`, and replace each occurrence with `r`.
767795
If `count` is provided, replace at most `count` occurrences.
@@ -774,13 +802,21 @@ If `pat` is a regular expression and `r` is a [`SubstitutionString`](@ref), then
774802
references in `r` are replaced with the corresponding matched text.
775803
To remove instances of `pat` from `string`, set `r` to the empty `String` (`""`).
776804
805+
The return value is a new string after the replacements. If the `io::IO` argument
806+
is supplied, the transformed string is instead written to `io` (returning `io`).
807+
(For example, this can be used in conjunction with an [`IOBuffer`](@ref) to re-use
808+
a pre-allocated buffer array in-place.)
809+
777810
Multiple patterns can be specified, and they will be applied left-to-right
778811
simultaneously, so only one pattern will be applied to any character, and the
779812
patterns will only be applied to the input text, not the replacements.
780813
781814
!!! compat "Julia 1.7"
782815
Support for multiple patterns requires version 1.7.
783816
817+
!!! compat "Julia 1.10"
818+
The `io::IO` argument requires version 1.10.
819+
784820
# Examples
785821
```jldoctest
786822
julia> replace("Python is a programming language.", "Python" => "Julia")
@@ -799,8 +835,12 @@ julia> replace("abcabc", "a" => "b", "b" => "c", r".+" => "a")
799835
"bca"
800836
```
801837
"""
838+
replace(io::IO, s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
839+
_replace_(io, String(s), pat_f, Int(count))
840+
802841
replace(s::AbstractString, pat_f::Pair...; count=typemax(Int)) =
803-
replace(String(s), pat_f..., count=count)
842+
_replace_(String(s), pat_f, Int(count))
843+
804844

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

doc/src/base/strings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Base.findlast(::AbstractChar, ::AbstractString)
5151
Base.findprev(::AbstractString, ::AbstractString, ::Integer)
5252
Base.occursin
5353
Base.reverse(::Union{String,SubString{String}})
54-
Base.replace(s::AbstractString, ::Pair...)
54+
Base.replace(::IO, s::AbstractString, ::Pair...)
5555
Base.eachsplit
5656
Base.split
5757
Base.rsplit

test/strings/util.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,28 @@ end
333333
# Issue 36953
334334
@test replace("abc", "" => "_", count=1) == "_abc"
335335

336+
# tests for io::IO API (in addition to internals exercised above):
337+
let buf = IOBuffer()
338+
replace(buf, "aaa", 'a' => 'z', count=0)
339+
replace(buf, "aaa", 'a' => 'z', count=1)
340+
replace(buf, "bbb", 'a' => 'z')
341+
replace(buf, "aaa", 'a' => 'z')
342+
@test String(take!(buf)) == "aaazaabbbzzz"
343+
end
344+
let tempfile = tempname()
345+
try
346+
open(tempfile, "w") do f
347+
replace(f, "aaa", 'a' => 'z', count=0)
348+
replace(f, "aaa", 'a' => 'z', count=1)
349+
replace(f, "bbb", 'a' => 'z')
350+
replace(f, "aaa", 'a' => 'z')
351+
print(f, "\n")
352+
end
353+
@test read(tempfile, String) == "aaazaabbbzzz\n"
354+
finally
355+
rm(tempfile, force=true)
356+
end
357+
end
336358
end
337359

338360
@testset "replace many" begin

0 commit comments

Comments
 (0)