text:
# Usage: run.jl SOLVERNAME TIMELIMIT DATAFOLDER INSTANCENAME

using ConicBenchmarkUtilities
using Pajarito
using ConicNonlinearBridge

# Print with full precision
function vec_to_string(v)
    terms = String[]
    for x in v
        b = IOBuffer()
        show(b, x)
        push!(terms, takebuf_string(b))
    end
    return string("[",join(terms,","),"]")
end

function solveprint(instance, solver)
    # Convert from cbf to our conic format
    (c, A, b, con_cones, var_cones, vartypes, sense, objoffset) = cbftompb(instance)
    if sense == :Max
        scale!(c, -1)
    end

    # Load, solve, and time
    m = MathProgBase.ConicModel(solver)
    timeall = time()
    MathProgBase.loadproblem!(m, c, A, b, con_cones, var_cones)
    MathProgBase.setvartype!(m, vartypes)
    MathProgBase.optimize!(m)
    timeall = time() - timeall
    timesolver = MathProgBase.getsolvetime(m)
    status = MathProgBase.status(m)

    x = []
    try
        x = MathProgBase.getsolution(m)
    end

    objval = NaN
    objbound = NaN
    nodecount = NaN
    try
        objval = MathProgBase.getobjval(m)
        if sense == :Max
            objval = -objval
        end
        objval += objoffset
    end
    try
        objbound = MathProgBase.getobjbound(m)
        if sense == :Max
            objbound = -objbound
        end
        objbound += objoffset
    end
    try
        nodecount = MathProgBase.getnodecount(m)
    end

    println("#STATUS# $status")
    println("#OBJVAL# $objval")
    println("#OBJBOUND# $objbound")
    println("#NODECOUNT# $nodecount")
    println("#TIMESOLVER# $timesolver")
    println("#TIMEALL# $timeall")
    println("#SOLUTION# $(vec_to_string(x))")

    @show CPLEX.HEURTIME[1]
end

solvermap = Dict(
    # Bonmin
    "BONMIN_OA" =>
    (["AmplNLWriter"], quote ConicNLPWrapper(nlp_solver=BonminNLSolver(["bonmin.algorithm=\"B-OA\"", "bonmin.time_limit=$tlim", "halt_on_ampl_error=yes", "bonmin.allowable_fraction_gap=$rgap", "bonmin.oa_log_level=1", "bonmin.bb_log_level=1"]), disaggregate_soc=false) end),
    "BONMIN_OADIS" =>
    (["AmplNLWriter"], quote ConicNLPWrapper(nlp_solver=BonminNLSolver(["bonmin.algorithm=\"B-OA\"", "bonmin.time_limit=$tlim", "halt_on_ampl_error=yes", "bonmin.allowable_fraction_gap=$rgap", "bonmin.oa_log_level=1", "bonmin.bb_log_level=1"]), disaggregate_soc=true) end),
    "BONMIN_BB" =>
    (["AmplNLWriter"], quote ConicNLPWrapper(nlp_solver=BonminNLSolver(["bonmin.algorithm=\"B-BB\"", "bonmin.time_limit=$tlim", "halt_on_ampl_error=yes", "bonmin.allowable_fraction_gap=$rgap", "bonmin.oa_log_level=1", "bonmin.bb_log_level=1"]), disaggregate_soc=false) end),

    # CBC
    "PAJ_CBC_ECOS" =>
    (["Cbc","ECOS"], quote PajaritoSolver(mip_solver=CbcSolver(logLevel=0, integerTolerance=tol_int, primalTolerance=tol_feas, ratioGap=tol_gap, check_warmstart=false), cont_solver=ECOSSolver(verbose=false), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic) end),
    "PAJ_CBC_ECOS_subopt" =>
    (["Cbc","ECOS"], quote PajaritoSolver(mip_solver=CbcSolver(logLevel=0, integerTolerance=tol_int, primalTolerance=tol_feas, ratioGap=tol_gap, check_warmstart=false), cont_solver=ECOSSolver(verbose=false), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_subopt_count=5, mip_subopt_solver=CbcSolver(logLevel=0, integerTolerance=tol_int, primalTolerance=tol_feas, ratioGap=0.01, check_warmstart=false, seconds=120.)) end),

    # GLPK
    "PAJ_GLPK_ECOS" =>
    (["GLPKMathProgInterface","ECOS"], quote PajaritoSolver(mip_solver=GLPKSolverMIP(msg_lev=GLPK.MSG_OFF, tol_int=tol_int, tol_bnd=tol_feas, mip_gap=tol_gap, presolve=true), cont_solver=ECOSSolver(verbose=false), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic) end),

    # SCIP
    "SCIP_MISOCP" =>
    (["SCIP"], quote SCIPSolver("limits/gap", rgap, "numerics/feastol", tol_feas, "limits/time", tlim) end),
    "PAJ_SCIP_ECOS" =>
    (["SCIP","ECOS"], quote PajaritoSolver(mip_solver=SCIPSolver("display/verblevel", 0, "limits/gap", tol_gap, "numerics/feastol", tol_feas), cont_solver=ECOSSolver(verbose=false), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic) end),
    "PAJ_SCIP_ECOS_subopt" =>
    (["SCIP","ECOS"], quote PajaritoSolver(mip_solver=SCIPSolver("display/verblevel", 0, "limits/gap", tol_gap, "numerics/feastol", tol_feas), cont_solver=ECOSSolver(verbose=false), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_subopt_count=5, mip_subopt_solver=SCIPSolver("display/verblevel", 0, "limits/gap", 0.01, "numerics/feastol", tol_feas, "limits/time", 120.)) end),

    # CPLEX
    "CPLEX_MISOCP" =>
    (["CPLEX"], quote CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_TILIM=tlim, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap) end),

    # Iter
    "PAJ_CPLEX_MOSEK_dualonly" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_assist=false) end),
    "PAJ_CPLEX_MOSEK_dualonlynoscale" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_assist=false, scale_subp_cuts=false) end),
    "PAJ_CPLEX_MOSEK_dualonlynopresolve" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap, CPX_PARAM_PREIND=0), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_assist=false) end),
    "PAJ_CPLEX_MOSEK_dualonlynoscalenopresolve" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap, CPX_PARAM_PREIND=0), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_assist=false, scale_subp_cuts=false) end),
    "PAJ_CPLEX_MOSEK" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic) end),
    "PAJ_CPLEX_MOSEK_primonlynorelax" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_only=true, solve_relax=false) end),

    # "PAJ_CPLEX_MOSEK_noconic" =>
    # (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, prim_cuts_only=true, solve_relax=false, solve_subp=false) end),

    # MSD
    "PAJ_CPLEX_MOSEK_msddualonly" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=true, prim_cuts_assist=false) end),
    "PAJ_CPLEX_MOSEK_msddualonlynoscale" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=true, prim_cuts_assist=false, scale_subp_cuts=false) end),
    "PAJ_CPLEX_MOSEK_msd" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=true) end),
    "PAJ_CPLEX_MOSEK_msdprimonlynorelax" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=true, prim_cuts_only=true, solve_relax=false) end),

    # "PAJ_CPLEX_MOSEK_msdnoconic" =>
    # (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=true, prim_cuts_only=true, solve_relax=false, solve_subp=false) end),
    #should maybe not use mip starts on msdnoconic! but then this helps that solver in comparisons because heur cb wastes time! could turn off on msd?

    # Other
    "PAJ_CPLEX_MOSEK_subopt" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=tol_gap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_subopt_count=5, mip_subopt_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=0, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=0.01, CPX_PARAM_TILIM=120.)) end),
    "PAJ_CPLEX_MOSEK_msdnostarts" =>
    (["CPLEX","Mosek"], quote PajaritoSolver(mip_solver=CplexSolver(CPX_PARAM_THREADS=1, CPX_PARAM_SCRIND=1, CPX_PARAM_EPINT=tol_int, CPX_PARAM_EPRHS=tol_feas, CPX_PARAM_EPGAP=rgap), cont_solver=MosekSolver(LOG=0, NUM_THREADS=1), log_level=logl, timeout=tlim, rel_gap=rgap, prim_cut_feas_tol=tol_conic, mip_solver_drives=true, use_mip_starts=false) end),
)

