Repository: invenia/NamedDims.jl (julia)

Issue:
add `rename(nda, :oldname=>:newname)`
It would be handy to be able to do renaming via giving the old and new name.
this would work like `Base.replace(::AbstractString, old=>new)`

Right now the only way to rename is to give a full list of what the new names should be

Pull request description:
Closes #129

Patch:
diff --git a/Project.toml b/Project.toml
index ea64c9c..8d07aa3 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "NamedDims"
 uuid = "356022a1-0364-5f58-8944-0da4b18d706f"
 authors = ["Invenia Technical Computing Corporation"]
-version = "0.2.25"
+version = "0.2.26"
 
 [deps]
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
diff --git a/src/name_core.jl b/src/name_core.jl
index 566951c..4d542da 100644
--- a/src/name_core.jl
+++ b/src/name_core.jl
@@ -129,6 +129,26 @@ function permute_dimnames(dimnames::NTuple{N, Symbol}, perm) where N
     return compile_time_return_hack(new_dimnames)
 end
 
+"""
+    _rename(dimnames::Tuple, old_new::Vararg{Pair})
+    _rename(dimname::Symbol, old_new::Vararg{Pair})
+
+For each pair `old=>new`, replace all occurences of `old` in tuple `namea` with `new`.
+If `dimname` is a Symbol, replace it with `new` of the pair with matching `old`, or
+return `dimname` if no pairs match.
+"""
+function _rename(dimnames::Tuple, old_new::Vararg{Pair})
+    return ntuple(i -> _rename(dimnames[i], old_new...), length(dimnames))
+end
+
+function _rename(dimname::Symbol, old_new::Vararg{Pair})
+    # Avoid looping over pairs explicitly because that allocates.
+    nt = ntuple(i -> dimname === first(old_new[i]) ? i : 0, length(old_new))
+    which = sum(nt)
+    which > length(old_new) && throw(ArgumentError("Duplicate old names not permitted in `rename`"))
+    return which == 0 ? dimname : last(old_new[which])
+end
+
 """
     tuple_issubset
 A version of `is_subset` sepecifically for `Tuple`s of `Symbol`s, that is `@pure`.
diff --git a/src/name_operations.jl b/src/name_operations.jl
index 0fa0d98..a84fa6c 100644
--- a/src/name_operations.jl
+++ b/src/name_operations.jl
@@ -1,16 +1,24 @@
-# This file is for functions that are new functions sepecifically for workin with
+# This file is for functions that are new functions specifically for working with
 # NamedDimsArrays, rather than overloads of existing functions
 
 """
     rename(nda::NamedDimsArray, names)
+    rename(nda::NamedDimsArray, pairs)
 
-Returns a new `NameDimsArray` with the given dimension `names`.
+Returns a new `NamedDimsArray` with the given dimension `names` or `pairs`
+of `old=>new` names.
 `rename` outright replaces the names; while still wrapping the same backing array.
 Unlike the constructor, it does not require that new names are compatible
 with the old names (though you do still need to match the number of dimensions).
 """
 rename(nda::NamedDimsArray, names) = NamedDimsArray(parent(nda), names)
 
+function rename(nda::NamedDimsArray, pairs::Vararg{Pair{Symbol, Symbol}})
+    names = dimnames(nda)
+    new_names = ntuple(i -> _rename(names[i], pairs...), length(names))
+    return NamedDimsArray(parent(nda), new_names)
+end
+
 """
     refine_names(x, names)
 


Test patch:
diff --git a/test/name_operations.jl b/test/name_operations.jl
index 99bf405..4c9d799 100644
--- a/test/name_operations.jl
+++ b/test/name_operations.jl
@@ -53,4 +53,21 @@ end
 
     @test dimnames(new_nda) === (:w, :x, :y, :z)
     @test parent(new_nda) === parent(nda)
+
+    pair = :a => :w
+    @test dimnames(rename(nda, pair)) === (:w, :b, :c, :d)
+    @test parent(rename(nda, pair)) === parent(nda)
+
+    pairs = (:a => :w, :b => :x, :c => :y, :d => :z)
+    @test dimnames(rename(nda, pairs...)) === (:w, :x, :y, :z)
+    @test parent(rename(nda, pairs...)) === parent(nda)
+
+    # parallel (rather than consecutive) computation of pairs, as in Base.replace
+    ppairs = (:a => :b, :b => :x)
+    @test dimnames(rename(nda, ppairs...)) === (:b, :x, :c, :d) # rather than (:x, :x, :c, :d)
+    @test parent(rename(nda, ppairs...)) === parent(nda)
+end
+
+@testset "rename allocations" begin
+    @test_modern 0 == @ballocated (()->NamedDims._rename((:a, :b), :a => :x, :b => :y))()
 end