From 158ddfecd76c0e680080dee6a3ece80d5759f24e Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sat, 10 Jun 2017 14:38:48 +0200 Subject: [PATCH] add replace & replace! for collections --- NEWS.md | 4 + base/exports.jl | 1 + base/set.jl | 238 ++++++++++++++++++++++++++++++++++ base/sysimg.jl | 6 +- doc/src/stdlib/collections.md | 4 + doc/src/stdlib/strings.md | 2 +- test/sets.jl | 53 ++++++++ 7 files changed, 304 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index afcf8bd858a3d..adebfc9466f87 100644 --- a/NEWS.md +++ b/NEWS.md @@ -475,6 +475,10 @@ Library improvements defined, linear-algebra function `transpose`. Similarly, `permutedims(v::AbstractVector)` will create a row matrix ([#24839]). + * A new `replace(A, old=>new)` function is introduced to replace `old` by `new` in + collection `A`. There are also two other methods with a different API, and + a mutating variant, `replace!` ([#22324]). + * `CartesianRange` changes ([#24715]): - Inherits from `AbstractArray`, and linear indexing can be used to provide linear-to-cartesian conversion ([#24715]) diff --git a/base/exports.jl b/base/exports.jl index 602624f32d430..c1383e91d5164 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -748,6 +748,7 @@ export randstring, repeat, replace, + replace!, repr, reverseind, rpad, diff --git a/base/set.jl b/base/set.jl index 8c1b03c893d7e..e8df2dfb716e7 100644 --- a/base/set.jl +++ b/base/set.jl @@ -544,3 +544,241 @@ end convert(::Type{T}, s::T) where {T<:AbstractSet} = s convert(::Type{T}, s::AbstractSet) where {T<:AbstractSet} = T(s) + + +## replace/replace! ## + +""" + replace!(A, old_new::Pair...; [count::Integer]) + +For each pair `old=>new` in `old_new`, replace all occurrences +of `old` in collection `A` by `new`. +If `count` is specified, then replace at most `count` occurrences in total. +See also [`replace`](@ref replace(A, old_new::Pair...)). + +# Examples +```jldoctest +julia> replace!([1, 2, 1, 3], 1=>0, 2=>4, count=2) +4-element Array{Int64,1}: + 0 + 4 + 1 + 3 + +julia> replace!(Set([1, 2, 3]), 1=>0) +Set([0, 2, 3]) +``` +""" +replace!(A, old_new::Pair...; count::Integer=typemax(Int)) = _replace!(A, eltype(A), count, old_new) + +# we use this wrapper because using directly eltype(A) as the type +# parameter below for Some degrades performance +function _replace!(A, ::Type{K}, count::Integer, old_new::Tuple{Vararg{Pair}}) where K + @inline function prednew(x) + for o_n in old_new + first(o_n) == x && return Some{K}(last(o_n)) + end + end + replace!(prednew, A, count=count) +end + +""" + replace!(pred::Function, A, new; [count::Integer]) + +Replace all occurrences `x` in collection `A` for which `pred(x)` is true +by `new`. + +# Examples +```jldoctest +julia> A = [1, 2, 3, 1]; + +julia> replace!(isodd, A, 0, count=2) +4-element Array{Int64,1}: + 0 + 2 + 0 + 1 +``` +""" +replace!(pred::Callable, A, new; count::Integer=typemax(Int)) = + replace!(x -> if pred(x) Some(new) end, A, count=count) + +""" + replace!(prednew::Function, A; [count::Integer]) + +For each value `x` in `A`, `prednew(x)` is called and must +return either `nothing`, in which case no replacement occurs, +or a value, possibly wrapped as a [`Some`](@ref) object, which +will be used as a replacement for `x`. + +# Examples +```jldoctest +julia> replace!(x -> isodd(x) ? 2x : nothing, [1, 2, 3, 4]) +4-element Array{Int64,1}: + 2 + 2 + 6 + 4 + +julia> replace!(Union{Int,Nothing}[0, 1, 2, nothing, 4], count=2) do x + x !== nothing && iseven(x) ? Some(nothing) : nothing + end +5-element Array{Union{Nothing,Int64},1}: + nothing + 1 + nothing + nothing + 4 + +julia> replace!(Dict(1=>2, 3=>4)) do kv + if first(kv) < 3; first(kv)=>3 end + end +Dict{Int64,Int64} with 2 entries: + 3 => 4 + 1 => 3 + +julia> replace!(x->2x, Set([3, 6])) +Set([6, 12]) +``` +""" +replace!(prednew::Callable, A; count::Integer=typemax(Int)) = + replace!(prednew, A, count=clamp(count, typemin(Int), typemax(Int)) % Int) + + + +""" + replace(A, old_new::Pair...; [count::Integer]) + +Return a copy of collection `A` where, for each pair `old=>new` in `old_new`, +all occurrences of `old` are replaced by `new`. +If `count` is specified, then replace at most `count` occurrences in total. +See also [`replace!`](@ref). + +# Examples +```jldoctest +julia> replace([1, 2, 1, 3], 1=>0, 2=>4, count=2) +4-element Array{Int64,1}: + 0 + 4 + 1 + 3 +``` +""" +replace(A, old_new::Pair...; count::Integer=typemax(Int)) = + _replace!(copy(A), eltype(A), count, old_new) + +""" + replace(pred::Function, A, new; [count::Integer]) + +Return a copy of collection `A` where all occurrences `x` for which +`pred(x)` is true are replaced by `new`. + +# Examples +```jldoctest +julia> replace(isodd, [1, 2, 3, 1], 0, count=2) +4-element Array{Int64,1}: + 0 + 2 + 0 + 1 +``` +""" +replace(pred::Callable, A, new; count::Integer=typemax(Int)) = + replace!(x -> if pred(x) Some(new) end, copy(A), count=count) + +""" + replace(prednew::Function, A; [count::Integer]) + +Return a copy of `A` where for each value `x` in `A`, `prednew(x)` is called +and must return either `nothing`, in which case no replacement occurs, +or a value, possibly wrapped as a [`Some`](@ref) object, which +will be used as a replacement for `x`. + +# Examples +```jldoctest +julia> replace(x -> isodd(x) ? 2x : nothing, [1, 2, 3, 4]) +4-element Array{Int64,1}: + 2 + 2 + 6 + 4 + +julia> replace(Union{Int,Nothing}[0, 1, 2, nothing, 4], count=2) do x + x !== nothing && iseven(x) ? Some(nothing) : nothing + end +5-element Array{Union{Nothing,Int64},1}: + nothing + 1 + nothing + nothing + 4 + +julia> replace(Dict(1=>2, 3=>4)) do kv + if first(kv) < 3; first(kv)=>3 end + end +Dict{Int64,Int64} with 2 entries: + 3 => 4 + 1 => 3 +``` +""" +replace(prednew::Callable, A; count::Integer=typemax(Int)) = replace!(prednew, copy(A), count=count) + +# Handle ambiguities +replace!(a::Callable, b::Pair; count::Integer=-1) = throw(MethodError(replace!, (a, b))) +replace!(a::Callable, b::Pair, c::Pair; count::Integer=-1) = throw(MethodError(replace!, (a, b, c))) +replace(a::Callable, b::Pair; count::Integer=-1) = throw(MethodError(replace, (a, b))) +replace(a::Callable, b::Pair, c::Pair; count::Integer=-1) = throw(MethodError(replace, (a, b, c))) +replace(a::AbstractString, b::Pair, c::Pair) = throw(MethodError(replace, (a, b, c))) + + +### replace! for AbstractDict/AbstractSet + +askey(k, ::AbstractDict) = k.first +askey(k, ::AbstractSet) = k + +function _replace_update_dict!(repl::Vector{<:Pair}, x, y::Some) + push!(repl, x => y.value) + true +end + +_replace_update_dict!(repl::Vector{<:Pair}, x, ::Nothing) = false +_replace_update_dict!(repl::Vector{<:Pair}, x, y) = _replace_update_dict!(repl, x, Some(y)) + +function replace!(prednew::Callable, A::Union{AbstractDict,AbstractSet}; count::Int=typemax(Int)) + count < 0 && throw(DomainError(count, "`count` must not be negative")) + count == 0 && return A + repl = Pair{eltype(A),eltype(A)}[] + c = 0 + for x in A + c += _replace_update_dict!(repl, x, prednew(x)) + c == count && break + end + for oldnew in repl + pop!(A, askey(first(oldnew), A)) + end + for oldnew in repl + push!(A, last(oldnew)) + end + A +end + +### AbstractArray + +function _replace_update!(A::AbstractArray, i::Integer, y::Some) + @inbounds A[i] = y.value + true +end + +_replace_update!(A::AbstractArray, i::Integer, ::Nothing) = false +_replace_update!(A::AbstractArray, i::Integer, y) = _replace_update!(A, i, Some(y)) + +function replace!(prednew::Callable, A::AbstractArray; count::Int=typemax(Int)) + count < 0 && throw(DomainError(count, "`count` must not be negative")) + count == 0 && return A + c = 0 + for i in eachindex(A) + c += _replace_update!(A, i, prednew(A[i])) + c == count && break + end + A +end diff --git a/base/sysimg.jl b/base/sysimg.jl index 616b2e10f27db..54a8decfede86 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -203,6 +203,9 @@ if !isdefined(Core, :Inference) Core.atdoc!(CoreDocs.docm) end +# Some type +include("some.jl") + include("dict.jl") include("set.jl") include("iterators.jl") @@ -255,9 +258,6 @@ include("multidimensional.jl") include("permuteddimsarray.jl") using .PermutedDimsArrays -# Some type -include("some.jl") - include("broadcast.jl") using .Broadcast diff --git a/doc/src/stdlib/collections.md b/doc/src/stdlib/collections.md index fd140e8e6ff44..ff7a1328f5ff1 100644 --- a/doc/src/stdlib/collections.md +++ b/doc/src/stdlib/collections.md @@ -133,6 +133,10 @@ Base.collect(::Type, ::Any) Base.issubset(::Any, ::Any) Base.filter Base.filter! +Base.replace(::Any, ::Pair...) +Base.replace(::Base.Callable, ::Any, ::Any) +Base.replace(::Base.Callable, ::Any) +Base.replace! ``` ## Indexable Collections diff --git a/doc/src/stdlib/strings.md b/doc/src/stdlib/strings.md index 0426b5ba3f6de..04892e3b87e70 100644 --- a/doc/src/stdlib/strings.md +++ b/doc/src/stdlib/strings.md @@ -38,7 +38,7 @@ Base.searchindex Base.rsearchindex Base.contains(::AbstractString, ::AbstractString) Base.reverse(::Union{String,SubString{String}}) -Base.replace +Base.replace(s::AbstractString, pat, f) Base.split Base.rsplit Base.strip diff --git a/test/sets.jl b/test/sets.jl index 66dcd34664c8a..66e1af0706554 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -471,3 +471,56 @@ end end end end + +@testset "replace! & replace" begin + maybe1(v, p) = if p Some(v) end + maybe2(v, p) = if p v end + + for maybe = (maybe1, maybe2) + a = [1, 2, 3, 1] + @test replace(x->maybe(2x, iseven(x)), a) == [1, 4, 3, 1] + @test replace!(x->maybe(2x, iseven(x)), a) === a + @test a == [1, 4, 3, 1] + @test replace(a, 1=>0) == [0, 4, 3, 0] + @test replace(a, 1=>0, count=1) == [0, 4, 3, 1] + @test replace!(a, 1=>2) === a + @test a == [2, 4, 3, 2] + + d = Dict(1=>2, 3=>4) + @test replace(x->x.first > 2, d, 0=>0) == Dict(1=>2, 0=>0) + @test replace!(x->maybe(x.first=>2*x.second, x.first > 2), d) === d + @test d == Dict(1=>2, 3=>8) + @test replace(d, (3=>8)=>(0=>0)) == Dict(1=>2, 0=>0) + @test replace!(d, (3=>8)=>(2=>2)) === d + @test d == Dict(1=>2, 2=>2) + @test replace(x->x.second == 2, d, 0=>0, count=1) in [Dict(1=>2, 0=>0), + Dict(2=>2, 0=>0)] + + s = Set([1, 2, 3]) + @test replace(x->maybe(2x, x>1), s) == Set([1, 4, 6]) + @test replace(x->maybe(2x, x>1), s, count=1) in [Set([1, 4, 3]), Set([1, 2, 6])] + @test replace(s, 1=>4) == Set([2, 3, 4]) + @test replace!(s, 1=>2) === s + @test s == Set([2, 3]) + + @test replace([1, 2], 1=>0, 2=>0, count=0) == [1, 2] # count=0 --> no replacements + end + # test collisions with AbstractSet/AbstractDict + @test replace!(x->2x, Set([3, 6])) == Set([6, 12]) + @test replace!(x->2x, Set([1:20;])) == Set([2:2:40;]) + @test replace!(kv -> (2kv[1] => kv[2]), Dict(1=>2, 2=>4, 4=>8, 8=>16)) == Dict(2=>2, 4=>4, 8=>8, 16=>16) + # test Some(nothing) + + a = [1, 2, nothing, 4] + @test replace(x -> x === nothing ? 0 : Some(nothing), a) == [nothing, nothing, 0, nothing] + @test replace(x -> x === nothing ? 0 : nothing, a) == [1, 2, 0, 4] + @test replace!(x -> x !== nothing ? Some(nothing) : nothing, a) == [nothing, nothing, nothing, nothing] + @test replace(iseven, Any[1, 2, 3, 4], nothing) == [1, nothing, 3, nothing] + @test replace(Any[1, 2, 3, 4], 1=>nothing, 3=>nothing) == [nothing, 2, nothing, 4] + s = Set([1, 2, nothing, 4]) + @test replace(x -> x === nothing ? 0 : Some(nothing), s) == Set([0, nothing]) + @test replace(x -> x === nothing ? 0 : nothing, s) == Set([1, 2, 0, 4]) + @test replace(x -> x !== nothing ? Some(nothing) : nothing, s) == Set([nothing]) + @test replace(iseven, Set(Any[1, 2, 3, 4]), nothing) == Set([1, nothing, 3, nothing]) + @test replace(Set(Any[1, 2, 3, 4]), 1=>nothing, 3=>nothing) == Set([nothing, 2, nothing, 4]) +end