function getsolver(solvername, tlim, logl, rgap)
    packages, solvername = solvermap[solvername]
    for p in packages
        eval(parse("using $p"))
    end
    return eval(solvername)
end

@assert length(ARGS) >= 3

# Save process ID for runmeta.jl
open("mypid", "w") do fd
    print(fd, getpid())
end

# General options
logl = 3
rgap = 1e-5
tol_conic = 1e-6

# Pajarito MIP solver options
tol_int = 1e-8
tol_feas = 1e-7
tol_gap = 0.

solvername = ARGS[1]
tlim = parse(Float64, ARGS[2])
datafolder = ARGS[3]

# Force Pajarito to compile on a small instance for the solver to avoid measuring compilation time, keep quiet
if startswith(solvername, "PAJ")
    TT = STDOUT
    open("/dev/null", "w") do fd
        redirect_stdout(fd)
        instance = readcbfdata(joinpath(datafolder, "estein4_A.cbf.gz"))
        solver = getsolver(solvername, 20., 0, rgap)
        solveprint(instance, solver)
    end
    redirect_stdout(TT)
end

instancename = ARGS[4]

# Interpret instance data as cbf
instance = readcbfdata(joinpath(datafolder, instancename))

solver = getsolver(solvername, tlim, logl, rgap)

println("#SOLVERNAME# $solvername")
println("#SOLVER# $solver")
println("#INSTANCE# $instancename")
println("#TIMELIMIT# $tlim")

# Attempt to solve, print solve info
timesolve = time()
try
    solveprint(instance, solver)
catch e
    println("#ERROR# $e")
end

max_stars_repo_name: mlubin/PajaritoSupplement

max_issues_repo_name: mlubin/PajaritoSupplement

max_forks_repo_name: mlubin/PajaritoSupplement


text:
#=

# Design Doc

- Each node *usually* creates ONE processor.
- Each processor has ONE `LocalScheduler`
- Each `LocalScheduler` sends its available resources to the global `Scheduler` on the driver processor periodically.
- `LocalScheduler` tries to initialize actors on the local processor when available resource meets. Otherwise, send the actor initializer to the global `Scheduler`.
- If there's no resource matches, the actor is put into the `StagingArea` and will be reschedulered periodically.

Auto scaling:

- When the whole cluster is under presure, the global `Scheduler` may send request to claim for more resources.
- When most nodes are idle, the global `Scheduler` may ask some nodes to exit and reschedule the actors on it.
=#

ext: jl


