Repository: JuliaML/TableTransforms.jl (julia)

Issue:
Add `Except` transform as the opposite of the `Only` transform
We would like to be able to write `Except(Continuous)` to mean the opposite of `Only(Continuous)`.

Pull request description:
closes #234

Patch:
diff --git a/docs/src/transforms.md b/docs/src/transforms.md
index 0e0fff83..220ccccc 100644
--- a/docs/src/transforms.md
+++ b/docs/src/transforms.md
@@ -14,12 +14,24 @@ Select
 Reject
 ```
 
+## Satisfies
+
+```@docs
+Satisfies
+```
+
 ## Only
 
 ```@docs
 Only
 ```
 
+## Except
+
+```@docs
+Except
+```
+
 ## Rename
 
 ```@docs
diff --git a/src/TableTransforms.jl b/src/TableTransforms.jl
index 649da076..2e7bc226 100644
--- a/src/TableTransforms.jl
+++ b/src/TableTransforms.jl
@@ -51,7 +51,9 @@ export
   # built-in
   Select,
   Reject,
+  Satisfies,
   Only,
+  Except,
   Rename,
   StdNames,
   Sort,
diff --git a/src/transforms.jl b/src/transforms.jl
index 50b4ca56..685e1796 100644
--- a/src/transforms.jl
+++ b/src/transforms.jl
@@ -265,7 +265,7 @@ end
 # ----------------
 
 include("transforms/select.jl")
-include("transforms/only.jl")
+include("transforms/satisfies.jl")
 include("transforms/rename.jl")
 include("transforms/stdnames.jl")
 include("transforms/sort.jl")
diff --git a/src/transforms/only.jl b/src/transforms/only.jl
deleted file mode 100644
index 2d218375..00000000
--- a/src/transforms/only.jl
+++ /dev/null
@@ -1,40 +0,0 @@
-# ------------------------------------------------------------------
-# Licensed under the MIT License. See LICENSE in the project root.
-# ------------------------------------------------------------------
-
-"""
-    Only(S)
-
-Selects only columns that have scientific type `S`.
-
-# Examples
-
-```julia
-import DataScienceTraits as DST
-Only(DST.Continuous)
-```
-"""
-struct Only <: StatelessFeatureTransform
-  scitype::DataType
-  Only(S::Type{<:SciType}) = new(S)
-end
-
-isrevertible(::Type{Only}) = true
-
-function applyfeat(transform::Only, feat, prep)
-  S = transform.scitype
-  cols = Tables.columns(feat)
-  names = Tables.columnnames(cols)
-  snames = filter(names) do name
-    column = Tables.getcolumn(cols, name)
-    elscitype(column) <: S
-  end
-  strans = Select(snames)
-  newfeat, sfcache = applyfeat(strans, feat, prep)
-  newfeat, (strans, sfcache)
-end
-
-function revertfeat(::Only, newfeat, fcache)
-  strans, sfcache = fcache
-  revertfeat(strans, newfeat, sfcache)
-end
diff --git a/src/transforms/satisfies.jl b/src/transforms/satisfies.jl
new file mode 100644
index 00000000..3586058a
--- /dev/null
+++ b/src/transforms/satisfies.jl
@@ -0,0 +1,68 @@
+# ------------------------------------------------------------------
+# Licensed under the MIT License. See LICENSE in the project root.
+# ------------------------------------------------------------------
+
+"""
+    Satisfies(pred)
+
+Selects the columns where `pred(column)` returns `true`.
+
+# Examples
+
+```julia
+Satisfies(allunique)
+Satisfies(x -> sum(x) > 100)
+Satisfies(x -> eltype(x) <: Integer)
+```
+"""
+struct Satisfies{F} <: StatelessFeatureTransform
+  pred::F
+end
+
+isrevertible(::Type{<:Satisfies}) = true
+
+function applyfeat(transform::Satisfies, feat, prep)
+  pred = transform.pred
+  cols = Tables.columns(feat)
+  names = Tables.columnnames(cols)
+  snames = filter(names) do name
+    x = Tables.getcolumn(cols, name)
+    pred(x)
+  end
+  strans = Select(snames)
+  newfeat, sfcache = applyfeat(strans, feat, prep)
+  newfeat, (strans, sfcache)
+end
+
+function revertfeat(::Satisfies, newfeat, fcache)
+  strans, sfcache = fcache
+  revertfeat(strans, newfeat, sfcache)
+end
+
+"""
+    Only(S)
+
+Selects the columns that have scientific type `S`.
+
+# Examples
+
+```julia
+import DataScienceTraits as DST
+Only(DST.Continuous)
+```
+"""
+Only(S::Type{<:SciType}) = Satisfies(x -> elscitype(x) <: S)
+
+"""
+    Except(S)
+
+Selects the columns that don't have scientific type `S`.
+
+# Examples
+
+```julia
+import DataScienceTraits as DST
+Except(DST.Continuous)
+```
+"""
+Except(S::Type{<:SciType}) = Satisfies(x -> !(elscitype(x) <: S))