text:
@testset "Transforms" begin
  # using MersenneTwister for compatibility between Julia versions
  rng = MersenneTwister(42)
  @testset "Select" begin
    a = rand(4000)
    b = rand(4000)
    c = rand(4000)
    d = rand(4000)
    e = rand(4000)
    f = rand(4000)
    t = Table(; a, b, c, d, e, f)

    T = Select(:f, :d)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:f, :d]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Select(:f, :d, :b)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:f, :d, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Select(:d, :c, :b)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Select(:e, :c, :b, :a)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:e, :c, :b, :a]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with tuples
    T = Select((:e, :c, :b, :a))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:e, :c, :b, :a]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with vectors
    T = Select([:e, :c, :b, :a])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:e, :c, :b, :a]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with strings
    T = Select("d", "c", "b")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with tuple of strings
    T = Select(("d", "c", "b"))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with vector of strings
    T = Select(["d", "c", "b"])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with integers
    T = Select(4, 3, 2)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with tuple of integers
    T = Select((4, 3, 2))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # selection with vector of integers
    T = Select([4, 3, 2])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :c, :b]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # reapply test
    T = Select(:b, :c, :d)
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # selection with Regex
    T = Select(r"[dcb]")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:b, :c, :d] # the order of columns is preserved
    tₒ = revert(T, n, c)
    @test t == tₒ

    # different columntypes
    t = (a = rand(3), b = rand(ComplexF64, 3))
    T = Select(:a) → Functional(identity)
    tₒ = revert(T, apply(T, t)...)
    @test tₒ == t
    @test Tables.schema(tₒ) == Tables.schema(t)
    T = Select(:b) → Functional(identity)
    tₒ = revert(T, apply(T, t)...)
    @test tₒ == t
    @test Tables.schema(tₒ) == Tables.schema(t)

    x1 = rand(4000)
    x2 = rand(4000)
    y1 = rand(4000)
    y2 = rand(4000)
    t = Table(; x1, x2, y1, y2)

    # select columns whose names contain the character x
    T = Select(r"x")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:x1, :x2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # select columns whose names contain the character y
    T = Select(r"y")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:y1, :y2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # row table
    rt = Tables.rowtable(t)
    T = Select(r"y")
    n, c = apply(T, rt)
    @test Tables.columnnames(n) == [:y1, :y2]
    rtₒ = revert(T, n, c)
    @test rt == rtₒ

    # throws: Select without arguments
    @test_throws ArgumentError Select()
    @test_throws ArgumentError Select(())

    # throws: empty selection
    @test_throws AssertionError apply(Select(r"a"), t)
    @test_throws AssertionError apply(Select(Symbol[]), t)
    @test_throws AssertionError apply(Select(String[]), t)

    # throws: columns that do not exist in the original table
    @test_throws AssertionError apply(Select(:x3, :y3), t)
    @test_throws AssertionError apply(Select([:x3, :y3]), t)
    @test_throws AssertionError apply(Select((:x3, :y3)), t)
    @test_throws AssertionError apply(Select("x3", "y3"), t)
    @test_throws AssertionError apply(Select(["x3", "y3"]), t)
    @test_throws AssertionError apply(Select(("x3", "y3")), t)
  end

  @testset "Reject" begin
    a = rand(4000)
    b = rand(4000)
    c = rand(4000)
    d = rand(4000)
    e = rand(4000)
    f = rand(4000)
    t = Table(; a, b, c, d, e, f)

    T = Reject(:f, :d)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :b, :c, :e]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Reject(:f, :d, :b)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :c, :e]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Reject(:d, :c, :b)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Reject(:e, :c, :b, :a)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with tuples
    T = Reject((:e, :c, :b, :a))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with vectors
    T = Reject([:e, :c, :b, :a])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:d, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with strings
    T = Reject("d", "c", "b")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with tuple of strings
    T = Reject(("d", "c", "b"))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with vector of strings
    T = Reject(["d", "c", "b"])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with integers
    T = Reject(4, 3, 2)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with tuple of integers
    T = Reject((4, 3, 2))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rejection with vector of integers
    T = Reject([4, 3, 2])
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # reapply test
    T = Reject(:b, :c, :d)
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # rejection with Regex
    T = Reject(r"[dcb]")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:a, :e, :f] # the order of columns is preserved
    tₒ = revert(T, n, c)
    @test t == tₒ

    x1 = rand(4000)
    x2 = rand(4000)
    y1 = rand(4000)
    y2 = rand(4000)
    t = Table(; x1, x2, y1, y2)

    # reject columns whose names contain the character x
    T = Reject(r"x")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:y1, :y2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # reject columns whose names contain the character y
    T = Reject(r"y")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == [:x1, :x2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # row table
    rt = Tables.rowtable(t)
    T = Reject(r"y")
    n, c = apply(T, rt)
    @test Tables.columnnames(n) == [:x1, :x2]
    rtₒ = revert(T, n, c)
    @test rt == rtₒ

    # throws: Reject without arguments
    @test_throws ArgumentError Reject()
    @test_throws ArgumentError Reject(())

    # throws: reject all columns
    @test_throws AssertionError apply(Reject(r"[xy]"), t)
    @test_throws AssertionError apply(Reject(:x1, :x2, :y1, :y2), t)
    @test_throws AssertionError apply(Reject([:x1, :x2, :y1, :y2]), t)
    @test_throws AssertionError apply(Reject((:x1, :x2, :y1, :y2)), t)
    @test_throws AssertionError apply(Reject("x1", "x2", "y1", "y2"), t)
    @test_throws AssertionError apply(Reject(["x1", "x2", "y1", "y2"]), t)
    @test_throws AssertionError apply(Reject(["x1", "x2", "y1", "y2"]), t)
  end

  @testset "TableSelection" begin
    a = rand(4000)
    b = rand(4000)
    c = rand(4000)
    d = rand(4000)
    e = rand(4000)
    f = rand(4000)
    t = Table(; a, b, c, d, e, f)

    # Tables.jl interface
    s = TableTransforms.TableSelection(t, [:a, :b, :e])
    @test Tables.istable(s) == true
    @test Tables.columnaccess(s) == true
    @test Tables.rowaccess(s) == false
    @test Tables.columns(s) === s
    @test Tables.columnnames(s) == [:a, :b, :e]
    @test Tables.schema(s).names == (:a, :b, :e)
    @test Tables.schema(s).types == (Float64, Float64, Float64)
    @test Tables.materializer(s) == Tables.materializer(t)

    # getcolumn
    cols = Tables.columns(t)
    @test Tables.getcolumn(s, :a) == Tables.getcolumn(cols, :a)
    @test Tables.getcolumn(s, 1) == Tables.getcolumn(cols, 1)
    @test Tables.getcolumn(s, 3) == Tables.getcolumn(cols, :e)

    # row table
    rt = Tables.rowtable(t)
    s = TableTransforms.TableSelection(rt, [:a, :b, :e])
    cols = Tables.columns(rt)
    @test Tables.getcolumn(s, :a) == Tables.getcolumn(cols, :a)
    @test Tables.getcolumn(s, 1) == Tables.getcolumn(cols, 1)
    @test Tables.getcolumn(s, 3) == Tables.getcolumn(cols, :e)

    # throws
    @test_throws AssertionError TableTransforms.TableSelection(t, [:a, :b, :z])
    s = TableTransforms.TableSelection(t, [:a, :b, :e])
    @test_throws ErrorException Tables.getcolumn(s, :f)
  end

  @testset "Filter" begin
    a = [3, 2, 1, 4, 5, 3]
    b = [2, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, 1]
    d = [4, 3, 7, 5, 4, 1]
    e = [5, 5, 2, 6, 5, 2]
    f = [4, 4, 3, 4, 5, 2]
    t = Table(; a, b, c, d, e, f)

    T = Filter(row -> all(≤(5), row))
    n, c = apply(T, t)
    @test n.a == [3, 2, 3]
    @test n.b == [2, 4, 5]
    @test n.c == [1, 1, 1]
    @test n.d == [4, 3, 1]
    @test n.e == [5, 5, 2]
    @test n.f == [4, 4, 2]

    # revert test
    @test isrevertible(T) == true
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Filter(row -> any(>(5), row))
    n, c = apply(T, t)
    @test n.a == [1, 4, 5]
    @test n.b == [4, 5, 8]
    @test n.c == [6, 2, 4]
    @test n.d == [7, 5, 4]
    @test n.e == [2, 6, 5]
    @test n.f == [3, 4, 5]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Filter(row -> row.a ≥ 3)
    n, c = apply(T, t)
    @test n.a == [3, 4, 5, 3]
    @test n.b == [2, 5, 8, 5]
    @test n.c == [1, 2, 4, 1]
    @test n.d == [4, 5, 4, 1]
    @test n.e == [5, 6, 5, 2]
    @test n.f == [4, 4, 5, 2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Filter(row -> row.c ≥ 2 && row.e > 4)
    n, c = apply(T, t)
    @test n.a == [4, 5]
    @test n.b == [5, 8]
    @test n.c == [2, 4]
    @test n.d == [5, 4]
    @test n.e == [6, 5]
    @test n.f == [4, 5]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Filter(row -> row.b == 4 || row.f == 4)
    n, c = apply(T, t)
    @test n.a == [3, 2, 1, 4]
    @test n.b == [2, 4, 4, 5]
    @test n.c == [1, 1, 6, 2]
    @test n.d == [4, 3, 7, 5]
    @test n.e == [5, 5, 2, 6]
    @test n.f == [4, 4, 3, 4]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # reapply test
    T = Filter(row -> all(≤(5), row))
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # row table
    rt = Tables.rowtable(t)
    T = Filter(row -> row.b == 4 || row.f == 4)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ
  end

  @testset "DropMissing" begin
    a = [3, 2, missing, 4, 5, 3]
    b = [missing, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, missing]
    d = [4, 3, 7, 5, 4, missing]
    e = [missing, 5, 2, 6, 5, 2]
    f = [4, missing, 3, 4, 5, 2]
    t = Table(; a, b, c, d, e, f)

    T = DropMissing()
    n, c = apply(T, t)
    @test n.a == [4, 5]
    @test n.b == [5, 8]
    @test n.c == [2, 4]
    @test n.d == [5, 4]
    @test n.e == [6, 5]
    @test n.f == [4, 5]

    # revert test
    @test isrevertible(T) == true
    tₒ = revert(T, n, c)
    cols = Tables.columns(t)
    colsₒ = Tables.columns(tₒ)
    colnames = Tables.columnnames(t)
    for n in colnames
      col = Tables.getcolumn(cols, n)
      colₒ = Tables.getcolumn(colsₒ, n)
      @test isequalmissing(col, colₒ)
    end

    # args...
    # integers
    T = DropMissing(1, 3, 4)
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # symbols
    T = DropMissing(:a, :c, :d)
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # strings
    T = DropMissing("a", "c", "d")
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # vector
    # integers
    T = DropMissing([1, 3, 4])
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # symbols
    T = DropMissing([:a, :c, :d])
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # strings
    T = DropMissing(["a", "c", "d"])
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # tuple
    # integers
    T = DropMissing((1, 3, 4))
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # symbols
    T = DropMissing((:a, :c, :d))
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # strings
    T = DropMissing(("a", "c", "d"))
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # regex
    T = DropMissing(r"[acd]")
    n, c = apply(T, t)
    @test isequalmissing(n.a, [3, 2, 4, 5])
    @test isequalmissing(n.b, [missing, 4, 5, 8])
    @test isequalmissing(n.c, [1, 1, 2, 4])
    @test isequalmissing(n.d, [4, 3, 5, 4])
    @test isequalmissing(n.e, [missing, 5, 6, 5])
    @test isequalmissing(n.f, [4, missing, 4, 5])

    # table schema after apply and revert
    T = DropMissing()
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    ttypes = Tables.schema(t).types
    ntypes = Tables.schema(n).types
    @test ntypes[1] == Int
    @test ntypes[2] == Int
    @test ntypes[3] == Int
    @test ntypes[4] == Int
    @test ntypes[5] == Int
    @test ntypes[6] == Int
    @test ttypes == Tables.schema(tₒ).types

    T = DropMissing([:a, :c, :d])
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    ntypes = Tables.schema(n).types
    @test ntypes[1] == Int
    @test ntypes[2] == Union{Missing,Int}
    @test ntypes[3] == Int
    @test ntypes[4] == Int
    @test ntypes[5] == Union{Missing,Int}
    @test ntypes[6] == Union{Missing,Int}
    @test ttypes == Tables.schema(tₒ).types

    T = DropMissing([:b, :e, :f])
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    ntypes = Tables.schema(n).types
    @test ntypes[1] == Union{Missing,Int}
    @test ntypes[2] == Int
    @test ntypes[3] == Union{Missing,Int}
    @test ntypes[4] == Union{Missing,Int}
    @test ntypes[5] == Int
    @test ntypes[6] == Int
    @test ttypes == Tables.schema(tₒ).types

    # reapply test
    T = DropMissing()
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # row table
    rt = Tables.rowtable(t)
    T = DropMissing()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    for (row, rowₒ) in zip(rt, rtₒ)
      @test isequalmissing(row, rowₒ)
    end

    # throws: empty tuple
    @test_throws ArgumentError DropMissing(())

    # throws: empty selection
    @test_throws AssertionError apply(DropMissing(r"g"), t)
    @test_throws AssertionError apply(DropMissing(Symbol[]), t)
    @test_throws AssertionError apply(DropMissing(String[]), t)

    # throws: columns that do not exist in the original table
    @test_throws AssertionError apply(DropMissing(:g, :h), t)
    @test_throws AssertionError apply(DropMissing([:g, :h]), t)
    @test_throws AssertionError apply(DropMissing((:g, :h)), t)
    @test_throws AssertionError apply(DropMissing("g", "h"), t)
    @test_throws AssertionError apply(DropMissing(["g", "h"]), t)
    @test_throws AssertionError apply(DropMissing(("g", "h")), t)
  end

  @testset "Rename" begin
    a = rand(4000)
    b = rand(4000)
    c = rand(4000)
    d = rand(4000)
    t = Table(; a, b, c, d)

    T = Rename(Dict(:a => :x))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :c, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Rename(Dict(:a => :x, :c => :y))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rename with dictionary of strings
    T = Rename(Dict("a" => "x", "c" => "y"))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rename with mixed dictionary
    T = Rename(Dict("a" => :x, :c => "y"))
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rename with string pairs
    T = Rename("a" => "x", "c" => "y")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rename with symbol pairs
    T = Rename(:a => :x, :c => :y)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # rename with mixed pairs
    T = Rename("a" => :x)
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :c, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Rename("a" => :x, :c => "y")
    n, c = apply(T, t)
    @test Tables.columnnames(n) == (:x, :b, :y, :d)
    tₒ = revert(T, n, c)
    @test t == tₒ

    # row table
    rt = Tables.rowtable(t)
    T = Rename(:a => :x, :c => :y)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ

    # reapply test
    T = Rename(:b => :x, :d => :y)
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2
  end

  @testset "StdNames" begin
    names = ["apple banana", "apple\tbanana", "apple_banana", "apple-banana", "apple_Banana"]
    for name in names
      @test TableTransforms._camel(name) == "AppleBanana"
      @test TableTransforms._snake(name) == "apple_banana"
      @test TableTransforms._upper(name) == "APPLEBANANA"
    end

    names = ["a", "A", "_a", "_A", "a ", "A "]
    for name in names
      @test TableTransforms._camel(name) == "A"
      @test TableTransforms._snake(name) == "a"
      @test TableTransforms._upper(name) == "A"
    end

    # special characters
    name = "a&B"
    @test TableTransforms._clean(name) == "aB"

    name = "apple#"
    @test TableTransforms._clean(name) == "apple"

    name = "apple-tree"
    @test TableTransforms._clean(name) == "apple-tree"

    # invariance test
    names = ["AppleTree", "BananaFruit", "PearSeed"]
    for name in names
      @test TableTransforms._camel(name) == name
    end

    names = ["apple_tree", "banana_fruit", "pear_seed"]
    for name in names
      @test TableTransforms._snake(name) == name
    end

    names = ["APPLETREE", "BANANAFRUIT", "PEARSEED"]
    for name in names
      @test TableTransforms._upper(name) == name
    end

    # uniqueness test
    names = (Symbol("AppleTree"), Symbol("apple tree"), Symbol("apple_tree"))
    cols = ([1,2,3], [4,5,6], [7,8,9])
    t = Table(; zip(names, cols)...)
    rt = Tables.rowtable(t)
    T = StdNames(:upper)
    n, c = apply(T, rt)
    columns = Tables.columns(n)
    columnnames = Tables.columnnames(columns)
    @test columnnames == (:APPLETREE, :APPLETREE_, :APPLETREE__)

    # row table test
    names = (:a, Symbol("apple tree"), Symbol("banana tree"))
    cols = ([1,2,3], [4,5,6], [7,8,9])
    t = Table(; zip(names, cols)...)
    rt = Tables.rowtable(t)
    T = StdNames()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    @test isrevertible(T)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ

    # reapply test
    T = StdNames()
    n1, c1 = apply(T, rt)
    n2 = reapply(T, n1, c1)
    @test n1 == n2
  end

  @testset "Replace" begin
    a = [3, 2, 1, 4, 5, 3]
    b = [2, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, 1]
    d = [4, 3, 7, 5, 4, 1]
    e = [5, 5, 2, 6, 5, 2]
    f = [4, 4, 3, 4, 5, 2]
    t = Table(; a, b, c, d, e, f)

    # replace with a value of the same type
    T = Replace(1 => -1, 5 => -5)
    n, c = apply(T, t)
    @test n.a == [3, 2, -1, 4, -5, 3]
    @test n.b == [2, 4, 4, -5, 8, -5]
    @test n.c == [-1, -1, 6, 2, 4, -1]
    @test n.d == [4, 3, 7, -5, 4, -1]
    @test n.e == [-5, -5, 2, 6, -5, 2]
    @test n.f == [4, 4, 3, 4, -5, 2]
    @test isrevertible(T) == true
    tₒ = revert(T, n, c)
    @test t == tₒ

    # table schema after apply and revert
    T = Replace(1 => -1, 5 => -5)
    n, c = apply(T, t)
    types = Tables.schema(t).types
    @test types == Tables.schema(n).types
    tₒ = revert(T, n, c)
    @test types == Tables.schema(tₒ).types

    # replace with a value of another type
    T = Replace(1 => 1.5, 5 => 5.5, 4 => true)
    n, c = apply(T, t)
    @test n.a == Real[3, 2, 1.5, true, 5.5, 3]
    @test n.b == Real[2, true, true, 5.5, 8, 5.5]
    @test n.c == Real[1.5, 1.5, 6, 2, true, 1.5]
    @test n.d == Real[true, 3, 7, 5.5, true, 1.5]
    @test n.e == Real[5.5, 5.5, 2, 6, 5.5, 2]
    @test n.f == Real[true, true, 3, true, 5.5, 2]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # table schema after apply and revert
    T = Replace(1 => 1.5, 5 => 5.5, 4 => true)
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    ttypes = Tables.schema(t).types
    ntypes = Tables.schema(n).types
    @test ntypes[1] == Real
    @test ntypes[2] == Real
    @test ntypes[3] == Real
    @test ntypes[4] == Real
    @test ntypes[5] == Real
    @test ntypes[6] == Real
    @test ttypes == Tables.schema(tₒ).types

    # no occurrences
    T = Replace(10 => 11, 20 => 30)
    n, c = apply(T, t)
    @test t == n
    tₒ = revert(T, n, c)
    @test t == tₒ

    # collumns with diferent types
    a = [3, 2, 1, 4, 5, 3]
    b = [2.5, 4.5, 4.7, 2.5, 2.5, 5.3]
    c = [true, false, false, false, true, false]
    d = ['a', 'b', 'c', 'd', 'e', 'a']
    t = Table(; a, b, c, d)

    T = Replace(3 => -3, 2.5 => 2.0, true => false, 'a' => 'A')
    n, c = apply(T, t)
    @test n.a == [-3, 2, 1, 4, 5, -3]
    @test n.b == [2.0, 4.5, 4.7, 2.0, 2.0, 5.3]
    @test n.c == [false, false, false, false, false, false]
    @test n.d == ['A', 'b', 'c', 'd', 'e', 'A']
    tₒ = revert(T, n, c)
    @test t == tₒ

    # row table
    rt = Tables.rowtable(t)
    T = Replace(3 => -3, 2.5 => 2.0)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ

    # throws
    @test_throws ArgumentError Replace()
  end

  @testset "Coalesce" begin
    a = [3, 2, missing, 4, 5, 3]
    b = [missing, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, missing]
    d = [4, 3, 7, 5, 4, missing]
    e = [missing, 5, 2, 6, 5, 2]
    f = [4, missing, 3, 4, 5, 2]
    t = Table(; a, b, c, d, e, f)

    T = Coalesce(0)
    n, c = apply(T, t)
    @test n.a == [3, 2, 0, 4, 5, 3]
    @test n.b == [0, 4, 4, 5, 8, 5]
    @test n.c == [1, 1, 6, 2, 4, 0]
    @test n.d == [4, 3, 7, 5, 4, 0]
    @test n.e == [0, 5, 2, 6, 5, 2]
    @test n.f == [4, 0, 3, 4, 5, 2]

    # revert test
    @test isrevertible(T) == true
    tₒ = revert(T, n, c)
    cols = Tables.columns(t)
    colsₒ = Tables.columns(tₒ)
    colnames = Tables.columnnames(t)
    for n in colnames
      col = Tables.getcolumn(cols, n)
      colₒ = Tables.getcolumn(colsₒ, n)
      @test isequalmissing(col, colₒ)
    end

    # table schema after apply and revert
    T = Coalesce(0)
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    ttypes = Tables.schema(t).types
    ntypes = Tables.schema(n).types
    @test ntypes[1] == Int
    @test ntypes[2] == Int
    @test ntypes[3] == Int
    @test ntypes[4] == Int
    @test ntypes[5] == Int
    @test ntypes[6] == Int
    @test ttypes == Tables.schema(tₒ).types

    # row table
    rt = Tables.rowtable(t)
    T = Coalesce(0)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    for (row, rowₒ) in zip(rt, rtₒ)
      @test isequalmissing(row, rowₒ)
    end
  end

  @testset "Coerce" begin
    x1 = [1.0, 2.0, 3.0, 4.0, 5.0]
    x2 = [1.0, 2.0, 3.0, 4.0, 5.0]
    x3 = [5.0, 5.0, 5.0, 5.0, 5.0]
    t = Table(;x1, x2, x3)

    T = Coerce(:x1=>Count, :x2=>Count)
    n, c = apply(T, t)
    @test eltype(n.x1) == Int
    @test eltype(n.x2) == Int
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test eltype(tₒ.x1) == eltype(t.x1)
    @test eltype(tₒ.x2) == eltype(t.x2)

    T = Coerce(:x1=>Multiclass, :x2=>Multiclass)
    n, c = apply(T, t)
    @test eltype(n.x1) <: CategoricalValue
    @test eltype(n.x2) <: CategoricalValue
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test eltype(tₒ.x1) == eltype(t.x1)
    @test eltype(tₒ.x2) == eltype(t.x2)

    # row table
    rt = Tables.rowtable(t)
    T = Coerce(:x1 => Count, :x2 => Count)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ
  end

  @testset "RowTable" begin
    a = [3, 2, 1, 4, 5, 3]
    b = [1, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, 1]
    t = Table(; a, b, c)
    T = RowTable()
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test typeof(n) <: Vector
    @test Tables.rowaccess(n)
    @test typeof(tₒ) <: Table
  end

  @testset "ColTable" begin
    a = [3, 2, 1, 4, 5, 3]
    b = [1, 4, 4, 5, 8, 5]
    c = [1, 1, 6, 2, 4, 1]
    t = Table(; a, b, c)
    T = ColTable()
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test typeof(n) <: NamedTuple
    @test Tables.columnaccess(n)
    @test typeof(tₒ) <: Table
  end

  @testset "Identity" begin
    x = rand(4000)
    y = rand(4000)
    t = Table(; x, y)
    T = Identity()
    n, c = apply(T, t)
    @test t == n
    tₒ = revert(T, n, c)
    @test t == tₒ

    # row table
    rt = Tables.rowtable(t)
    T = Identity()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test rt == rtₒ
  end

  @testset "Center" begin
    # using rng for reproducible results
    x = rand(rng, Normal(2, 1), 4000)
    y = rand(rng, Normal(5, 1), 4000)
    t = Table(; x, y)
    T = Center()
    n, c = apply(T, t)
    μ = mean(Tables.matrix(n), dims=1)
    @test isapprox(μ[1], 0; atol=1e-6)
    @test isapprox(μ[2], 0; atol=1e-6)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # visual tests
    if visualtests
      p₁ = scatter(t.x, t.y, label="Original")
      p₂ = scatter(n.x, n.y, label="Center")
      p = plot(p₁, p₂, layout=(1,2))

      @test_reference joinpath(datadir, "center.png") p
    end

    # row table
    rt = Tables.rowtable(t)
    T = Center()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)
  end

  @testset "Scale" begin
    # constant column
    x = fill(3.0, 10)
    y = rand(10)
    t = Table(; x, y)
    T = MinMax()
    n, c = apply(T, t)
    @test n.x == x
    @test n.y != y
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # using rng for reproducible results
    x = rand(rng, Normal(4, 3), 4000)
    y = rand(rng, Normal(7, 5), 4000)
    t = Table(; x, y)
    T = Scale(low=0, high=1)
    n, c = apply(T, t)
    @test all(≤(1), n.x)
    @test all(≥(0), n.x)
    @test all(≤(1), n.y)
    @test all(≥(0), n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # visual tests
    if visualtests
      p₁ = scatter(t.x, t.y, label="Original")
      p₂ = scatter(n.x, n.y, label="Scale")
      p = plot(p₁, p₂, layout=(1,2))

      @test_reference joinpath(datadir, "scale.png") p
    end

    # row table
    rt = Tables.rowtable(t)
    T = Scale()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)

    # columntype does not change
    for FT in (Float16, Float32)
      t = Table(; x=rand(FT, 10))
      for T in (MinMax(), Scale(FT(0), FT(0.5)))
        n, c = apply(T, t)
        @test Tables.columntype(t, :x) == Tables.columntype(n, :x)
        tₒ = revert(T, n, c)
        @test Tables.columntype(t, :x) == Tables.columntype(tₒ, :x)
      end
    end
  end

  @testset "ZScore" begin
    # using rng for reproducible results
    x = rand(rng, Normal(7, 10), 4000)
    y = rand(rng, Normal(15, 2), 4000)
    t = Table(; x, y)
    T = ZScore()
    n, c = apply(T, t)
    μ = mean(Tables.matrix(n), dims=1)
    σ = std(Tables.matrix(n), dims=1)
    @test isapprox(μ[1], 0; atol=1e-6)
    @test isapprox(σ[1], 1; atol=1e-6)
    @test isapprox(μ[2], 0; atol=1e-6)
    @test isapprox(σ[2], 1; atol=1e-6)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # visual tests
    if visualtests
      p₁ = scatter(t.x, t.y, label="Original")
      p₂ = scatter(n.x, n.y, label="ZScore")
      p = plot(p₁, p₂, layout=(1,2))

      @test_reference joinpath(datadir, "zscore.png") p
    end

    # row table
    rt = Tables.rowtable(t)
    T = ZScore()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)
  end

  @testset "Quantile" begin
    t = (z=rand(1000),)
    n, c = apply(Quantile(), t)
    r = revert(Quantile(), n, c)
    @test all(-4 .< extrema(n.z) .< 4)
    @test all(0 .≤ extrema(r.z) .≤ 1)

    # constant column
    x = fill(3.0, 10)
    y = rand(10)
    t = Table(; x, y)
    T = Quantile()
    n, c = apply(T, t)
    @test maximum(abs, n.x - x) < 0.1
    @test n.y != y
    tₒ = revert(T, n, c)
    @test tₒ.x == t.x

    # row table
    rt = Tables.rowtable(t)
    T = Quantile()
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    for (row, rowₒ) in zip(rt, rtₒ)
      @test row.x == rowₒ.x
    end
  end

  @testset "Functional" begin
    x = π*rand(1500)
    y = π*rand(1500)
    t = Table(; x, y)
    T = Functional(cos)
    n, c = apply(T, t)
    @test all(x -> -1 ≤ x ≤ 1, n.x)
    @test all(y -> -1 ≤ y ≤ 1, n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = 2*(rand(1500) .- 0.5)
    y = 2*(rand(1500) .- 0.5)
    t = Table(; x, y)
    T = Functional(acos)
    n, c = apply(T, t)
    @test all(x -> 0 ≤ x ≤ π, n.x)
    @test all(y -> 0 ≤ y ≤ π, n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = π*(rand(1500) .- 0.5)
    y = π*(rand(1500) .- 0.5)
    t = Table(; x, y)
    T = Functional(sin)
    n, c = apply(T, t)
    @test all(x -> -1 ≤ x ≤ 1, n.x)
    @test all(y -> -1 ≤ y ≤ 1, n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = 2*(rand(1500) .- 0.5)
    y = 2*(rand(1500) .- 0.5)
    t = Table(; x, y)
    T = Functional(asin)
    n, c = apply(T, t)
    @test all(x -> -π/2 ≤ x ≤ π/2, n.x)
    @test all(y -> -π/2 ≤ y ≤ π/2, n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = rand(Normal(0,25), 1500)
    y = x + rand(Normal(10,2), 1500)
    t = Table(; x, y)
    T = Functional(exp)
    n, c = apply(T, t)
    @test all(>(0), n.x)
    @test all(>(0), n.y)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = rand(Normal(0,25), 1500)
    y = x + rand(Normal(10,2), 1500)
    t = Table(; x, y)
    T = Functional(x -> x)
    n, c = apply(T, t)
    @test t == n
    @test isrevertible(T) == false

    # functor tests
    x = rand(1500)
    y = rand(1500)
    t = Table(; x, y)
    f = Polynomial(1, 2, 3) # f(x) = 1 + 2x + 3x²
    T = Functional(f)
    n, c = apply(T, t)
    @test f.(x) == n.x
    @test f.(y) == n.y
    @test all(≥(1), n.x)
    @test all(≥(1), n.y)
    @test isrevertible(T) == false

    # apply functions to specific columns
    x = π*rand(1500)
    y = π*(rand(1500) .- 0.5)
    z = x + y
    t = Table(; x, y, z)
    T = Functional(:x => cos, :y => sin)
    n, c = apply(T, t)
    @test all(x -> -1 ≤ x ≤ 1, n.x)
    @test all(y -> -1 ≤ y ≤ 1, n.y)
    @test t.z == n.z
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = 2*(rand(1500) .- 0.5)
    y = 2*(rand(1500) .- 0.5)
    z = x + y
    t = Table(; x, y, z)
    T = Functional("x" => acos, "y" => asin)
    n, c = apply(T, t)
    @test all(x -> 0 ≤ x ≤ π, n.x)
    @test all(y -> -π/2 ≤ y ≤ π/2, n.y)
    @test t.z == n.z
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    T = Functional(:x => cos, :y => sin)
    @test isrevertible(T) == true
    T = Functional("x" => cos, "y" => sin)
    @test isrevertible(T) == true
    T = Functional(:x => abs, :y => sin)
    @test isrevertible(T) == false
    T = Functional("x" => abs, "y" => sin)
    @test isrevertible(T) == false

    # row table
    x = π*rand(1500)
    y = π*rand(1500)
    t = Table(; x, y)
    rt = Tables.rowtable(t)
    T = Functional(cos)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)

    # throws
    @test_throws ArgumentError Functional()
    t = Table(x = rand(15), y = rand(15))
    T = Functional(Polynomial(1, 2, 3))
    n, c = apply(T, t)
    @test_throws AssertionError revert(T, n, c)
    T = Functional(:x => abs, :y => sin)
    n, c = apply(T, t)
    @test_throws AssertionError revert(T, n, c)
  end

  @testset "Levels" begin
    a = categorical(["yes", "no", "no", "no", "yes"])
    b = categorical([1, 2, 4, 2, 8], ordered=false)
    c = categorical([1, 2, 1, 2, 1])
    d = [1, 23, 5, 7, 7]
    e = categorical([2, 3, 1, 4, 1])
    t = Table(; a, b, c, d, e)

    T = Levels(:a => ["yes", "no"], :c => [1, 2, 4], :d => [1, 23, 5, 7], :e => 1:5)
    n, c = apply(T, t)
    @test levels(n.a) == ["yes", "no"]
    @test levels(n.c) == [1, 2, 4]
    @test levels(n.e) == [1, 2, 3, 4, 5]
    tₒ = revert(T, n, c)
    @test levels(tₒ.a) == ["no", "yes"]
    @test levels(tₒ.c) == [1, 2]
    @test levels(tₒ.e) == [1, 2, 3, 4]

    T = Levels("a" => ["yes", "no"], "c" => [1, 2, 4])
    n, c = apply(T, t)
    @test levels(n.a) == ["yes", "no"]
    @test levels(n.c) == [1, 2, 4]
    tₒ = revert(T, n, c)
    @test levels(tₒ.a) == ["no", "yes"]
    @test levels(tₒ.c) == [1, 2]

    T = Levels(:a => ["yes", "no"], :c => [1, 2, 4], :d => [1, 23, 5, 7], ordered=[:b])
    n, c = apply(T, t)
    @test levels(n.a) == ["yes", "no"]
    @test isordered(n.b) == true
    @test levels(n.c) == [1, 2, 4]
    tₒ = revert(T, n, c)
    @test levels(tₒ.a) == ["no", "yes"]
    @test levels(tₒ.c) == [1, 2]
    @test isordered(tₒ.b) == false

    T = Levels("a" => ["yes", "no"], "c" => [1, 2, 4], "e" => 5:-1:1, ordered=["b", "e"])
    n, c = apply(T, t)
    @test isordered(n.b) == true
    @test levels(n.e) == [5, 4, 3, 2, 1]
    @test levels(n.c) == [1, 2, 4]
    tₒ = revert(T, n, c)
    @test isordered(tₒ.b) == false
    @test levels(tₒ.e) == [1, 2, 3, 4]

    T = Levels(:a => ["yes", "no"], :c => [1, 2, 4], :d => [1, 23, 5, 7], ordered=[:a, :b, :d])
    n, c = apply(T, t)
    @test levels(n.a) == ["yes", "no"]
    @test isordered(n.a) == true
    @test isordered(n.b) == true
    @test isordered(n.d) == true
    tₒ = revert(T, n, c)
    @test typeof(tₒ.d) == Vector{Int64}
    @test isordered(tₒ.a) == false
    @test isordered(tₒ.b) == false
  end

  @testset "Sort" begin
    a = [5, 3, 1, 2]
    b = [2, 4, 8, 5]
    c = [3, 2, 1, 4]
    d = [4, 3, 7, 5]
    t = Table(; a, b, c, d)

    T = Sort(:a)
    n, c = apply(T, t)
    @test Tables.schema(t) == Tables.schema(n)
    @test n.a == [1, 2, 3, 5]
    @test n.b == [8, 5, 4, 2]
    @test n.c == [1, 4, 2, 3]
    @test n.d == [7, 5, 3, 4]
    @test isrevertible(T) == true
    tₒ = revert(T, n, c)
    @test t == tₒ

    # descending order test
    T = Sort(:b, rev=true)
    n, c = apply(T, t)
    @test Tables.schema(t) == Tables.schema(n)
    @test n.a == [1, 2, 3, 5]
    @test n.b == [8, 5, 4, 2]
    @test n.c == [1, 4, 2, 3]
    @test n.d == [7, 5, 3, 4]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # random test
    a = rand(200)
    b = rand(200)
    c = rand(200)
    d = rand(200)
    t = Table(; a, b, c, d)

    T = Sort(:c)
    n, c = apply(T, t)

    @test Tables.schema(t) == Tables.schema(n)
    @test issetequal(Tables.rowtable(t), Tables.rowtable(n))
    @test issorted(Tables.getcolumn(n, :c))
    tₒ = revert(T, n, c)
    @test t == tₒ

    # sort by multiple columns
    a = [-2, -1, -2, 2, 1, -1, 1, 2]
    b = [-8, -4, 6, 9, 8, 2, 2, -8]
    c = [-3, 6, 5, 4, -8, -7, -1, -10]
    t = Table(; a, b, c)

    T = Sort(1, 2)
    n, c = apply(T, t)
    @test n.a == [-2, -2, -1, -1, 1, 1, 2, 2]
    @test n.b == [-8, 6, -4, 2, 2, 8, -8, 9]
    @test n.c == [-3, 5, 6, -7, -1, -8, -10, 4]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Sort([:a, :c], rev=true)
    n, c = apply(T, t)
    @test n.a == [2, 2, 1, 1, -1, -1, -2, -2]
    @test n.b == [9, -8, 2, 8, -4, 2, 6, -8]
    @test n.c == [4, -10, -1, -8, 6, -7, 5, -3]
    tₒ = revert(T, n, c)
    @test t == tₒ

    T = Sort(("b", "c"), by=row -> abs.(row))
    n, c = apply(T, t)
    @test n.a == [1, -1, -1, -2, -2, 1, 2, 2]
    @test n.b == [2, 2, -4, 6, -8, 8, -8, 9]
    @test n.c == [-1, -7, 6, 5, -3, -8, -10, 4]
    tₒ = revert(T, n, c)
    @test t == tₒ

    # throws: Sort without arguments
    @test_throws ArgumentError Sort()
    @test_throws ArgumentError Sort(())

    # throws: empty selection
    @test_throws AssertionError apply(Sort(r"x"), t)
    @test_throws AssertionError apply(Sort(Symbol[]), t)
    @test_throws AssertionError apply(Sort(String[]), t)

    # throws: columns that do not exist in the original table
    @test_throws AssertionError apply(Sort([:d, :e]), t)
    @test_throws AssertionError apply(Sort(("d", "e")), t)

    # Invalid kwarg
    @test_throws MethodError apply(Sort(:a, :b, test=1), t)
  end

  @testset "EigenAnalysis" begin
    # PCA test
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    t = Table(; x, y)
    T = EigenAnalysis(:V)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Σ[1,1] > 1
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test Σ[2,2] > 1
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # DRS test
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    t = Table(; x, y)
    T = EigenAnalysis(:VD)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # SDS test
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    t = Table(; x, y)
    T = EigenAnalysis(:VDV)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # using rng for reproducible results
    x = rand(rng, Normal(0, 10), 4000)
    y = x + rand(rng, Normal(0, 2), 4000)
    t₁ = Table(; x, y)
    t₂, c₂ = apply(EigenAnalysis(:V), t₁)
    t₃, c₃ = apply(EigenAnalysis(:VD), t₁)
    t₄, c₄ = apply(EigenAnalysis(:VDV), t₁)
    t₅, c₅ = apply(PCA(), t₁)
    t₆, c₆ = apply(DRS(), t₁)
    t₇, c₇ = apply(SDS(), t₁)

    # visual tests
    if visualtests
      p₁ = scatter(t₁.x, t₁.y, label="Original")
      p₂ = scatter(t₂.PC1, t₂.PC2, label="V")
      p₃ = scatter(t₃.PC1, t₃.PC2, label="VD")
      p₄ = scatter(t₄.PC1, t₄.PC2, label="VDV")
      p₅ = scatter(t₅.PC1, t₅.PC2, label="PCA")
      p₆ = scatter(t₆.PC1, t₆.PC2, label="DRS")
      p₇ = scatter(t₇.PC1, t₇.PC2, label="SDS")
      p = plot(p₁, p₂, p₃, p₄, layout=(2,2))
      q = plot(p₂, p₃, p₄, p₅, p₆, p₇, layout=(2,3))

      @test_reference joinpath(datadir, "eigenanalysis-1.png") p
      @test_reference joinpath(datadir, "eigenanalysis-2.png") q
    end

    # row table
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    t = Table(; x, y)
    rt = Tables.rowtable(t)
    T = EigenAnalysis(:V)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)

    # maxdim
    x = randn(1000)
    y = x + randn(1000)
    z = 2x - y + randn(1000)
    t = Table(; x, y, z)

    # PCA
    T = PCA(maxdim=2)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)

    # DRS
    T = DRS(maxdim=2)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)

    # SDS
    T = SDS(maxdim=2)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)

    # pratio
    a = randn(rng, 1000)
    b = randn(rng, 1000)
    c = a + randn(rng, 1000)
    d = b - randn(rng, 1000)
    e = 3d + c - randn(rng, 1000)
    t = Table(; a, b, c, d, e)

    # PCA
    T = PCA(pratio=0.90)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2, :PC3)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[1,3], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[2,3], 0; atol=1e-6)
    @test isapprox(Σ[3,1], 0; atol=1e-6)
    @test isapprox(Σ[3,2], 0; atol=1e-6)

    # DRS
    T = DRS(pratio=0.90)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2, :PC3)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[1,3], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[2,3], 0; atol=1e-6)
    @test isapprox(Σ[3,1], 0; atol=1e-6)
    @test isapprox(Σ[3,2], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)
    @test isapprox(Σ[3,3], 1; atol=1e-6)

    # SDS
    T = SDS(pratio=0.90)
    n, c = apply(T, t)
    Σ = cov(Tables.matrix(n))
    @test Tables.columnnames(n) == (:PC1, :PC2, :PC3)
    @test isapprox(Σ[1,2], 0; atol=1e-6)
    @test isapprox(Σ[1,3], 0; atol=1e-6)
    @test isapprox(Σ[2,1], 0; atol=1e-6)
    @test isapprox(Σ[2,3], 0; atol=1e-6)
    @test isapprox(Σ[3,1], 0; atol=1e-6)
    @test isapprox(Σ[3,2], 0; atol=1e-6)
    @test isapprox(Σ[1,1], 1; atol=1e-6)
    @test isapprox(Σ[2,2], 1; atol=1e-6)
    @test isapprox(Σ[3,3], 1; atol=1e-6)
  end

  @testset "Sequential" begin
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    T = Scale(low=0.2, high=0.8) → EigenAnalysis(:VDV)
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    T = Select(:x, :z) → ZScore() → EigenAnalysis(:V) → Scale(low=0, high=1)
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # reapply with Sequential transform
    t = Table(x=rand(1000))
    T = ZScore() → Quantile()
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # row table
    rt = Tables.rowtable(t)
    T = Scale(low=0.2, high=0.8) → EigenAnalysis(:VDV)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)
  end

  @testset "Parallel" begin
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    T = Scale(low=0.3, high=0.6) ⊔ EigenAnalysis(:VDV)
    n, c = apply(T, t)
    tₒ = revert(T, n, c)
    @test Tables.matrix(t) ≈ Tables.matrix(tₒ)

    # check cardinality of Parallel
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    T = ZScore() ⊔ EigenAnalysis(:V)
    n = T(t)
    @test length(Tables.columnnames(n)) == 6

    # distributivity with respect to Sequential
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    T₁ = Center()
    T₂ = Scale(low=0.2, high=0.8)
    T₃ = EigenAnalysis(:VD)
    P₁ = T₁ → (T₂ ⊔ T₃)
    P₂ = (T₁ → T₂) ⊔ (T₁ → T₃)
    n₁ = P₁(t)
    n₂ = P₂(t)
    @test Tables.matrix(n₁) ≈ Tables.matrix(n₂)

    # reapply with Parallel transform
    t = Table(x=rand(1000))
    T = ZScore() ⊔ Quantile()
    n1, c1 = apply(T, t)
    n2 = reapply(T, t, c1)
    @test n1 == n2

    # row table
    x = rand(Normal(0, 10), 1500)
    y = x + rand(Normal(0, 2), 1500)
    z = y + rand(Normal(0, 5), 1500)
    t = Table(; x, y, z)
    rt = Tables.rowtable(t)
    T = Scale(low=0.3, high=0.6) ⊔ EigenAnalysis(:VDV)
    n, c = apply(T, rt)
    @test Tables.isrowtable(n)
    rtₒ = revert(T, n, c)
    @test Tables.matrix(rt) ≈ Tables.matrix(rtₒ)

    # https://github.com/JuliaML/TableTransforms.jl/issues/80
    t = (a=rand(3), b=rand(3))
    T = Select(:a) ⊔ Select(:b)
    n, c = apply(T, t)
    @test t == n
    tₒ = revert(T, n, c)
    @test tₒ == t
  end

  @testset "Miscellaneous" begin
    # make sure transforms work with
    # single-column tables
    t = Table(x=rand(10000))
    n, c = apply(ZScore(), t)
    r = revert(ZScore(), n, c)
    @test isapprox(mean(n.x), 0.0, atol=1e-8)
    @test isapprox(std(n.x), 1.0, atol=1e-8)
    @test isapprox(mean(r.x), mean(t.x), atol=1e-8)
    @test isapprox(std(r.x), std(t.x), atol=1e-8)
  end
end


text:
const DSE = DocStringExtensions

module M

export f

f(x) = x

g(x = 1, y = 2, z = 3; kwargs...) = x

typealias A{T} Union{Vector{T}, Matrix{T}}

h_1(x::A) = x
h_2(x::A{Int}) = x

type T
    a
    b
    c
end

immutable K
    K(; a = 1) = new()
end

abstract AbstractType <: Integer

immutable CustomType{S, T <: Integer} <: Integer
end

bitstype 8 BitType8

bitstype 32 BitType32 <: Real

end

@testset "" begin
    @testset "format" begin
        # Setup.
        doc = Docs.DocStr(Core.svec(), Nullable(), Dict())
        buf = IOBuffer()

        # Errors.
        @test_throws ErrorException DSE.format(nothing, buf, doc)

        @testset "imports & exports" begin
            # Module imports.
            doc.data = Dict(
                :binding => Docs.Binding(Main, :M),
                :typesig => Union{},
            )
            DSE.format(IMPORTS, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n  - `Base`\n")
            @test contains(str, "\n  - `Core`\n")

            # Module exports.
            DSE.format(EXPORTS, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n  - [`f`](@ref)\n")
        end

        @testset "type fields" begin
            doc.data = Dict(
                :binding => Docs.Binding(M, :T),
                :fields => Dict(
                    :a => "one",
                    :b => "two",
                ),
            )
            DSE.format(FIELDS, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "  - `a`")
            @test contains(str, "  - `b`")
            @test contains(str, "  - `c`")
            @test contains(str, "one")
            @test contains(str, "two")
        end

        @testset "method lists" begin
            doc.data = Dict(
                :binding => Docs.Binding(M, :f),
                :typesig => Tuple{Any},
                :module => M,
            )
            DSE.format(METHODLIST, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "```julia")
            @test contains(str, "f(x)")
            @test contains(str, "[`$(joinpath("DocStringExtensions", "test", "tests.jl"))")
        end

        @testset "method signatures" begin
            doc.data = Dict(
                :binding => Docs.Binding(M, :f),
                :typesig => Tuple{Any},
                :module => M,
            )
            DSE.format(SIGNATURES, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\nf(x)\n")
            @test contains(str, "\n```\n")

            doc.data = Dict(
                :binding => Docs.Binding(M, :g),
                :typesig => Union{Tuple{}, Tuple{Any}},
                :module => M,
            )
            DSE.format(SIGNATURES, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\ng()\n")
            @test contains(str, "\ng(x)\n")
            @test contains(str, "\n```\n")

            doc.data = Dict(
                :binding => Docs.Binding(M, :g),
                :typesig => Union{Tuple{}, Tuple{Any}, Tuple{Any, Any}, Tuple{Any, Any, Any}},
                :module => M,
            )
            DSE.format(SIGNATURES, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\ng()\n")
            @test contains(str, "\ng(x)\n")
            @test contains(str, "\ng(x, y)\n")
            @test contains(str, "\ng(x, y, z; kwargs...)\n")
            @test contains(str, "\n```\n")
        end

        @testset "type definitions" begin
            doc.data = Dict(
                :binding => Docs.Binding(M, :AbstractType),
                :typesig => Union{},
                :module => M,
            )
            DSE.format(TYPEDEF, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\nabstract AbstractType <: Integer\n")
            @test contains(str, "\n```\n")

            doc.data = Dict(
                :binding => Docs.Binding(M, :CustomType),
                :typesig => Union{},
                :module => M,
            )
            DSE.format(TYPEDEF, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\nimmutable CustomType{S, T<:Integer} <: Integer\n")
            @test contains(str, "\n```\n")

            doc.data = Dict(
                :binding => Docs.Binding(M, :BitType8),
                :typesig => Union{},
                :module => M,
            )
            DSE.format(TYPEDEF, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\nbitstype 8 BitType8\n")
            @test contains(str, "\n```\n")

            doc.data = Dict(
                :binding => Docs.Binding(M, :BitType32),
                :typesig => Union{},
                :module => M,
            )
            DSE.format(TYPEDEF, buf, doc)
            str = takebuf_string(buf)
            @test contains(str, "\n```julia\n")
            @test contains(str, "\nbitstype 32 BitType32 <: Real")
            @test contains(str, "\n```\n")
        end
    end
    @testset "utilities" begin
        @testset "keywords" begin
            @test DSE.keywords(M.T, first(methods(M.T))) == Symbol[]
            @test DSE.keywords(M.K, first(methods(M.K))) == [:a]
            @test DSE.keywords(M.f, first(methods(M.f))) == Symbol[]
            let f = (() -> ()),
                m = first(methods(f))
                @test DSE.keywords(f, m) == Symbol[]
            end
            let f = ((a) -> ()),
                m = first(methods(f))
                @test DSE.keywords(f, m) == Symbol[]
            end
            let f = ((; a = 1) -> ()),
                m = first(methods(f))
                @test DSE.keywords(f, m) == [:a]
            end
            let f = ((; a = 1, b = 2) -> ()),
                m = first(methods(f))
                @test DSE.keywords(f, m) == [:a, :b]
            end
            let f = ((; a...) -> ()),
                m = first(methods(f))
                @test DSE.keywords(f, m) == [Symbol("a...")]
            end
        end
        @testset "arguments" begin
            @test DSE.arguments(first(methods(M.T))) == [:a, :b, :c]
            @test DSE.arguments(first(methods(M.K))) == Symbol[]
            @test DSE.arguments(first(methods(M.f))) == [:x]
            let m = first(methods(() -> ()))
                @test DSE.arguments(m) == Symbol[]
            end
            let m = first(methods((a) -> ()))
                @test DSE.arguments(m) == [:a]
            end
            let m = first(methods((; a = 1) -> ()))
                @test DSE.arguments(m) == Symbol[]
            end
            let m = first(methods((x; a = 1, b = 2) -> ()))
                @test DSE.arguments(m) == Symbol[:x]
            end
            let m = first(methods((; a...) -> ()))
                @test DSE.arguments(m) == Symbol[]
            end
        end
        @testset "printmethod" begin
            let b = Docs.Binding(M, :T),
                f = M.T,
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "T(a, b, c)"
            end
            let b = Docs.Binding(M, :K),
                f = M.K,
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "K(; a)"
            end
            let b = Docs.Binding(M, :f),
                f = M.f,
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "f(x)"
            end
            let b = Docs.Binding(Main, :f),
                f = () -> (),
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "f()"
            end
            let b = Docs.Binding(Main, :f),
                f = (a) -> (),
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "f(a)"
            end
            let b = Docs.Binding(Main, :f),
                f = (; a = 1) -> (),
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "f(; a)"
            end
            let b = Docs.Binding(Main, :f),
                f = (; a = 1, b = 2) -> (),
                m = first(methods(f))
                # Keywords are not ordered, so check for both combinations.
                @test DSE.printmethod(b, f, m) in ("f(; a, b)", "f(; b, a)")
            end
            let b = Docs.Binding(Main, :f),
                f = (; a...) -> (),
                m = first(methods(f))
                @test DSE.printmethod(b, f, m) == "f(; a...)"
            end
            let b = Docs.Binding(Main, :f),
                f = (; a = 1, b = 2, c...) -> (),
                m = first(methods(f))
                # Keywords are not ordered, so check for both combinations.
                @test DSE.printmethod(b, f, m) in ("f(; a, b, c...)", "f(; b, a, c...)")
            end
        end
        @testset "getmethods" begin
            @test length(DSE.getmethods(M.f, Union{})) == 1
            @test length(DSE.getmethods(M.f, Tuple{})) == 0
            @test length(DSE.getmethods(M.f, Union{Tuple{}, Tuple{Any}})) == 1
        end
        @testset "methodgroups" begin
            @test length(DSE.methodgroups(M.f, Tuple{Any}, M)) == 1
            @test length(DSE.methodgroups(M.f, Tuple{Any}, M)[1]) == 1
            @test length(DSE.methodgroups(M.h_1, Tuple{M.A}, M)) == 1
            @test length(DSE.methodgroups(M.h_1, Tuple{M.A}, M)[1]) == 1
            @test length(DSE.methodgroups(M.h_2, Tuple{M.A{Int}}, M)) == 1
            @test length(DSE.methodgroups(M.h_2, Tuple{M.A{Int}}, M)[1]) == 1
        end
        @testset "alltypesigs" begin
            @test DSE.alltypesigs(Union{}) == Core.svec()
            @test DSE.alltypesigs(Union{Tuple{}}) == Core.svec(Tuple{})
            @test DSE.alltypesigs(Tuple{}) == Core.svec(Tuple{})
        end
        @testset "groupby" begin
            let groups = DSE.groupby(Int, Vector{Int}, collect(1:10)) do each
                    mod(each, 3), each
                end
                @test groups == Pair{Int, Vector{Int}}[
                    0 => [3, 6, 9],
                    1 => [1, 4, 7, 10],
                    2 => [2, 5, 8],
                ]
            end
        end
        @testset "url" begin
            @test !isempty(DSE.url(first(methods(sin))))
            @test !isempty(DSE.url(first(methods(DSE.parsedocs))))
            @test !isempty(DSE.url(first(methods(M.f))))
            @test !isempty(DSE.url(first(methods(M.K))))
        end
        @testset "comparemethods" begin
            let f = first(methods(M.f)),
                g = first(methods(M.g))
                @test !DSE.comparemethods(f, f)
                @test DSE.comparemethods(f, g)
                @test !DSE.comparemethods(g, f)
            end
        end
    end
end

DSE.parsedocs(DSE)