Test patch:
diff --git a/test/shows.jl b/test/shows.jl
index eca153f5..9c5fcff5 100644
--- a/test/shows.jl
+++ b/test/shows.jl
@@ -42,18 +42,18 @@
     └─ selector = [:a, :b, :c]"""
   end
 
-  @testset "Only" begin
-    T = Only(DST.Continuous)
+  @testset "Satisfies" begin
+    T = Satisfies(allunique)
 
     # compact mode
     iostr = sprint(show, T)
-    @test iostr == "Only(DataScienceTraits.Continuous)"
+    @test iostr == "Satisfies(allunique)"
 
     # full mode
     iostr = sprint(show, MIME("text/plain"), T)
     @test iostr == """
-    Only transform
-    └─ scitype = DataScienceTraits.Continuous"""
+    Satisfies transform
+    └─ pred = allunique"""
   end
 
   @testset "Rename" begin
diff --git a/test/transforms.jl b/test/transforms.jl
index f1e4e38b..25787516 100644
--- a/test/transforms.jl
+++ b/test/transforms.jl
@@ -1,7 +1,7 @@
 transformfiles = [
   "select.jl",
   "rename.jl",
-  "only.jl",
+  "satisfies.jl",
   "stdnames.jl",
   "sort.jl",
   "sample.jl",
diff --git a/test/transforms/only.jl b/test/transforms/only.jl
deleted file mode 100644
index dd311a48..00000000
--- a/test/transforms/only.jl
+++ /dev/null
@@ -1,25 +0,0 @@
-@testset "Only" begin
-  @test isrevertible(Only(DST.Continuous))
-
-  a = rand(10)
-  b = rand(Float32, 10)
-  c = rand(1:9, 10)
-  d = rand('a':'z', 10)
-  t = Table(; a, b, c, d)
-
-  T = Only(DST.Continuous)
-  n, c = apply(T, t)
-  @test Tables.schema(n).names == (:a, :b)
-  @test Tables.getcolumn(n, :a) == t.a
-  @test Tables.getcolumn(n, :b) == t.b
-  tₒ = revert(T, n, c)
-  @test t == tₒ
-
-  T = Only(DST.Categorical)
-  n, c = apply(T, t)
-  @test Tables.schema(n).names == (:c, :d)
-  @test Tables.getcolumn(n, :c) == t.c
-  @test Tables.getcolumn(n, :d) == t.d
-  tₒ = revert(T, n, c)
-  @test t == tₒ
-end
diff --git a/test/transforms/satisfies.jl b/test/transforms/satisfies.jl
new file mode 100644
index 00000000..ef077634
--- /dev/null
+++ b/test/transforms/satisfies.jl
@@ -0,0 +1,73 @@
+@testset "Satisfies" begin
+  @test isrevertible(Satisfies(allunique))
+
+  a = [1, 2, 3, 4, 5, 6]
+  b = [6, 5, 4, 3, 2, 1]
+  c = [1, 2, 3, 4, 6, 6]
+  d = [6, 6, 4, 3, 2, 1]
+  t = Table(; a, b, c, d)
+
+  T = Satisfies(allunique)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:a, :b)
+  @test Tables.getcolumn(n, :a) == t.a
+  @test Tables.getcolumn(n, :b) == t.b
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+
+  T = Satisfies(x -> sum(x) > 21)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:c, :d)
+  @test Tables.getcolumn(n, :c) == t.c
+  @test Tables.getcolumn(n, :d) == t.d
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+end
+
+@testset "Only" begin
+  a = rand(10)
+  b = rand(Float32, 10)
+  c = rand(1:9, 10)
+  d = rand('a':'z', 10)
+  t = Table(; a, b, c, d)
+
+  T = Only(DST.Continuous)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:a, :b)
+  @test Tables.getcolumn(n, :a) == t.a
+  @test Tables.getcolumn(n, :b) == t.b
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+
+  T = Only(DST.Categorical)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:c, :d)
+  @test Tables.getcolumn(n, :c) == t.c
+  @test Tables.getcolumn(n, :d) == t.d
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+end
+
+@testset "Except" begin
+  a = rand(10)
+  b = rand(Float32, 10)
+  c = rand(1:9, 10)
+  d = rand('a':'z', 10)
+  t = Table(; a, b, c, d)
+
+  T = Except(DST.Categorical)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:a, :b)
+  @test Tables.getcolumn(n, :a) == t.a
+  @test Tables.getcolumn(n, :b) == t.b
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+
+  T = Except(DST.Continuous)
+  n, c = apply(T, t)
+  @test Tables.schema(n).names == (:c, :d)
+  @test Tables.getcolumn(n, :c) == t.c
+  @test Tables.getcolumn(n, :d) == t.d
+  tₒ = revert(T, n, c)
+  @test t == tₒ
+end