## Copyright (C) 2026 Andreas Bertsatos <abertsatos@biol.uoa.gr>
##
## This file is part of the datatypes package for GNU Octave.
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 3 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

## A table may hold a datetime variable, and several assertions below render one
## through 'table2cell', 'csv2table', 'summary' and 'groupsummary'.  Those
## renderings use the default display formats, which persist across sessions, so
## a user who has set one with 'datetime.setDefaultFormats' would otherwise see
## this file fail.  Save whatever is stored, run at the factory defaults, and put
## it back in the closing block; a format that was never set is restored as
## absent rather than as its factory value.
##
## The saved formats live in a persistent inside this helper rather than in a
## '%!shared' variable, because the '%!shared' blocks further down clear any
## shared variable declared before them, and the assertions that need the guard
## sit on both sides of them.
%!function datetimeFormatGuard (mode)
%!  persistent saved
%!  if (strcmp (mode, 'save'))
%!    saved = {'', ''};
%!    if (ispref ('datatypes', 'datetime_DefaultFormat'))
%!      saved{1} = getpref ('datatypes', 'datetime_DefaultFormat');
%!    endif
%!    if (ispref ('datatypes', 'datetime_DefaultDateFormat'))
%!      saved{2} = getpref ('datatypes', 'datetime_DefaultDateFormat');
%!    endif
%!  endif
%!  datetime.setDefaultFormats ('reset');
%!  if (strcmp (mode, 'restore'))
%!    if (! isempty (saved{1}))
%!      datetime.setDefaultFormats ('default', saved{1});
%!    endif
%!    if (! isempty (saved{2}))
%!      datetime.setDefaultFormats ('defaultdate', saved{2});
%!    endif
%!  endif
%!endfunction
%!test
%! datetimeFormatGuard ('save');

################################################################################
##                    ** Create Table and Convert Type **                     ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'table'            'table2array'      'table2cell'       'table2struct'    ##
##                                                                            ##
################################################################################

## Test the constructor
%!test  # variables become table columns; names auto-detected from inputname
%! Name = {"John"; "Mary"; "Peter"; "Barbara"};
%! Age = [23; 34; 42; 28];
%! Height = [167; 163; 183; 178];
%! T = table (Name, Age, Height);
%! assert_equal (size (T), [4, 3]);
%! assert_equal (size (T.Age), [4, 1]);
%! assert_equal (mean (T.Age), 31.75);
%! assert_equal (T.Properties.VariableNames, {"Name", "Age", "Height"});
%!test  # an empty constructor yields a 0x0 table
%! T = table ();
%! assert_equal (size (T), [0, 0]);
%! assert_equal (height (T), 0);
%! assert_equal (width (T), 0);
%! assert_equal (class (T), "table");
%! assert_equal (istable (T), true);
%!test  # a single variable gives a one-variable table with an auto name
%! x = [1; 2; 3];
%! T = table (x);
%! assert_equal (size (T), [3, 1]);
%! assert_equal (T.Properties.VariableNames, {"x"});
%! assert_equal (T.x, [1; 2; 3]);
%!test  # an unnamed expression falls back to 'Var<N>' names
%! T = table ([1; 2], [3; 4]);
%! assert_equal (T.Properties.VariableNames, {"Var1", "Var2"});
%!test  # a multicolumn variable counts as a single table variable
%! T = table ([1, 2; 3, 4; 5, 6], 'VariableNames', {'bp'});
%! assert_equal (size (T), [3, 1]);
%! assert_equal (width (T), 1);
%! assert_equal (size (T.bp), [3, 2]);
%!test  # 'VariableNames' overrides the detected names
%! Name = {"John"; "Mary"; "Peter"; "Barbara"};
%! Age = [23; 34; 42; 28];
%! Height = [167; 163; 183; 178];
%! T = table (Name, Age, Height, "VariableNames", {"A", "B", "C"});
%! assert_equal (T.Properties.VariableNames, {"A", "B", "C"});
%!test  # 'VariableNames' may be given as a string array
%! T = table (1, 2, 'VariableNames', string ({'AA', 'BB'}));
%! assert_equal (T.Properties.VariableNames, {'AA', 'BB'});
%! assert_equal (class (T.Properties.VariableNames), 'cell');
%!test  # a multicolumn variable is kept intact
%! Name = {"John"; "Mary"; "Peter"; "Barbara"};
%! Age = [23; 34; 42; 28];
%! Height = [167; 163; 183; 178];
%! BloodPressure = [114, 83; 119, 75; 115, 73; 107, 80];
%! T = table (Name, Age, Height, BloodPressure);
%! assert_equal (size (T), [4, 4]);
%! assert_equal (size (T.BloodPressure), [4, 2]);
%!test  # 'RowNames' labels the rows without adding a variable
%! T = table ([1; 2; 3], 'VariableNames', {'x'}, ...
%!            'RowNames', {'a'; 'b'; 'c'});
%! assert_equal (size (T), [3, 1]);
%! assert_equal (T.Properties.RowNames, {'a'; 'b'; 'c'});
%!test  # 'RowNames' may be given as a string array
%! T = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', string ({'a', 'b'}));
%! assert_equal (T.Properties.RowNames, {'a'; 'b'});
%!test  # 'DimensionNames' sets the row/variable dimension labels
%! T = table ([1; 2], 'VariableNames', {'x'}, ...
%!            'DimensionNames', {'Samples', 'Measures'});
%! assert_equal (T.Properties.DimensionNames, {'Samples', 'Measures'});
%!test  # the default dimension names are 'Row' and 'Variables'
%! T = table ([1; 2]);
%! assert_equal (T.Properties.DimensionNames, {'Row', 'Variables'});
%!test  # heterogeneous variable types are preserved
%! T = table ([1; 2], logical ([1; 0]), {'a'; 'b'}, string ({'c'; 'd'}), ...
%!            'VariableNames', {'n', 'l', 'c', 's'});
%! assert_equal (class (T.n), 'double');
%! assert_equal (class (T.l), 'logical');
%! assert_equal (class (T.c), 'cell');
%! assert_equal (class (T.s), 'string');
%!test  # variables with zero rows are allowed
%! T = table (zeros (0, 1), cell (0, 1), 'VariableNames', {'a', 'b'});
%! assert_equal (size (T), [0, 2]);
%! assert_equal (height (T), 0);
%! assert_equal (width (T), 2);
%!test  # 'VariableDescriptions' and 'VariableUnits' default to empty
%! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'});
%! assert_equal (T.Properties.VariableDescriptions, {'', ''});
%! assert_equal (T.Properties.VariableUnits, {'', ''});
%!test  # a nested table is accepted as a variable
%! T = table ([1; 2], table ([3; 4], [5; 6]), 'VariableNames', {'a', 'b'});
%! assert_equal (width (T), 2);
%! assert_equal (class (T.b), 'table');
## Test the constructor -- 'Size'/'VariableTypes' preallocation
%!test  # preallocates a table of the requested size with default values
%! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', 'string'});
%! assert_equal (size (T), [4, 3]);
%! assert_equal (T.Properties.VariableNames, {'Var1', 'Var2', 'Var3'});
%!test  # 'VariableNames' names the preallocated variables
%! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', 'string'}, ...
%!            'VariableNames', {'A', 'B', 'C'});
%! assert_equal (T.Properties.VariableNames, {'A', 'B', 'C'});
%!test  # numeric and logical types default to zeros/false
%! T = table ('Size', [2, 3], 'VariableTypes', {'double', 'int8', 'logical'});
%! assert_equal (T.Var1, [0; 0]);
%! assert_equal (T.Var2, int8 ([0; 0]));
%! assert_equal (class (T.Var2), 'int8');
%! assert_equal (T.Var3, [false; false]);
%!test  # the 'doublenan'/'singlenan' types default to NaN
%! T = table ('Size', [3, 2], 'VariableTypes', {'doublenan', 'singlenan'});
%! assert_equal (all (isnan (T.Var1)), true);
%! assert_equal (class (T.Var2), 'single');
%! assert_equal (all (isnan (T.Var2)), true);
%!test  # categorical, datetime, duration and string default to missing
%! T = table ('Size', [2, 4], 'VariableTypes', ...
%!            {'categorical', 'datetime', 'duration', 'string'});
%! assert_equal (all (ismissing (T.Var1)), true);
%! assert_equal (all (isnat (T.Var2)), true);
%! assert_equal (seconds (T.Var3), [0; 0]);
%! assert_equal (all (ismissing (T.Var4)), true);
%!test  # cellstr, cell and struct types preallocate accordingly
%! T = table ('Size', [3, 3], 'VariableTypes', {'cellstr', 'cell', 'struct'});
%! assert_equal (iscellstr (T.Var1), true);
%! assert_equal (T.Var1, {''; ''; ''});
%! assert_equal (class (T.Var2), 'cell');
%! assert_equal (class (T.Var3), 'struct');
%!test  # the 'char' type preallocates a cell array of empty character vectors
%! T = table ('Size', [3, 1], 'VariableTypes', {'char'});
%! assert_equal (class (T.Var1), 'cell');
%! assert_equal (iscellstr (T.Var1), true);
%! assert_equal (T.Var1, {''; ''; ''});
%!test  # a 'table' typed variable preallocates an empty nested table
%! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', 'table'}, ...
%!            'VariableNames', {'A', 'B', 'C'});
%! assert_equal (size (T), [4, 3]);
%! assert_equal (class (T.A), 'double');
%! assert_equal (class (T.B), 'datetime');
%! assert_equal (class (T.C), 'table');
%! assert_equal (size (T.C), [0, 1]);
%!test  # 'RowNames' labels the preallocated rows
%! T = table ('Size', [3, 1], 'VariableTypes', {'double'}, ...
%!            'RowNames', {'a'; 'b'; 'c'});
%! assert_equal (T.Properties.RowNames, {'a'; 'b'; 'c'});
## Test input validation for the constructor
%!error <table: 'VariableNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "VariableNames", {2, 3});
%!error <table: 'VariableNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "VariableNames", [2, 3]);
%!error <table: 'VariableNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "VariableNames", [true, true]);
%!error <table: 'VariableNames' must contain nonempty names.> ...
%! table ([1; 2], [3; 4], "VariableNames", {"", "A"});
%!error <table: 'RowNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "RowNames", {2, 3});
%!error <table: 'RowNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "RowNames", [2, 3]);
%!error <table: 'RowNames' must be either a cell array of character vectors or a string array.> ...
%! table ([1; 2], [3; 4], "RowNames", [true, false]);
%!error <table: 'DimensionNames' must be either a two-element cell array of character vectors or a two-element string array.> ...
%! table ([1; 2], [3; 4], "DimensionNames", {2, 3});
%!error <table: 'DimensionNames' must be either a two-element cell array of character vectors or a two-element string array.> ...
%! table ([1; 2], [3; 4], "DimensionNames", [2, 3]);
%!error <table: 'DimensionNames' must be either a two-element cell array of character vectors or a two-element string array.> ...
%! table ([1; 2], [3; 4], "DimensionNames", [true, false]);
%!error <table: 'DimensionNames' must be either a two-element cell array of character vectors or a two-element string array.> ...
%! table ([1; 2], [3; 4], "DimensionNames", {"A", "B", "C"});
%!error <table: 'DimensionNames' cannot include the reserved name: 'Properties'.> ...
%! table (1, "DimensionNames", {"Properties", "Variables"});
%!error <table: 'DimensionNames' cannot include the reserved name: 'VariableNames'.> ...
%! table (1, "DimensionNames", {"VariableNames", "Row"});
%!error <table: duplicate dimension and variable name: 'Variables'> ...
%! table (1, 'VariableNames', {'Variables'});
%!error <table: 'VariableTypes' must be a cellstring array of the same number of elements as defined in SZ\(2\).> ...
%! table ("Size", [4, 2], "VariableTypes", {"A", "B", "C"});
%!error <table: 'VariableTypes' must be a cellstring array of the same number of elements as defined in SZ\(2\).> ...
%! table ("Size", [4, 3], "VariableTypes", ["A", "B", "C"]);
%!error <table: 'VariableTypes' must be a cellstring array of the same number of elements as defined in SZ\(2\).> ...
%! table ("Size", [4, 3], "VariableTypes", {1, 2, 3});
%!error <table: inconsistent number of 'VariableNames' and 'VariableTypes'.> ...
%! table ("Size", [4, 3], "VariableTypes", {"double", "double", "logical"}, ...
%!        "VariableNames", {"A", "B"});
%!error <table: inconsistent number of 'RowNames' and rows defined in SZ\(1\).> ...
%! table ("Size", [4, 3], "VariableTypes", {"double", "double", "logical"}, ...
%!        "RowNames", {"A", "B", "C"});
%!error <table: unsupported variable type: 'uint128'.> ...
%! table ("Size", [4, 1], "VariableTypes", {"uint128"});
%!error <table: 'Size' must be a two-element numeric vector.> ...
%! table ("Size", 3, "VariableTypes", {"double"});
%!error <table: 'Size' must be a two-element numeric vector.> ...
%! table ("Size", [3, 1, 2], "VariableTypes", {"double"});
%!error <table: 'timetable' variable type not supported yet.> ...
%! table ("Size", [4, 3], "VariableTypes", {"double", "double", "timetable"}, ...
%!        "VariableNames", {"A", "B", "C"});
%!error <table: 'timetable' variable type not supported yet.> ...
%! table ("Size", [4, 3], "VariableTypes", {"double", "double", "timetable"});
%!error <table: duplicate variable names: Age> ...
%! Age = [1; 2];
%! table (Age, Age);
%!error <table: duplicate variable names: Age, Height> ...
%! Age = [1; 2];
%! Height = [3; 4];
%! table (Age, Age, Height, Height);
%!error <table: inconsistent number of variable names \(1\) and variable values \(2\)> ...
%! table ([1; 2], [3; 4], "VariableNames", {"A"});
%!error <table: variable values must not have more than 2 dimensions: input 1 'A' has 3.> ...
%! table (ones (2, 2, 2), [1; 2], "VariableNames", {"A", "B"});
%!error <table: variable values must not have more than 2 dimensions: input 2 'B' has 3.> ...
%! table ([1; 2], ones (2, 2, 2), "VariableNames", {"A", "B"});
%!error <table: inconsistent sizes between variables: var 'A' has 1 rows; var 'B' has 5 rows.> ...
%! table (struct ("age", 1), [1; 2; 3; 4; 5], "VariableNames", {"A", "B"});
%!error <table: inconsistent sizes between variables: var 'A' has 5 rows; var 'B' has 1 rows.> ...
%! table ([1; 2; 3; 4; 5], struct ("age", 1), "VariableNames", {"A", "B"});
%!error <table: elements in 'RowNames' must be unique.> ...
%! table ("Size", [4, 2], "VariableTypes", {"double", "double"}, ...
%!        "RowNames", {"A", "B", "B", "D"});
%!error <table: the number of 'RowNames' \(2\) must equal the number of rows \(3\).> ...
%! table ([1; 2; 3], "RowNames", {"a", "b"});

%!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 133; 119];
%! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80];
%! T = table (Age, Smoker, Height, Weight, BloodPressure);

## Test 'table2array' method
%!test  # the canonical patient table concatenates into a numeric matrix
%! assert_equal (table2array (T), [Age, Smoker, Height, Weight, BloodPressure]);
%!test  # single-column numeric variables concatenate into a matrix
%! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! assert_equal (table2array (A), [1, 4; 2, 5; 3, 6]);
%!test  # multicolumn variables expand into multiple columns
%! A = table ([1, 2; 3, 4], [5; 6], 'VariableNames', {'a', 'b'});
%! M = table2array (A);
%! assert_equal (size (M), [2, 3]);
%! assert_equal (M, [1, 2, 5; 3, 4, 6]);
%!test  # a single variable is returned as-is
%! M = table2array (table ([1; 2; 3], 'VariableNames', {'x'}));
%! assert_equal (M, [1; 2; 3]);
%! assert_equal (class (M), 'double');
%!test  # the result takes the dominant numeric type (double + single -> single)
%! M = table2array (table ([1; 2], single ([3; 4]), 'VariableNames', {'a', 'b'}));
%! assert_equal (class (M), 'single');
%!test  # an integer variable dominates a double variable
%! M = table2array (table (int8 ([1; 2]), [3; 4], 'VariableNames', {'a', 'b'}));
%! assert_equal (class (M), 'int8');
%!test  # a logical variable widens to double
%! M = table2array (table (logical ([1; 0]), [3; 4], 'VariableNames', {'a', 'b'}));
%! assert_equal (class (M), 'double');
%!test  # homogeneous string variables yield a string array
%! M = table2array (table (string ({'a'; 'b'}), string ({'c'; 'd'}), ...
%!                         'VariableNames', {'x', 'y'}));
%! assert_equal (class (M), 'string');
%! assert_equal (size (M), [2, 2]);
%!test  # homogeneous categorical variables yield a categorical array
%! M = table2array (table (categorical ({'a'; 'b'}), categorical ({'c'; 'd'}), ...
%!                         'VariableNames', {'x', 'y'}));
%! assert_equal (class (M), 'categorical');
%! assert_equal (size (M), [2, 2]);
%!test  # homogeneous datetime variables yield a datetime array
%! d = datetime (2020, 1, [1; 2]);
%! M = table2array (table (d, d, 'VariableNames', {'x', 'y'}));
%! assert_equal (class (M), 'datetime');
%! assert_equal (size (M), [2, 2]);
%!test  # an empty table returns an empty array
%! assert_equal (table2array (table ()), []);
%!test  # homogeneous cell variables yield a cell array
%! M = table2array (table ({'a'; 'b'}, {'c'; 'd'}, 'VariableNames', {'x', 'y'}));
%! assert_equal (class (M), 'cell');
%! assert_equal (size (M), [2, 2]);
## Test input validation for 'table2array' method: a mix of cell and non-cell
## variables cannot form a homogeneous array (matches MATLAB, names the pair).
%!error <table.table2array: cannot concatenate the table variables 'A' and 'B', because their types are double and cell.> ...
%! table2array (table ([1; 2; 3; 4; 5], {1; 2; 3; 4; 5}, "VariableNames", {"A", "B"}));

## Test 'table2cell' method
%!test  # the output cell array has the same size as the table
%! A = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! C = table2cell (A);
%! assert_equal (size (C), [3, 2]);
%! assert_equal (C{1, 1}, 1);
%! assert_equal (C{1, 2}, 'a');
%!test  # the patient table round-trips its numeric block
%! C = table2cell (T);
%! assert_equal (cell2mat (C(:, [1:4])), [Age, Smoker, Height, Weight]);
%!test  # a multicolumn variable becomes a row vector in a single cell
%! C = table2cell (table ([1, 2; 3, 4], 'VariableNames', {'bp'}));
%! assert_equal (size (C), [2, 1]);
%! assert_equal (C{1}, [1, 2]);
%! assert_equal (C{2}, [3, 4]);
%!test  # a cellstr variable keeps its char contents
%! C = table2cell (table ({'a'; 'b'}, 'VariableNames', {'y'}));
%! assert_equal (C{1}, 'a');
%! assert_equal (C{2}, 'b');
%!test  # a logical variable is placed scalar-wise
%! C = table2cell (table (logical ([1; 0]), 'VariableNames', {'l'}));
%! assert_equal (C{1}, true);
%! assert_equal (C{2}, false);
%!test  # a string variable is returned as character vectors
%! C = table2cell (table (string ({'a'; 'b'}), 'VariableNames', {'s'}));
%! assert_equal (C{1}, 'a');
%! assert_equal (class (C{1}), 'char');
%!test  # a categorical variable is returned as character vectors
%! C = table2cell (table (categorical ({'lo'; 'hi'}), 'VariableNames', {'c'}));
%! assert_equal (C{1}, 'lo');
%! assert_equal (C{2}, 'hi');
%!test  # a datetime variable is returned as character vectors
%! C = table2cell (table (datetime (2020, 1, [1; 2]), 'VariableNames', {'d'}));
%! assert_equal (C{1}, '01-Jan-2020');
%! assert_equal (class (C{1}), 'char');
%!test  # a duration variable is returned as character vectors
%! C = table2cell (table (days ([1.5; 2.5]), 'VariableNames', {'d'}));
%! assert_equal (C{1}, '1.5 days');
%!test  # a struct variable yields one struct per cell
%! A = table ([struct('x', 1); struct('x', 2)], 'VariableNames', {'s'});
%! C = table2cell (A);
%! assert_equal (size (C), [2, 1]);
%! assert_equal (class (C{1}), 'struct');
%! assert_equal (C{2}.x, 2);
%!test  # row names are not included in the output
%! A = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1'; 'r2'});
%! C = table2cell (A);
%! assert_equal (size (C), [2, 1]);
%! assert_equal (C{1}, 1);

## Test 'table2struct' method
%!test  # default output is an m-by-1 struct array, one element per row
%! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'x', 'y'});
%! S = table2struct (A);
%! assert_equal (size (S), [3, 1]);
%! assert_equal (numel (S), 3);
%! assert_equal (fieldnames (S), {'x'; 'y'});
%! assert_equal ([S.x], [1, 2, 3]);
%! assert_equal (S(2).y, 5);
%!test  # the patient table maps each row to a struct element
%! S = table2struct (T);
%! assert_equal ([S.Age], Age');
%! assert_equal (reshape ([S.BloodPressure], 2, 5)', BloodPressure);
%! assert_equal (numel (S), height (T));
%!test  # a multicolumn variable becomes a row vector field
%! A = table ([1; 2], [3, 4; 5, 6], 'VariableNames', {'a', 'bp'});
%! S = table2struct (A);
%! assert_equal (S(1).bp, [3, 4]);
%! assert_equal (S(2).bp, [5, 6]);
%!test  # the struct array preserves object variable types
%! d = datetime (2020, 1, [1; 2]);
%! c = categorical ({'lo'; 'hi'});
%! S = table2struct (table (d, c, 'VariableNames', {'D', 'C'}));
%! assert_equal (class (S(1).D), 'datetime');
%! assert_equal (class (S(1).C), 'categorical');
%! assert_equal (cellstr (S(2).C), {'hi'});
%!test  # 'ToScalar' true returns one struct with whole-column fields
%! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'x', 'y'});
%! S = table2struct (A, 'ToScalar', true);
%! assert_equal (numel (S), 1);
%! assert_equal (S.x, [1; 2; 3]);
%! assert_equal (S.y, [4; 5; 6]);
%!test  # 'ToScalar' preserves object variable types
%! d = datetime (2020, 1, [1; 2]);
%! S = table2struct (table (d, categorical ({'a'; 'b'}), ...
%!                          'VariableNames', {'D', 'C'}), 'ToScalar', true);
%! assert_equal (class (S.D), 'datetime');
%! assert_equal (class (S.C), 'categorical');
%!test  # 'ToScalar' also accepts a numeric 1 for true
%! S = table2struct (table ([1; 2], 'VariableNames', {'x'}), 'ToScalar', 1);
%! assert_equal (numel (S), 1);
%! assert_equal (S.x, [1; 2]);
%!test  # 'ToScalar' false gives the default struct array
%! S = table2struct (table ([1; 2], 'VariableNames', {'x'}), 'ToScalar', false);
%! assert_equal (numel (S), 2);
%!test  # row names are not carried into the struct
%! A = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1'; 'r2'});
%! S = table2struct (A);
%! assert_equal (fieldnames (S), {'x'});
## Test input validation for 'table2struct' method
%!error <table.table2struct: wrong number of input arguments.> ...
%! table2struct (table (1), "ToScalar");
%!error <table.table2struct: wrong optional input argument.> ...
%! table2struct (table (1), "Scalar", 1);

################################################################################
##                            ** Save to Files **                             ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'table2csv'                                                                ##
##                                                                            ##
################################################################################

## Test 'table2csv' method (round-trips through 'csv2table')
%!test  # numeric, cellstr, and double variables round-trip by value and name
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, [1.5; 2.5; 3.5], ...
%!            'VariableNames', {'id', 'name', 'val'});
%! T.Properties.VariableDescriptions = {'the id', 'the name', 'the value'};
%! T.Properties.VariableUnits = {'#', 'str', 'kg'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableNames, {'id', 'name', 'val'});
%!   assert_equal (R.id, [1; 2; 3]);
%!   assert_equal (R.val, [1.5; 2.5; 3.5]);
%!   assert_equal (R.name, {'a'; 'b'; 'c'});
%!   assert_equal (R.Properties.VariableDescriptions, {'the id', 'the name', 'the value'});
%!   assert_equal (R.Properties.VariableUnits, {'#', 'str', 'kg'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # integer and single variables preserve their class through the vartype row
%! T = table (int8 ([1; 2]), single ([1.5; 2.5]), uint16 ([10; 20]), ...
%!            'VariableNames', {'a', 'b', 'c'});
%! T.Properties.VariableDescriptions = {'sbyte', 'real', 'ushort'};
%! T.Properties.VariableUnits = {'au', 'au', 'au'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.a), 'int8');
%!   assert_equal (class (R.b), 'single');
%!   assert_equal (class (R.c), 'uint16');
%!   assert_equal (R.a, int8 ([1; 2]));
%!   assert_equal (R.b, single ([1.5; 2.5]));
%!   assert_equal (R.Properties.VariableDescriptions, {'sbyte', 'real', 'ushort'});
%!   assert_equal (R.Properties.VariableUnits, {'au', 'au', 'au'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a logical variable round-trips as a logical column
%! T = table ([true; false; true], 'VariableNames', {'flag'});
%! T.Properties.VariableDescriptions = {'a flag'};
%! T.Properties.VariableUnits = {'bool'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.flag), 'logical');
%!   assert_equal (R.flag, [true; false; true]);
%!   assert_equal (R.Properties.VariableDescriptions, {'a flag'});
%!   assert_equal (R.Properties.VariableUnits, {'bool'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn variable splits into same-named columns and is rejoined
%! T = table ([1, 2; 3, 4], 'VariableNames', {'M'});
%! T.Properties.VariableDescriptions = {'a pair'};
%! T.Properties.VariableUnits = {'cm'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (width (R), 1);
%!   assert_equal (R.M, [1, 2; 3, 4]);
%!   assert_equal (R.Properties.VariableDescriptions, {'a pair'});
%!   assert_equal (R.Properties.VariableUnits, {'cm'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # NA and NaN are written as distinct tokens and round-trip through csv2table
%! T = table ([1; NA; NaN], 'VariableNames', {'v'});
%! T.Properties.VariableDescriptions = {'measurement'};
%! T.Properties.VariableUnits = {'mm'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (regexp (txt, '^NA$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^NaN$', 'lineanchors', 'once')), true);
%!   R = csv2table (fn);
%!   assert_equal (class (R.v), 'double');
%!   assert_equal (R.v(1), 1);
%!   assert_equal (isna (R.v'), [false, true, false]);
%!   assert_equal (isnan (R.v'), [false, true, true]);
%!   assert_equal (R.Properties.VariableDescriptions, {'measurement'});
%!   assert_equal (R.Properties.VariableUnits, {'mm'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a string variable round-trips as a string column
%! T = table (string ({'a'; 'b'}), 'VariableNames', {'s'});
%! T.Properties.VariableDescriptions = {'a label'};
%! T.Properties.VariableUnits = {'txt'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.s), 'string');
%!   assert_equal (cellstr (R.s), {'a'; 'b'});
%!   assert_equal (R.Properties.VariableDescriptions, {'a label'});
%!   assert_equal (R.Properties.VariableUnits, {'txt'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a datetime variable round-trips through its display strings
%! T = table (datetime (2020, 1, [1; 2; 3]), 'VariableNames', {'d'});
%! T.Properties.VariableDescriptions = {'a date'};
%! T.Properties.VariableUnits = {'cal'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.d), 'datetime');
%!   assert_equal (cellstr (R.d), {'01-Jan-2020'; '02-Jan-2020'; '03-Jan-2020'});
%!   assert_equal (R.Properties.VariableDescriptions, {'a date'});
%!   assert_equal (R.Properties.VariableUnits, {'cal'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a duration variable round-trips through its display strings
%! T = table (duration ([1; 2], [30; 45], [0; 0]), 'VariableNames', {'du'});
%! T.Properties.VariableDescriptions = {'elapsed'};
%! T.Properties.VariableUnits = {'hms'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.du), 'duration');
%!   assert_equal (cellstr (R.du), {'01:30:00'; '02:45:00'});
%!   assert_equal (R.Properties.VariableDescriptions, {'elapsed'});
%!   assert_equal (R.Properties.VariableUnits, {'hms'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a calendarDuration is written as text; csv2table reads it back as cellstr (lossy)
%! T = table (calendarDuration ([1; 2], [0; 1], [0; 0]), 'VariableNames', {'cd'});
%! T.Properties.VariableDescriptions = {'span'};
%! T.Properties.VariableUnits = {'ymd'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (regexp (txt, '^"calendarDuration"$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^"1y"$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^"2y 1mo"$', 'lineanchors', 'once')), true);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (R.cd, {'1y'; '2y 1mo'});
%!   assert_equal (R.Properties.VariableDescriptions, {'span'});
%!   assert_equal (R.Properties.VariableUnits, {'ymd'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
## Test warning here
%!warning <csv2table: 'calendarDuration' strings are not converted.> ...
%! T = table (calendarDuration ([1; 2], [0; 1], [0; 0]), 'VariableNames', {'cd'});
%! T.Properties.VariableDescriptions = {'span'};
%! T.Properties.VariableUnits = {'ymd'};
%! fn = [tempname() '.csv'];
%! table2csv (T, fn);
%! R = csv2table (fn);
%!test  # a categorical is written as labels; csv2table reads it back as cellstr (lossy)
%! T = table (categorical ({'lo'; 'hi'; 'lo'}), 'VariableNames', {'c'});
%! T.Properties.VariableDescriptions = {'level'};
%! T.Properties.VariableUnits = {'cat'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (regexp (txt, '^"categorical"$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^"lo"$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^"hi"$', 'lineanchors', 'once')), true);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (R.c, {'lo'; 'hi'; 'lo'});
%!   assert_equal (R.Properties.VariableDescriptions, {'level'});
%!   assert_equal (R.Properties.VariableUnits, {'cat'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
## Test warning here
%!warning <csv2table: 'categorical' strings are not converted.> ...
%! T = table (categorical ({'lo'; 'hi'; 'lo'}), 'VariableNames', {'c'});
%! T.Properties.VariableDescriptions = {'level'};
%! T.Properties.VariableUnits = {'cat'};
%! fn = [tempname() '.csv'];
%! table2csv (T, fn);
%! R = csv2table (fn);
%!test  # row names are written under a 'RowNames' column and restored
%! T = table ([1; 2], 'VariableNames', {'v'}, 'RowNames', {'r1'; 'r2'});
%! T.Properties.VariableDescriptions = {'a value'};
%! T.Properties.VariableUnits = {'kg'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.RowNames, {'r1'; 'r2'});
%!   assert_equal (R.v, [1; 2]);
%!   assert_equal (R.Properties.VariableDescriptions, {'a value'});
%!   assert_equal (R.Properties.VariableUnits, {'kg'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a nested table is split with two vartype rows and rebuilt on round-trip
%! nt = table ([10; 20], [30; 40], 'VariableNames', {'p', 'q'});
%! T = table ([1; 2], nt, 'VariableNames', {'id', 'nest'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   assert_equal (! isempty (strfind (fileread (fn), 'varTypes 2 rows')), true);
%!   R = csv2table (fn);
%!   assert_equal (class (R.nest), 'table');
%!   assert_equal (R.nest.p, [10; 20]);
%!   assert_equal (R.nest.q, [30; 40]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # descriptions and units of a nested table round-trip on both levels
%! nt = table ([10; 20], [30; 40], 'VariableNames', {'p', 'q'});
%! nt.Properties.VariableDescriptions = {'inner p', 'inner q'};
%! nt.Properties.VariableUnits = {'mm', 'cm'};
%! T = table ([1; 2], nt, 'VariableNames', {'id', 'nest'});
%! T.Properties.VariableDescriptions = {'an id', 'a nest'};
%! T.Properties.VariableUnits = {'#', 'box'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, 'varDescriptions 2 rows; varUnits 2 rows')), true);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableDescriptions, {'an id', 'a nest'});
%!   assert_equal (R.Properties.VariableUnits, {'#', 'box'});
%!   assert_equal (R.nest.Properties.VariableDescriptions, {'inner p', 'inner q'});
%!   assert_equal (R.nest.Properties.VariableUnits, {'mm', 'cm'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a struct variable is split per field and rebuilt on round-trip
%! sv = struct ('x', {1; 2}, 'y', {3; 4});
%! T = table ([1; 2], sv, 'VariableNames', {'id', 's'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   s = R.s;
%!   assert_equal (class (s), 'struct');
%!   assert_equal ([s.x], [1, 2]);
%!   assert_equal ([s.y], [3, 4]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # the top-level description and units of a struct variable round-trip
%! sv = struct ('x', {1; 2}, 'y', {3; 4});
%! T = table ([1; 2], sv, 'VariableNames', {'id', 's'});
%! T.Properties.VariableDescriptions = {'an id', 'a struct'};
%! T.Properties.VariableUnits = {'#', 'rec'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableDescriptions, {'an id', 'a struct'});
%!   assert_equal (R.Properties.VariableUnits, {'#', 'rec'});
%!   assert_equal (class (R.s), 'struct');
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # descriptions are written when any variable carries one (partial)
%! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'});
%! T.Properties.VariableDescriptions = {'has one', ''};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   assert_equal (! isempty (strfind (fileread (fn), 'varDescriptions 1 rows')), true);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableDescriptions, {'has one', ''});
%!   assert_equal (R.a, [1; 2]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # partial units round-trip through csv (any variable, not all)
%! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'});
%! T.Properties.VariableUnits = {'', 'kg'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableUnits, {'', 'kg'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect
%!test  # a zone-aware datetime keeps its TimeZone through csv
%! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', 'America/New_York');
%! T = table (dt, 'VariableNames', {'t'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.t.TimeZone, 'America/New_York');
%!   assert_equal (cellstr (char (R.t)), cellstr (char (dt)));
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect
%!test  # variable descriptions and units are written to the header and restored
%! T = table ([1; 2], 'VariableNames', {'v'});
%! T.Properties.VariableDescriptions = {'mass'};
%! T.Properties.VariableUnits = {'kg'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, 'varDescriptions 1 rows; varUnits 1 rows')), true);
%!   assert_equal (! isempty (regexp (txt, '^"mass"$', 'lineanchors', 'once')), true);
%!   assert_equal (! isempty (regexp (txt, '^"kg"$', 'lineanchors', 'once')), true);
%!   R = csv2table (fn);
%!   assert_equal (R.v, [1; 2]);
%!   assert_equal (R.Properties.VariableDescriptions, {'mass'});
%!   assert_equal (R.Properties.VariableUnits, {'kg'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # FILE accepts a char vector, a cellstr, and a string scalar
%! T = table ([1; 2], 'VariableNames', {'v'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   assert_equal (exist (fn, 'file') > 0, true);
%!   delete (fn);
%!   table2csv (T, {fn});
%!   assert_equal (exist (fn, 'file') > 0, true);
%!   delete (fn);
%!   table2csv (T, string (fn));
%!   assert_equal (exist (fn, 'file') > 0, true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a complex table of many variable types round-trips with descriptions and units
%! T = table ([1; 2; 3], single ([1.5; 2.5; 3.5]), int16 ([10; 20; 30]), ...
%!            [true; false; true], string ({'x'; 'y'; 'z'}), ...
%!            datetime (2021, 3, [4; 5; 6]), ...
%!            duration ([1; 2; 3], [0; 30; 0], [0; 0; 0]), ...
%!            categorical ({'lo'; 'hi'; 'lo'}), [1, 2; 3, 4; 5, 6], ...
%!            'VariableNames', ...
%!            {'id', 'score', 'cnt', 'ok', 'name', 'when', 'dur', 'grade', 'bp'});
%! T.Properties.VariableDescriptions = ...
%!   {'identifier', 'a score', 'a count', 'flag', 'a name', 'a date', ...
%!    'elapsed', 'level', 'pair'};
%! T.Properties.VariableUnits = ...
%!   {'#', 'pts', 'n', 'bool', 'txt', 'cal', 'hms', 'cat', 'cm'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (width (R), 9);
%!   assert_equal (R.id, [1; 2; 3]);
%!   assert_equal (class (R.score), 'single');
%!   assert_equal (R.score, single ([1.5; 2.5; 3.5]));
%!   assert_equal (class (R.cnt), 'int16');
%!   assert_equal (R.cnt, int16 ([10; 20; 30]));
%!   assert_equal (R.ok, [true; false; true]);
%!   assert_equal (class (R.name), 'string');
%!   assert_equal (cellstr (R.name), {'x'; 'y'; 'z'});
%!   assert_equal (class (R.when), 'datetime');
%!   assert_equal (cellstr (R.when), {'04-Mar-2021'; '05-Mar-2021'; '06-Mar-2021'});
%!   assert_equal (class (R.dur), 'duration');
%!   assert_equal (cellstr (R.dur), {'01:00:00'; '02:30:00'; '03:00:00'});
%!   assert_equal (R.grade, {'lo'; 'hi'; 'lo'});
%!   assert_equal (R.bp, [1, 2; 3, 4; 5, 6]);
%!   assert_equal (R.Properties.VariableNames, ...
%!                 {'id', 'score', 'cnt', 'ok', 'name', 'when', 'dur', 'grade', 'bp'});
%!   assert_equal (R.Properties.VariableDescriptions, ...
%!                 {'identifier', 'a score', 'a count', 'flag', 'a name', ...
%!                  'a date', 'elapsed', 'level', 'pair'});
%!   assert_equal (R.Properties.VariableUnits, ...
%!                 {'#', 'pts', 'n', 'bool', 'txt', 'cal', 'hms', 'cat', 'cm'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a complex table with row names round-trips data, row names, descriptions, and units
%! T = table ([10; 20], string ({'aa'; 'bb'}), datetime (2022, 6, [1; 2]), ...
%!            'VariableNames', {'n', 'tag', 'day'}, ...
%!            'RowNames', {'first'; 'second'});
%! T.Properties.VariableDescriptions = {'a number', 'a tag', 'a day'};
%! T.Properties.VariableUnits = {'u1', 'u2', 'u3'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.RowNames, {'first'; 'second'});
%!   assert_equal (R.n, [10; 20]);
%!   assert_equal (cellstr (R.tag), {'aa'; 'bb'});
%!   assert_equal (class (R.day), 'datetime');
%!   assert_equal (cellstr (R.day), {'01-Jun-2022'; '02-Jun-2022'});
%!   assert_equal (R.Properties.VariableDescriptions, {'a number', 'a tag', 'a day'});
%!   assert_equal (R.Properties.VariableUnits, {'u1', 'u2', 'u3'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # every integer class (full range, exact) and the float classes round-trip
%! T = table (int8 ([intmin('int8'); intmax('int8')]), ...
%!            int16 ([intmin('int16'); intmax('int16')]), ...
%!            int32 ([intmin('int32'); intmax('int32')]), ...
%!            int64 ([intmin('int64'); intmax('int64')]), ...
%!            uint8 ([0; intmax('uint8')]), uint16 ([0; intmax('uint16')]), ...
%!            uint32 ([0; intmax('uint32')]), uint64 ([0; intmax('uint64')]), ...
%!            single ([1.5; -2.5]), [1.25; -3.75], ...
%!            'VariableNames', ...
%!            {'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', 'sg', 'db'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.i8), 'int8');
%!   assert_equal (class (R.i16), 'int16');
%!   assert_equal (class (R.i32), 'int32');
%!   assert_equal (class (R.i64), 'int64');
%!   assert_equal (class (R.u8), 'uint8');
%!   assert_equal (class (R.u16), 'uint16');
%!   assert_equal (class (R.u32), 'uint32');
%!   assert_equal (class (R.u64), 'uint64');
%!   assert_equal (class (R.sg), 'single');
%!   assert_equal (class (R.db), 'double');
%!   assert_equal (R.i8, int8 ([intmin('int8'); intmax('int8')]));
%!   assert_equal (R.i32, int32 ([intmin('int32'); intmax('int32')]));
%!   assert_equal (R.i64, int64 ([intmin('int64'); intmax('int64')]));
%!   assert_equal (R.u32, uint32 ([0; intmax('uint32')]));
%!   assert_equal (R.u64, uint64 ([0; intmax('uint64')]));
%!   assert_equal (R.sg, single ([1.5; -2.5]));
%!   assert_equal (R.db, [1.25; -3.75]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn string variable splits and rejoins as a 2-D string
%! T = table (string ({'a', 'b'; 'c', 'd'}), 'VariableNames', {'s'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.s), 'string');
%!   assert_equal (size (R.s), [2, 2]);
%!   assert_equal (cellstr (R.s), {'a', 'b'; 'c', 'd'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn datetime variable round-trips as a 2-D datetime
%! T = table ([datetime(2020, 1, [1; 2]), datetime(2021, 1, [1; 2])], ...
%!            'VariableNames', {'d'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.d), 'datetime');
%!   assert_equal (size (R.d), [2, 2]);
%!   assert_equal (cellstr (R.d(:, 1)), {'01-Jan-2020'; '02-Jan-2020'});
%!   assert_equal (cellstr (R.d(:, 2)), {'01-Jan-2021'; '02-Jan-2021'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn duration variable round-trips as a 2-D duration
%! T = table ([duration(1, 0, 0) .* [1; 2], duration(2, 0, 0) .* [1; 2]], ...
%!            'VariableNames', {'du'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.du), 'duration');
%!   assert_equal (size (R.du), [2, 2]);
%!   assert_equal (cellstr (R.du(:, 1)), {'01:00:00'; '02:00:00'});
%!   assert_equal (cellstr (R.du(:, 2)), {'02:00:00'; '04:00:00'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn logical variable round-trips as a 2-D logical
%! T = table (logical ([1, 0; 0, 1]), 'VariableNames', {'L'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.L), 'logical');
%!   assert_equal (R.L, logical ([1, 0; 0, 1]));
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn integer variable preserves its class and shape
%! T = table (int16 ([10, 20; 30, 40]), 'VariableNames', {'M'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.M), 'int16');
%!   assert_equal (R.M, int16 ([10, 20; 30, 40]));
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn categorical is written as labels and read back as 2-D cellstr (lossy)
%! T = table (categorical ({'x', 'y'; 'y', 'x'}), 'VariableNames', {'c'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (R.c, {'x', 'y'; 'y', 'x'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a datetime variable with a NaT round-trips, restoring the missing entry
%! T = table ([datetime(2020, 1, 1); NaT; datetime(2020, 1, 3)], 'VariableNames', {'d'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.d), 'datetime');
%!   assert_equal (isnat (R.d'), [false, true, false]);
%!   assert_equal (cellstr (R.d([1, 3])), {'01-Jan-2020'; '03-Jan-2020'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a duration variable with a NaN round-trips, restoring the missing entry
%! T = table (duration ([1; NaN; 3], [0; 0; 0], [0; 0; 0]), 'VariableNames', {'du'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.du), 'duration');
%!   assert_equal (isnan (R.du'), [false, true, false]);
%!   assert_equal (cellstr (R.du([1, 3])), {'01:00:00'; '03:00:00'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a missing string value is written as an empty field and read back as ""
%! T = table (string ({'a'; missing; 'c'}), 'VariableNames', {'s'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.s), 'string');
%!   assert_equal (cellstr (R.s), {'a'; ''; 'c'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # text with embedded commas, double quotes, and padding survives quoting
%! T = table ({'a,b'; 'he said "hi"'; '  pad  '; 'plain'}, 'VariableNames', {'s'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   assert_equal (! isempty (strfind (fileread (fn), '"he said ""hi"""')), true);
%!   R = csv2table (fn);
%!   assert_equal (R.s, {'a,b'; 'he said "hi"'; '  pad  '; 'plain'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a nested table of special types round-trips (categorical is lossy)
%! inner = table (datetime(2020, 1, [1; 2]), duration ([1; 2], [0; 0], [0; 0]), ...
%!                string ({'p'; 'q'}), categorical ({'lo'; 'hi'}), ...
%!                'VariableNames', {'dt', 'du', 'st', 'ct'});
%! T = table ([1; 2], inner, 'VariableNames', {'id', 'nest'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (class (R.nest), 'table');
%!   assert_equal (class (R.nest.dt), 'datetime');
%!   assert_equal (cellstr (R.nest.dt), {'01-Jan-2020'; '02-Jan-2020'});
%!   assert_equal (class (R.nest.du), 'duration');
%!   assert_equal (cellstr (R.nest.du), {'01:00:00'; '02:00:00'});
%!   assert_equal (class (R.nest.st), 'string');
%!   assert_equal (cellstr (R.nest.st), {'p'; 'q'});
%!   assert_equal (R.nest.ct, {'lo'; 'hi'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a nested table with a multicolumn inner variable round-trips
%! inner = table ([1, 2; 3, 4], {'a'; 'b'}, 'VariableNames', {'mm', 'lab'});
%! T = table ([9; 8], inner, 'VariableNames', {'id', 'nest'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (R.nest.mm, [1, 2; 3, 4]);
%!   assert_equal (R.nest.lab, {'a'; 'b'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a table mixing multicolumn, nested, and several types round-trips with metadata
%! inner = table (duration ([1; 2], [0; 0], [0; 0]), string ({'p'; 'q'}), ...
%!                categorical ({'lo'; 'hi'}), 'VariableNames', {'idu', 'ist', 'ict'});
%! inner.Properties.VariableDescriptions = {'inner dur', 'inner str', 'inner cat'};
%! inner.Properties.VariableUnits = {'h', 't', 'c'};
%! T = table ([duration(1, 0, 0) .* [1; 2], duration(2, 0, 0) .* [1; 2]], ...
%!            string ({'aa'; 'bb'}), categorical ({'x'; 'y'}), ...
%!            [10, 11, 12; 20, 21, 22], inner, ...
%!            'VariableNames', {'mdur', 'tag', 'grade', 'mat', 'nest'});
%! T.Properties.VariableDescriptions = {'mc dur', 'a tag', 'a grade', 'a matrix', 'nested'};
%! T.Properties.VariableUnits = {'h', 't', 'g', 'm', 'n'};
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   warning ('off', 'all', 'local');
%!   R = csv2table (fn);
%!   assert_equal (R.Properties.VariableNames, {'mdur', 'tag', 'grade', 'mat', 'nest'});
%!   assert_equal (class (R.mdur), 'duration');
%!   assert_equal (size (R.mdur), [2, 2]);
%!   assert_equal (cellstr (R.tag), {'aa'; 'bb'});
%!   assert_equal (R.grade, {'x'; 'y'});
%!   assert_equal (R.mat, [10, 11, 12; 20, 21, 22]);
%!   assert_equal (class (R.nest), 'table');
%!   assert_equal (cellstr (R.nest.idu), {'01:00:00'; '02:00:00'});
%!   assert_equal (cellstr (R.nest.ist), {'p'; 'q'});
%!   assert_equal (R.nest.ict, {'lo'; 'hi'});
%!   assert_equal (R.Properties.VariableDescriptions, ...
%!                 {'mc dur', 'a tag', 'a grade', 'a matrix', 'nested'});
%!   assert_equal (R.Properties.VariableUnits, {'h', 't', 'g', 'm', 'n'});
%!   assert_equal (R.nest.Properties.VariableDescriptions, {'inner dur', 'inner str', 'inner cat'});
%!   assert_equal (R.nest.Properties.VariableUnits, {'h', 't', 'c'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # full-range 64-bit integers round-trip exactly (written/read without double)
%! ii = [intmin('int64'); intmin('int64') + 1; intmax('int64')];
%! uu = [uint64(0); intmax('uint64') - uint64(1); intmax('uint64')];
%! T = table (ii, uu, 'VariableNames', {'i', 'u'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   assert_equal (! isempty (strfind (fileread (fn), '18446744073709551615')), true);
%!   R = csv2table (fn);
%!   assert_equal (R.i, ii);
%!   assert_equal (R.u, uu);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn 64-bit integer variable with extreme values round-trips
%! M = int64 ([intmax('int64'), 1; -5, intmin('int64')]);
%! T = table (M, 'VariableNames', {'M'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   table2csv (T, fn);
%!   R = csv2table (fn);
%!   assert_equal (class (R.M), 'int64');
%!   assert_equal (R.M, M);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
## Test input validation for 'table2csv' method
%!error <table.table2csv: cannot open file '.*' for writing.> ...
%! table2csv (table (1), fullfile (tempname (), 'x.csv'));

################################################################################
##                            ** Save to Files **                             ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'table2ods'                                                                ##
##                                                                            ##
################################################################################

## Test 'table2ods' method (round-trips through 'ods2table')
%!test  # datetime and duration are written as native ODS date/time cells
%! T = table (datetime (2024, 1, 15, 10, 30, 15), duration (25, 30, 0), ...
%!            'VariableNames', {'d', 'e'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   table2ods (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, 'office:value-type="date"')), true);
%!   assert_equal (! isempty (strfind (txt, 'office:date-value="2024-01-15T10:30:15"')), true);
%!   assert_equal (! isempty (strfind (txt, 'office:value-type="time"')), true);
%!   assert_equal (! isempty (strfind (txt, 'office:time-value="PT25H30M00S"')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a full-range int64 is written with its exact digits
%! T = table (int64 (9223372036854775807), 'VariableNames', {'i'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   table2ods (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, '9223372036854775807')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # the hidden metadata sheet records the variable types and names
%! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'v', 'g'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   table2ods (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, '__datatypes_meta__')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a compressed '.ods' is a ZIP whose contents round-trip through ods2table
%! T = table ([1; 2; 3], datetime (2024, 1, [1; 2; 3]), {'a'; 'b'; 'c'}, ...
%!            'VariableNames', {'n', 'd', 'g'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   table2ods (T, fn);
%!   R = ods2table (fn);
%!   assert_equal (R.Properties.VariableNames, {'n', 'd', 'g'});
%!   assert_equal (R.n, [1; 2; 3]);
%!   assert_equal (R.g, {'a'; 'b'; 'c'});
%!   assert_equal (class (R.d), 'datetime');
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # the ODS package leads with a compliant stored 'mimetype' entry
%! T = table ([1; 2; 3], 'VariableNames', {'n'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   table2ods (T, fn);
%!   fid = fopen (fn, 'r');  hdr = fread (fid, 38, 'uint8')';  fclose (fid);
%!   flag = hdr(7) + hdr(8) * 256;         # general-purpose bit flag
%!   method = hdr(9) + hdr(10) * 256;      # compression method
%!   namelen = hdr(27) + hdr(28) * 256;
%!   assert_equal (hdr(1:4), [80, 75, 3, 4]);       # ZIP magic "PK\x03\x04"
%!   assert_equal (method, 0);                      # stored, not deflated
%!   assert_equal (flag, 0);                        # no data descriptor / extras
%!   assert_equal (char (hdr(31:30+namelen)), 'mimetype');
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
## Incremental 'table2ods': build a multi-sheet workbook one table at a time,
## read back through 'ods2struct'
%!test
%! A = table ([1; 2; 3], 'VariableNames', {'a'});
%! B = table ({'x'; 'y'}, 'VariableNames', {'b'});
%! C = table ([9; 8], 'VariableNames', {'c'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   table2ods (A, fn, 'Sheet', 'first');
%!   table2ods (B, fn, 'Sheet', 'second');
%!   table2ods (C, fn, 'Sheet', 'third');
%!   r = ods2struct (fn);
%!   assert_equal (fieldnames (r), {'first'; 'second'; 'third'});
%!   assert_equal (r.first.a, [1; 2; 3]);
%!   assert_equal (r.second.b, {'x'; 'y'});
%!   assert_equal (r.third.c, [9; 8]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## Incremental 'table2ods' 'append' adds rows to a sheet, others preserved
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! more = table ([6; 7], 'VariableNames', {'a'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   table2ods (A, fn, 'Sheet', 'first');
%!   table2ods (B, fn, 'Sheet', 'second');
%!   table2ods (more, fn, 'Sheet', 'first', 'WriteMode', 'append');
%!   r = ods2struct (fn);
%!   assert_equal (r.first.a, [1; 2; 6; 7]);
%!   assert_equal (r.second.b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## Incremental 'table2ods' 'overwritesheet' replaces one sheet, keeps the rest;
## native types survive the read-modify-rewrite
%!test
%! A = table (datetime (2024, 1, [1; 2]), 'VariableNames', {'d'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! repl = table (seconds ([10; 20]), 'VariableNames', {'t'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   table2ods (A, fn, 'Sheet', 'first');
%!   table2ods (B, fn, 'Sheet', 'second');
%!   table2ods (repl, fn, 'Sheet', 'first', 'WriteMode', 'overwritesheet');
%!   r = ods2struct (fn);
%!   assert_equal (class (r.first.t), 'duration');
%!   assert_equal (isequaln (seconds (r.first.t), [10; 20]), true);
%!   assert_equal (r.second.b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'table2ods' 'replacefile' discards the existing workbook
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4], 'VariableNames', {'b'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   table2ods (A, fn, 'Sheet', 'first');
%!   table2ods (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile');
%!   [~, ~, ~, nm] = __ods2table__ (fn);
%!   assert_equal (nm(:), {'second'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## Test input validation for 'table2ods' method
%!error <table.table2ods: 'WriteMode' 'bogus' is not valid.> ...
%! table2ods (table (1), [tempname() '.ods'], 'WriteMode', 'bogus');
%!error <table.table2ods: unknown option 'Bogus'.> ...
%! table2ods (table (1), [tempname() '.ods'], 'Bogus', 1);
%!error <table.table2ods: nested tables and structs are not supported; flatten them before writing.> ...
%! inner = table ([1; 2], 'VariableNames', {'a'}); ...
%! T = table (inner, [3; 4], 'VariableNames', {'nested', 'x'}); ...
%! table2ods (T, [tempname() '.fods']);
%!error <table.table2ods: nested tables and structs are not supported; flatten them before writing.> ...
%! s = struct ('f', {1; 2}); ...
%! T = table (s, [3; 4], 'VariableNames', {'st', 'x'}); ...
%! table2ods (T, [tempname() '.fods']);
%!error <table.table2ods: FILE must have a '.ods' or '.fods' extension.> ...
%! table2ods (table (1), [tempname() '.txt']);

################################################################################
##                            ** Save to Files **                             ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'writetable'                                                               ##
##                                                                            ##
################################################################################

## Test 'writetable' method (round-trips through 'readtable')
%!test  # text round-trip: names header, numeric/text detection
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, [1.5; 2.5; 3.5], ...
%!            'VariableNames', {'id', 'name', 'val'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn);
%!   R = readtable (fn);
%!   assert_equal (R.Properties.VariableNames, {'id', 'name', 'val'});
%!   assert_equal (R.id, [1; 2; 3]);
%!   assert_equal (R.name, {'a'; 'b'; 'c'});
%!   assert_equal (R.val, [1.5; 2.5; 3.5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # text: no type metadata is written, only a names row then data
%! T = table ([1; 2], {'x'; 'y'}, 'VariableNames', {'a', 'b'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn);
%!   txt = fileread (fn);
%!   assert_equal (! isempty (regexp (txt, '^a,b$', 'lineanchors', 'once')), true);
%!   assert_equal (isempty (strfind (txt, 'varTypes')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # 'WriteVariableNames' false omits the header row
%! T = table ([1; 2], 'VariableNames', {'a'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn, 'WriteVariableNames', false);
%!   txt = fileread (fn);
%!   assert_equal (isempty (strfind (txt, 'a')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # 'Delimiter' and 'QuoteStrings' control the text output
%! T = table ({'x,y'; 'z'}, 'VariableNames', {'s'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn, 'Delimiter', 'semi', 'QuoteStrings', 'none');
%!   txt = fileread (fn);
%!   assert_equal (! isempty (strfind (txt, 'x,y')), true);
%!   assert_equal (isempty (strfind (txt, '"')), true);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # 'WriteRowNames' writes the row names as a leading column
%! T = table ([10; 20], 'VariableNames', {'v'}, 'RowNames', {'r1', 'r2'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn, 'WriteRowNames', true);
%!   R = readtable (fn, 'ReadRowNames', true);
%!   assert_equal (R.Properties.RowNames, {'r1'; 'r2'});
%!   assert_equal (R.v, [10; 20]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a multicolumn variable is split into name_1, name_2, ...
%! T = table ([1 2; 3 4], {'p'; 'q'}, 'VariableNames', {'mat', 'tag'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn);
%!   R = readtable (fn);
%!   assert_equal (R.Properties.VariableNames, {'mat_1', 'mat_2', 'tag'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # spreadsheet round-trip preserves native datetime/duration types
%! T = table ([1; 2], datetime (2024, 1, [1; 2]), seconds ([30; 90]), ...
%!            'VariableNames', {'a', 'd', 'e'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (T, fn);
%!   R = readtable (fn);
%!   assert_equal (R.a, [1; 2]);
%!   assert_equal (class (R.d), 'datetime');
%!   assert_equal (class (R.e), 'duration');
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
%!test  # a writetable '.ods' exposes a visible names row (read back as names)
%! T = table ([1; 2], {'x'; 'y'}, 'VariableNames', {'aa', 'bb'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (T, fn);
%!   R = readtable (fn);
%!   assert_equal (R.Properties.VariableNames, {'aa', 'bb'});
%!   assert_equal (R.aa, [1; 2]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file'))
%!     delete (fn);
%!   endif
%! end_unwind_protect
## 'writetable' to a named sheet, read back by that name
%!test
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'x', 'y'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (T, fn, 'Sheet', 'MySheet');
%!   R = readtable (fn, 'Sheet', 'MySheet');
%!   assert_equal (R.Properties.VariableNames, {'x', 'y'});
%!   assert_equal (R.x, [1; 2; 3]);
%!   assert_equal (R.y, [10; 20; 30]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'writetable' 'Range' anchors the block; readtable auto-trims to it, and an
## explicit read range recovers the same data.
%!test
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'x', 'y'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (T, fn, 'Range', 'C5');
%!   R = readtable (fn);
%!   assert_equal (R.Properties.VariableNames, {'x', 'y'});
%!   assert_equal (R.x, [1; 2; 3]);
%!   R2 = readtable (fn, 'Range', 'C5:D8');
%!   assert_equal (R2.x, [1; 2; 3]);
%!   assert_equal (R2.y, [10; 20; 30]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'writetable' flat '.fods' with a 'Range' offset round-trips
%!test
%! T = table ([5; 6], [50; 60], 'VariableNames', {'a', 'b'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   writetable (T, fn, 'Range', 'B3');
%!   R = readtable (fn);
%!   assert_equal (R.a, [5; 6]);
%!   assert_equal (R.b, [50; 60]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## text 'WriteMode' 'append' adds rows below without repeating the header
%!test
%! T = table ([1; 2], [10; 20], 'VariableNames', {'x', 'y'});
%! T2 = table ([3; 4], [30; 40], 'VariableNames', {'x', 'y'});
%! fn = [tempname() '.csv'];
%! unwind_protect
%!   writetable (T, fn);
%!   writetable (T2, fn, 'WriteMode', 'append');
%!   R = readtable (fn);
%!   assert_equal (R.x, [1; 2; 3; 4]);
%!   assert_equal (R.y, [10; 20; 30; 40]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'writetable' to a new sheet of an existing workbook preserves other sheets
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second');
%!   assert_equal (readtable (fn, 'Sheet', 'first').a, [1; 2]);
%!   assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'WriteMode' 'append' adds rows to an existing sheet; other sheets untouched
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! more = table ([6; 7], 'VariableNames', {'a'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second');
%!   writetable (more, fn, 'Sheet', 'first', 'WriteMode', 'append');
%!   assert_equal (readtable (fn, 'Sheet', 'first').a, [1; 2; 6; 7]);
%!   assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'WriteMode' 'overwritesheet' replaces one sheet, preserving the others
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! repl = table ([9], 'VariableNames', {'z'});
%! fn = [tempname() '.fods'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second');
%!   writetable (repl, fn, 'Sheet', 'first', 'WriteMode', 'overwritesheet');
%!   R = readtable (fn, 'Sheet', 'first');
%!   assert_equal (R.Properties.VariableNames, {'z'});
%!   assert_equal (R.z, 9);
%!   assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## Writing to an existing workbook with no 'Sheet' targets the first sheet
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! Z = table ([9; 9], 'VariableNames', {'z'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second');
%!   writetable (Z, fn);                         # no 'Sheet' -> first sheet
%!   [~, ~, ~, nm] = __ods2table__ (fn);
%!   assert_equal (nm(:), {'first'; 'second'});  # no stray 'Sheet1' added
%!   assert_equal (readtable (fn, 'Sheet', 'first').Properties.VariableNames, {'z'});
%!   assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'WriteMode' 'replacefile' overwrites the whole workbook
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! fn = [tempname() '.ods'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile');
%!   [~, ~, ~, nm] = __ods2table__ (fn);
%!   assert_equal (nm(:), {'second'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## Test input validation for 'writetable' method
%!error <table.writetable: writetable does not support writing nested tables.*splitvars.*> ...
%! inner = table ([1; 2], 'VariableNames', {'a'}); ...
%! T = table (inner, [3; 4], 'VariableNames', {'nested', 'x'}); ...
%! writetable (T, [tempname() '.csv']);
%!error <table.writetable: '.xls' Excel files are not supported; use '.xlsx', '.ods', or a text format.> ...
%! writetable (table (1), [tempname() '.xls']);
%!error <table.writetable: 'Sheet' and 'Range' are not supported for text files.> ...
%! writetable (table (1), [tempname() '.csv'], 'Sheet', 'S1');
%!error <table.writetable: 'Sheet' and 'Range' are not supported for text files.> ...
%! writetable (table (1), [tempname() '.csv'], 'Range', 'A1');
%!error <table.writetable: 'WriteMode' 'inplace' is not valid for text files; use 'overwrite' or 'append'.> ...
%! writetable (table (1), [tempname() '.csv'], 'WriteMode', 'inplace');
%!error <table.writetable: 'WriteMode' 'bogus' is not valid for spreadsheet files.> ...
%! writetable (table (1), [tempname() '.ods'], 'WriteMode', 'bogus');
%!error <table.writetable: 'Range' is not supported with 'WriteMode' 'append'.> ...
%! writetable (table (1), [tempname() '.ods'], 'WriteMode', 'append', 'Range', 'B2');
%!error <table.writetable: unknown option 'Bogus'.> ...
%! writetable (table (1), [tempname() '.csv'], 'Bogus', 1);

## '.xlsx' write into an existing workbook adds/preserves sheets and appends
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4; 5], 'VariableNames', {'b'});
%! more = table ([6; 7], 'VariableNames', {'a'});
%! fn = [tempname() '.xlsx'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second');
%!   writetable (more, fn, 'Sheet', 'first', 'WriteMode', 'append');
%!   r = xlsx2struct (fn);
%!   assert_equal (fieldnames (r), {'first'; 'second'});
%!   assert_equal (r.first.a, [1; 2; 6; 7]);
%!   assert_equal (r.second.b, [3; 4; 5]);
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

## 'replacefile' overwrites the whole Excel workbook
%!test
%! A = table ([1; 2], 'VariableNames', {'a'});
%! B = table ([3; 4], 'VariableNames', {'b'});
%! fn = [tempname() '.xlsx'];
%! unwind_protect
%!   writetable (A, fn, 'Sheet', 'first');
%!   writetable (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile');
%!   [~, ~, ~, nm] = __xlsx2table__ (fn);
%!   assert_equal (nm(:), {'second'});
%! unwind_protect_cleanup
%!   if (exist (fn, 'file')); delete (fn); endif
%! end_unwind_protect

################################################################################
##                         ** Summary Information **                          ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'summary'          'height'           'width'            'head'            ##
## 'tail'                                                                     ##
##                                                                            ##
################################################################################

%!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 133; 119];
%! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80];
%! T = table (Age, Smoker, Height, Weight, BloodPressure);

## Test 'summary' returns a struct with the expected per-variable fields
%!test
%! tt = table (Age, Smoker, BloodPressure);
%! ss = summary (tt);
%! assert_equal (isstruct (ss), true);
%! assert_equal (fieldnames (ss), {'Age'; 'Smoker'; 'BloodPressure'});
%! assert_equal (ss.Age.Size, [5, 1]);
%! assert_equal (ss.Age.Type, 'double');
%! assert_equal ([ss.Age.Min, ss.Age.Median, ss.Age.Max], [38, 40, 49]);
%! assert_equal (ss.Age.NumMissing, 0);
%! assert_equal (ss.Smoker.Size, [5, 1]);
%! assert_equal (ss.Smoker.True, 3);
%! assert_equal (ss.Smoker.False, 2);
%! assert_equal (ss.BloodPressure.Size, [5, 2]);
## Test 'summary' numeric NaN handling and per-column statistics
%!test
%! ss = summary (table ([1; 2; NaN; 4], [NaN, 2; 3, NaN; 5, 6; 7, 8], ...
%!                      'VariableNames', {'a', 'b'}));
%! assert_equal ([ss.a.Min, ss.a.Median, ss.a.Max], [1, 2, 4]);
%! assert_equal (ss.a.NumMissing, 1);
%! assert_equal (ss.b.Min, [3, 2]);
%! assert_equal (ss.b.Median, [5, 6]);
%! assert_equal (ss.b.Max, [7, 8]);
%! assert_equal (ss.b.NumMissing, [1, 1]);
## Test 'summary' of logical, string, cell, and categorical variables
%!test
%! ss = summary (table (logical ([1; 1; 0]), string ({'a'; 'b'; 'c'}), ...
%!                      {'x'; 'y'; 'z'}, categorical ({'p'; 'q'; 'p'}), ...
%!                      'VariableNames', {'L', 'S', 'C', 'Cat'}));
%! assert_equal (ss.L.True, 2);
%! assert_equal (ss.L.False, 1);
%! assert_equal (isfield (ss.S, 'Min'), false);
%! assert_equal (isfield (ss.C, 'Min'), false);
%! assert_equal (isfield (ss.Cat, 'Min'), false);
%! assert_equal (ss.Cat.Type, 'categorical');
## Test 'summary' of a duration variable (typed stats, no TimeStep field)
%!test
%! Dv = duration ([1; 2; 3], [0; 30; 0], [0; 0; 0]);
%! ss = summary (table (Dv, 'VariableNames', {'D'}));
%! assert_equal (ss.D.Type, 'duration');
%! assert_equal (class (ss.D.Min), 'duration');
%! assert_equal (seconds (ss.D.Min), 3600);
%! assert_equal (seconds (ss.D.Median), 9000);
%! assert_equal (seconds (ss.D.Max), 10800);
%! assert_equal (ss.D.NumMissing, 0);
%! assert_equal (isfield (ss.D, 'TimeStep'), false);
## Test 'summary' duration omits NaN from statistics and counts missing
%!test
%! Dv = duration ([1; NaN; 3], [0; 0; 0], [0; 0; 0]);
%! ss = summary (table (Dv, 'VariableNames', {'D'}));
%! assert_equal (seconds (ss.D.Min), 3600);
%! assert_equal (seconds (ss.D.Max), 10800);
%! assert_equal (seconds (ss.D.Median), 7200);
%! assert_equal (ss.D.NumMissing, 1);
## Test 'summary' of a datetime variable (typed stats, no TimeStep field)
%!test
%! Tv = datetime ([2020; 2021; 2022], 1, 1);
%! ss = summary (table (Tv, 'VariableNames', {'T'}));
%! assert_equal (ss.T.Type, 'datetime');
%! assert_equal (class (ss.T.Min), 'datetime');
%! assert_equal (datevec (ss.T.Min), [2020, 1, 1, 0, 0, 0]);
%! assert_equal (datevec (ss.T.Median), [2021, 1, 1, 0, 0, 0]);
%! assert_equal (datevec (ss.T.Max), [2022, 1, 1, 0, 0, 0]);
%! assert_equal (ss.T.NumMissing, 0);
%! assert_equal (isfield (ss.T, 'TimeStep'), false);
## Test 'summary' datetime excludes NaT and counts it as missing
%!test
%! Tv = datetime ([2020; NaN; 2022], 1, 1);
%! ss = summary (table (Tv, 'VariableNames', {'T'}));
%! assert_equal (datevec (ss.T.Min), [2020, 1, 1, 0, 0, 0]);
%! assert_equal (datevec (ss.T.Max), [2022, 1, 1, 0, 0, 0]);
%! assert_equal (ss.T.NumMissing, 1);
## Test 'summary' of calendarDuration reports only Size, Type, and NumMissing
%!test
%! Cv = calendarDuration ([1; 2; 3], [0; 1; 2], [0; 0; 0]);
%! ss = summary (table (Cv, 'VariableNames', {'C'}));
%! assert_equal (ss.C.Type, 'calendarDuration');
%! assert_equal (ss.C.Size, [3, 1]);
%! assert_equal (isfield (ss.C, 'Min'), false);
%! assert_equal (ss.C.NumMissing, 0);
%! ss = summary (table (calendarDuration ([1; NaN], 0, 0), ...
%!                      'VariableNames', {'C'}));
%! assert_equal (ss.C.NumMissing, 1);
## Test 'summary' captures variable Description and Units in the struct
%!test
%! tt = table ([1; 2; 3], 'VariableNames', {'v'});
%! tt.Properties.VariableUnits = {'kg'};
%! tt.Properties.VariableDescriptions = {'a weight'};
%! ss = summary (tt);
%! assert_equal (ss.v.Units, 'kg');
%! assert_equal (ss.v.Description, 'a weight');
## Test 'summary' of an empty table does not error
%!test
%! et = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'}, ...
%!             'VariableNames', {'a', 'b'});
%! ss = summary (et);
%! assert_equal (fieldnames (ss), {'a'; 'b'});
%! str = evalc ("summary (et);");
%! assert_equal (! isempty (strfind (str, 'Variables:')), true);
## Test 'summary' printed output for numeric, logical, and missing values
%!test
%! str = evalc (["summary (table ([1; 2; NaN], logical ([1; 0; 1]), ", ...
%!               "'VariableNames', {'a', 'b'}));"]);
%! assert_equal (! isempty (strfind (str, 'Variables:')), true);
%! assert_equal (! isempty (strfind (str, 'Median')), true);
%! assert_equal (! isempty (strfind (str, 'NumMissing 1')), true);
%! assert_equal (! isempty (strfind (str, 'True')), true);
%! assert_equal (! isempty (strfind (str, 'False')), true);
## Test 'summary' printed output shows table and variable Description
%!test
%! tt = table ([1; 2; 3], 'VariableNames', {'v'});
%! tt.Properties.Description = 'My table';
%! tt.Properties.VariableDescriptions = {'a weight'};
%! str = evalc ("summary (tt);");
%! assert_equal (! isempty (strfind (str, 'Description:  My table')), true);
%! assert_equal (! isempty (strfind (str, 'Description: a weight')), true);
## Test 'summary' printed output for datetime and duration variables
%!test
%! sd = evalc (["summary (table (duration ([1; 2; 3], 0, 0), ", ...
%!              "'VariableNames', {'D'}));"]);
%! assert_equal (! isempty (strfind (sd, 'Min       01:00:00')), true);
%! assert_equal (! isempty (strfind (sd, 'Max       03:00:00')), true);
%! st = evalc (["summary (table (datetime (2020, 1, [1; 2; 3]), ", ...
%!              "'VariableNames', {'T'}));"]);
%! assert_equal (! isempty (strfind (st, 'Min       01-Jan-2020')), true);
%! assert_equal (! isempty (strfind (st, 'Max       03-Jan-2020')), true);
## Test 'summary' printed output for calendarDuration shows only NumMissing
%!test
%! sc = evalc (["summary (table (calendarDuration ([1; NaN], 0, 0), ", ...
%!              "'VariableNames', {'C'}));"]);
%! assert_equal (! isempty (strfind (sc, 'NumMissing 1')), true);
%! assert_equal (isempty (strfind (sc, 'Min')), true);

## Test 'height': number of rows, equivalent to size (TBL, 1)
%!assert_equal (height (T), 5);
%!assert_equal (height (T), size (T, 1));
%!assert_equal (class (height (T)), 'double');
## Test 'height' of a single-row table
%!assert_equal (height (table (1, 2, 3)), 1);
## Test 'height' of an empty table with no variables
%!assert_equal (height (table ()), 0);
## Test 'height' of a table with variables but zero rows
%!assert_equal (height (table ('Size', [0, 3], ...
%!               'VariableTypes', {'double', 'double', 'double'})), 0);
## Test 'height' of a preallocated table reports the requested row count
%!assert_equal (height (table ('Size', [7, 2], ...
%!               'VariableTypes', {'double', 'double'})), 7);
## Test 'height' is unaffected by multicolumn variables
%!assert_equal (height (table (Age, BloodPressure)), 5);
## Test 'height' counts rows regardless of row names
%!assert_equal (height (table (Age, Smoker, 'RowNames', LastName)), 5);

## Test 'width': number of variables, equivalent to size (TBL, 2)
%!assert_equal (width (T), 5);
%!assert_equal (width (T), size (T, 2));
%!assert_equal (class (width (T)), 'double');
## Test 'width' counts variables, not the total number of columns
%!assert_equal (width (T), 5);
%!assert_equal (size (T.BloodPressure, 2), 2);
## Test 'width' of a single-variable table
%!assert_equal (width (table (Age)), 1);
## Test 'width' of an empty table with no variables
%!assert_equal (width (table ()), 0);
## Test 'width' of a table with zero rows still counts its variables
%!assert_equal (width (table ('Size', [0, 3], ...
%!               'VariableTypes', {'double', 'double', 'double'})), 3);

## Test 'head' returns the first K rows in a new table
%!test
%! h = head (T, 3);
%! assert_equal (height (h), 3);
%! assert_equal (h.Age, Age(1:3));
%! assert_equal (class (h), 'table');
## Test 'head' default count is eight rows
%!assert_equal (height (head (T)), 5);
%!test
%! big = table ((1:20)');
%! assert_equal (height (head (big)), 8);
%! h = head (big);
%! assert_equal (h.Var1, (1:8)');
## Test 'head' with K equal to the number of rows returns the whole table
%!assert_equal (height (head (T, 5)), 5);
## Test 'head' with K greater than the number of rows returns all rows
%!assert_equal (height (head (T, 99)), 5);
## Test 'head' with K of one returns just the first row
%!test
%! h1 = head (T, 1);
%! assert_equal (height (h1), 1);
%! assert_equal (h1.Age, Age(1));
## Test 'head' of an empty (zero-row) table returns an empty table
%!test
%! e = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'});
%! assert_equal (size (head (e, 3)), [0, 2]);
## Test 'head' preserves variable names, row names, and properties
%!test
%! R = table (Age, Smoker, 'RowNames', LastName);
%! R.Properties.VariableUnits = {'Yrs', ''};
%! R.Properties.Description = 'demo';
%! h = head (R, 2);
%! assert_equal (h.Properties.VariableNames, {'Age', 'Smoker'});
%! assert_equal (h.Properties.RowNames, LastName(1:2));
%! assert_equal (h.Properties.VariableUnits, {'Yrs', ''});
%! assert_equal (h.Properties.Description, 'demo');
## Test 'head' preserves multicolumn variables
%!test
%! h = head (table (Age, BloodPressure), 2);
%! assert_equal (h.BloodPressure, BloodPressure(1:2, :));
## Test 'head' preserves special data type variables
%!test
%! du = duration ((1:5)', 0, 0);
%! ca = categorical ({'a'; 'b'; 'c'; 'd'; 'e'});
%! h = head (table (du, ca), 2);
%! assert_equal (class (h.du), 'duration');
%! assert_equal (class (h.ca), 'categorical');
%! assert_equal (seconds (h.du), [3600; 7200]);
%! assert_equal (cellstr (h.ca), {'a'; 'b'});
## Test 'head' with no output argument displays the first K rows
%!test
%! R = table (Age, Smoker, 'RowNames', LastName);
%! str = evalc ('head (R, 2);');
%! assert_equal (! isempty (strfind (str, 'Sanchez')), true);
%! assert_equal (! isempty (strfind (str, 'Johnson')), true);
%! assert_equal (isempty (strfind (str, 'Li')), true);
## Test input validation for 'head'
%!error <table.head: K must be a positive integer scalar value.> ...
%! head (T, 1.5);
%!error <table.head: K must be a positive integer scalar value.> ...
%! head (T, 0);
%!error <table.head: K must be a positive integer scalar value.> ...
%! head (T, -2);
%!error <table.head: K must be a positive integer scalar value.> ...
%! head (T, [1, 2]);
%!error <table.head: invalid number of output arguments.> ...
%! [out1, out2] = head (T, 2);

## Test 'tail' returns the last K rows in a new table
%!test
%! t = tail (T, 3);
%! assert_equal (height (t), 3);
%! assert_equal (t.Age, Age(3:5));
%! assert_equal (class (t), 'table');
## Test 'tail' default count is eight rows
%!assert_equal (height (tail (T)), 5);
%!test
%! big = table ((1:20)');
%! assert_equal (height (tail (big)), 8);
%! t = tail (big);
%! assert_equal (t.Var1, (13:20)');
## Test 'tail' with K equal to the number of rows returns the whole table
%!assert_equal (height (tail (T, 5)), 5);
## Test 'tail' preserves the original row order
%!test
%! t = tail (T, 5);
%! assert_equal (t.Age, Age);
## Test 'tail' with K greater than the number of rows returns all rows
%!assert_equal (height (tail (T, 99)), 5);
## Test 'tail' with K of one returns just the last row
%!test
%! t1 = tail (T, 1);
%! assert_equal (height (t1), 1);
%! assert_equal (t1.Age, Age(5));
## Test 'tail' of an empty (zero-row) table returns an empty table
%!test
%! e = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'});
%! assert_equal (size (tail (e, 3)), [0, 2]);
## Test 'tail' preserves variable names, row names, and properties
%!test
%! R = table (Age, Smoker, 'RowNames', LastName);
%! R.Properties.VariableUnits = {'Yrs', ''};
%! R.Properties.Description = 'demo';
%! t = tail (R, 2);
%! assert_equal (t.Properties.VariableNames, {'Age', 'Smoker'});
%! assert_equal (t.Properties.RowNames, LastName(4:5));
%! assert_equal (t.Properties.VariableUnits, {'Yrs', ''});
%! assert_equal (t.Properties.Description, 'demo');
## Test 'tail' preserves multicolumn variables
%!test
%! t = tail (table (Age, BloodPressure), 2);
%! assert_equal (t.BloodPressure, BloodPressure(4:5, :));
## Test 'tail' preserves special data type variables
%!test
%! du = duration ((1:5)', 0, 0);
%! ca = categorical ({'a'; 'b'; 'c'; 'd'; 'e'});
%! t = tail (table (du, ca), 2);
%! assert_equal (class (t.du), 'duration');
%! assert_equal (class (t.ca), 'categorical');
%! assert_equal (seconds (t.du), [14400; 18000]);
%! assert_equal (cellstr (t.ca), {'d'; 'e'});
## Test 'tail' with no output argument displays the last K rows
%!test
%! R = table (Age, Smoker, 'RowNames', LastName);
%! str = evalc ('tail (R, 2);');
%! assert_equal (! isempty (strfind (str, 'Diaz')), true);
%! assert_equal (! isempty (strfind (str, 'Brown')), true);
%! assert_equal (isempty (strfind (str, 'Sanchez')), true);
## Test input validation for 'tail'
%!error <table.tail: K must be a positive integer scalar value.> ...
%! tail (T, 1.5);
%!error <table.tail: K must be a positive integer scalar value.> ...
%! tail (T, 0);
%!error <table.tail: K must be a positive integer scalar value.> ...
%! tail (T, -2);
%!error <table.tail: K must be a positive integer scalar value.> ...
%! tail (T, [1, 2]);
%!error <table.tail: invalid number of output arguments.> ...
%! [out1, out2] = tail (T, 2);

################################################################################
##                     ** Sort, Filter, and Rearrange **                      ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'sortrows'         'unique'           'issortedrows'     'topkrows'        ##
## 'addvars'          'renamevars'       'movevars'         'removevars'      ##
## 'splitvars'        'mergevars'        'convertvars'      'rows2vars'       ##
## 'stack'            'unstack'          'inner2outer'      'addprop'         ##
## 'rmprop'                                                                   ##
##                                                                            ##
################################################################################

%!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 133; 119];
%! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80];
%! T = table (Age, Smoker, Height, Weight, BloodPressure);

## Test 'sortrows' and 'issortedrows' methods
%!test
%! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName);
%! assert_equal (issortedrows (tblA, "RowNames"), false);
%! [sorted_tblA, index] = sortrows (tblA, "RowNames");
%! assert_equal (index, [5; 4; 2; 3; 1]);
%! assert_equal (issortedrows (sorted_tblA, "RowNames"), true);
%! [sorted_tblA, index] = sortrows (tblA, "RowNames", "descend");
%! assert_equal (index, [1; 3; 2; 4; 5]);
%! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true);
%!test  # duration variable (regression: 'days' is a method, not a property)
%! A = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'D', 'S'});
%! [S, index] = sortrows (A);
%! assert_equal (hours (S.D), [1;2;3]);
%! assert_equal (S.S, {'b';'c';'a'});
%! assert_equal (index, [2;3;1]);
%!test
%! tblA.Properties.DimensionNames(1) = "Patients";
%! assert_equal (issortedrows (tblA, "Patients"), false);
%! [sorted_tblA, index] = sortrows (tblA, "Patients");
%! assert_equal (index, [5; 4; 2; 3; 1]);
%! assert_equal (issortedrows (sorted_tblA, "Patients"), true);
%! [sorted_tblA, index] = sortrows (tblA, "Patients", "descend");
%! assert_equal (index, [1; 3; 2; 4; 5]);
%! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true);
%!test
%! [sorted_tblA, index] = sortrows (tblA);
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (sorted_tblA), true);
%! [sorted_tblA, index] = sortrows (tblA, [], "ascend");
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (sorted_tblA, [], "ascend"), true);
%! [sorted_tblA, index] = sortrows (tblA, [], "descend");
%! assert_equal (index, [5; 2; 4; 1; 3]);
%! assert_equal (issortedrows (sorted_tblA, [], "descend"), true);
%!test
%! [tblB, index] = sortrows (tblA, "Height");
%! assert_equal (index, [3; 5; 4; 2; 1]);
%! assert_equal (tblB.Properties.RowNames, ...
%!         {"Li"; "Brown"; "Diaz"; "Johnson"; "Sanchez"})
%! assert_equal (issortedrows (tblB, "Height"), true);
%!test
%! LastName = {"Sweet"; "Jacobson"; "Wang"; "Joiner"; "Berger"};
%! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName);
%! [tblB, index] = sortrows (tblA, {"Height", "Weight"}, {"ascend", "descend"});
%! assert_equal (index, [3; 5; 4; 2; 1]);
%! assert_equal (tblB.Properties.RowNames, ...
%!         {"Wang"; "Berger"; "Joiner"; "Jacobson"; "Sweet"})
%! assert_equal (issortedrows (tblB, {"Height", "Weight"}, ...
%!                             {"ascend", "descend"}), true);
%!test
%! [tblB, index] = sortrows (tblA, {"BloodPressure"}, {"ascend"});
%! assert_equal (index, [2; 4; 5; 1; 3]);
%! assert_equal (tblB.BloodPressure, sortrows (BloodPressure));
%! assert_equal (issortedrows (tblB, {"BloodPressure"}, {"ascend"}), true);
%! [tblB, index] = sortrows (tblA, {"BloodPressure"}, {"descend"});
%! assert_equal (index, flip ([2; 4; 5; 1; 3]));
%! assert_equal (tblB.BloodPressure, sortrows (BloodPressure, -1));
%! assert_equal (issortedrows (tblB, "BloodPressure", "descend"), true);
%!test
%! [tblB, index] = sortrows (tblA, {"Height", "RowNames"}, {"ascend"});
%! assert_equal (index, [5; 3; 4; 2; 1]);
%! assert_equal (tblB.Properties.RowNames, ...
%!         {"Berger"; "Wang"; "Joiner"; "Jacobson"; "Sweet"})
%! assert_equal (tblB.Height, sortrows (Height));
%! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, {"ascend"}), true);
%! [tblB, index] = sortrows (tblA, {"Height", "RowNames"}, ...
%!                                 {"ascend", "descend"});
%! assert_equal (index, [3; 5; 4; 2; 1]);
%! assert_equal (tblB.Properties.RowNames, ...
%!         {"Wang"; "Berger"; "Joiner"; "Jacobson"; "Sweet"})
%! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, ...
%!                             {"ascend", "descend"}), true);
%!test
%! [tblB, index] = sortrows (tblA, [true, true, false, false]);
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (tblB, [true, true, false, false]), true);
%! [tblB, index] = sortrows (tblA, [true, true, false, false], "ascend");
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (tblB, [true, true, false, false], "ascend"), true);
%! [tblB, index] = sortrows (tblA, [true, true, false, false], "descend");
%! assert_equal (index, flip ([3; 1; 4; 2; 5]));
%! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), true);
%! [tblB, index] = sortrows (tblA, [true, true, false, false], ...
%!                                 {"ascend", "descend"});
%! assert_equal (index, [1; 3; 4; 2; 5]);
%! assert_equal (issortedrows (tblB, [true, true, false, false], ...
%!                             {"ascend", "descend"}), true);
%!test
%! [sorted_tblA, indexN] = sortrows (tblA, 1);
%! assert_equal (issortedrows (sorted_tblA, 1), true);
%! [sorted_tblA, indexV] = sortrows (tblA, "Age");
%! assert_equal (issortedrows (sorted_tblA, "Age"), true);
%! assert_equal (indexN, indexV);
%! [sorted_tblA, indexN] = sortrows (tblA, -1);
%! assert_equal (issortedrows (sorted_tblA, -1), true);
%! [sorted_tblA, indexV] = sortrows (tblA, "Age", "descend");
%! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true);
%! assert_equal (indexN, indexV);
%! [sorted_tblA, indexN] = sortrows (tblA, -1, "ascend");
%! assert_equal (issortedrows (sorted_tblA, -1, "ascend"), true);
%! [~, indexV] = sortrows (tblA, "Age");
%! assert_equal (indexN, indexV);
%! [sorted_tblA, indexN] = sortrows (tblA, [-1, 2]);
%! assert_equal (issortedrows (sorted_tblA, [-1, 2]), true);
%! [~, indexV] = sortrows (tblA, {"Age", "Height"}, {"descend", "ascend"});
%! assert_equal (indexN, indexV);
%!test
%! TT = table (Age, table (Age, Height), BloodPressure, 'RowNames', LastName);
%! [sorted_TT, index] = sortrows (TT);
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (sorted_TT), true);
%! [sorted_TT, index] = sortrows (TT, [1, 2]);
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (sorted_TT, [1, 2]), true);
%! [sorted_TT, index] = sortrows (TT, [1, -2]);
%! assert_equal (index, [1; 3; 4; 2; 5]);
%! assert_equal (issortedrows (sorted_TT, [1, -2]), true);
%!test
%! Weights = [176;NaN;131;133;NaN];
%! tblN = table (Age, Height, Weights, BloodPressure, 'RowNames', LastName);
%! [tblB, index] = sortrows (tblN, "Weights", "MissingPlacement", "first");
%! assert_equal (index, [2; 5; 3; 4; 1]);
%! assert_equal (issortedrows (tblB, "Weights", "MissingPlacement", "first"), true);
%! [tblB, index] = sortrows (tblN, "Weights");
%! assert_equal (index, [3; 4; 1; 2; 5]);
%! assert_equal (issortedrows (tblB, "Weights"), true);
%!test
%! tblN = table (Age, Height, Weight, BloodPressure, LastName);
%! [tblB, index] = sortrows (tblN, vartype ("numeric"));
%! assert_equal (index, [3; 1; 4; 2; 5]);
%! assert_equal (issortedrows (tblB, vartype ("numeric")), true);
%! [tblB, index] = sortrows (tblN, vartype ("cellstr"));
%! assert_equal (index, [5; 2; 4; 1; 3]);
%! assert_equal (issortedrows (tblB, vartype ("cellstr")), true);
%!test  # special-type sort keys (datetime / categorical / string / calendarDuration)
%! S = {'b'; 'a'; 'c'; 'a'};
%! dt = datetime (2020, [3; 1; 2; 1], 1);
%! A = table (dt, S, 'VariableNames', {'D', 'S'});
%! [tblB, index] = sortrows (A, 'D');
%! assert_equal (index, [2; 4; 3; 1]);
%! assert_equal (issortedrows (tblB, 'D'), true);
%! [tblB, index] = sortrows (A, 'D', 'descend');
%! assert_equal (index, [1; 3; 2; 4]);
%! assert_equal (issortedrows (tblB, 'D', 'descend'), true);
%! c = categorical ({'mid'; 'low'; 'high'; 'low'}, {'low', 'mid', 'high'}, ...
%!                  'Ordinal', true);
%! [tblB, index] = sortrows (table (c, 'VariableNames', {'C'}), 'C');
%! assert_equal (index, [2; 4; 1; 3]);
%! assert_equal (issortedrows (tblB, 'C'), true);
%! st = string ({'banana'; 'apple'; 'cherry'; 'apple'});
%! [tblB, index] = sortrows (table (st, 'VariableNames', {'St'}));
%! assert_equal (index, [2; 4; 1; 3]);
%! assert_equal (issortedrows (tblB), true);
%! cd = calendarDuration ([0; 2; 1; 0], [3; 1; 2; 1], [0; 0; 0; 0]);
%! [tblB, index] = sortrows (table (cd, 'VariableNames', {'CD'}));
%! assert_equal (index, [4; 1; 3; 2]);
%! assert_equal (issortedrows (tblB), true);
%!test  # 'ComparisonMethod' with complex data ('abs' and 'real')
%! z = [3+4i; -1+0i; 0+2i; -5+0i];
%! A = table (z, 'VariableNames', {'Z'});
%! [tblB, index] = sortrows (A, 'Z', 'ComparisonMethod', 'abs');
%! assert_equal (index, [2; 3; 1; 4]);
%! [tblB, index] = sortrows (A, 'Z', 'ComparisonMethod', 'real');
%! assert_equal (index, [4; 2; 3; 1]);
%!test  # 'MissingPlacement' 'last' and 'auto'
%! W = [176; NaN; 131; 133; NaN];
%! tblN = table (Age, W, 'VariableNames', {'Age', 'W'});
%! [tblB, index] = sortrows (tblN, 'W', 'MissingPlacement', 'last');
%! assert_equal (index, [3; 4; 1; 2; 5]);
%! assert_equal (issortedrows (tblN, 'W', 'MissingPlacement', 'last'), false);
%! [tblB, index] = sortrows (tblN, 'W', 'MissingPlacement', 'auto');
%! assert_equal (index, [3; 4; 1; 2; 5]);
## Test input validation for 'sortrows' method
%!error <table.sortrows: 'MissingPlacement' parameter can be either 'auto', 'first', or 'last'.> ...
%! sortrows (tblA, "MissingPlacement", "param");
%!error <table.sortrows: 'ComparisonMethod' parameter can be either 'auto', 'real', or 'abs'.> ...
%! sortrows (tblA, "ComparisonMethod", "param");
%!error <table.sortrows: invalid number of input arguments.> ...
%! sortrows (tblA, [], "ascend", "param");
%!error <table.sortrows: invalid value for DIRECTION argument.> ...
%! sortrows (tblA, [], "aaascend");
%!error <table.sortrows: DIRECTION must be a scalar input when 'RowNames' or 'rowDimNames' are selected.> ...
%! sortrows (tblA, "RowNames", {"ascend", "ascend"});
%!error <table.sortrows: DIRECTION must be a scalar input when 'RowNames' or 'rowDimNames' are selected.> ...
%! sortrows (tblA, "Row", {"ascend", "ascend"});
%!error <table.sortrows: logical indexing vector does not match table width.> ...
%! sortrows (tblA, [true, true, false]);
%!error <table.sortrows: invalid size for DIRECTION argument.> ...
%! sortrows (tblA, "Age", {"ascend", "ascend"});
%!error <table.sortrows: numerical indexing must be a vector of nonzero integers.> ...
%! sortrows (tblA, [1, 0, 3]);
%!error <table.sortrows: numerical index exceeds table dimensions.> ...
%! sortrows (tblA, [1, 6, 3]);
%!error <table.sortrows: invalid size for DIRECTION argument.> ...
%! sortrows (tblA, [1, 3, 2], {"ascend", "ascend"});
%!error <table.sortrows: VARS indexes non-existing variable names.> ...
%! sortrows (tblA, {"Age", "whatever"});
%!error <table.sortrows: invalid size for DIRECTION argument.> ...
%! sortrows (tblA, {"Age", "Height", "Weight"}, {"ascend", "ascend"});
%!error <table.sortrows: DIRECTION must be a scalar input when variables are indexed with a 'vartype' object.> ...
%! sortrows (tblA, vartype ("numeric"), {"ascend", "ascend"});
%!error <table.sortrows: cannot sort variables of 'cell' type.> ...
%! sortrows (table ({5; 4; 3}));
%!error <table.sortrows: cannot sort variables of 'struct' type.> ...
%! sortrows (table ([struct("s", 4); struct("s", 5); struct("s", 6)]));
%!error <table.sortrows: cannot sort nested tables with mixed data types.> ...
%! sortrows (table (Age, table (Age, LastName), "RowNames", LastName))

## Test 'unique' method
%!test
%! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'};
%! Age = [38; 43; 38; 40; 38];
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 185; 131];
%! A = table (Age, Height, Weight, 'RowNames', Name);
%! [C, ia, ic] = unique (A);
%! assert_equal (ia, [3; 1; 4; 2]);
%! assert_equal (ic, [2; 4; 1; 3; 1]);
%! assert_equal (C.Age, [38; 38; 40; 43]);
%! assert_equal (C.Height, [64; 71; 67; 69]);
%! assert_equal (C.Weight, [131; 176; 185; 163]);
%! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'});
%!test  # duration variable (regression: 'days' is a method, not a property)
%! A = table (hours ([2;1;2;3]), {'a';'b';'c';'a'}, 'VariableNames', {'D','S'});
%! C = unique (A);
%! assert_equal (hours (C.D), [1;2;2;3]);
%! assert_equal (C.S, {'b';'a';'c';'a'});
%!test
%! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'};
%! Age = [38; 43; 38; 40; 38];
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 185; 131];
%! A = table (Age, Height, Weight, 'RowNames', Name);
%! [C, ia, ic] = unique (A, "sorted");
%! assert_equal (ia, [3; 1; 4; 2]);
%! assert_equal (ic, [2; 4; 1; 3; 1]);
%! assert_equal (C.Age, [38; 38; 40; 43]);
%! assert_equal (C.Height, [64; 71; 67; 69]);
%! assert_equal (C.Weight, [131; 176; 185; 163]);
%! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'});
%!test
%! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'};
%! Age = [38; 43; 38; 40; 38];
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 185; 131];
%! A = table (Age, Height, Weight, 'RowNames', Name);
%! [C, ia, ic] = unique (A, "stable");
%! assert_equal (ia, [1; 2; 3; 4]);
%! assert_equal (ic, [1; 2; 3; 4; 3]);
%! assert_equal (C.Age, [38; 43; 38; 40]);
%! assert_equal (C.Height, [71; 69; 64; 67]);
%! assert_equal (C.Weight, [176; 163; 131; 185]);
%! assert_equal (C.Properties.RowNames, {'Fred'; 'Betty'; 'Bob'; 'George'});
%!test
%! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'};
%! Age = [38; 43; 38; 40; 38];
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 185; 131];
%! A = table (Age, Height, Weight, 'RowNames', Name);
%! [C, ia, ic] = unique (A, "first");
%! assert_equal (ia, [3; 1; 4; 2]);
%! assert_equal (ic, [2; 4; 1; 3; 1]);
%! assert_equal (C.Age, [38; 38; 40; 43]);
%! assert_equal (C.Height, [64; 71; 67; 69]);
%! assert_equal (C.Weight, [131; 176; 185; 163]);
%! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'});
%!test
%! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'};
%! Age = [38; 43; 38; 40; 38];
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 185; 131];
%! A = table (Age, Height, Weight, 'RowNames', Name);
%! [C, ia, ic] = unique (A, "last");
%! assert_equal (ia, [5; 1; 4; 2]);
%! assert_equal (ic, [2; 4; 1; 3; 1]);
%! assert_equal (C.Age, [38; 38; 40; 43]);
%! assert_equal (C.Height, [64; 71; 67; 69]);
%! assert_equal (C.Weight, [131; 176; 185; 163]);
%! assert_equal (C.Properties.RowNames, {'Jane'; 'Fred'; 'George'; 'Betty'});
%!test  # special-type variables (datetime / categorical / calendarDuration)
%! dt = datetime (2020, [2; 1; 2; 3], 1);
%! [C, ia, ic] = unique (table (dt, 'VariableNames', {'D'}));
%! assert_equal (ia, [2; 1; 4]);
%! assert_equal (ic, [2; 1; 2; 3]);
%! c = categorical ({'mid'; 'low'; 'mid'; 'high'}, {'low', 'mid', 'high'}, ...
%!                  'Ordinal', true);
%! [C, ia, ic] = unique (table (c, 'VariableNames', {'C'}));
%! assert_equal (ia, [2; 1; 4]);
%! assert_equal (ic, [2; 1; 2; 3]);
%! cd = calendarDuration ([0; 1; 0; 2], [3; 0; 3; 0], [0; 0; 0; 0]);
%! [C, ia, ic] = unique (table (cd, 'VariableNames', {'CD'}));
%! assert_equal (ia, [1; 2; 4]);
%! assert_equal (ic, [1; 2; 1; 3]);
%!test  # text variables sort lexicographically, 'stable' keeps encounter order
%! S = {'banana'; 'apple'; 'banana'; 'cherry'};
%! [C, ia, ic] = unique (table (S, 'VariableNames', {'S'}));
%! assert_equal (C.S, {'apple'; 'banana'; 'cherry'});
%! assert_equal (ia, [2; 1; 4]);
%! assert_equal (ic, [2; 1; 2; 3]);
%! C = unique (table (S, 'VariableNames', {'S'}), 'stable');
%! assert_equal (C.S, {'banana'; 'apple'; 'cherry'});
%! St = string ({'banana'; 'apple'; 'banana'; 'cherry'});
%! C = unique (table (St, 'VariableNames', {'St'}));
%! assert_equal (cellstr (C.St), {'apple'; 'banana'; 'cherry'});
%!test  # nested numeric table variable (regression: brace-indexed extraction)
%! Ag = [38; 43; 38]; Hi = [71; 69; 71];
%! [C, ia, ic] = unique (table (table (Ag, Hi), 'VariableNames', {'N'}));
%! assert_equal (ia, [1; 2]);
%! assert_equal (ic, [1; 2; 1]);
## Test input validation for 'unique' method
%!error <table.unique: too many input arguments.> unique (tblA, 1, 2);
%!error <table.unique: invalid option 'some'.> unique (tblA, "some");
%!error <table.unique: cannot find unique rows for variables of 'cell' type.> ...
%! unique (table ({2; 2; 3}), "sorted");
%!error <table.unique: cannot find unique rows for variables of 'struct' type.> ...
%! unique (table (struct ("a", {2; 2; 3})), "sorted");
%!error <table.unique: cannot find unique rows for nested tables with mixed data types.> ...
%! unique (table (table (Age, LastName)), "sorted");

## Test 'topkrows' method
%!test
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 133; 119];
%! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80];
%! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName);
%! [sorted_tblA, index] = topkrows (tblA, 3, "RowNames");
%! assert_equal (index, [1; 3; 2]);
%! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true);
%! [sorted_tblA, index] = topkrows (tblA, 4, "RowNames", "descend");
%! assert_equal (index, [1; 3; 2; 4]);
%! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true);
%!test
%! tblA.Properties.DimensionNames(1) = "Patients";
%! assert_equal (issortedrows (tblA, "Patients"), false);
%! [sorted_tblA, index] = topkrows (tblA, 2, "Patients");
%! assert_equal (index, [1; 3]);
%! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true);
%! [sorted_tblA, index] = topkrows (tblA, 5, "Patients", "descend");
%! assert_equal (index, [1; 3; 2; 4; 5]);
%! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true);
%!test
%! [sorted_tblA, index] = topkrows (tblA, 3);
%! assert_equal (index, [5; 2; 4]);
%! assert_equal (issortedrows (sorted_tblA, [], "descend"), true);
%! [sorted_tblA, index] = topkrows (tblA, 3, ":", "ascend");
%! assert_equal (index, [3; 1; 4]);
%! assert_equal (issortedrows (sorted_tblA, [], "ascend"), true);
%! [sorted_tblA, index] = topkrows (tblA, 3, [], "descend");
%! assert_equal (index, [5; 2; 4]);
%! assert_equal (issortedrows (sorted_tblA, [], "descend"), true);
%!test
%! [tblB, index] = topkrows (tblA, 3, "Height");
%! assert_equal (index, [1; 2; 4]);
%! assert_equal (tblB.Properties.RowNames, {"Sanchez"; "Johnson"; "Diaz"});
%! assert_equal (issortedrows (tblB, "Height", "descend"), true);
%!test
%! LastName = {"Sweet"; "Jacobson"; "Wang"; "Joiner"; "Berger"};
%! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName);
%! [tblB, index] = topkrows (tblA, 2, {"Height", "Weight"}, ...
%!                                    {"ascend", "descend"});
%! assert_equal (index, [3; 5]);
%! assert_equal (tblB.Properties.RowNames, {"Wang"; "Berger"});
%! assert_equal (issortedrows (tblB, {"Height", "Weight"}, ...
%!                             {"ascend", "descend"}), true);
%!test
%! [tblB, index] = topkrows (tblA, 4, {"BloodPressure"}, {"ascend"});
%! assert_equal (index, [2; 4; 5; 1]);
%! assert_equal (issortedrows (tblB, {"BloodPressure"}, {"ascend"}), true);
%! [tblB, index] = topkrows (tblA, 3, {"BloodPressure"}, {"descend"});
%! assert_equal (index, [3; 1; 5]);
%! assert_equal (issortedrows (tblB, "BloodPressure", "descend"), true);
%!test
%! [tblB, index] = topkrows (tblA, 3, {"Height", "RowNames"}, {"ascend"});
%! assert_equal (index, [5; 3; 4]);
%! assert_equal (tblB.Properties.RowNames, {"Berger"; "Wang"; "Joiner"})
%! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, {"ascend"}), true);
%! [tblB, index] = topkrows (tblA, 4, {"Height", "RowNames"}, ...
%!                                    {"ascend", "descend"});
%! assert_equal (index, [3; 5; 4; 2]);
%! assert_equal (tblB.Properties.RowNames, {"Wang"; "Berger"; "Joiner"; "Jacobson"});
%! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, ...
%!                             {"ascend", "descend"}), true);
%!test
%! [tblB, index] = topkrows (tblA, 3, [true, true, false, false]);
%! assert_equal (index, [5; 2; 4]);
%! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), ...
%!               true);
%! [tblB, index] = topkrows (tblA, 2, [true, true, false, false], "ascend");
%! assert_equal (index, [3; 1]);
%! assert_equal (issortedrows (tblB, [true, true, false, false], "ascend"), true);
%! [tblB, index] = topkrows (tblA, 5, [true, true, false, false], "descend");
%! assert_equal (index, flip ([3; 1; 4; 2; 5]));
%! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), true);
%! [tblB, index] = topkrows (tblA, 6, [true, true, false, false], ...
%!                                    {"ascend", "descend"});
%! assert_equal (index, [1; 3; 4; 2; 5]);
%! assert_equal (issortedrows (tblB, [true, true, false, false], ...
%!                             {"ascend", "descend"}), true);
%!test
%! ## A positive numeric index sorts descending (opposite of sortrows), so a
%! ## positive index matches the descending default of a named variable.
%! [sorted_tblA, indexN] = topkrows (tblA, 2, 1);
%! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true);
%! [sorted_tblA, indexV] = topkrows (tblA, 2, "Age");
%! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true);
%! assert_equal (indexN, indexV);
%! assert_equal (indexN, [5; 2]);
%! ## A negative numeric index sorts ascending.
%! [sorted_tblA, indexN] = topkrows (tblA, 3, -1);
%! assert_equal (issortedrows (sorted_tblA, "Age", "ascend"), true);
%! [sorted_tblA, indexV] = topkrows (tblA, 3, "Age", "ascend");
%! assert_equal (indexN, indexV);
%! assert_equal (indexN, [1; 3; 4]);
%! ## An explicit DIRECTION ignores the sign of a numeric index.
%! [sorted_tblA, indexN] = topkrows (tblA, 5, -1, "ascend");
%! [~, indexV] = topkrows (tblA, 5, "Age", "ascend");
%! assert_equal (indexN, indexV);
%! [sorted_tblA, indexN] = topkrows (tblA, 3, [-1, 2]);
%! [~, indexV] = topkrows (tblA, 3, {"Age", "Height"}, {"ascend", "descend"});
%! assert_equal (indexN, indexV);
%! assert_equal (indexN, [1; 3; 4]);
%!test
%! TT = table (Age, table (Age, Height), BloodPressure, 'RowNames', LastName);
%! [sorted_TT, index] = topkrows (TT, 4);
%! assert_equal (index, [5; 2; 4; 1]);
%! assert_equal (issortedrows (sorted_TT, [], "descend"), true);
%! [sorted_TT, index] = topkrows (TT, 3, [1, 2]);
%! assert_equal (index, [5; 2; 4]);
%! assert_equal (issortedrows (sorted_TT, [-1, -2]), true);
%! [sorted_TT, index] = topkrows (TT, 2, [1, -2]);
%! assert_equal (index, [5; 2]);
%! assert_equal (issortedrows (sorted_TT, [-1, 2]), true);
%!test
%! Weights = [176;NaN;131;133;NaN];
%! tblN = table (Age, Height, Weights, BloodPressure, 'RowNames', LastName);
%! [tblB, index] = topkrows (tblN, 4, "Weights", "MissingPlacement", "first");
%! assert_equal (index, [2; 5; 1; 4]);
%! assert_equal (issortedrows (tblB, "Weights", "descend", ...
%!                             "MissingPlacement", "first"), true);
%! [tblB, index] = topkrows (tblN, 3, "Weights");
%! assert_equal (index, [2; 5; 1]);
%! assert_equal (issortedrows (tblB, "Weights", "descend"), true);
%!test
%! tblN = table (Age, Height, Weight, BloodPressure, LastName);
%! [tblB, index] = topkrows (tblN, 3, vartype ("numeric"));
%! assert_equal (index, [5; 2; 4]);
%! assert_equal (issortedrows (tblB, vartype ("numeric"), "descend"), true);
%! [tblB, index] = topkrows (tblN, 3, vartype ("cellstr"));
%! assert_equal (index, [3; 1; 4]);
%! assert_equal (issortedrows (tblB, vartype ("cellstr"), "descend"), true);
%!test  # special-type sort keys (datetime / categorical / string / calendarDuration)
%! dt = datetime (2020, [3; 1; 2; 1], 1);
%! [tblB, index] = topkrows (table (dt, 'VariableNames', {'D'}), 2);
%! assert_equal (index, [1; 3]);
%! assert_equal (issortedrows (tblB, 'D', 'descend'), true);
%! c = categorical ({'mid'; 'low'; 'high'}, {'low', 'mid', 'high'}, ...
%!                  'Ordinal', true);
%! [tblB, index] = topkrows (table (c, 'VariableNames', {'C'}), 2);
%! assert_equal (index, [3; 1]);
%! st = string ({'b'; 'a'; 'c'});
%! [tblB, index] = topkrows (table (st, 'VariableNames', {'S'}), 2);
%! assert_equal (index, [3; 1]);
%! cd = calendarDuration ([0; 2; 1], [3; 1; 2], [0; 0; 0]);
%! [tblB, index] = topkrows (table (cd, 'VariableNames', {'CD'}), 2);
%! assert_equal (index, [2; 3]);
%!test  # K edge cases: zero, equal to height, larger than height
%! Ag = [38; 43; 38];
%! A = table (Ag, 'VariableNames', {'A'});
%! tblB = topkrows (A, 0);
%! assert_equal (height (tblB), 0);
%! assert_equal (width (tblB), 1);
%! [tblB, index] = topkrows (A, 3);
%! assert_equal (index, [2; 1; 3]);
%! [tblB, index] = topkrows (A, 10);
%! assert_equal (index, [2; 1; 3]);
%! assert_equal (height (tblB), 3);
%!test  # 'ComparisonMethod' passes through to sortrows
%! z = [3+4i; -1+0i; 0+2i; -5+0i];
%! [tblB, index] = topkrows (table (z, 'VariableNames', {'Z'}), 2, 'Z', ...
%!                           'ComparisonMethod', 'abs');
%! assert_equal (index, [4; 1]);
## Test input validation for 'topkrows' method
%!error <table.topkrows: K must be a nonnegative integer scalar.> ...
%! topkrows (tblA, -1)
%!error <table.topkrows: K must be a nonnegative integer scalar.> ...
%! topkrows (tblA, 2.5)

## Test 'addvars' method
%!test
%! load patients
%! T1 = table (Age, Height, Weight);
%! T2 = addvars (T1, ones (size (Age)));
%! assert_equal (size (head (T2, 3)), [3, 4]);
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Height', 'Weight', 'Var4'});
%!test
%! load patients
%! T1 = table (LastName, Age, Height, Weight);
%! assert_equal (size (head (T1, 3)), [3, 4]);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Height', 'Weight'});
%! T2 = addvars (T1, Gender, Smoker);
%! assert_equal (size (head (T2, 3)), [3, 6]);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Age', 'Height', 'Weight', 'Gender', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Gender);
%! assert_equal (size (head (T1, 3)), [3, 2]);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Gender'});
%! T2 = addvars (T1, Age, 'Before', 'Gender');
%! assert_equal (size (head (T2, 5)), [5, 3]);
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Age', 'Gender'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender);
%! assert_equal (size (head (T1, 4)), [4, 3]);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender'});
%! T2 = addvars (T1, Height, Weight, 'After', 'Age');
%! assert_equal (size (head (T2, 4)), [4, 5]);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Age', 'Height', 'Weight', 'Gender'});
%!test
%! load patients
%! T1 = table (LastName, Age, Height, Weight, Gender);
%! assert_equal (size (head (T1, 3)), [3, 5]);
%! assert_equal (T1.Properties.VariableNames, ...
%!         {'LastName', 'Age', 'Height', 'Weight', 'Gender'});
%! T2 = addvars (T1, Smoker, 'After', 1);
%! assert_equal (size (head (T2, 3)), [3, 6]);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Smoker', 'Age', 'Height', 'Weight', 'Gender'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! assert_equal (size (head (T1, 3)), [3, 4]);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', 'Smoker'});
%! T2 = addvars (T1, [Diastolic Systolic], 'NewVariableNames', 'BloodPressure');
%! assert_equal (size (head (T2, 3)), [3, 5]);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Age', 'Gender', 'Smoker', 'BloodPressure'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! assert_equal (size (head (T1, 3)), [3, 4]);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', 'Smoker'});
%! T2 = addvars (T1, Height, Weight, 'Before', 'Smoker', ...
%!                                   'NewVariableNames', {'Inches', 'Pounds'});
%! assert_equal (size (head (T2, 3)), [3, 6]);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Age', 'Gender', 'Inches', 'Pounds', 'Smoker'});
%!test  # 'Before'/'After' with logical-vector and numeric LOCATION
%! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'});
%! nv = [7; 8];
%! R = addvars (T, nv, 'Before', [false, true, false], ...
%!              'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'});
%! R = addvars (T, nv, 'After', [false, true, false], ...
%!              'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'C'});
%! R = addvars (T, nv, 'Before', 2, 'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'});
%! R = addvars (T, nv, 'Before', 1, 'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'X', 'A', 'B', 'C'});
%! R = addvars (T, nv, 'After', 3, 'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'C', 'X'});
%! R = addvars (T, nv, 'After', string ('B'), 'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'C'});
%! R = addvars (T, nv, 'Before', string ('B'), 'NewVariableNames', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'});
%!test  # nested-table variable and string-array NewVariableNames
%! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'});
%! inner = table ([7;8], [9;10], 'VariableNames', {'P', 'Q'});
%! R = addvars (T, inner, 'NewVariableNames', 'N');
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'N'});
%! assert_equal (istable (R.N), true);
%! assert_equal (R.N.P, [7; 8]);
%! R = addvars (T, [11;12], [13;14], 'NewVariableNames', string ({'X', 'Y'}));
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'Y'});
## Test input validation for 'addvars' method
%!error <table.addvars: cannot use both 'After' and 'Before' options.> ...
%! addvars (tblA, Smoker, 'After', 1, 'Before', 2);
%!error <table.addvars: LOCATION must index a single variable.> ...
%! addvars (tblA, Smoker, 'After', [true, false, false, true]);
%!error <table.addvars: LOCATION must index a single variable.> ...
%! addvars (tblA, Smoker, 'Before', [true, false, false, true]);
%!error <table.addvars: LOCATION must be either a scalar integer, a character vector, or a logical vector indexing a single table variable.> ...
%! addvars (tblA, Smoker, 'After', [1, 2]);
%!error <table.addvars: LOCATION must be either a scalar integer, a character vector, or a logical vector indexing a single table variable.> ...
%! addvars (tblA, Smoker, 'Before', [1, 2]);
%!error <table.addvars: NEWNAMES does not match the number of new variables.> ...
%! addvars (tblA, Smoker, 'NewVariableNames', {'A', 'B'});
%!error <table.addvars: NEWNAMES contains duplicate names.> ...
%! addvars (tblA, Smoker, Smoker, 'NewVariableNames', {'A', 'A'});
%!error <table.addvars: new variable name 'Height' already exists.> ...
%! addvars (tblA, Smoker, 'NewVariableNames', {'Height'});
%!error <table.addvars: new variable names 'Height', 'Weight' already exist.> ...
%! addvars (tblA, Smoker, Smoker, 'NewVariableNames', {'Height', 'Weight'});

## Test 'rename' method
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', 'Smoker'});
%! T2 = renamevars (T1, {'LastName', 'Gender'}, {'Name', 'Sex'});
%! assert_equal (T2.Properties.VariableNames, {'Name', 'Age', 'Sex', 'Smoker'});
%!test
%! T1 = array2table (rand (5, 3));
%! assert_equal (T1.Properties.VariableNames, {'Var1', 'Var2', 'Var3'});
%! allVars = 1:width (T1);
%! newNames = cellfun (@(x) sprintf ("Rand%i",x), num2cell (allVars), ...
%!                     "UniformOutput", false);
%! T2 = renamevars (T1, allVars, newNames);
%! assert_equal (T2.Properties.VariableNames, {'Rand1', 'Rand2', 'Rand3'});
%!test  # VARS forms: char, string array, logical vector, vartype
%! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'});
%! R = renamevars (T, 'B', 'X');
%! assert_equal (R.Properties.VariableNames, {'A', 'X', 'C'});
%! R = renamevars (T, string ({'A', 'C'}), string ({'X', 'Z'}));
%! assert_equal (R.Properties.VariableNames, {'X', 'B', 'Z'});
%! R = renamevars (T, [true, false, true], {'X', 'Z'});
%! assert_equal (R.Properties.VariableNames, {'X', 'B', 'Z'});
%! R = renamevars (T, vartype ('numeric'), {'X', 'Y', 'Z'});
%! assert_equal (R.Properties.VariableNames, {'X', 'Y', 'Z'});
%!test  # renaming preserves data and per-variable metadata
%! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'});
%! T.Properties.VariableDescriptions = {'da', 'db'};
%! T.Properties.VariableUnits = {'ua', 'ub'};
%! R = renamevars (T, 'A', 'X');
%! assert_equal (R.X, [1; 2]);
%! assert_equal (R.Properties.VariableDescriptions, {'da', 'db'});
%! assert_equal (R.Properties.VariableUnits, {'ua', 'ub'});
## Test input validation for 'renamevars' method
%!error <table.renamevars: too few input arguments.> ...
%! renamevars (tblA, {"Age", "Smoker"});
%!error <table.renamevars: too few input arguments.> ...
%! renamevars (tblA, {"Age"}, {});
%!error <table.renamevars: too few input arguments.> ...
%! renamevars (tblA, {}, {"NewName"});
%!error <table.renamevars: NEWNAMES must be either a character vector, a cell array of character vectors, or a string array.> ...
%! renamevars (tblA, {"Age", "Smoker"}, {"Age", 4});
%!error <table.renamevars: NEWNAMES contains duplicate names.> ...
%! renamevars (tblA, {"Age", "Smoker"}, {"Age", "Age"});
%!error <table.renamevars: cannot index non-existing variable: 'Smoker'.> ...
%! renamevars (tblA, {"Age", "Smoker"}, {"Age", "User"});
%!error <table.renamevars: number of names in NEWNAMES do not match the selected variables specified by VARS.> ...
%! renamevars (tblA, {"Age", "Height"}, {"Age", "Smoker", "Height"});
%!error <table.renamevars: newly assigned variable name already exists.> ...
%! renamevars (tblA, {"Age", "Height"}, {"Age", "Weight"});

## Test 'movevars' method
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', 'Smoker'});
%! T2 = movevars (T1, {'LastName', 'Gender'});
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker', 'LastName', 'Gender'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, vartype ("cellstr"));
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker', 'LastName', 'Gender'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, vartype ("numeric"));
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker', 'Age'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, {'LastName', 'Gender'}, 'After', 'Age');
%! assert_equal (T2.Properties.VariableNames, {'Age', 'LastName', 'Gender', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, [true, false, true, false], 'After', 'Age');
%! assert_equal (T2.Properties.VariableNames, {'Age', 'LastName', 'Gender', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, [3, 4], 'Before', 'Age');
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker', 'Age'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, [3, 4], 'Before', 2);
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker', 'Age'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = movevars (T1, [3, 4], 'Before', [false, true, false, false]);
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker', 'Age'});
%!test  # VARS forms (string array, char single) and LOCATION edges
%! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', 'D'});
%! R = movevars (T, string ({'A', 'C'}));
%! assert_equal (R.Properties.VariableNames, {'B', 'D', 'A', 'C'});
%! R = movevars (T, 'A');
%! assert_equal (R.Properties.VariableNames, {'B', 'C', 'D', 'A'});
%! R = movevars (T, {'C', 'D'}, 'Before', 1);
%! assert_equal (R.Properties.VariableNames, {'C', 'D', 'A', 'B'});
%! R = movevars (T, 'C', 'Before', 'A');
%! assert_equal (R.Properties.VariableNames, {'C', 'A', 'B', 'D'});
%! R = movevars (T, 'A', 'After', 4);
%! assert_equal (R.Properties.VariableNames, {'B', 'C', 'D', 'A'});
%! R = movevars (T, 'A', 'After', string ('C'));
%! assert_equal (R.Properties.VariableNames, {'B', 'C', 'A', 'D'});
%! R = movevars (T, 'D', 'Before', string ('B'));
%! assert_equal (R.Properties.VariableNames, {'A', 'D', 'B', 'C'});
## Test input validation for 'movevars' method
%!error <table.movevars: too few input arguments.> movevars (tblA);
%!error <table.movevars: too few input arguments.> movevars (tblA, {});
%!error <table.movevars: cannot use both 'After' and 'Before' options.> ...
%! movevars (tblA, 1, "After", "Height", "Before", "Weight");
%!error <table.movevars: LOCATION must index a single variable.> ...
%! movevars (tblA, 1, "After", [true, true, false, false]);
%!error <table.movevars: LOCATION must be either a scalar integer, a character vector, or a logical vector indexing a single table variable.> ...
%! movevars (tblA, 1, "After", [1, 2]);
%!error <table.movevars: LOCATION must be either a scalar integer, a character vector, or a logical vector indexing a single table variable.> ...
%! movevars (tblA, 1, "After", {"Height"});
%!error <table.movevars: LOCATION does not index an existing variable.> ...
%! movevars (tblA, 1, "After", "Smoker");
%!error <table.movevars: cannot index non-existing variable: 'Smoker'.> ...
%! movevars (tblA, "Smoker", "After", 2);
%!error <table.movevars: LOCATION variable cannot be moved.> ...
%! movevars (tblA, {"Age", "Height"}, "After", 2);
%!error <table.movevars: LOCATION variable cannot be moved.> ...
%! movevars (tblA, {"Height", "Weight"}, "Before", "Height");

## Test 'removevars' method
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = removevars (T1, {'LastName', 'Gender'});
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = removevars (T1, [true, false, true, false]);
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = removevars (T1, [1, 3]);
%! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'});
%!test
%! load patients
%! T1 = table (LastName, Age, Gender, Smoker);
%! T2 = removevars (T1, vartype ("numeric"));
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker'});
%!test  # VARS forms (string array, single char) and removing all variables
%! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'});
%! R = removevars (T, string ({'A', 'C'}));
%! assert_equal (R.Properties.VariableNames, {'B'});
%! R = removevars (T, 'B');
%! assert_equal (R.Properties.VariableNames, {'A', 'C'});
%! R = removevars (T, {'A', 'B', 'C'});
%! assert_equal (width (R), 0);
%!test  # custom variable properties shrink with the removed variables
%! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'});
%! T = addprop (T, 'Note', 'variable');
%! T.Properties.CustomProperties.Note = {'na', 'nb', 'nc'};
%! R = removevars (T, 'B');
%! assert_equal (R.Properties.VariableNames, {'A', 'C'});
%! assert_equal (R.Properties.CustomProperties.Note, {'na', 'nc'});
%! R = removevars (T, [1, 3]);
%! assert_equal (R.Properties.CustomProperties.Note, {'nb'});
## Test input validation for 'removevars' method
%!error <table.removevars: too few input arguments.> removevars (tblA);
%!error <table: variable index must be a vector.> removevars (tblA, ones (2));
%!error <table: variable logical index does not match table width.> ...
%! removevars (tblA, [true, true, false]);
%!error <table: variable index out of bounds: requested index 5; table has 4 variables.> ...
%! removevars (tblA, [1, 5]);
%!error <table: variable index out of bounds: requested index 7; table has 4 variables.> ...
%! removevars (tblA, [1, 2, 7]);
%!error <table: no such variable in table: 'Smoker'.> ...
%! removevars (tblA, "Smoker");
%!error <table: no such variables in table: 'Smoker', 'Health'.> ...
%! removevars (tblA, {"Smoker", "Health"});
%!error <table: unsupported variable indexing operand type: 'cell'.> ...
%! removevars (tblA, {1});
%!error <table: unsupported variable indexing operand type: 'struct'.> ...
%! removevars (tblA, struct ("A", 1));

## Test 'splitvars' method
%!test
%! A = [1:3]';
%! B = rand (3);
%! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'};
%! D = {"A"; "B"; "C"};
%! T1 = table (A, B, C, D);
%! T2 = splitvars (T1);
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D'});
%!test
%! A = [1:3]';
%! B = rand (3);
%! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'};
%! D = {"A"; "B"; "C"};
%! T1 = table (A, B, C, D);
%! T2 = addvars (T1, table ([5; 5; 5]));
%! assert_equal (size (T2.Var5), [3, 1]);
%! T3 = splitvars (T2);
%! assert_equal (T3.Properties.VariableNames, ...
%!         {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1'});
%! assert_equal (size (T3.Var1), [3, 1]);
%!test
%! A = [1:3]';
%! B = rand (3);
%! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'};
%! D = {"A"; "B"; "C"};
%! T1 = table (A, B, C, D);
%! T2 = addvars (T1, table ([5, 6; 5, 6; 5, 6]));
%! assert_equal (size (T2.Var5), [3, 1]);
%! T3 = splitvars (T2);
%! assert_equal (T3.Properties.VariableNames, ...
%!         {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1'});
%! assert_equal (size (T3.Var1), [3, 2]);
%!test
%! A = [1:3]';
%! B = rand (3);
%! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'};
%! D = {"A"; "B"; "C"};
%! T1 = table (A, B, C, D);
%! A = [5; 5; 5];
%! T2 = addvars (T1, table (A));
%! T3 = splitvars (T2);
%! assert_equal (T3.Properties.VariableNames, ...
%!         {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var5_A'});
%!test
%! A = [1:3]';
%! B = rand (3);
%! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'};
%! D = {"A"; "B"; "C"};
%! T1 = table (A, B, C, D);
%! A = [5; 5; 5];
%! T2 = addvars (T1, table ([5, 6; 5, 6; 5, 6]), table (A));
%! T3 = splitvars (T2);
%! assert_equal (T3.Properties.VariableNames, ...
%!         {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1', 'Var6_A'});
%!test
%! load patients
%! Personal_Data = table (Gender, Age);
%! BMI_Data = table (Height, Weight);
%! BloodPressure = table (Systolic, Diastolic);
%! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure);
%! T2 = splitvars (T1, 'BloodPressure');
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Personal_Data', 'BMI_Data', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = table (Gender, Age);
%! BMI_Data = table (Height, Weight);
%! BloodPressure = table (Systolic, Diastolic);
%! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure);
%! T2 = splitvars (T1, {'BMI_Data', 'BloodPressure'});
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Personal_Data', ...
%!         'Height', 'Weight', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = table (Gender, Age);
%! BMI_Data = table (Height, Weight);
%! BloodPressure = table (Systolic, Diastolic);
%! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure);
%! T2 = splitvars (T1, [2, 4]);
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Age', ...
%!                                       'BMI_Data', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = table (Gender, Age);
%! BMI_Data = table (Height, Weight);
%! BloodPressure = table (Systolic, Diastolic);
%! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure);
%! T2 = splitvars (T1, [false, true, false, true]);
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Age', ...
%!                                       'BMI_Data', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = [Age, Height, Weight];
%! BloodPressure = [Systolic, Diastolic];
%! T1 = table (LastName, Gender, Personal_Data, BloodPressure);
%! T2 = splitvars (T1, 'BloodPressure', ...
%!                 'NewVariableNames', {'Systolic','Diastolic'});
%! assert_equal (T2.Properties.VariableNames, ...
%!         {'LastName', 'Gender', 'Personal_Data', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = [Age, Height, Weight];
%! BloodPressure = [Systolic, Diastolic];
%! T1 = table (LastName, Gender, Personal_Data, BloodPressure);
%! T2 = splitvars (T1, {'Personal_Data', 'BloodPressure'}, ...
%!                 'NewVariableNames', {{'Age', 'Height', 'Weight'}, ...
%!                                      {'Systolic', 'Diastolic'}});
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ...
%!         'Age', 'Height', 'Weight', 'Systolic', 'Diastolic'});
%!test
%! load patients
%! Personal_Data = [Age, Height, Weight];
%! BloodPressure = [Systolic, Diastolic];
%! T1 = table (LastName, Gender, Personal_Data, BloodPressure);
%! new_PD = string ({'Age', 'Height', 'Weight'});
%! T2 = splitvars (T1, {'Personal_Data', 'BloodPressure'}, ...
%!                 'NewVariableNames', {new_PD, {'Systolic', 'Diastolic'}});
%! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ...
%!         'Age', 'Height', 'Weight', 'Systolic', 'Diastolic'});
%!test  # duplicate inner names across nested tables get disambiguated
%! n1 = table ([1;2], [3;4], 'VariableNames', {'X', 'Y'});
%! n2 = table ([5;6], [7;8], 'VariableNames', {'X', 'Y'});
%! T = table (n1, n2, 'VariableNames', {'P', 'Q'});
%! R = splitvars (T);
%! assert_equal (R.Properties.VariableNames, {'P_X', 'P_Y', 'Q_X', 'Q_Y'});
%! assert_equal (R.P_X, [1; 2]);
%! assert_equal (R.Q_Y, [7; 8]);
%!test  # splitting a multicolumn special-type variable preserves its class
%! d = hours ([1, 2; 3, 4; 5, 6]);
%! R = splitvars (table (d, 'VariableNames', {'D'}));
%! assert_equal (R.Properties.VariableNames, {'D_1', 'D_2'});
%! assert_equal (class (R.D_1), 'duration');
%! assert_equal (hours (R.D_1), [1; 3; 5]);
%! assert_equal (hours (R.D_2), [2; 4; 6]);
%!test  # string-array VARS, single-column var ignored, data preserved
%! n = table ([1;2], [3;4], 'VariableNames', {'X', 'Y'});
%! T = table ([9;9], n, 'VariableNames', {'A', 'N'});
%! R = splitvars (T, string ('N'));
%! assert_equal (R.Properties.VariableNames, {'A', 'X', 'Y'});
%! T = table ([1;2], [3, 4; 5, 6], 'VariableNames', {'S', 'M'});
%! R = splitvars (T, 'S');
%! assert_equal (R.Properties.VariableNames, {'S', 'M'});
%! R = splitvars (table ([10, 20; 30, 40], 'VariableNames', {'M'}));
%! assert_equal (R.M_1, [10; 30]);
%! assert_equal (R.M_2, [20; 40]);
## Test input validation for 'splitvars' method
%!error <table.splitvars: too many input arguments.> splitvars (tblA, 1, 2, 3, 4);
%!error <table.splitvars: invalid input for 'NewVariableNames'.> ...
%! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', {1})
%!error <table.splitvars: invalid input for 'NewVariableNames'.> ...
%! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', 1)
%!error <table.splitvars: invalid input for 'NewVariableNames'.> ...
%! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', "new_name")
%!error <table.splitvars: invalid input for 'NewVariableNames'.> ...
%! splitvars (table ([5, 6; 5, 6; 5, 6], ones (3, 2)), ":", ...
%!            'NewVariableNames', {{"A", "B"}, {"new_name", 1}})
%!error <table.splitvars: wrong number of 'NewVariableNames'.> ...
%! splitvars (table ([5, 6; 5, 6; 5, 6], ones (3, 2)), ":", ...
%!            'NewVariableNames', {{"A", "B"}, {"C", "D", "E"}})

## Test 'mergevars' method
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, [2, 3]);
%! assert_equal (T2.Properties.VariableNames, {'A', 'Var2', 'D'});
%! assert_equal (isequal (T2.Var2, cat (2, B, C)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, [2, 3], 'NewVariableName', 'Merged');
%! assert_equal (T2.Properties.VariableNames, {'A', 'Merged', 'D'});
%! assert_equal (isequal (T2.Merged, cat (2, B, C)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, [2, 3], 'MergeAsTable', true);
%! assert_equal (T2.Properties.VariableNames, {'A', 'Var2', 'D'});
%! assert_equal (isequal (T2.Var2, table (B, C)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, [2, 3], 'NewVariableName', 'Merged', 'MergeAsTable', 1);
%! assert_equal (T2.Properties.VariableNames, {'A', 'Merged', 'D'});
%! assert_equal (isequal (T2.Merged, table (B, C)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, {"A", "B"});
%! assert_equal (T2.Properties.VariableNames, {'Var1', 'C', 'D'});
%! assert_equal (isequal (T2.Var1, cat (2, A, B)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, {"A", "B"}, 'NewVariableName', 'Merged');
%! assert_equal (T2.Properties.VariableNames, {'Merged', 'C', 'D'});
%! assert_equal (isequal (T2.Merged, cat (2, A, B)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, {"A", "B"}, 'MergeAsTable', true);
%! assert_equal (T2.Properties.VariableNames, {'Var1', 'C', 'D'});
%! assert_equal (isequal (T2.Var1, table (A, B)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! T2 = mergevars (T1, {"A", "B"}, 'NewVariableName', 'Merged', ...
%!                                 'MergeAsTable', true);
%! assert_equal (T2.Properties.VariableNames, {'Merged', 'C', 'D'});
%! assert_equal (isequal (T2.Merged, table (A, B)), true);
%!test  # string-array VARS and string-scalar NewVariableName
%! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', 'D'});
%! R = mergevars (T, string ({'B', 'C'}), 'NewVariableName', string ('M'));
%! assert_equal (R.Properties.VariableNames, {'A', 'M', 'D'});
%! assert_equal (R.M, [3, 5; 4, 6]);
%!test  # logical and vartype VARS, and a single-variable merge is a no-op
%! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', 'D'});
%! R = mergevars (T, [false, true, true, false]);
%! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'D'});
%! R = mergevars (T, vartype ('numeric'));
%! assert_equal (R.Properties.VariableNames, {'Var1'});
%! R = mergevars (T, 2);
%! assert_equal (R.Properties.VariableNames, {'A', 'B', 'C', 'D'});
%!test  # non-contiguous VARS merge keeps the first-merged position
%! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', 'D'});
%! R = mergevars (T, [2, 4]);
%! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'C'});
%! assert_equal (R.Var2, [3, 7; 4, 8]);
%!test  # merging special-type variables preserves their class
%! T = table (hours ([1;2]), hours ([3;4]), 'VariableNames', {'P', 'Q'});
%! R = mergevars (T, [1, 2]);
%! assert_equal (class (R.Var1), 'duration');
%! assert_equal (hours (R.Var1), [1, 3; 2, 4]);
%!test  # custom properties of the first merged variable are preserved
%! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', 'D'});
%! T = addprop (T, 'Note', 'variable');
%! T.Properties.CustomProperties.Note = {'nA', 'nB', 'nC', 'nD'};
%! R = mergevars (T, [2, 3]);
%! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'D'});
%! assert_equal (R.Properties.CustomProperties.Note, {'nA', 'nB', 'nD'});
## Test input validation for 'mergevars' method
%!error <table.mergevars: too few input arguments.> mergevars (tblA);
%!error <table.mergevars: invalid input for 'MergeAsTable'.> ...
%! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', 'on');
%!error <table.mergevars: invalid input for 'MergeAsTable'.> ...
%! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', 2);
%!error <table.mergevars: invalid input for 'MergeAsTable'.> ...
%! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', [true, true]);
%!error <table.mergevars: invalid input for 'NewVariableName'.> ...
%! mergevars (tblA, {'Height', 'Weight'}, 'NewVariableName', 2)
%!error <table.mergevars: assigned 'NewVariableName' already exists.> ...
%! mergevars (tblA, {'Height', 'Weight'}, 'NewVariableName', 'Age');
%!error <table.mergevars: selected variables cannot be merged into a multicolumn variable due to incompatible variable types.> ...
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! mergevars (addvars (tblA, LastName), {'Height', 'LastName'});

## Test 'convertvars' method
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! assert_equal (isa (T1.A, "numeric"), true);
%! assert_equal (isa (T1.B, "numeric"), true);
%! T2 = convertvars (T1, {"A", "B"}, "string");
%! assert_equal (isa (T2.A, "string"), true);
%! assert_equal (isa (T2.B, "string"), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! assert_equal (isa (T1.D, "string"), true);
%! T2 = convertvars (T1, 4, "cellstr");
%! assert_equal (iscellstr (T2.D), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! assert_equal (isa (T1.D, "string"), true);
%! T2 = convertvars (T1, [1:3], @(x) sqrt (x));
%! assert_equal (isequal (T2.A, sqrt (A)), true);
%! assert_equal (isequal (T2.B, sqrt (B)), true);
%! assert_equal (isequal (T2.C, sqrt (C)), true);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = string ({"a"; "b"; "c"});
%! T1 = table (A, B, C, D);
%! assert_equal (isa (T1.D, "string"), true);
%! T2 = convertvars (T1, [1:3], @(x) [x, x, x]);
%! assert_equal (size (T2.A, 2), 3);
%! assert_equal (size (T2.B, 2), 3);
%! assert_equal (size (T2.C, 2), 3);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = {"a"; "b"; "c"};
%! T1 = table (A, B, C, D);
%! T2 = convertvars (T1, "D", @(x) cellstr (upper (char (x))));
%! assert_equal (T2.D, {"A"; "B"; "C"});
%!test  # VARS forms: string array, logical vector, vartype
%! T = table ([1;2;3], [4;5;6], {'x'; 'y'; 'z'}, 'VariableNames', {'A', 'B', 'C'});
%! R = convertvars (T, string ({'A', 'B'}), 'string');
%! assert_equal (isa (R.A, 'string'), true);
%! assert_equal (isa (R.B, 'string'), true);
%! R = convertvars (T, [true, false, false], 'single');
%! assert_equal (class (R.A), 'single');
%! R = convertvars (T, vartype ('numeric'), 'int32');
%! assert_equal (class (R.A), 'int32');
%! assert_equal (class (R.B), 'int32');
%!test  # conversion to special data types (categorical, duration)
%! T = table ([1;2;3], {'x'; 'y'; 'z'}, 'VariableNames', {'A', 'C'});
%! R = convertvars (T, 'C', 'categorical');
%! assert_equal (class (R.C), 'categorical');
%! R = convertvars (T, 'A', @hours);
%! assert_equal (class (R.A), 'duration');
%! assert_equal (hours (R.A), [1; 2; 3]);
## Test input validation for 'convertvars' method
%!error <table.convertvars: too few input arguments.> convertvars (tblA);
%!error <table.convertvars: too few input arguments.> ...
%! convertvars (tblA, "Height");
%!error <table.convertvars: DATATYPE must be a character vector.> ...
%! convertvars (tblA, "Height", ["c","h";"a","r"]);
%!error <table.convertvars: DATATYPE must be either a character vector or a function handle; got a 'cell'.> ...
%! convertvars (tblA, "Height", {"char"});
%!error <table.convertvars: DATATYPE must be either a character vector or a function handle; got a 'double'.> ...
%! convertvars (tblA, "Height", 25);
%!error <table.convertvars: specified DATATYPE conversion cannot be applied on selected variable 'Height'.> ...
%! convertvars (tblA, "Height", "struct");
%!error <table.convertvars: specified DATATYPE conversion on 'Height' does not return the appropriate amount of rows.> ...
%! convertvars (tblA, "Height", @(x) sum (x));

## Test 'rows2vars' method
%!test
%! load patients
%! T1 = table (LastName, Gender, Age, Height, Weight);
%! T2 = rows2vars (T1);
%! assert_equal (size (T1, 2), size (T2, 1));
%! assert_equal (size (T1, 1) + 1, size (T2, 2));
%! assert_equal (T2.OriginalVariableNames, ...
%!         {"LastName"; "Gender"; "Age"; "Height"; "Weight"});
%! assert_equal (T2.Var1, {"Smith"; "Male"; 38; 71; 176});
%!test
%! load patients
%! T1 = table (Gender, Age, Height, Weight, "RowNames", LastName);
%! T2 = rows2vars (T1);
%! assert_equal (T2.OriginalVariableNames, {"Gender"; "Age"; "Height"; "Weight"});
%! assert_equal (T2.Smith, {"Male"; 38; 71; 176});
%! assert_equal (T2.Johnson, {"Male"; 43; 69; 163});
%! assert_equal (T2.Williams, {"Female"; 38; 64; 131});
%!test
%! load patients
%! T1 = table (LastName, Gender, Age, Height, Weight);
%! T2 = rows2vars (T1, "VariableNamesSource", "LastName");
%! assert_equal (T2.OriginalVariableNames, {"Gender"; "Age"; "Height"; "Weight"});
%! assert_equal (T2.Smith, {"Male"; 38; 71; 176});
%! assert_equal (T2.Johnson, {"Male"; 43; 69; 163});
%! assert_equal (T2.Williams, {"Female"; 38; 64; 131})
%!test
%! load patients
%! T1 = table (LastName, Gender, Age, Height, Weight);
%! T2 = rows2vars (T1, "DataVariables", {"LastName", "Gender", "Age"});
%! assert_equal (size (T2, 1), 3);
%! assert_equal (size (T1, 1) + 1, size (T2, 2));
%! assert_equal (T2.OriginalVariableNames, {"LastName"; "Gender"; "Age"});
%! assert_equal (T2.Var1, {"Smith"; "Male"; 38});
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! T1 = table (A, B, C);
%! T2 = rows2vars (T1);
%! assert_equal (class (T2.Var1), "double");
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! D = {"a"; "b"; "c"};
%! T1 = table (A, B, C, D);
%! T2 = rows2vars (T1);
%! assert_equal (class (T2.Var1), "cell");
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! T1 = table (A, B, C);
%! T2 = rows2vars (T1, "VariableNamesSource", "A");
%! assert_equal (size (T2), [2, 4]);
%! assert_equal (T2.Properties.VariableNames(1), {"OriginalVariableNames"});
%! assert_equal (T2.Properties.VariableNames(2:4), {"x1", "x2", "x3"});
%! assert_equal (T2.x1, [5; 3.14]);
%!test
%! A = [1:3]';
%! B = [5; 11; 12];
%! C = [3.14; 2.72; 1.37];
%! T1 = table (A, B, C);
%! T2 = rows2vars (T1, "VariableNamesSource", "A", ...
%!                     "VariableNamingRule", "preserve");
%! assert_equal (size (T2), [2, 4]);
%! assert_equal (T2.Properties.VariableNames(1), {"OriginalVariableNames"});
%! assert_equal (T2.Properties.VariableNames(2:4), {"1", "2", "3"});
%! assert_equal (T2.('1'), [5; 3.14]);
%!test  # DataVariables forms: numeric, logical, string array, vartype
%! T = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'});
%! R = rows2vars (T, 'DataVariables', [1, 2]);
%! assert_equal (R.OriginalVariableNames, {'A'; 'B'});
%! R = rows2vars (T, 'DataVariables', [true, false, true]);
%! assert_equal (R.OriginalVariableNames, {'A'; 'C'});
%! R = rows2vars (T, 'DataVariables', string ({'A', 'C'}));
%! assert_equal (R.OriginalVariableNames, {'A'; 'C'});
%! R = rows2vars (T, 'DataVariables', vartype ('numeric'));
%! assert_equal (R.OriginalVariableNames, {'A'; 'B'; 'C'});
%!test  # VariableNamesSource by numeric index and from a string variable
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'A', 'B'});
%! R = rows2vars (T, 'VariableNamesSource', 1);
%! assert_equal (R.Properties.VariableNames, ...
%!               {'OriginalVariableNames', 'x1', 'x2', 'x3'});
%! assert_equal (R.OriginalVariableNames, {'B'});
%! T2 = table (string ({'p'; 'q'; 'r'}), [4;5;6], 'VariableNames', {'S', 'B'});
%! R = rows2vars (T2, 'VariableNamesSource', 'S');
%! assert_equal (R.Properties.VariableNames, ...
%!               {'OriginalVariableNames', 'p', 'q', 'r'});
%! assert_equal (R.OriginalVariableNames, {'B'});
## Test input validation for  'rows2vars' method
%!error <table.rows2vars: 'DataVariables' index a non-existing variable: 'Some'.> ...
%! rows2vars (tblA, "DataVariables", "Some");
%!error <table.rows2vars: 'VariableNamesSource' must index a single variable.> ...
%! rows2vars (tblA, "VariableNamesSource", {"LastName", "Age"});
%!error <table.rows2vars: 'VariableNamesSource' indexes a non-existing variable: 'Some'.> ...
%! rows2vars (tblA, "VariableNamesSource", {"Some"});
%!error <table.rows2vars: the number of names taken from the variable specified in 'VariableNamesSource' does not match the number of rows in input table.> ...
%! rows2vars (tblA, "VariableNamesSource", {"Age"});
%!error <table.rows2vars: 'VariableNamesSource' cannot specify a variable that is also specified by 'DataVariables'.> ...
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! rows2vars (table (LastName, Age), "DataVariables", "LastName", "VariableNamesSource", "LastName");
%!error <table.rows2vars: invalid input for 'VariableNamingRule'.> ...
%! rows2vars (tblA, "VariableNamingRule", "somerule");
%!error <table.rows2vars: input table must not contain nested tables.> ...
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! rows2vars (table (tblA, LastName));
%!error <table.rows2vars: input table must not contain multicolumn variables.> ...
%! rows2vars (tblA);

## Test 'stack' method
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3);
%! S = stack (U, 1:3);
%! assert_equal (size (S), [12, 2]);
%! assert_equal (class (S.Test1_Test2_Test3_Indicator), "categorical");
%! assert_equal (cellstr (S.Test1_Test2_Test3_Indicator), ...
%!         repmat ({"Test1"; "Test2"; "Test3"}, 4, 1));
%! assert_equal (S.Test1_Test2_Test3, [Test1';Test2';Test3'](:));
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3);
%! [S, idx] = stack (U, 1:2);
%! assert_equal (idx, [1; 1; 2; 2; 3; 3; 4; 4]);
%! assert_equal (cellstr (S.Test1_Test2_Indicator), ...
%!         repmat ({"Test1"; "Test2"}, 4, 1));
%! assert_equal (S.Test3, repelem (Test3, 2, 1));
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3);
%! S = stack (U, 1:3, 'NewDataVariableName', 'NewData', ...
%!                    'IndexVariableName', 'Index');
%! assert_equal (size (S), [12, 2]);
%! assert_equal (class (S.Index), "categorical");
%! assert_equal (cellstr (S.Index), repmat ({"Test1"; "Test2"; "Test3"}, 4, 1));
%! assert_equal (S.NewData, [Test1';Test2';Test3'](:));
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! Test4 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3, Test4);
%! S = stack (U, 1:2, 'ConstantVariables', "Test4");
%! assert_equal (S.Properties.VariableNames, ...
%!         {"Test4", "Test1_Test2_Indicator", "Test1_Test2"});
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! Test4 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3, Test4);
%! S = stack (U, {"Test1", "Test4"}, 'ConstantVariables', "Test2");
%! assert_equal (S.Properties.VariableNames, ...
%!         {"Test2", "Test1_Test4_Indicator", "Test1_Test4"});
%!test
%! Test1 = [93; 57; 87; 89];
%! Test2 = [89; 77; 92; 86];
%! Test3 = [95; 62; 89; 91];
%! Test4 = [95; 62; 89; 91];
%! U = table (Test1, Test2, Test3, Test4);
%! S = stack (U, {"Test1", "Test4"}, 'ConstantVariables', "Test2", ...
%!            'NewDataVariableName', 'NewData', 'IndexVariableName', 'Index');
%! assert_equal (S.Properties.VariableNames, ...
%!         {"Test2", "Index", "NewData"});
%!test  # units and descriptions are inherited from the first stacked variable
%! Test1 = [93; 57];
%! Test2 = [89; 77];
%! U = table (Test1, Test2);
%! U.Properties.VariableUnits = {'cm', 'kg'};
%! U.Properties.VariableDescriptions = {'height', 'weight'};
%! S = stack (U, 1:2);
%! assert_equal (S.Properties.VariableUnits, {'', 'cm'});
%! assert_equal (S.Properties.VariableDescriptions, {'Data indicator', 'height'});
%!test  # multiple variable groups stack into one data variable each
%! T1 = [1; 2; 3];
%! T2 = [4; 5; 6];
%! T3 = [7; 8; 9];
%! T4 = [10; 11; 12];
%! U = table (T1, T2, T3, T4);
%! [S, idx] = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}});
%! assert_equal (S.Properties.VariableNames, {'Indicator', 'T1_T2', 'T3_T4'});
%! assert_equal (class (S.Indicator), 'double');
%! assert_equal (S.Indicator, [1; 2; 1; 2; 1; 2]);
%! assert_equal (S.T1_T2, [1; 4; 2; 5; 3; 6]);
%! assert_equal (S.T3_T4, [7; 10; 8; 11; 9; 12]);
%! assert_equal (idx, [1; 1; 2; 2; 3; 3]);
%!test  # multi-group inherits per-group metadata; indicator stays generic
%! T1 = [1; 2];
%! T2 = [3; 4];
%! T3 = [5; 6];
%! T4 = [7; 8];
%! U = table (T1, T2, T3, T4);
%! U.Properties.VariableUnits = {'u1', 'u2', 'u3', 'u4'};
%! U.Properties.VariableDescriptions = {'d1', 'd2', 'd3', 'd4'};
%! S = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}});
%! assert_equal (S.Properties.VariableUnits, {'', 'u1', 'u3'});
%! assert_equal (S.Properties.VariableDescriptions, ...
%!         {'Data indicator', 'd1', 'd3'});
%!test  # multi-group accepts one new data name per group and an index name
%! T1 = [1; 2];
%! T2 = [3; 4];
%! T3 = [5; 6];
%! T4 = [7; 8];
%! U = table (T1, T2, T3, T4);
%! S = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}}, ...
%!            'NewDataVariableName', {'A', 'B'}, 'IndexVariableName', 'Idx');
%! assert_equal (S.Properties.VariableNames, {'Idx', 'A', 'B'});
## Test input validation for  'stack' method
%!error <table.stack: too few input arguments.> stack (tblA);
%!error <table.stack: VARS index a non-existing variable: 'Some'.> ...
%! stack (tblA, "Some");
%!error <table.stack: all groups of variables to stack must be the same size.> ...
%! stack (tblA, {{'Height', 'Weight'}, {'Age'}});
%!error <table.stack: 'ConstantVariables' index a non-existing  variable: 'Some'.> ...
%! stack (tblA, 1:3, "ConstantVariables", "Some");
%!error <table.stack: 'ConstantVariables' cannot contain any variables to be stacked as specified by VARS.> ...
%! stack (tblA, 1:3, "ConstantVariables", "Weight");
%!error <table.stack: 'NewDataVariableName' must be a character vector, or a cellstring or string array.> ...
%! stack (tblA, 1:3, "NewDataVariableName", 3);
%!error <table.stack: the number of 'NewDataVariableName' names must equal the number of variable groups to stack.> ...
%! stack (tblA, 1:2, "NewDataVariableName", {'A', 'B'});
%!error <table.stack: 'IndexVariableName' must be either a character vector, or a cellstring or string scalar.> ...
%! stack (tblA, 1:3, "IndexVariableName", 3);

## Test 'unstack' method
%!test  # a single variable is unstacked by a categorical indicator
%! Storm = [3; 3; 1; 3; 1; 1; 4; 2; 4; 2; 4; 2];
%! Town = categorical ({'Natick'; 'Worcester'; 'Natick'; 'Boston'; ...
%!                      'Boston'; 'Worcester'; 'Boston'; 'Natick'; ...
%!                      'Worcester'; 'Worcester'; 'Natick'; 'Boston'});
%! Snowfall = [0; 3; 5; 5; 9; 10; 12; 13; 15; 16; 17; 21];
%! S = table (Storm, Town, Snowfall);
%! [U, idx] = unstack (S, 'Snowfall', 'Town');
%! assert_equal (U.Properties.VariableNames, ...
%!         {'Storm', 'Boston', 'Natick', 'Worcester'});
%! assert_equal (U.Storm, [3; 1; 4; 2]);
%! assert_equal (U.Boston, [5; 9; 12; 21]);
%! assert_equal (U.Natick, [0; 5; 17; 13]);
%! assert_equal (U.Worcester, [3; 10; 15; 16]);
%! assert_equal (idx, [1; 3; 7; 8]);
%!test  # duplicate group/indicator pairs are aggregated by @sum (numeric default)
%! Storm = [3; 3];
%! Town = categorical ({'Boston'; 'Boston'});
%! Snowfall = [5; 100];
%! U = unstack (table (Storm, Town, Snowfall), 'Snowfall', 'Town');
%! assert_equal (U.Boston, 105);
%!test  # a custom AggregationFunction overrides the default
%! G = [1; 1; 1; 2];
%! T = categorical ({'a'; 'a'; 'b'; 'a'});
%! V = [3; 9; 5; 7];
%! U = unstack (table (G, T, V), 'V', 'T', 'AggregationFunction', @max);
%! assert_equal (U.a, [9; 7]);
%! assert_equal (U.b, [5; NaN]);
%!test  # a numeric indicator derives valid variable names
%! G = [1; 1; 2; 2];
%! T = [10; 20; 10; 20];
%! V = [1; 2; 3; 4];
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (U.Properties.VariableNames, {'G', 'x10', 'x20'});
%! assert_equal (U.x10, [1; 3]);
%! assert_equal (U.x20, [2; 4]);
%!test  # ConstantVariables take their value from the first row of each group
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = [1; 2; 3; 4];
%! C = [100; 101; 200; 201];
%! U = unstack (table (G, T, V, C), 'V', 'T', 'ConstantVariables', 'C');
%! assert_equal (U.Properties.VariableNames, {'G', 'C', 'a', 'b'});
%! assert_equal (U.C, [100; 200]);
%!test  # explicit GroupingVariables restrict the grouping set
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = [1; 2; 3; 4];
%! C = [100; 101; 200; 201];
%! U = unstack (table (G, T, V, C), 'V', 'T', 'GroupingVariables', 'G');
%! assert_equal (U.Properties.VariableNames, {'G', 'a', 'b'});
%!test  # NewDataVariableNames rename the unstacked variables
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = [1; 2; 3; 4];
%! U = unstack (table (G, T, V), 'V', 'T', 'NewDataVariableNames', {'AA', 'BB'});
%! assert_equal (U.Properties.VariableNames, {'G', 'AA', 'BB'});
%!test  # VariableNamingRule 'preserve' keeps invalid identifiers verbatim
%! G = [1; 1; 2; 2];
%! T = categorical ({'New York'; 'New York'; 'Los Angeles'; 'Los Angeles'});
%! V = [1; 2; 3; 4];
%! U = unstack (table (G, T, V), 'V', 'T', 'VariableNamingRule', 'preserve');
%! assert_equal (U.Properties.VariableNames, {'G', 'Los Angeles', 'New York'});
%!test  # the default 'modify' rule produces valid identifiers
%! G = [1; 1; 2; 2];
%! T = categorical ({'New York'; 'New York'; 'Los Angeles'; 'Los Angeles'});
%! V = [1; 2; 3; 4];
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (U.Properties.VariableNames, {'G', 'LosAngeles', 'NewYork'});
%!test  # rows with missing grouping values are excluded from unstacking
%! Storm = [3; 3; 1; 3; 1; 1; 4; 2; 4; 2; 4; 2];
%! Town = categorical ({'Natick'; 'Worcester'; 'Natick'; 'Boston'; ...
%!                      'Boston'; 'Worcester'; 'Boston'; 'Natick'; ...
%!                      'Worcester'; 'Worcester'; 'Natick'; 'Boston'});
%! Snowfall = [0; 3; 5; 5; 9; 10; 12; 13; 15; 16; 17; 21];
%! Storm(1) = NaN;
%! S = table (Storm, Town, Snowfall);
%! [U, idx] = unstack (S, 'Snowfall', 'Town');
%! assert_equal (U.Storm, [3; 1; 4; 2]);
%! assert_equal (U.Natick, [NaN; 5; 17; 13]);
%! assert_equal (U.Boston, [5; 9; 12; 21]);
%! assert_equal (idx, [2; 3; 7; 8]);
%!test  # with no grouping variables every row collapses to a single output row
%! Town = categorical ({'Boston'; 'Natick'; 'Worcester'; 'Boston'});
%! Snowfall = [5; 10; 15; 20];
%! [U, idx] = unstack (table (Town, Snowfall), 'Snowfall', 'Town');
%! assert_equal (size (U), [1, 3]);
%! assert_equal (U.Boston, 25);
%! assert_equal (idx, 1);
%!test  # categorical data are unstacked (default @unique, no conflict)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = categorical ({'p'; 'q'; 'r'; 's'});
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (cellstr (U.a), {'p'; 'r'});
%! assert_equal (cellstr (U.b), {'q'; 's'});
%!test  # logical data are unstacked (regression: default no longer crashes)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! B = logical ([1; 0; 1; 0]);
%! U = unstack (table (G, T, B), 'B', 'T');
%! assert_equal (U.a, [true; true]);
%! assert_equal (U.b, [false; false]);
%!test  # string data are unstacked (regression: default no longer crashes)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = string ({'p'; 'q'; 'r'; 's'});
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (cellstr (U.a), {'p'; 'r'});
%! assert_equal (cellstr (U.b), {'q'; 's'});
%!test  # cellstr data are unstacked (regression: default no longer crashes)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = {'p'; 'q'; 'r'; 's'};
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (U.a, {'p'; 'r'});
%! assert_equal (U.b, {'q'; 's'});
%!test  # duration data are unstacked (default @unique, no conflict)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = hours ([1; 2; 3; 4]);
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (hours (U.a), [1; 3]);
%! assert_equal (hours (U.b), [2; 4]);
%!test  # missing duration cells fill with zero, identical conflicts collapse
%! G = [1; 1; 1; 2];
%! T = categorical ({'a'; 'a'; 'b'; 'b'});
%! V = hours ([2; 2; 3; 4]);
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (hours (U.a), [2; 0]);
%! assert_equal (hours (U.b), [3; 4]);
%!test  # calendarDuration data are unstacked (default @unique, no conflict)
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! V = calmonths ([1; 2; 3; 4]);
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (calmonths (U.a), [1; 3]);
%! assert_equal (calmonths (U.b), [2; 4]);
%!test  # missing calendarDuration cells fill with zero, identical conflicts collapse
%! G = [1; 1; 1; 2];
%! T = categorical ({'a'; 'a'; 'b'; 'b'});
%! V = calmonths ([1; 1; 2; 3]);
%! U = unstack (table (G, T, V), 'V', 'T');
%! assert_equal (calmonths (U.a), [1; 0]);
%! assert_equal (calmonths (U.b), [2; 3]);
%!test  # multiple variables are unstacked with composite names
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! N = [10; 20; 30; 40];
%! M = [1; 2; 3; 4];
%! U = unstack (table (G, T, N, M), {'N', 'M'}, 'T');
%! assert_equal (U.Properties.VariableNames, {'G', 'N_a', 'N_b', 'M_a', 'M_b'});
%! assert_equal (U.N_a, [10; 30]);
%! assert_equal (U.N_b, [20; 40]);
%! assert_equal (U.M_a, [1; 3]);
%! assert_equal (U.M_b, [2; 4]);
%!test  # multiple variables of different types are unstacked together
%! G = [1; 1; 2; 2];
%! T = categorical ({'a'; 'b'; 'a'; 'b'});
%! N = [10; 20; 30; 40];
%! C = categorical ({'p'; 'q'; 'r'; 's'});
%! U = unstack (table (G, T, N, C), {'N', 'C'}, 'T');
%! assert_equal (U.N_a, [10; 30]);
%! assert_equal (cellstr (U.C_a), {'p'; 'r'});
## Test input validation for  'unstack' method
%!error <table.unstack: too few input arguments.> unstack (tblA);
%!error <table.unstack: too few input arguments.> unstack (tblA, 'Height');
%!error <table.unstack: too few input arguments.> unstack (tblA, [], 'Height');
%!error <table.unstack: VARS index a non-existing variable: 'some'.> ...
%! unstack (tblA, 'some', 'Height');
%!error <table.unstack: VARS must not index nested tables.> ...
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! unstack (table (table (Age), Smoker, Height), 'Var1', 'Height');
%!error <table.unstack: IVAR must index a single variable.> ...
%! unstack (tblA, 'Height', {'Smoker', 'Age'});
%!error <table.unstack: IVAR indexes a non-existing variable: 'some'.> ...
%! unstack (tblA, 'Height', 'some');
%!error <table.unstack: IVAR must index a single column variable.> ...
%! unstack (tblA, 'Height', 'BloodPressure');
%!error <table.unstack: IVAR cannot be any of the variables to be unstacked as specified by VARS.> ...
%! unstack (tblA, 'Height', 'Height');
%!error <table.unstack: IVAR indexes a variable of invalid type: 'cell'.> ...
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! unstack (table ({4;5;6;7;8}, Smoker, Height), 'Height', 'Var1');
%!error <table.unstack: 'ConstantVariables' index a non-existing variable: 'some'.> ...
%! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'some');
%!error <table.unstack: 'ConstantVariables' cannot contain any variables to be unstacked as specified by VARS.> ...
%! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'Height');
%!error <table.unstack: 'ConstantVariables' cannot contain the indicator variable as specified by IVAR.> ...
%! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'Age');
%!error <table.unstack: 'GroupingVariables' index a non-existing variable: 'some'.> ...
%! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'some');
%!error <table.unstack: 'GroupingVariables' cannot contain any variables to be unstacked as specified by VARS.> ...
%! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'Height');
%!error <table.unstack: 'GroupingVariables' cannot contain the indicator variable as specified by IVAR.> ...
%! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'Age');
%!warning <table.unstack: 'GroupingVariables' index a variable of invalid type: 'Var1', which is ignored.> ...
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Age = [38; 43; 38; 40; 49];
%! unstack (table ({4;5;6;7;8}, Smoker, Height, Age), 'Height', 'Age', 'GroupingVariables', 'Var1');
%!error <table.unstack: 'ConstantVariables' cannot contain any grouping variables as specified by 'GroupingVariables'.> ...
%! unstack (tblA, 'BloodPressure', 'Height', 'ConstantVariables', 'Age', 'GroupingVariables', 'Age');
%!error <table.unstack: 'NewDataVariableNames' must be either a cell array of character vectors, or a string array.> ...
%! unstack (tblA, 'Height', 'Age', 'NewDataVariableNames', [1:5]);
%!error <table.unstack: 'NewDataVariableNames' do not match the number of unique values in the indicator variable.> ...
%! unstack (tblA, 'Height', 'Age', 'NewDataVariableNames', {"A","B","C"});
%!error <table.unstack: 'AggregationFunction' must be a function handle.> ...
%! unstack (tblA, 'Height', 'Age', 'AggregationFunction', 1);
%!error <table.unstack: 'AggregationFunction' must be a function handle.> ...
%! unstack (tblA, 'Height', 'Age', 'AggregationFunction', 'isnumeric');
%!error <table.unstack: 'AggregationFunction' must return a scalar value.> ...
%! unstack (table ([1; 1; 2], categorical ({'a'; 'a'; 'b'}), ...
%!                 categorical ({'x'; 'y'; 'z'})), 'Var3', 'Var2');
%!error <table.unstack: 'AggregationFunction' must return a scalar value.> ...
%! unstack (table ([1; 1; 2], categorical ({'a'; 'a'; 'b'}), ...
%!                 hours ([1; 2; 3])), 'Var3', 'Var2');
%!error <table.unstack: invalid input for 'VariableNamingRule'.> ...
%! unstack (tblA, {'Weight', 'Height'}, 'Age', 'VariableNamingRule', 'somerule');

## Test 'inner2outer' method
%!test  # basic transpose: two nested tables sharing inner names
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'});
%! T = inner2outer (table (A, B, 'VariableNames', {'A', 'B'}));
%! assert_equal (T.Properties.VariableNames, {'X', 'Y'});
%! assert_equal (T.X.Properties.VariableNames, {'A', 'B'});
%! assert_equal (T.X.A, [1; 2; 3]);
%! assert_equal (T.X.B, [7; 8; 9]);
%! assert_equal (T.Y.A, [4; 5; 6]);
%! assert_equal (T.Y.B, [10; 11; 12]);
%!test  # non-nested variables keep their relative position
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'});
%! C = [100; 200; 300];
%! T = inner2outer (table (C, A, B, 'VariableNames', {'C', 'A', 'B'}));
%! assert_equal (T.Properties.VariableNames, {'C', 'X', 'Y'});
%! assert_equal (T.C, [100; 200; 300]);
%! T = inner2outer (table (A, C, B, 'VariableNames', {'A', 'C', 'B'}));
%! assert_equal (T.Properties.VariableNames, {'X', 'Y', 'C'});
%!test  # inner names are regrouped by name regardless of their order
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! Br = table ([10;11;12], [7;8;9], 'VariableNames', {'Y', 'X'});
%! T = inner2outer (table (A, Br, 'VariableNames', {'A', 'B'}));
%! assert_equal (T.Properties.VariableNames, {'X', 'Y'});
%! assert_equal (T.X.A, [1; 2; 3]);
%! assert_equal (T.X.B, [7; 8; 9]);
%! assert_equal (T.Y.A, [4; 5; 6]);
%! assert_equal (T.Y.B, [10; 11; 12]);
%!test  # mismatched inner names: union, single-source names become plain vars
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! Bz = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Z'});
%! T = inner2outer (table (A, Bz, 'VariableNames', {'A', 'B'}));
%! assert_equal (T.Properties.VariableNames, {'X', 'Y', 'Z'});
%! assert_equal (istable (T.X), true);
%! assert_equal (T.X.Properties.VariableNames, {'A', 'B'});
%! assert_equal (istable (T.Y), false);
%! assert_equal (T.Y, [4; 5; 6]);
%! assert_equal (istable (T.Z), false);
%! assert_equal (T.Z, [10; 11; 12]);
%!test  # three nested tables
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'});
%! D = table ([13;14;15], [16;17;18], 'VariableNames', {'X', 'Y'});
%! T = inner2outer (table (A, B, D, 'VariableNames', {'A', 'B', 'D'}));
%! assert_equal (T.X.Properties.VariableNames, {'A', 'B', 'D'});
%! assert_equal (T.X.D, [13; 14; 15]);
%!test  # inner variable metadata is inherited; parent row names are preserved
%! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'});
%! A.Properties.VariableUnits = {'kg', 'm'};
%! A.Properties.VariableDescriptions = {'mass', 'length'};
%! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'});
%! B.Properties.VariableUnits = {'kg', 'm'};
%! B.Properties.VariableDescriptions = {'mass', 'length'};
%! T = inner2outer (table (A, B, 'VariableNames', {'A', 'B'}, ...
%!                         'RowNames', {'r1', 'r2', 'r3'}));
%! assert_equal (T.Properties.RowNames, {'r1'; 'r2'; 'r3'});
%! assert_equal (T.X.Properties.VariableDescriptions, {'mass', 'mass'});
%! assert_equal (T.X.Properties.VariableUnits, {'kg', 'kg'});
%! assert_equal (T.Y.Properties.VariableDescriptions, {'length', 'length'});
%! assert_equal (T.Y.Properties.VariableUnits, {'m', 'm'});
%! assert_equal (isempty (T.X.Properties.RowNames), true);
## Test input validation for 'inner2outer' method
%!error <table.inner2outer: TBLA must have at least one variable that contains a table.> ...
%! inner2outer (table ([1; 2]));
%!error <table.inner2outer: an inner variable name clashes with an existing variable name in TBLA.> ...
%! inner2outer (table (table ([1;2], 'VariableNames', {'A'}), [3;4], ...
%!                     'VariableNames', {'N', 'A'}));

## Test 'addprop' method
%!test  # custom properties of both kinds are added
%! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'});
%! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'});
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'});
%!test  # a single property is added with character-vector inputs
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, 'Note', 'table');
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'});
%!test  # values are assigned to a 'table' custom property with dot syntax
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, 'Note', 'table');
%! T.Properties.CustomProperties.Note = 'hello';
%! assert_equal (T.Properties.CustomProperties.Note, 'hello');
%!test  # a 'variable' custom property holds one value per variable
%! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'});
%! T = addprop (T, 'Flags', 'variable');
%! T.Properties.CustomProperties.Flags = {'f1', 'f2'};
%! assert_equal (T.Properties.CustomProperties.Flags, {'f1', 'f2'});
%!test  # properties can be appended to a table that already has some
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, 'Note', 'table');
%! T = addprop (T, 'Flags', 'variable');
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'});
%!test  # property names and types may be given as a string array
%! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'});
%! T = addprop (T, string ({'Note', 'Flags'}), string ({'table', 'variable'}));
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'});
## Test input validation for 'addprop' method
%!error <table.addprop: too few input arguments.> ...
%! addprop (table (1), {'a'});
%!error <table.addprop: invalid input type for 'propertyNames'.> ...
%! addprop (table (1), 5, 'table');
%!error <table.addprop: invalid input type for 'propertyTypes'.> ...
%! addprop (table (1), 'a', 5);
%!error <table.addprop: the number of 'propertyTypes' must equal the number of 'propertyNames'.> ...
%! addprop (table (1), {'a', 'b'}, {'table'});
%!error <table.addprop: 'propertyNames' cannot contain duplicate names.> ...
%! addprop (table (1), {'a', 'a'}, {'table', 'variable'});
%!error <table.addprop: custom property 'Note' already exists.> ...
%! addprop (addprop (table (1), 'Note', 'table'), 'Note', 'table');
%!error <table.addprop: custom property 'Flags' already exists.> ...
%! addprop (addprop (table (1), 'Flags', 'table'), {'New', 'Flags'}, {'table', 'table'});
%!error <table.addprop: custom property '1bad' does not have a valid name.> ...
%! addprop (table (1), '1bad', 'table');
%!error <table.addprop: invalid value for 'propertyTypes'.> ...
%! addprop (table (1), 'a', 'foo');

## Custom-property merge semantics across multi-table operations
%!test  # horzcat: table-scoped first-wins, variable-scoped concatenate
%! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'});
%! A = addprop (A, 'ts', 'table');    A.Properties.CustomProperties.ts = 'A';
%! A = addprop (A, 'vs', 'variable'); A.Properties.CustomProperties.vs = [10 11];
%! B = table ([7; 8], 'VariableNames', {'b'});
%! B = addprop (B, 'ts', 'table');    B.Properties.CustomProperties.ts = 'B';
%! B = addprop (B, 'vs', 'variable'); B.Properties.CustomProperties.vs = [20];
%! H = [A, B];
%! assert_equal (H.Properties.CustomProperties.ts, 'A');
%! assert_equal (H.Properties.CustomProperties.vs, [10 11 20]);
%!test  # horzcat: a table lacking a variable-scoped property NaN-fills its block
%! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'});
%! A = addprop (A, 'vs', 'variable'); A.Properties.CustomProperties.vs = [10 11];
%! C = table ([7; 8], 'VariableNames', {'c'});
%! H = [A, C];
%! assert_equal (H.Properties.CustomProperties.vs, [10 11 NaN]);
%!test  # horzcat: disjoint table-scoped props are unioned, table before variable
%! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'});
%! A = addprop (A, 'ts', 'table');    A.Properties.CustomProperties.ts = 'A';
%! A = addprop (A, 'vs', 'variable'); A.Properties.CustomProperties.vs = [10 11];
%! D = table ([7; 8], 'VariableNames', {'d'});
%! D = addprop (D, 'other', 'table'); D.Properties.CustomProperties.other = 'D';
%! H = [A, D];
%! assert_equal (fieldnames (H.Properties.CustomProperties), {'ts'; 'other'; 'vs'});
%! assert_equal (H.Properties.CustomProperties.other, 'D');
%! assert_equal (H.Properties.CustomProperties.vs, [10 11 NaN]);
%!test  # vertcat keeps the first table's custom properties (identical variables)
%! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'});
%! A = addprop (A, 'ts', 'table');    A.Properties.CustomProperties.ts = 'A';
%! A = addprop (A, 'vs', 'variable'); A.Properties.CustomProperties.vs = [10 11];
%! A2 = table ([5; 6], [7; 8], 'VariableNames', {'k', 'a'});
%! A2 = addprop (A2, 'ts', 'table');    A2.Properties.CustomProperties.ts = 'A2';
%! A2 = addprop (A2, 'vs', 'variable'); A2.Properties.CustomProperties.vs = [30 31];
%! V = [A; A2];
%! assert_equal (V.Properties.CustomProperties.ts, 'A');
%! assert_equal (V.Properties.CustomProperties.vs, [10 11]);
%!test  # splitvars replicates a split variable's variable-scoped entry
%! S = table ([1; 2], [3 4; 5 6], 'VariableNames', {'x', 'm'});
%! S = addprop (S, 'vs', 'variable'); S.Properties.CustomProperties.vs = [100 200];
%! SS = splitvars (S, 'm');
%! assert_equal (SS.Properties.CustomProperties.vs, [100 200 200]);
%!test  # join maps variable-scoped props per source (key+left from left, right from right)
%! L = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'});
%! L = addprop (L, 'ts', 'table');    L.Properties.CustomProperties.ts = 'L';
%! L = addprop (L, 'vs', 'variable'); L.Properties.CustomProperties.vs = [10 11];
%! R = table ([1; 2], [5; 6], 'VariableNames', {'k', 'b'});
%! R = addprop (R, 'ts', 'table');    R.Properties.CustomProperties.ts = 'R';
%! R = addprop (R, 'vs', 'variable'); R.Properties.CustomProperties.vs = [20 21];
%! J = innerjoin (L, R);
%! assert_equal (J.Properties.CustomProperties.ts, 'L');
%! assert_equal (J.Properties.CustomProperties.vs, [10 11 21]);
%!test  # unstack replicates the unstacked variable's entry onto its new columns
%! T = table ([1; 1; 2; 2], categorical ({'x'; 'y'; 'x'; 'y'}), [10; 20; 30; 40], ...
%!            'VariableNames', {'g', 'k', 'v'});
%! T = addprop (T, 'ts', 'table');    T.Properties.CustomProperties.ts = 'T';
%! T = addprop (T, 'vs', 'variable'); T.Properties.CustomProperties.vs = [1 2 3];
%! U = unstack (T, 'v', 'k');
%! assert_equal (U.Properties.VariableNames, {'g', 'x', 'y'});
%! assert_equal (U.Properties.CustomProperties.ts, 'T');
%! assert_equal (U.Properties.CustomProperties.vs, [1 3 3]);

## Test 'rmprop' method
%!test  # a custom property is removed, others are kept
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'});
%! T = rmprop (T, 'Note');
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'});
%!test  # several custom properties are removed in one call
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, {'Note', 'Flags', 'Tag'}, {'table', 'variable', 'table'});
%! T = rmprop (T, {'Note', 'Tag'});
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'});
%!test  # property names may be given as a string array
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'});
%! T = rmprop (T, string ({'Note', 'Flags'}));
%! assert_equal (fieldnames (T.Properties.CustomProperties), cell (0, 1));
%!test  # duplicate names are tolerated (the property is removed once)
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'});
%! T = rmprop (T, {'Note', 'Note'});
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'});
%!test  # names that do not match an existing property are silently ignored
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'});
%! T = rmprop (T, {'Note', 'Missing'});
%! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'});
%!test  # removing from a table without custom properties is a no-op
%! T = table ([1; 2], 'VariableNames', {'x'});
%! T = rmprop (T, 'Missing');
%! assert_equal (T.Properties.CustomProperties, []);
## Test input validation for 'rmprop' method
%!error <table.rmprop: too few input arguments.> ...
%! rmprop (table (1));
%!error <table.rmprop: invalid input type for 'propertyNames'.> ...
%! rmprop (table (1), 5);

################################################################################
##                       ** Join and Set Operations **                        ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'join'             'innerjoin'        'outerjoin'        'union'           ##
## 'intersect'        'ismember'         'setdiff'          'setxor'          ##
##                                                                            ##
################################################################################

## Test 'join' method
%!test
%! L = table ([3;1;2;1], {'a';'b';'c';'d'}, 'VariableNames', {'Key', 'L'}, ...
%!            'RowNames', {'r1', 'r2', 'r3', 'r4'});
%! R = table ([1;2;3], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! [T, ixR] = join (L, R);
%! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'});
%! assert_equal (T.Key, [3;1;2;1]);
%! assert_equal (T.R, {'z';'x';'y';'x'});
%! assert_equal (ixR, [3;1;2;1]);
%! assert_equal (T.Properties.RowNames, {'r1';'r2';'r3';'r4'});
## 'join' with 'LeftKeys'/'RightKeys'
%!test
%! L = table ([1;2], (10:11)', 'VariableNames', {'KL', 'A'});
%! R = table ([2;1], {'x';'y'}, 'VariableNames', {'KR', 'B'});
%! T = join (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR');
%! assert_equal (T.Properties.VariableNames, {'KL', 'A', 'B'});
%! assert_equal (T.A, [10;11]);
%! assert_equal (T.B, {'y';'x'});
## 'join' with 'Keys' and 'RightVariables'
%!test
%! L = table ([1;2;3], (1:3)', 'VariableNames', {'K', 'A'});
%! R = table ([3;2;1], {'x';'y';'z'}, (4:6)', 'VariableNames', {'K', 'B', 'C'});
%! T = join (L, R, 'Keys', 'K', 'RightVariables', 'B');
%! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'});
%! assert_equal (T.B, {'z';'y';'x'});
## 'join' suffixes shared non-key names with the input argument names
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'});
%! R = table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'});
%! T = join (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR');
%! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_L', 'V_R'});
## 'join' with 'KeepOneCopy' drops the right copy of the named variable
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'});
%! R = table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'});
%! T = join (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR', 'KeepOneCopy', 'V');
%! assert_equal (T.Properties.VariableNames, {'KeyL', 'V'});
%! assert_equal (T.V, {'a';'b'});
## 'join' on a duration key variable
%!test
%! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'});
%! R = table (hours ([1;2;3]), {'x';'y';'z'}, 'VariableNames', {'K', 'R'});
%! [T, ixR] = join (L, R);
%! assert_equal (hours (T.K), [3;1;2]);
%! assert_equal (T.R, {'z';'x';'y'});
%! assert_equal (ixR, [3;1;2]);
## 'join' carries a categorical key and a calendarDuration variable
%!test
%! L = table (categorical ({'a';'b'}), calmonths ([5;6]), ...
%!            'VariableNames', {'K', 'M'});
%! R = table (categorical ({'b';'a'}), {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = join (L, R);
%! assert_equal (cellstr (T.K), {'a';'b'});
%! assert_equal (calmonths (T.M), [5;6]);
%! assert_equal (T.R, {'y';'x'});
## 'join' on a datetime key variable
%!test
%! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ...
%!            'VariableNames', {'K', 'L'});
%! R = table (datetime (2020, 1, [1;2;3]), {'x';'y';'z'}, ...
%!            'VariableNames', {'K', 'R'});
%! [T, ixR] = join (L, R);
%! assert_equal (datevec (T.K)(:,3), [3;1;2]);
%! assert_equal (T.R, {'z';'x';'y'});
%! assert_equal (ixR, [3;1;2]);
## 'join' with 'LeftVariables' restricts the left columns kept
%!test
%! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ...
%!            'VariableNames', {'K', 'A', 'C'});
%! R = table ([3;2;1], {'x';'y';'z'}, 'VariableNames', {'K', 'B'});
%! T = join (L, R, 'LeftVariables', {'K', 'A'});
%! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'});
%! assert_equal (T.A, [10;11;12]);
%! assert_equal (T.B, {'z';'y';'x'});
## 'join' suffixes anonymous inputs with '_left'/'_right'
%!test
%! T = join (table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}), ...
%!           table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}), ...
%!           'LeftKeys', 'KeyL', 'RightKeys', 'KeyR');
%! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_left', 'V_right'});
%!error <table.join: too few input arguments.> ...
%! join (table ([1;2]));
%!error <table.join: both inputs must be tables.> ...
%! join (table ([1;2]), 5);
%!error <table.join: 'Keys' cannot be combined with 'LeftKeys' or 'RightKeys'.> ...
%! join (table (1), table (1), 'Keys', 1, 'LeftKeys', 1);
%!error <table.join: 'LeftKeys' and 'RightKeys' must be specified together.> ...
%! join (table (1, 'VariableNames', {'a'}), ...
%!       table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a');
%!error <table.join: 'LeftKeys' and 'RightKeys' must reference the same number of variables.> ...
%! join (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ...
%!       table ([1;2], 'VariableNames', {'c'}), ...
%!       'LeftKeys', {'a', 'b'}, 'RightKeys', 'c');
%!error <table.join: cannot find any common key variables between the two tables.> ...
%! join (table ((1:3)', 'VariableNames', {'AA'}), ...
%!       table ((1:3)', 'VariableNames', {'BB'}));
%!error <table.join: invalid optional input argument.> ...
%! join (table ([1;2], 'VariableNames', {'K'}), ...
%!       table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1);
%!error <table.join: the key variables of TBLR must contain unique combinations of values.> ...
%! join (table ([1;2], 'VariableNames', {'K'}), ...
%!       table ([1;1;2], {'x';'y';'z'}, 'VariableNames', {'K', 'R'}));
%!error <table.join: the key variables of TBLR must contain all values of the key variables of TBLL.> ...
%! join (table ([1;9], 'VariableNames', {'K'}), ...
%!       table ([1;2], {'x';'y'}, 'VariableNames', {'K', 'R'}));

## Test 'innerjoin' method
%!test
%! L = table ([1;2;3;2], {'a';'b';'c';'d'}, 'VariableNames', {'Key', 'L'});
%! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! [T, ixL, ixR] = innerjoin (L, R);
%! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'});
%! assert_equal (T.Key, [2;2;3]);
%! assert_equal (T.L, {'b';'d';'c'});
%! assert_equal (T.R, {'x';'x';'y'});
%! assert_equal (ixL, [2;4;3]);
%! assert_equal (ixR, [1;1;2]);
## Inner join drops row names
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'Key', 'L'}, ...
%!            'RowNames', {'r1', 'r2'});
%! R = table ([2;1], {'x';'y'}, 'VariableNames', {'Key', 'R'});
%! T = innerjoin (L, R);
%! assert_equal (isempty (T.Properties.RowNames), true);
## Cartesian product on repeated keys (m*n rows)
%!test
%! L = table ([1;1], {'a';'b'}, 'VariableNames', {'K', 'L'});
%! R = table ([1;1], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = innerjoin (L, R);
%! assert_equal (height (T), 4);
%! assert_equal (T.L, {'a';'a';'b';'b'});
%! assert_equal (T.R, {'x';'y';'x';'y'});
%! assert_equal (ixL, [1;1;2;2]);
%! assert_equal (ixR, [1;2;1;2]);
## No matching keys yields an empty result with the correct variables
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'K', 'L'});
%! R = table ([8;9], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = innerjoin (L, R);
%! assert_equal (height (T), 0);
%! assert_equal (T.Properties.VariableNames, {'K', 'L', 'R'});
## Cellstr key variable
%!test
%! L = table ({'a';'b';'c'}, (1:3)', 'VariableNames', {'K', 'N'});
%! R = table ({'b';'a'}, {'Y';'X'}, 'VariableNames', {'K', 'M'});
%! T = innerjoin (L, R);
%! assert_equal (T.K, {'a';'b'});
%! assert_equal (T.N, [1;2]);
%! assert_equal (T.M, {'X';'Y'});
## String key variable
%!test
%! L = table (string ({'a';'b'}), (1:2)', 'VariableNames', {'K', 'N'});
%! R = table (string ({'b'}), {'Z'}, 'VariableNames', {'K', 'M'});
%! T = innerjoin (L, R);
%! assert_equal (cellstr (T.K), {'b'});
%! assert_equal (T.N, 2);
%! assert_equal (T.M, {'Z'});
## Categorical key variable, sorted by category code
%!test
%! L = table (categorical ({'a';'b';'c'}), (1:3)', 'VariableNames', {'K', 'N'});
%! R = table (categorical ({'c';'a'}), {'P';'Q'}, 'VariableNames', {'K', 'M'});
%! T = innerjoin (L, R);
%! assert_equal (cellstr (T.K), {'a';'c'});
%! assert_equal (T.N, [1;3]);
%! assert_equal (T.M, {'Q';'P'});

## 'Keys' option selects the key explicitly
%!test
%! L = table ([1;2;3], (1:3)', 'VariableNames', {'K', 'A'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'B'});
%! T = innerjoin (L, R, 'Keys', 'K');
%! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'});
%! assert_equal (T.K, [2;3]);

## 'LeftKeys'/'RightKeys' for differently-named keys
%!test
%! L = table ([1;2;3], (10:12)', 'VariableNames', {'KL', 'A'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'KR', 'B'});
%! [T, ixL, ixR] = innerjoin (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR');
%! assert_equal (T.Properties.VariableNames, {'KL', 'A', 'B'});
%! assert_equal (T.KL, [2;3]);
%! assert_equal (T.A, [11;12]);
%! assert_equal (T.B, {'x';'y'});
%! assert_equal (ixL, [2;3]);
%! assert_equal (ixR, [1;2]);

## 'LeftVariables' and 'RightVariables' restrict the output columns
%!test
%! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ...
%!            'VariableNames', {'K', 'A', 'C'});
%! R = table ([2;3], {'x';'y'}, (5:6)', 'VariableNames', {'K', 'B', 'D'});
%! T = innerjoin (L, R, 'LeftVariables', 'A', 'RightVariables', 'B');
%! assert_equal (T.Properties.VariableNames, {'A', 'B'});
%! assert_equal (T.A, [11;12]);
%! assert_equal (T.B, {'x';'y'});
## Shared non-key variable names get '_Tleft'/'_Tright' suffixes
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'});
%! R = table ([2;1], {'p';'q'}, 'VariableNames', {'KeyR', 'V'});
%! T = innerjoin (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR');
%! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_L', 'V_R'});
%! assert_equal (T.V_L, {'a';'b'});
%! assert_equal (T.V_R, {'q';'p'});
## Multiple key variables
%!test
%! L = table ([1;1;2], {'a';'b';'a'}, (1:3)', ...
%!            'VariableNames', {'K1', 'K2', 'N'});
%! R = table ([1;2], {'b';'a'}, {'x';'y'}, ...
%!            'VariableNames', {'K1', 'K2', 'M'});
%! T = innerjoin (L, R);
%! assert_equal (T.Properties.VariableNames, {'K1', 'K2', 'N', 'M'});
%! assert_equal (T.K1, [1;2]);
%! assert_equal (T.K2, {'b';'a'});
%! assert_equal (T.N, [2;3]);
%! assert_equal (T.M, {'x';'y'});

## 'innerjoin' on a duration key variable
%!test
%! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'});
%! R = table (hours ([1;2;3]), {'x';'y';'z'}, 'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = innerjoin (L, R);
%! assert_equal (hours (T.K), [1;2;3]);
%! assert_equal (T.R, {'x';'y';'z'});
%! assert_equal (ixL, [2;3;1]);
%! assert_equal (ixR, [1;2;3]);
## 'innerjoin' on a calendarDuration key variable
%!test
%! L = table (calmonths ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'});
%! R = table (calmonths ([2;1]), {'x';'y'}, 'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = innerjoin (L, R);
%! assert_equal (calmonths (T.K), [1;2]);
%! assert_equal (T.L, {'b';'c'});
%! assert_equal (T.R, {'y';'x'});
%! assert_equal (ixL, [2;3]);
%! assert_equal (ixR, [2;1]);
## 'innerjoin' on a datetime key variable, sorted by key
%!test
%! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ...
%!            'VariableNames', {'K', 'L'});
%! R = table (datetime (2020, 1, [1;2;3]), {'x';'y';'z'}, ...
%!            'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = innerjoin (L, R);
%! assert_equal (datevec (T.K)(:,3), [1;2;3]);
%! assert_equal (T.R, {'x';'y';'z'});
%! assert_equal (ixL, [2;3;1]);
%! assert_equal (ixR, [1;2;3]);
## 'innerjoin' suffixes anonymous inputs with '_left'/'_right'
%!test
%! T = innerjoin (table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}), ...
%!                table ([2;1], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}), ...
%!                'LeftKeys', 'KeyL', 'RightKeys', 'KeyR');
%! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_left', 'V_right'});
%!error <table.innerjoin: too few input arguments.> ...
%! innerjoin (table ([1;2]));
%!error <table.innerjoin: both inputs must be tables.> ...
%! innerjoin (table ([1;2]), 5);
%!error <table.innerjoin: 'Keys' cannot be combined with 'LeftKeys' or 'RightKeys'.> ...
%! innerjoin (table (1), table (1), 'Keys', 1, 'LeftKeys', 1);
%!error <table.innerjoin: 'LeftKeys' and 'RightKeys' must be specified together.> ...
%! innerjoin (table (1, 'VariableNames', {'a'}), ...
%!            table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a');
%!error <table.innerjoin: 'LeftKeys' and 'RightKeys' must reference the same number of variables.> ...
%! innerjoin (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ...
%!            table ([1;2], 'VariableNames', {'c'}), ...
%!            'LeftKeys', {'a', 'b'}, 'RightKeys', 'c');
%!error <table.innerjoin: cannot find any common key variables between the two tables.> ...
%! innerjoin (table ((1:3)', 'VariableNames', {'AA'}), ...
%!            table ((1:3)', 'VariableNames', {'BB'}));
%!error <table.innerjoin: invalid optional input argument.> ...
%! innerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1);
%!error <table.innerjoin: key variables have incompatible types.> ...
%! innerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ({'1';'2'}, 'VariableNames', {'K'}));

## Test 'outerjoin' method
%!test
%! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'});
%! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! [T, ixL, ixR] = outerjoin (L, R);
%! assert_equal (T.Properties.VariableNames, {'Key_L', 'L', 'Key_R', 'R'});
%! assert_equal (height (T), 4);
%! assert_equal (T.Key_L, [1;2;3;NaN]);
%! assert_equal (T.Key_R, [NaN;2;3;5]);
%! assert_equal (T.L, {'a';'b';'c';''});
%! assert_equal (T.R, {'';'x';'y';'z'});
%! assert_equal (ixL, [1;2;3;0]);
%! assert_equal (ixR, [0;1;2;3]);
## Outer join drops row names
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'Key', 'L'}, ...
%!            'RowNames', {'r1', 'r2'});
%! R = table ([2;1], {'x';'y'}, 'VariableNames', {'Key', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (isempty (T.Properties.RowNames), true);

## 'Type' 'left' keeps all left rows and only matching right rows
%!test
%! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'});
%! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! [T, ixL, ixR] = outerjoin (L, R, 'Type', 'left');
%! assert_equal (height (T), 3);
%! assert_equal (T.Key_L, [1;2;3]);
%! assert_equal (ixL, [1;2;3]);
%! assert_equal (ixR, [0;1;2]);
## 'Type' 'right' keeps all right rows and only matching left rows
%!test
%! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'});
%! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! [T, ixL, ixR] = outerjoin (L, R, 'Type', 'right');
%! assert_equal (height (T), 3);
%! assert_equal (T.Key_R, [2;3;5]);
%! assert_equal (ixL, [2;3;0]);
%! assert_equal (ixR, [1;2;3]);

## 'MergeKeys' merges the key pair into a single column
%!test
%! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'});
%! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'});
%! T = outerjoin (L, R, 'MergeKeys', true);
%! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'});
%! assert_equal (T.Key, [1;2;3;5]);
%! assert_equal (T.L, {'a';'b';'c';''});
%! assert_equal (T.R, {'';'x';'y';'z'});
## 'Keys' with multiple key variables, merged
%!test
%! L = table ([1;1;2], {'a';'b';'a'}, (1:3)', ...
%!            'VariableNames', {'K1', 'K2', 'N'});
%! R = table ([1;2], {'b';'a'}, {'x';'y'}, ...
%!            'VariableNames', {'K1', 'K2', 'M'});
%! T = outerjoin (L, R, 'Keys', {'K1', 'K2'}, 'MergeKeys', true);
%! assert_equal (T.Properties.VariableNames, {'K1', 'K2', 'N', 'M'});
%! assert_equal (height (T), 3);
%! assert_equal (T.K1, [1;1;2]);
%! assert_equal (T.K2, {'a';'b';'a'});
## 'MergeKeys' with differently-named keys uses the left key name
%!test
%! L = table ([1;2], (10:11)', 'VariableNames', {'KL', 'A'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'KR', 'B'});
%! T = outerjoin (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR', ...
%!                'MergeKeys', true);
%! assert_equal (T.Properties.VariableNames, {'KL_KR', 'A', 'B'});
%! assert_equal (T.KL_KR, [1;2;3]);
%! assert_equal (T.A, [10;11;NaN]);
%! assert_equal (T.B, {'';'x';'y'});
## String variables in unmatched rows are filled with <missing>
%!test
%! L = table ([1;2], string ({'a';'b'}), 'VariableNames', {'K', 'S'});
%! R = table ([2;3], {'p';'q'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.S), [false;false;true]);
%! assert_equal (T.R, {'';'p';'q'});
## Categorical variables in unmatched rows are filled with <undefined>
%!test
%! L = table ([1;2], {'a';'b'}, 'VariableNames', {'K', 'L'});
%! R = table ([2;3], categorical ({'p';'q'}), 'VariableNames', {'K', 'C'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.C), [true;false;false]);
## Cartesian product on repeated keys in a full outer join
%!test
%! L = table ([1;1;9], {'a';'b';'z'}, 'VariableNames', {'K', 'L'});
%! R = table ([1;1], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (height (T), 5);
%! assert_equal (T.L, {'a';'a';'b';'b';'z'});
%! assert_equal (T.R, {'x';'y';'x';'y';''});
## An empty input table yields all-missing fill on that side
%!test
%! E = table (zeros (0, 1), cell (0, 1), 'VariableNames', {'K', 'L'});
%! R = table ([1;2], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (E, R);
%! assert_equal (height (T), 2);
%! assert_equal (T.K_R, [1;2]);
%! assert_equal (T.L, {'';''});
%! assert_equal (T.R, {'x';'y'});

## 'LeftVariables'/'RightVariables' restrict the output columns
%!test
%! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ...
%!            'VariableNames', {'K', 'A', 'C'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'B'});
%! T = outerjoin (L, R, 'LeftVariables', 'A', 'RightVariables', 'B', ...
%!                'Type', 'left');
%! assert_equal (T.Properties.VariableNames, {'A', 'B'});
%! assert_equal (T.A, [10;11;12]);
%! assert_equal (T.B, {'';'x';'y'});

## 'outerjoin' on a duration key fills unmatched keys with missing
%!test
%! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'});
%! R = table (hours ([2;5]), {'p';'q'}, 'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = outerjoin (L, R);
%! assert_equal (hours (T.K_L), [1;2;3;NaN]);
%! assert_equal (ismissing (T.K_L), [false;false;false;true]);
%! assert_equal (T.R, {'';'p';'';'q'});
%! assert_equal (ixL, [2;3;1;0]);
%! assert_equal (ixR, [0;1;0;2]);
## 'outerjoin' fills an unmatched duration variable with missing
%!test
%! L = table ([1;2], hours ([10;20]), 'VariableNames', {'K', 'D'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.D), [false;false;true]);
%! assert_equal (hours (T.D), [10;20;NaN]);
## 'outerjoin' fills an unmatched calendarDuration variable with missing
%!test
%! L = table ([1;2], calmonths ([5;6]), 'VariableNames', {'K', 'CD'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.CD), [false;false;true]);
## 'outerjoin' fills an unmatched string variable with <missing>
%!test
%! L = table ([1;2], string ({'a';'b'}), 'VariableNames', {'K', 'S'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.S), [false;false;true]);
## 'outerjoin' on a datetime key fills unmatched keys with NaT
%!test
%! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ...
%!            'VariableNames', {'K', 'L'});
%! R = table (datetime (2020, 1, [2;5]), {'p';'q'}, 'VariableNames', {'K', 'R'});
%! [T, ixL, ixR] = outerjoin (L, R);
%! assert_equal (datevec (T.K_L)(:,3), [1;2;3;NaN]);
%! assert_equal (ismissing (T.K_L), [false;false;false;true]);
%! assert_equal (T.R, {'';'p';'';'q'});
%! assert_equal (ixL, [2;3;1;0]);
%! assert_equal (ixR, [0;1;0;2]);
## 'outerjoin' fills an unmatched datetime variable with NaT
%!test
%! L = table ([1;2], datetime (2021, 1, [10;20]), 'VariableNames', {'K', 'D'});
%! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'});
%! T = outerjoin (L, R);
%! assert_equal (ismissing (T.D), [false;false;true]);
%!error <table.outerjoin: too few input arguments.> ...
%! outerjoin (table ([1;2]));
%!error <table.outerjoin: both inputs must be tables.> ...
%! outerjoin (table ([1;2]), 5);
%!error <table.outerjoin: 'Type' must be 'full', 'left', or 'right'.> ...
%! outerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ([1;2], 'VariableNames', {'K'}), 'Type', 'bogus');
%!error <table.outerjoin: 'MergeKeys' must be a logical scalar.> ...
%! outerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ([1;2], 'VariableNames', {'K'}), 'MergeKeys', 3);
%!error <table.outerjoin: 'Keys' cannot be combined with 'LeftKeys' or 'RightKeys'.> ...
%! outerjoin (table (1), table (1), 'Keys', 1, 'LeftKeys', 1);
%!error <table.outerjoin: 'LeftKeys' and 'RightKeys' must be specified together.> ...
%! outerjoin (table (1, 'VariableNames', {'a'}), ...
%!            table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a');
%!error <table.outerjoin: 'LeftKeys' and 'RightKeys' must reference the same number of variables.> ...
%! outerjoin (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ...
%!            table ([1;2], 'VariableNames', {'c'}), ...
%!            'LeftKeys', {'a', 'b'}, 'RightKeys', 'c');
%!error <table.outerjoin: cannot find any common key variables between the two tables.> ...
%! outerjoin (table ((1:3)', 'VariableNames', {'AA'}), ...
%!            table ((1:3)', 'VariableNames', {'BB'}));
%!error <table.outerjoin: invalid optional input argument.> ...
%! outerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1);
%!error <table.outerjoin: key variables have incompatible types.> ...
%! outerjoin (table ([1;2], 'VariableNames', {'K'}), ...
%!            table ({'1';'2'}, 'VariableNames', {'K'}));

## Test 'union' method
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! [U, ia, ib] = union (A, B);
%! assert_equal (U.Properties.VariableNames, {'N', 'S'});
%! assert_equal (U.N, [1;2;3;5]);
%! assert_equal (U.S, {'a';'b';'c';'e'});
%! assert_equal (ia, [2;3;1]);
%! assert_equal (ib, 2);
## 'union' with 'stable' set order
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! U = union (A, B, 'stable');
%! assert_equal (U.N, [3;1;2;5]);
%! assert_equal (U.S, {'c';'a';'b';'e'});
## 'union' ignores variable order (output keeps the first table's order)
%!test
%! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ({'a';'e'}, [1;5], 'VariableNames', {'S', 'N'});
%! U = union (A, B);
%! assert_equal (U.Properties.VariableNames, {'N', 'S'});
%! assert_equal (U.N, [1;3;5]);
%! assert_equal (U.S, {'a';'c';'e'});
## 'union' drops row names (rows come from both tables)
%!test
%! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'r1', 'r2'});
%! B = table ([2;5], {'b';'e'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'q1', 'q2'});
%! U = union (A, B);
%! assert_equal (U.N, [1;2;3;5]);
%! assert_equal (isempty (U.Properties.RowNames), true);
## 'union' keeps the result's own sorted order when A and B rows interleave
%!test
%! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'});
%! U = union (A, B);
%! assert_equal (U.N, [1;2;3;4]);
%! assert_equal (U.S, {'a';'b';'c';'d'});
## 'union' 'stable' keeps appearance order when A and B rows interleave
%!test
%! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'});
%! U = union (A, B, 'stable');
%! assert_equal (U.N, [3;1;4;2]);
%! assert_equal (U.S, {'c';'a';'d';'b'});

## Test 'intersect' method
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! [I, ia, ib] = intersect (A, B);
%! assert_equal (I.N, [1;2]);
%! assert_equal (I.S, {'a';'b'});
%! assert_equal (ia, [2;3]);
%! assert_equal (ib, [3;1]);
## 'intersect' with no common rows yields an empty table
%!test
%! A = table ([1;2], {'a';'b'}, 'VariableNames', {'N', 'S'});
%! B = table ([8;9], {'x';'y'}, 'VariableNames', {'N', 'S'});
%! I = intersect (A, B);
%! assert_equal (height (I), 0);
%! assert_equal (I.Properties.VariableNames, {'N', 'S'});
## 'intersect' keeps the row names of the matched rows from the first table
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'r1', 'r2', 'r3', 'r4'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! I = intersect (A, B);
%! assert_equal (I.N, [1;2]);
%! assert_equal (I.Properties.RowNames, {'r2';'r3'});

## Test 'setdiff' method
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! [D, ia] = setdiff (A, B);
%! assert_equal (D.N, 3);
%! assert_equal (D.S, {'c'});
%! assert_equal (ia, 1);
## 'setdiff' with 'stable' set order
%!test
%! A = table ([5;3;5], {'e';'c';'e'}, 'VariableNames', {'N', 'S'});
%! B = table ([1], {'a'}, 'VariableNames', {'N', 'S'});
%! D = setdiff (A, B, 'stable');
%! assert_equal (D.N, [5;3]);
## 'setdiff' keeps the row names of the retained rows from the first table
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'r1', 'r2', 'r3', 'r4'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! D = setdiff (A, B);
%! assert_equal (D.N, 3);
%! assert_equal (D.Properties.RowNames, {'r1'});

## Test 'setxor' method
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! [X, ia, ib] = setxor (A, B);
%! assert_equal (X.N, [3;5]);
%! assert_equal (X.S, {'c';'e'});
%! assert_equal (ia, 1);
%! assert_equal (ib, 2);
## 'setxor' drops row names (rows come from both tables)
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'r1', 'r2', 'r3', 'r4'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}, ...
%!            'RowNames', {'q1', 'q2', 'q3'});
%! X = setxor (A, B);
%! assert_equal (X.N, [3;5]);
%! assert_equal (isempty (X.Properties.RowNames), true);
## 'setxor' keeps the result's own sorted order when A and B rows interleave
%!test
%! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'});
%! X = setxor (A, B);
%! assert_equal (X.N, [1;2;3;4]);
%! assert_equal (X.S, {'a';'b';'c';'d'});

## Test 'ismember' method
%!test
%! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'});
%! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'});
%! [TF, ixB] = ismember (A, B);
%! assert_equal (TF, [false;true;true;true]);
%! assert_equal (ixB, [0;3;1;3]);
## Set operations work on text and numeric keys via the row proxy
%!test
%! A = table (categorical ({'a';'b';'c'}), [1;2;3], 'VariableNames', {'C', 'N'});
%! B = table (categorical ({'b';'d'}), [2;4], 'VariableNames', {'C', 'N'});
%! I = intersect (A, B);
%! assert_equal (cellstr (I.C), {'b'});
%! assert_equal (I.N, 2);

## 'union' over string, categorical, duration and calendarDuration variables
%!test
%! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ...
%!            hours ([1;2;3]), calmonths ([1;2;3]), ...
%!            'VariableNames', {'S', 'C', 'D', 'M'});
%! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ...
%!            calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'});
%! U = union (A, B);
%! assert_equal (cellstr (U.S), {'a';'b';'c';'d'});
%! assert_equal (cellstr (U.C), {'x';'y';'z';'w'});
%! assert_equal (hours (U.D), [1;2;3;4]);
%! assert_equal (calmonths (U.M), [1;2;3;4]);

## 'intersect' over string, categorical, duration and calendarDuration variables
%!test
%! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ...
%!            hours ([1;2;3]), calmonths ([1;2;3]), ...
%!            'VariableNames', {'S', 'C', 'D', 'M'});
%! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ...
%!            calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'});
%! I = intersect (A, B);
%! assert_equal (cellstr (I.S), {'b'});
%! assert_equal (cellstr (I.C), {'y'});
%! assert_equal (hours (I.D), 2);
%! assert_equal (calmonths (I.M), 2);

## 'setdiff' over string, categorical, duration and calendarDuration variables
%!test
%! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ...
%!            hours ([1;2;3]), calmonths ([1;2;3]), ...
%!            'VariableNames', {'S', 'C', 'D', 'M'});
%! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ...
%!            calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'});
%! D = setdiff (A, B);
%! assert_equal (cellstr (D.S), {'a';'c'});
%! assert_equal (hours (D.D), [1;3]);

## 'setxor' over string, categorical, duration and calendarDuration variables
%!test
%! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ...
%!            hours ([1;2;3]), calmonths ([1;2;3]), ...
%!            'VariableNames', {'S', 'C', 'D', 'M'});
%! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ...
%!            calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'});
%! X = setxor (A, B);
%! assert_equal (cellstr (X.S), {'a';'c';'d'});
%! assert_equal (hours (X.D), [1;3;4]);

## 'ismember' over string, categorical, duration and calendarDuration variables
%!test
%! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ...
%!            hours ([1;2;3]), calmonths ([1;2;3]), ...
%!            'VariableNames', {'S', 'C', 'D', 'M'});
%! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ...
%!            calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'});
%! [TF, ixB] = ismember (A, B);
%! assert_equal (TF, [false;true;false]);
%! assert_equal (ixB, [0;1;0]);
## Set operations over a datetime key variable (rowProxies datetime path)
%!test
%! A = table (datetime (2020, 1, [3;1;2]), {'c';'a';'b'}, ...
%!            'VariableNames', {'D', 'S'});
%! B = table (datetime (2020, 1, [2;5]), {'b';'e'}, 'VariableNames', {'D', 'S'});
%! U = union (A, B);
%! assert_equal (datevec (U.D)(:,3), [1;2;3;5]);
%! I = intersect (A, B);
%! assert_equal (I.S, {'b'});
%! D = setdiff (A, B);
%! assert_equal (D.S, {'a';'c'});
%! X = setxor (A, B);
%! assert_equal (X.S, {'a';'c';'e'});
%! [TF, ixB] = ismember (A, B);
%! assert_equal (TF, [false;false;true]);
%! assert_equal (ixB, [0;0;1]);
%!error <table.union: too few input arguments.> ...
%! union (table ([1;2]));
%!error <table.union: both inputs must be tables.> ...
%! union (table ([1;2]), 5);
%!error <table.union: the two tables must have the same variable names.> ...
%! union (table ([1;2], 'VariableNames', {'N'}), ...
%!        table ([1;2], 'VariableNames', {'Z'}));
%!error <table.union: SETORDER must be either 'sorted' or 'stable'.> ...
%! union (table ([1;2], 'VariableNames', {'N'}), ...
%!        table ([1;2], 'VariableNames', {'N'}), 'bogus');
%!error <table.union: too many input arguments.> ...
%! union (table ([1;2], 'VariableNames', {'N'}), ...
%!        table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra');
%!error <table.intersect: too few input arguments.> ...
%! intersect (table ([1;2]));
%!error <table.intersect: both inputs must be tables.> ...
%! intersect (table ([1;2]), 5);
%!error <table.intersect: the two tables must have the same variable names.> ...
%! intersect (table ([1;2], 'VariableNames', {'N'}), ...
%!            table ([1;2], 'VariableNames', {'Z'}));
%!error <table.intersect: SETORDER must be either 'sorted' or 'stable'.> ...
%! intersect (table ([1;2], 'VariableNames', {'N'}), ...
%!            table ([1;2], 'VariableNames', {'N'}), 'bogus');
%!error <table.intersect: too many input arguments.> ...
%! intersect (table ([1;2], 'VariableNames', {'N'}), ...
%!            table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra');
%!error <table.setdiff: too few input arguments.> ...
%! setdiff (table ([1;2]));
%!error <table.setdiff: both inputs must be tables.> ...
%! setdiff (table ([1;2]), 5);
%!error <table.setdiff: the two tables must have the same variable names.> ...
%! setdiff (table ([1;2], 'VariableNames', {'N'}), ...
%!          table ([1;2], 'VariableNames', {'Z'}));
%!error <table.setdiff: SETORDER must be either 'sorted' or 'stable'.> ...
%! setdiff (table ([1;2], 'VariableNames', {'N'}), ...
%!          table ([1;2], 'VariableNames', {'N'}), 'bogus');
%!error <table.setdiff: too many input arguments.> ...
%! setdiff (table ([1;2], 'VariableNames', {'N'}), ...
%!          table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra');
%!error <table.setxor: too few input arguments.> ...
%! setxor (table ([1;2]));
%!error <table.setxor: both inputs must be tables.> ...
%! setxor (table ([1;2]), 5);
%!error <table.setxor: the two tables must have the same variable names.> ...
%! setxor (table ([1;2], 'VariableNames', {'N'}), ...
%!         table ([1;2], 'VariableNames', {'Z'}));
%!error <table.setxor: SETORDER must be either 'sorted' or 'stable'.> ...
%! setxor (table ([1;2], 'VariableNames', {'N'}), ...
%!         table ([1;2], 'VariableNames', {'N'}), 'bogus');
%!error <table.setxor: too many input arguments.> ...
%! setxor (table ([1;2], 'VariableNames', {'N'}), ...
%!         table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra');
%!error <table.ismember: too few input arguments.> ...
%! ismember (table ([1;2]));
%!error <table.ismember: both inputs must be tables.> ...
%! ismember (table ([1;2]), 5);
%!error <table.ismember: the two tables must have the same variable names.> ...
%! ismember (table ([1;2], 'VariableNames', {'N'}), ...
%!           table ([1;2], 'VariableNames', {'Z'}));

################################################################################
##                           ** Missing Values **                             ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'anymissing'       'ismissing'        'rmmissing'        'fillmissing'     ##
## 'standardizeMissing'                                                       ##
##                                                                            ##
################################################################################

## Test 'anymissing' method
%!test
%! T = table (categorical ({'1';'2';'';'4'}));
%! assert_equal (anymissing (T), true);
%!test
%! T = table (categorical ({'1';'2';'3';'4'}));
%! assert_equal (anymissing (T), false);
%!test
%! T = table (calyears ([1; 2; NaN; 4]));
%! assert_equal (anymissing (T), true);
%!test
%! T = table (calyears ([1; 2; 3; 4]));
%! assert_equal (anymissing (T), false);
%!test
%! T = table (days ([1; 2; NaN; 4]));
%! assert_equal (anymissing (T), true);
%!test
%! T = table (days ([1; 2; 3; 4]));
%! assert_equal (anymissing (T), false);
%!test
%! T = table (string ([1; 2; NaN; 4]));
%! assert_equal (anymissing (T), true);
%!test
%! T = table (string ([1; 2; 3; 4]));
%! assert_equal (anymissing (T), false);
%!test
%! T = table ({'1'; '2'; ''; '4'});
%! assert_equal (anymissing (T), true);
%!test
%! T = table ({'1'; '2'; 'NaN'; '4'});
%! assert_equal (anymissing (T), false);
%!test
%! T = table (['1'; '2'; ' '; '4']);
%! assert_equal (anymissing (T), true);
%!test
%! T = table (['1'; '2'; 'NaN'; '4']);
%! assert_equal (anymissing (T), false);
%!test
%! T = table (['1'; '2'; 'N'; '4']);
%! assert_equal (anymissing (T), false);

## Test 'ismissing' method
%!test
%! TF = ismissing (table (['   '; ' 2 '; 'NaN'; '4  ']), ' 2');
%! assert_equal (TF, [false; true; false; false]);
%!test
%! TF = ismissing (table (['   '; ' 2 '; 'NaN'; '4  ']), '2');
%! assert_equal (TF, [false; false; false; false]);
%!test
%! TF = ismissing (table (['   '; '2  '; 'NaN'; '4  ']), '2');
%! assert_equal (TF, [false; true; false; false]);
%!test
%! TF = ismissing (table (['   '; ' 2 '; 'NaN'; '4  ']));
%! assert_equal (TF, [true; false; false; false]);
%!test
%! TF = ismissing (table ({'1'; '2'; ''; '4'}));
%! assert_equal (TF, [false; false; true; false]);
%!test
%! TF = ismissing (table ({'1'; '2'; ''; '4'}), 'OutputFormat', 'tabular');
%! assert_equal (class (TF), 'table');
%! assert_equal (TF.Var1, [false; false; true; false]);
%!test
%! TF = ismissing (table ({'1'; '2'; ''; '4'}), {'4'}, 'OutputFormat', 'tabular');
%! assert_equal (class (TF), 'table');
%! assert_equal (TF.Var1, [false; false; false; true]);
%!test
%! TF = ismissing (table ({'1'; '2'; ''; '4'}, string ([1; 2; 3; 4])), {'4', '3'}, 'OutputFormat', 'tabular');
%! assert_equal (class (TF), 'table');
%! assert_equal (TF.Var1, [false; false; false; true]);
%! assert_equal (TF.Var2, [false; false; true; true]);
%!test
%! TF = ismissing (table ({'1'; '2'; ''; '4'}, string ([1; 2; 3; 4])), {'4', '3'});
%! assert_equal (class (TF), 'logical');
%! assert_equal (TF, [false, false; false, false; false, true; true, true]);
%!test
%! TF = ismissing (table (days ([1; 2; NaN; 4]), calweeks ([1; 2; NaN; 4])));
%! assert_equal (TF, [false, false; false, false; true, true; false, false]);
%!test  # a scalar duration indicator matches a duration variable
%! TF = ismissing (table (hours ([1; 2; 3]), 'VariableNames', {'d'}), hours (2));
%! assert_equal (TF, [false; true; false]);
%!test  # a NaN indicator also flags genuinely missing duration entries
%! TF = ismissing (table (hours ([1; NaN; 3]), 'VariableNames', {'d'}), NaN);
%! assert_equal (TF, [false; true; false]);

## Test 'rmmissing' method
%!test  # removes every row that contains a missing value
%! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'});
%! R = rmmissing (T);
%! assert_equal (size (R), [1, 2]);
%! assert_equal (R.x, 1);
%! assert_equal (R.y, {'a'});
%!test  # the second output is the removed-row mask
%! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'});
%! [~, TF] = rmmissing (T);
%! assert_equal (TF, [false; true; true]);
%!test  # a table with no missing values is returned unchanged
%! T = table ([1; 2; 3], 'VariableNames', {'x'});
%! [R, TF] = rmmissing (T);
%! assert_equal (isequal (R, T), true);
%! assert_equal (TF, [false; false; false]);
%!test  # 'DataVariables' restricts which variables are inspected
%! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'});
%! [R, TF] = rmmissing (T, 'DataVariables', 'x');
%! assert_equal (TF, [false; true; false]);
%! assert_equal (R.x, [1; 3]);
%!test  # 'MinNumMissing' needs at least N missing values in a row
%! T = table ([1; NaN; NaN], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'});
%! [R, TF] = rmmissing (T, 'MinNumMissing', 2);
%! assert_equal (TF, [false; false; true]);
%! assert_equal (height (R), 2);
%!test  # 'MissingLocations' as a logical matrix overrides the derived mask
%! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! [R, TF] = rmmissing (T, 'MissingLocations', logical ([0, 0; 1, 0; 0, 1]));
%! assert_equal (TF, [false; true; true]);
%! assert_equal (R.a, 1);
%!test  # 'MissingLocations' as a table of logicals overrides the derived mask
%! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! ML = table (logical ([0; 1; 0]), logical ([0; 0; 1]), ...
%!             'VariableNames', {'a', 'b'});
%! [R, TF] = rmmissing (T, 'MissingLocations', ML);
%! assert_equal (TF, [false; true; true]);
%! assert_equal (R.a, 1);
%!test  # 'DataVariables' with 'MissingLocations' inspects only the named vars
%! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! ML = table (logical ([0; 1; 0]), 'VariableNames', {'a'});
%! [R, TF] = rmmissing (T, 'DataVariables', 'a', 'MissingLocations', ML);
%! assert_equal (TF, [false; true; false]);
%! assert_equal (R.b, [4; 6]);
## Test input validation for 'rmmissing' method
%!error <table.rmmissing: 'MinNumMissing' must be a positive integer.> ...
%! rmmissing (table ([1; NaN]), 'MinNumMissing', 1.5);
%!error <table.rmmissing: 'DataVariables' index a non-existing variable: 'zzz'.> ...
%! rmmissing (table ([1; NaN], 'VariableNames', {'x'}), 'DataVariables', 'zzz');
%!error <table.rmmissing: 'MissingLocations' must be a table with logical variables.> ...
%! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ...
%!            'MissingLocations', table ([1; 2; 3], 'VariableNames', {'a'}));
%!error <table.rmmissing: 'MissingLocations' must be a table with the same variable sizes as the input table or the part of it referenced by 'DataVariables'.> ...
%! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ...
%!            'MissingLocations', table (logical ([0; 1]), 'VariableNames', {'a'}));
%!error <table.rmmissing: 'MissingLocations' must be a table with the same variable names as the input table or the part of it referenced by 'DataVariables'.> ...
%! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ...
%!            'MissingLocations', table (logical ([0; 1; 0]), 'VariableNames', {'b'}));

## Test 'fillmissing' method -- 'constant' method
%!test  # a scalar fill value is broadcast to the targeted numeric variable
%! T = table ([1; NaN; 3; NaN], 'VariableNames', {'x'});
%! [R, TF] = fillmissing (T, 'constant', 0);
%! assert_equal (R.x, [1; 0; 3; 0]);
%! assert_equal (TF, [false; true; false; true]);
%!test  # a cell array gives one fill value per variable
%! T = table ([1; NaN; 3], {'a'; ''; 'c'}, 'VariableNames', {'x', 'y'});
%! R = fillmissing (T, 'constant', {0, 'z'});
%! assert_equal (R.x, [1; 0; 3]);
%! assert_equal (R.y, {'a'; 'z'; 'c'});
%!test  # a vector gives one fill value per variable
%! T = table ([1; NaN], [NaN; 4], 'VariableNames', {'a', 'b'});
%! R = fillmissing (T, 'constant', [7, 9]);
%! assert_equal (R.a, [1; 7]);
%! assert_equal (R.b, [9; 4]);
%!test  # 'constant' fills a string variable
%! s = string ({'a'; 'b'; 'c'});
%! s(2) = string (missing);
%! R = fillmissing (table (s, 'VariableNames', {'s'}), 'constant', ...
%!                  string ('Z'));
%! assert_equal (cellstr (R.s), {'a'; 'Z'; 'c'});
%!test  # 'constant' fills a categorical variable
%! c = categorical ({'p'; ''; 'r'});
%! R = fillmissing (table (c, 'VariableNames', {'c'}), 'constant', ...
%!                  categorical ({'q'}));
%! assert_equal (cellstr (R.c), {'p'; 'q'; 'r'});
%!test  # 'constant' fills a duration variable
%! d = hours ([1; NaN; 3]);
%! R = fillmissing (table (d, 'VariableNames', {'d'}), 'constant', hours (9));
%! assert_equal (hours (R.d), [1; 9; 3]);
%!test  # 'constant' fills a datetime variable
%! t = datetime (2020, 1, [1; 1; 3]);
%! t(2) = NaT;
%! R = fillmissing (table (t, 'VariableNames', {'t'}), 'constant', ...
%!                  datetime (2020, 1, 2));
%! assert_equal (datevec (R.t)(:,3), [1; 2; 3]);
%!test  # 'DataVariables' restricts the fill to a subset of variables
%! T = table ([1; NaN], [NaN; 4], 'VariableNames', {'a', 'b'});
%! [R, TF] = fillmissing (T, 'constant', 0, 'DataVariables', 'a');
%! assert_equal (R.a, [1; 0]);
%! assert_equal (R.b, [NaN; 4]);
%! assert_equal (TF, [false, false; true, false]);
## Test 'fillmissing' method -- 'previous'/'next'/'nearest' methods
%!test  # 'previous' carries the last non-missing value forward
%! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'});
%! [R, TF] = fillmissing (T, 'previous');
%! assert_equal (R.x, [1; 1; 1; 4]);
%! assert_equal (TF, [false; true; true; false]);
%!test  # 'next' carries the next non-missing value backward
%! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'});
%! R = fillmissing (T, 'next');
%! assert_equal (R.x, [1; 4; 4; 4]);
%!test  # 'nearest' uses the closest value; ties favor the next one
%! T = table ([1; NaN; NaN; NaN; 5], 'VariableNames', {'x'});
%! R = fillmissing (T, 'nearest');
%! assert_equal (R.x, [1; 1; 5; 5; 5]);
%!test  # leading missing entries are left missing by 'previous'
%! T = table ([NaN; NaN; 3], 'VariableNames', {'x'});
%! [R, TF] = fillmissing (T, 'previous');
%! assert_equal (R.x, [NaN; NaN; 3]);
%! assert_equal (TF, [false; false; false]);
%!test  # the neighbor methods work on a string variable
%! s = string ({'a'; 'b'; 'c'; 'd'});
%! s(2) = string (missing);
%! s(3) = string (missing);
%! R = fillmissing (table (s, 'VariableNames', {'s'}), 'previous');
%! assert_equal (cellstr (R.s), {'a'; 'a'; 'a'; 'd'});
%!test  # the neighbor methods work on a categorical variable
%! c = categorical ({'p'; ''; ''; 's'});
%! R = fillmissing (table (c, 'VariableNames', {'c'}), 'next');
%! assert_equal (cellstr (R.c), {'p'; 's'; 's'; 's'});
%!test  # the neighbor methods work on a datetime variable
%! t = datetime (2020, 1, [1; 1; 4]);
%! t(2) = NaT;
%! R = fillmissing (table (t, 'VariableNames', {'t'}), 'next');
%! assert_equal (datevec (R.t)(:,3), [1; 4; 4]);
%!test  # a multicolumn variable is filled column by column
%! T = table ([1, 10; NaN, NaN; 3, 30]);
%! [R, TF] = fillmissing (T, 'previous');
%! assert_equal (R.Var1, [1, 10; 1, 10; 3, 30]);
%! assert_equal (TF, [false; true; false]);
%!test  # the method name is case-insensitive
%! T = table ([1; NaN; 3], 'VariableNames', {'x'});
%! R = fillmissing (T, 'Previous');
%! assert_equal (R.x, [1; 1; 3]);
## Test 'fillmissing' method -- 'linear' method
%!test  # interior gaps are linearly interpolated
%! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'});
%! [R, TF] = fillmissing (T, 'linear');
%! assert_equal (R.x, [1; 2; 3; 4]);
%! assert_equal (TF, [false; true; true; false]);
%!test  # 'EndValues' defaults to 'extrap' (linear extrapolation)
%! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'});
%! R = fillmissing (T, 'linear');
%! assert_equal (R.x, [0; 2; 4; 6]);
%!test  # 'EndValues' 'none' leaves leading and trailing gaps missing
%! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'});
%! R = fillmissing (T, 'linear', 'EndValues', 'none');
%! assert_equal (R.x, [NaN; 2; 4; NaN]);
%!test  # 'EndValues' accepts a numeric constant for the end gaps
%! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'});
%! R = fillmissing (T, 'linear', 'EndValues', 0);
%! assert_equal (R.x, [0; 2; 4; 0]);
%!test  # 'linear' leaves non-numeric variables unchanged
%! T = table ([1; NaN; 3], {'a'; ''; 'c'}, 'VariableNames', {'x', 'y'});
%! R = fillmissing (T, 'linear');
%! assert_equal (R.x, [1; 2; 3]);
%! assert_equal (R.y, {'a'; ''; 'c'});
%!test  # fewer than two anchors cannot be interpolated
%! T = table ([NaN; 5; NaN], 'VariableNames', {'x'});
%! R = fillmissing (T, 'linear', 'EndValues', 'none');
%! assert_equal (R.x, [NaN; 5; NaN]);
%!test  # a table without missing values is returned unchanged
%! T = table ([1; 2; 3], 'VariableNames', {'x'});
%! [R, TF] = fillmissing (T, 'previous');
%! assert_equal (isequal (R, T), true);
%! assert_equal (TF, [false; false; false]);
## Test input validation for 'fillmissing' method
%!error <table.fillmissing: too few input arguments.> ...
%! fillmissing (table (1));
%!error <table.fillmissing: METHOD must be a character vector.> ...
%! fillmissing (table (1), 5);
%!error <table.fillmissing: unknown method 'bogus'.> ...
%! fillmissing (table (1), 'bogus');
%!error <table.fillmissing: method 'movmean' is not supported yet.> ...
%! fillmissing (table (1), 'movmean', 3);
%!error <table.fillmissing: the 'constant' method requires a fill value.> ...
%! fillmissing (table (1), 'constant');
%!error <table.fillmissing: a cell array of fill values must have one element per targeted variable.> ...
%! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'constant', {1, 2});
%!error <table.fillmissing: the fill value must be a scalar, a vector with one element per targeted variable, or a cell array of per-variable values.> ...
%! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'constant', [1, 2, 3]);
%!error <table.fillmissing: the fill value is incompatible with variable 'y'.> ...
%! fillmissing (table ([1; NaN], {'a'; ''}, 'VariableNames', {'x', 'y'}), 'constant', 0);
%!error <table.fillmissing: 'DataVariables' index a non-existing variable: 'zzz'.> ...
%! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'previous', 'DataVariables', 'zzz');
%!error <table.fillmissing: unsupported 'EndValues' option 'bogus'.> ...
%! fillmissing (table ([NaN; 2; 3], 'VariableNames', {'x'}), 'linear', 'EndValues', 'bogus');
%!error <table.fillmissing: 'ReplaceValues' set to false is not supported yet.> ...
%! fillmissing (table (1), 'previous', 'ReplaceValues', false);

## Test 'standardizeMissing' method
%!test  # a numeric scalar indicator becomes NaN in numeric variables
%! T = table ([1; 99; 3], 'VariableNames', {'x'});
%! R = standardizeMissing (T, 99);
%! assert_equal (R.x, [1; NaN; 3]);
%!test  # a numeric vector indicator matches any of its values
%! T = table ([1; -99; 3; 0], 'VariableNames', {'x'});
%! R = standardizeMissing (T, [-99, 0]);
%! assert_equal (R.x, [1; NaN; 3; NaN]);
%!test  # a text indicator standardizes a cellstr variable to ''
%! T = table ({'a'; 'NA'; 'c'}, 'VariableNames', {'y'});
%! R = standardizeMissing (T, 'NA');
%! assert_equal (R.y, {'a'; ''; 'c'});
%! assert_equal (cellfun (@isempty, R.y), [false; true; false]);
%!test  # a text indicator standardizes a string variable to <missing>
%! T = table (string ({'a'; 'NA'; 'c'}), 'VariableNames', {'s'});
%! R = standardizeMissing (T, 'NA');
%! assert_equal (ismissing (R.s), [false; true; false]);
%!test  # a text indicator standardizes a categorical variable to <undefined>
%! T = table (categorical ({'a'; 'NA'; 'c'}), 'VariableNames', {'c'});
%! R = standardizeMissing (T, {'NA'});
%! assert_equal (ismissing (R.c), [false; true; false]);
%!test  # a cell indicator mixes numeric and text across a mixed table
%! T = table ([1; 5; 99], {'a'; 'x'; 'c'}, 'VariableNames', {'n', 't'});
%! R = standardizeMissing (T, {99, 'x'});
%! assert_equal (R.n, [1; 5; NaN]);
%! assert_equal (R.t, {'a'; ''; 'c'});
%!test  # numeric and text indicators only touch compatible variables
%! T = table ([1; 5; 99], {'a'; '99'; 'c'}, 'VariableNames', {'n', 't'});
%! R = standardizeMissing (T, 99);
%! assert_equal (R.n, [1; 5; NaN]);
%! assert_equal (R.t, {'a'; '99'; 'c'});
%!test  # 'DataVariables' restricts the standardization to a subset
%! T = table ([1; 99], [99; 2], 'VariableNames', {'a', 'b'});
%! R = standardizeMissing (T, 99, 'DataVariables', 'a');
%! assert_equal (R.a, [1; NaN]);
%! assert_equal (R.b, [99; 2]);
%!test  # integer variables have no standard missing value; pass through
%! T = table (int32 ([1; 99; 3]), 'VariableNames', {'x'});
%! R = standardizeMissing (T, 99);
%! assert_equal (R.x, int32 ([1; 99; 3]));
## Test input validation for 'standardizeMissing' method
%!error <table.standardizeMissing: too few input arguments.> ...
%! standardizeMissing (table (1));
%!error <table.standardizeMissing: invalid INDICATOR of class 'struct'.> ...
%! standardizeMissing (table (1), struct ('a', 1));
%!error <table.standardizeMissing: unsupported indicator element of class 'struct'.> ...
%! standardizeMissing (table (1), {struct('a', 1)});
%!error <table.standardizeMissing: 'DataVariables' index a non-existing variable: 'zzz'.> ...
%! standardizeMissing (table ([1; 99], 'VariableNames', {'x'}), 99, 'DataVariables', 'zzz');

################################################################################
##                  ** Apply Functions to Table Contents **                   ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'pivot'            'groupcounts'      'groupfilter'      'groupsummary'    ##
## 'grouptransform'   'findgroups'       'splitapply'       'rowfun'          ##
## 'varfun'                                                                   ##
##                                                                            ##
################################################################################

## 'findgroups' labels rows by sorted unique value; group k is the k-th value
%!assert_equal (findgroups (table ([1; 3; 1; 2])), [1; 3; 1; 2])
%!assert_equal (findgroups (table ([30; 10; 10; 20])), [3; 1; 1; 2])
%!assert_equal (findgroups (table ({'b'; 'a'; 'b'; 'c'})), [2; 1; 2; 3])
%!assert_equal (findgroups (table (logical ([1; 0; 1; 0]))), [2; 1; 2; 1])
%!test
%! ## Missing values map to NaN in G (numeric, text, datetime)
%! assert_equal (findgroups (table ([1; NaN; 2; 1])), [1; NaN; 2; 1]);
%! assert_equal (findgroups (table ({'b'; ''; 'a'})), [2; NaN; 1]);
%! assert_equal (findgroups (table ([datetime(2026,1,2); NaT; ...
%!                                   datetime(2026,1,1)])), [2; NaN; 1]);
%!test
%! ## Multiple variables group by unique sorted combinations
%! T = table ([1; 1; 2; 1], {'b'; 'a'; 'a'; 'b'});
%! assert_equal (findgroups (T), [2; 1; 3; 2]);
%!test
%! ## Second output is the sorted-unique identifier table
%! [G, TID] = findgroups (table ([30; 10; 10; 20]));
%! assert_equal (G, [3; 1; 1; 2]);
%! assert_equal (TID.Var1, [10; 20; 30]);
%! assert_equal (TID.Properties.VariableNames, {'Var1'});
%!error <T must have at least one variable> findgroups (table ())

## 'splitapply' splits rows by group number and concatenates per-group results
%!assert_equal (splitapply (@(x) mean (x), table ([1; 2; 3; 4]), [1; 1; 2; 2]), ...
%!              [1.5; 3.5])
%!assert_equal (splitapply (@(x) sum (x), table ([1; 2; 3]), [2; 1; 2]), [2; 4])
%!test
%! ## Each table variable is passed to FUNC as a separate argument
%! T = table ([1; 2; 3; 4], [10; 20; 30; 40]);
%! y = splitapply (@(a, b) sum (a) + sum (b), T, [1; 1; 2; 2]);
%! assert_equal (y, [33; 77]);
%!test
%! ## Multiple outputs are concatenated separately
%! [lo, hi] = splitapply (@(x) deal (min (x), max (x)), ...
%!                        table ([4; 1; 3; 2]), [1; 1; 2; 2]);
%! assert_equal (lo, [1; 2]);
%! assert_equal (hi, [4; 3]);
%!test
%! ## NaN group numbers omit the corresponding rows
%! y = splitapply (@(x) mean (x), table ([1; 2; 99; 4]), [1; 1; NaN; 2]);
%! assert_equal (y, [1.5; 4]);
%!error <G must be a numeric vector> splitapply (@mean, table ([1; 2]), [1; 1; 1])
%!error <G must contain every integer> splitapply (@mean, table ([1; 2]), [1; 3])
%!error <FUNC must be a function handle> splitapply (1, table ([1; 2]), [1; 2])

## 'varfun' applies FUNC to each variable; output names are FUNC_VARNAME
%!test
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = varfun (@sum, T);
%! assert_equal (B.Properties.VariableNames, {'sum_a', 'sum_b'});
%! assert_equal (B.sum_a, 6);
%! assert_equal (B.sum_b, 60);
%!test
%! ## Anonymous functions are named with the 'Fun' prefix
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = varfun (@(x) max (x), T);
%! assert_equal (B.Properties.VariableNames, {'Fun_a', 'Fun_b'});
%! assert_equal (B.Fun_b, 30);
%!test
%! ## 'OutputFormat' 'uniform' concatenates scalar results into an array
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! assert_equal (varfun (@mean, T, 'OutputFormat', 'uniform'), [2, 20]);
%!test
%! ## 'OutputFormat' 'cell' returns the results in a cell array
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! assert_equal (varfun (@sum, T, 'OutputFormat', 'cell'), {6, 60});
%!test
%! ## 'InputVariables' restricts the variables passed to FUNC
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = varfun (@sum, T, 'InputVariables', 'a');
%! assert_equal (B.Properties.VariableNames, {'sum_a'});
%! assert_equal (B.sum_a, 6);
%!test
%! ## 'GroupingVariables' add grouping columns and a GroupCount column
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'});
%! B = varfun (@sum, T, 'GroupingVariables', 'g');
%! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'sum_x'});
%! assert_equal (B.g, [1; 2]);
%! assert_equal (B.GroupCount, [2; 2]);
%! assert_equal (B.sum_x, [30; 70]);
%!test
%! ## Grouped 'uniform' output holds only the per-group results
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'});
%! v = varfun (@sum, T, 'GroupingVariables', 'g', 'OutputFormat', 'uniform');
%! assert_equal (v, [30; 70]);
%!test
%! ## A grouped FUNC may return several rows per group; the grouping and
%! ## GroupCount columns are replicated to match the stacked results
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'});
%! B = varfun (@(x) x * 2, T, 'GroupingVariables', 'g');
%! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Fun_x'});
%! assert_equal (B.g, [1; 1; 2; 2]);
%! assert_equal (B.GroupCount, [2; 2; 2; 2]);
%! assert_equal (B.Fun_x, [20; 40; 60; 80]);
%!test
%! ## Grouped 'cell' output holds one cell per group with the full result
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'});
%! C = varfun (@(x) x * 2, T, 'GroupingVariables', 'g', 'OutputFormat', 'cell');
%! assert_equal (size (C), [2, 1]);
%! assert_equal (C{1}, [20; 40]);
%! assert_equal (C{2}, [60; 80]);
%!error <table.varfun: FUNC must be a function handle.> varfun (1, table ([1; 2]))
%!error <table.varfun: invalid 'OutputFormat' value 'bogus'.> ...
%! varfun (@sum, table ([1; 2]), 'OutputFormat', 'bogus')
%!error <table.varfun: there are no variables to which to apply FUNC.> ...
%! varfun (@sum, table ([1; 2], 'VariableNames', {'g'}), 'GroupingVariables', 'g')
%!error <table.varfun: OutputFormat 'uniform' requires FUNC to return a scalar for each call.> ...
%! varfun (@(x) x, table ([1; 2; 3]), 'OutputFormat', 'uniform')

## 'rowfun' applies FUNC to each row; default output names are Var1, Var2, ...
%!test
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = rowfun (@(x, y) x + y, T);
%! assert_equal (B.Properties.VariableNames, {'Var1'});
%! assert_equal (B.Var1, [11; 22; 33]);
%!test
%! ## Multiple outputs with 'OutputVariableNames'
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = rowfun (@(x, y) deal (x + y, x * y), T, ...
%!             'OutputVariableNames', {'add', 'mul'});
%! assert_equal (B.Properties.VariableNames, {'add', 'mul'});
%! assert_equal (B.add, [11; 22; 33]);
%! assert_equal (B.mul, [10; 40; 90]);
%!test
%! ## 'SeparateInputs' false passes the row as a single concatenated argument
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! B = rowfun (@(row) sum (row), T, 'SeparateInputs', false);
%! assert_equal (B.Var1, [11; 22; 33]);
%!test
%! ## 'InputVariables' restricts and orders the variables passed to FUNC
%! T = table ([1; 2; 3], [10; 20; 30], [100; 200; 300], ...
%!            'VariableNames', {'a', 'b', 'c'});
%! B = rowfun (@(x, y) x + y, T, 'InputVariables', {'a', 'c'});
%! assert_equal (B.Var1, [101; 202; 303]);
%!test
%! ## 'ExtractCellContents' unwraps cell-valued variables before the call
%! T = table ({1; 2; 3}, 'VariableNames', {'a'});
%! B = rowfun (@(x) x * 2, T, 'ExtractCellContents', true);
%! assert_equal (B.Var1, [2; 4; 6]);
%!test
%! ## 'OutputFormat' 'uniform' and 'cell'
%! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'});
%! assert_equal (rowfun (@(x, y) x + y, T, 'OutputFormat', 'uniform'), ...
%!               [11; 22; 33]);
%! assert_equal (rowfun (@(x, y) x + y, T, 'OutputFormat', 'cell'), ...
%!               {11; 22; 33});
%!test
%! ## Ungrouped output preserves the row names of the input table
%! T = table ([1; 2], [10; 20], 'VariableNames', {'a', 'b'}, ...
%!            'RowNames', {'r1', 'r2'});
%! B = rowfun (@(x, y) x + y, T);
%! assert_equal (B.Properties.RowNames, T.Properties.RowNames);
%!test
%! ## 'GroupingVariables' apply FUNC per group with a GroupCount column; the
%! ## default output name continues past the grouping and GroupCount columns
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'});
%! B = rowfun (@(x) sum (x), T, 'GroupingVariables', 'g');
%! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'});
%! assert_equal (B.g, [1; 2]);
%! assert_equal (B.GroupCount, [2; 2]);
%! assert_equal (B.Var3, [30; 70]);
%!test
%! ## A grouped FUNC may return several rows per group; the grouping and
%! ## GroupCount columns are replicated to match the stacked results
%! T = table ([1; 1; 2; 2], [10; 20; 30; 40], [5; 6; 7; 8], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! B = rowfun (@(x, y) x + y, T, 'GroupingVariables', 'g');
%! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'});
%! assert_equal (B.g, [1; 1; 2; 2]);
%! assert_equal (B.GroupCount, [2; 2; 2; 2]);
%! assert_equal (B.Var3, [15; 26; 37; 48]);
%!error <table.rowfun: FUNC must be a function handle.> rowfun (1, table ([1; 2]))
%!error <table.rowfun: 'SeparateInputs' must be a logical scalar.> ...
%! rowfun (@(x) x, table ([1; 2]), 'SeparateInputs', 'yes')
%!error <table.rowfun: the number of 'OutputVariableNames' must equal 'NumOutputs'.> ...
%! rowfun (@(x) x, table ([1; 2]), 'OutputVariableNames', {'p', 'q'}, 'NumOutputs', 1)

## 'groupsummary' with no method returns the grouping variables and GroupCount
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g');
%! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount'});
%! assert_equal (G.g, [1; 2]);
%! assert_equal (G.GroupCount, [3; 2]);
## A named method appends a '<method>_<datavar>' column for each data variable
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupsummary (T, 'g', 'sum');
%! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'sum_x', 'sum_y'});
%! assert_equal (G.GroupCount, [3; 2]);
%! assert_equal (G.sum_x, [80; 70]);
%! assert_equal (G.sum_y, [8; 7]);
## 'DataVars' restricts the variables the method is applied to
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupsummary (T, 'g', 'mean', 'x');
%! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'mean_x'});
%! assert_equal (G.mean_x, [80/3; 35]);
## Several methods and data variables are ordered data variable first, then method
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupsummary (T, 'g', {'mean', 'sum'}, {'x', 'y'});
%! assert_equal (G.Properties.VariableNames, ...
%!               {'g', 'GroupCount', 'mean_x', 'sum_x', 'mean_y', 'sum_y'});
%! assert_equal (G.mean_x, [80/3; 35]);
%! assert_equal (G.sum_x, [80; 70]);
%! assert_equal (G.mean_y, [8/3; 3.5]);
%! assert_equal (G.sum_y, [8; 7]);
## Function handles are named 'fun<n>' by their position among the methods
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', @std);
%! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'fun1_x'});
%! assert_equal (G.fun1_x, [std([10; 20; 50]); std([30; 40])]);
## Multiple function handles keep their 'fun<n>' index across data variables
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupsummary (T, 'g', {@std, @(v) median(v)}, {'x', 'y'});
%! assert_equal (G.Properties.VariableNames, ...
%!               {'g', 'GroupCount', 'fun1_x', 'fun2_x', 'fun1_y', 'fun2_y'});
%! assert_equal (G.fun2_x, [median([10; 20; 50]); median([30; 40])]);
## A function handle may return a row, producing a multi-column result variable
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', @(v) [min(v), max(v)]);
%! assert_equal (G.fun1_x, [10, 50; 30, 40]);
## 'min', 'max', 'range', 'std', 'var', 'median', 'mode' spot checks
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', {'min', 'max', 'range', 'var', 'median', 'mode'});
%! assert_equal (G.min_x, [10; 30]);
%! assert_equal (G.max_x, [50; 40]);
%! assert_equal (G.range_x, [40; 10]);
%! assert_equal (G.var_x, [var([10; 20; 50]); var([30; 40])]);
%! assert_equal (G.median_x, [20; 35]);
%! assert_equal (G.mode_x, [10; 30]);
## 'nnz' counts the nonzero values in the group, omitting NaN
%!test
%! T = table ([1; 1; 2; 2], [0; 2; 0; 0], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 'nnz');
%! assert_equal (G.nnz_x, [1; 0]);
## Named methods omit NaN in the data; 'nummissing' counts them
%!test
%! T = table ([1; 1; 2; 2], [NaN; 20; NaN; NaN], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', {'mean', 'nummissing'});
%! assert_equal (G.mean_x, [20; NaN]);
%! assert_equal (G.nummissing_x, [1; 2]);
## 'numunique' counts the unique non-missing values, for any data type
%!test
%! T = table ([1; 1; 2; 2], {'a'; 'a'; 'b'; ''}, 'VariableNames', {'g', 's'});
%! G = groupsummary (T, 'g', {'numunique', 'nummissing'});
%! assert_equal (G.numunique_s, [1; 1]);
%! assert_equal (G.nummissing_s, [0; 1]);
## Missing values in a grouping variable form their own group, sorted last
%!test
%! T = table ([1; 1; 2; NaN; 2], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 'mean');
%! assert_equal (G.g, [1; 2; NaN]);
%! assert_equal (G.GroupCount, [2; 2; 1]);
%! assert_equal (G.mean_x, [15; 40; 40]);
## 'IncludeMissingGroups' false drops the rows with a missing grouping value
%!test
%! T = table ([1; 1; 2; NaN; 2], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 'mean', 'IncludeMissingGroups', false);
%! assert_equal (G.g, [1; 2]);
%! assert_equal (G.GroupCount, [2; 2]);
%! assert_equal (G.mean_x, [15; 40]);
## Several grouping variables group by their unique combinations, missing last
%!test
%! T = table ([1; 1; 2; NaN; 2], {'b'; 'a'; 'a'; 'a'; 'b'}, [10; 20; 30; 40; 50], ...
%!            'VariableNames', {'g1', 'g2', 'x'});
%! G = groupsummary (T, {'g1', 'g2'}, 'sum');
%! assert_equal (G.g1, [1; 1; 2; 2; NaN]);
%! assert_equal (G.g2, {'a'; 'b'; 'a'; 'b'; 'a'});
%! assert_equal (G.GroupCount, [1; 1; 1; 1; 1]);
%! assert_equal (G.sum_x, [20; 10; 30; 50; 40]);
## A categorical grouping variable keeps its type in the output
%!test
%! T = table (categorical ({'a'; 'b'; 'a'; 'c'}), [1; 2; 3; 4], ...
%!            'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 'sum');
%! assert_equal (iscategorical (G.g), true);
%! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'});
%! assert_equal (G.sum_x, [4; 2; 4]);
## An ordinal categorical grouping variable groups by category order
%!test
%! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ...
%!                  {'low', 'medium', 'high'}, 'Ordinal', true);
%! T = table (c, [1; 2; 3; 4], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 'sum');
%! assert_equal (cellstr (G.g), {'low'; 'medium'; 'high'});
%! assert_equal (G.sum_x, [6; 1; 3]);
%!error <table.groupsummary: 'bogus' is not a supported method name.> ...
%! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'bogus')
%!error <table.groupsummary: too many positional arguments.> ...
%! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'sum', 'x', 5)
%!error <table.groupsummary: at least one grouping variable is required.> ...
%! groupsummary (table ([1; 2], 'VariableNames', {'g'}), false)
%!error <table.groupsummary: named method 'mean' is not supported for variables of type 'cell'; use a function handle \(variable 'x'\).> ...
%! groupsummary (table ([1; 2], {'a'; 'b'}, 'VariableNames', {'g', 'x'}), 'g', 'mean')

## 'IncludeEmptyGroups' adds the unused categories of a categorical grouping
## variable as empty groups (GroupCount 0, sum 0, NaN for the rest)
%!test
%! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'});
%! T = table (c, [10; 20; 30], 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', {'sum', 'mean', 'max', 'numunique'}, 'x', ...
%!                   'IncludeEmptyGroups', true);
%! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'});
%! assert_equal (G.GroupCount, [2; 1; 0]);
%! assert_equal (G.sum_x, [30; 30; 0]);
%! assert_equal (G.mean_x, [15; 30; NaN]);
%! assert_equal (G.max_x, [20; 30; NaN]);
%! assert_equal (G.numunique_x, [2; 1; 0]);
## A GROUPBINS edge vector groups a numeric variable by bin interval
%!test
%! T = table ([1; 3; 5; 7; 9; 11], (1:6)', 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', [0 6 12], 'sum', 'x');
%! assert_equal (iscategorical (G.disc_g), true);
%! assert_equal (cellstr (G.disc_g), {'[0, 6)'; '[6, 12]'});
%! assert_equal (G.GroupCount, [3; 3]);
%! assert_equal (G.sum_x, [6; 15]);
## A GROUPBINS number of bins makes equal-width bins over the data range
%!test
%! T = table ([1; 2; 3; 4], (1:4)', 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', 3, 'sum', 'x');
%! assert_equal (cellstr (G.disc_g), {'[1, 2)'; '[2, 3)'; '[3, 4]'});
%! assert_equal (G.GroupCount, [1; 1; 2]);
## 'IncludedEdge' 'right' makes the right bin edge the inclusive one
%!test
%! T = table ([0; 6; 12], (1:3)', 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', [0 6 12], 'sum', 'x', 'IncludedEdge', 'right');
%! assert_equal (cellstr (G.disc_g), {'[0, 6]'; '(6, 12]'});
%! assert_equal (G.GroupCount, [2; 1]);
## Binning combined with 'IncludeEmptyGroups' keeps the empty bins
%!test
%! T = table ([1; 2; 3; 20], (1:4)', 'VariableNames', {'g', 'x'});
%! G = groupsummary (T, 'g', [0 5 10 25], 'sum', 'x', 'IncludeEmptyGroups', true);
%! assert_equal (cellstr (G.disc_g), {'[0, 5)'; '[5, 10)'; '[10, 25]'});
%! assert_equal (G.GroupCount, [3; 0; 1]);
%! assert_equal (G.sum_x, [6; 0; 4]);
## GROUPBINS time-unit 'month' bins a datetime by calendar month (month_<var>)
%!test
%! t = datetime (2023, [1; 1; 2; 3; 3], [1; 2; 3; 4; 5]);
%! T = table (t, (1:5)', 'VariableNames', {'t', 'x'});
%! G = groupsummary (T, 't', 'month', 'sum', 'x');
%! assert_equal (G.Properties.VariableNames, {'month_t', 'GroupCount', 'sum_x'});
%! assert_equal (iscategorical (G.month_t), true);
%! assert_equal (cellstr (G.month_t), {'Jan-2023'; 'Feb-2023'; 'Mar-2023'});
%! assert_equal (G.GroupCount, [2; 1; 2]);
%! assert_equal (G.sum_x, [3; 3; 9]);
## Time-unit 'year' point labels; empty periods appear with IncludeEmptyGroups
%!test
%! t = datetime ([2020; 2020; 2023], 1, 1);
%! T = table (t, (1:3)', 'VariableNames', {'t', 'x'});
%! G = groupsummary (T, 't', 'year', 'sum', 'x', 'IncludeEmptyGroups', true);
%! assert_equal (numel (categories (G.year_t)), 4);
%! assert_equal (cellstr (G.year_t), {'2020'; '2021'; '2022'; '2023'});
%! assert_equal (G.GroupCount, [2; 0; 0; 1]);
## Time-unit 'day' and 'quarter' labels
%!test
%! t = datetime (2024, [1; 4], [3; 10]);
%! T = table (t, (1:2)', 'VariableNames', {'t', 'x'});
%! assert_equal (cellstr (groupsummary (T, 't', 'day', 'sum', 'x').day_t), ...
%!               {'03-Jan-2024'; '10-Apr-2024'});
%! assert_equal (cellstr (groupsummary (T, 't', 'quarter', 'sum', 'x').quarter_t), ...
%!               {'Q1 2024'; 'Q2 2024'});
## A duration bin width bins a duration variable (labelled in its unit, disc_<var>)
%!test
%! d = minutes ([100; 200; 300]);
%! T = table (d, (1:3)', 'VariableNames', {'d', 'x'});
%! G = groupsummary (T, 'd', minutes (90), 'sum', 'x');
%! assert_equal (G.Properties.VariableNames, {'disc_d', 'GroupCount', 'sum_x'});
%! assert_equal (cellstr (G.disc_d), {'[90 min, 180 min)'; '[180 min, 270 min)'; ...
%!               '[270 min, 360 min]'});
## Time-unit binning of a numeric variable is rejected
%!error <table.groupsummary: binning grouping variable 'g' by time unit 'month' is only supported for datetime variables.> ...
%! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'month', 'sum')
%!error <table.groupsummary: 'IncludedEdge' must be 'left' or 'right'.> ...
%! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 2, 'sum', 'IncludedEdge', 'mid')
%!error <table.groupsummary: binning is not supported for grouping variable 'g' of type 'cell'.> ...
%! groupsummary (table ({'a'; 'b'}, [3; 4], 'VariableNames', {'g', 'x'}), 'g', 2, 'sum')
%!error <table.groupsummary: binning grouping variable 'g' by time unit 'month' is only supported for datetime variables.> ...
%! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'month', 'sum')

## 'groupcounts' returns the grouping variables, GroupCount, and Percent
%!test
%! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', 'x'});
%! G = groupcounts (T, 'g');
%! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'Percent'});
%! assert_equal (G.g, [1; 2]);
%! assert_equal (G.GroupCount, [3; 2]);
%! assert_equal (G.Percent, [60; 40]);
## Percent is each group's count as a percentage of the total row count
%!test
%! T = table ([1; 1; 1; 2], 'VariableNames', {'g'});
%! G = groupcounts (T, 'g');
%! assert_equal (G.GroupCount, [3; 1]);
%! assert_equal (G.Percent, [75; 25]);
## Several grouping variables group by their unique combinations, missing last
%!test
%! T = table ([1; 1; 2; NaN; 2], {'b'; 'a'; 'a'; 'a'; 'b'}, ...
%!            'VariableNames', {'g1', 'g2'});
%! G = groupcounts (T, {'g1', 'g2'});
%! assert_equal (G.g1, [1; 1; 2; 2; NaN]);
%! assert_equal (G.g2, {'a'; 'b'; 'a'; 'b'; 'a'});
%! assert_equal (G.GroupCount, [1; 1; 1; 1; 1]);
%! assert_equal (G.Percent, [20; 20; 20; 20; 20]);
## Missing values in a grouping variable form their own group, sorted last
%!test
%! T = table ([1; 1; 2; NaN; 2], 'VariableNames', {'g'});
%! G = groupcounts (T, 'g');
%! assert_equal (G.g, [1; 2; NaN]);
%! assert_equal (G.GroupCount, [2; 2; 1]);
%! assert_equal (G.Percent, [40; 40; 20]);
## 'IncludeMissingGroups' false drops the rows with a missing grouping value
%!test
%! T = table ([1; 1; 2; NaN; 2], 'VariableNames', {'g'});
%! G = groupcounts (T, 'g', 'IncludeMissingGroups', false);
%! assert_equal (G.g, [1; 2]);
%! assert_equal (G.GroupCount, [2; 2]);
%! assert_equal (G.Percent, [50; 50]);
## A categorical grouping variable keeps its type and groups by category order
%!test
%! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ...
%!                  {'low', 'medium', 'high'}, 'Ordinal', true);
%! T = table (c, 'VariableNames', {'g'});
%! G = groupcounts (T, 'g');
%! assert_equal (iscategorical (G.g), true);
%! assert_equal (cellstr (G.g), {'low'; 'medium'; 'high'});
%! assert_equal (G.GroupCount, [2; 1; 1]);
%!error <table.groupcounts: at least one grouping variable is required.> ...
%! groupcounts (table ([1; 2], 'VariableNames', {'g'}), false)

## 'IncludeEmptyGroups' adds unused categorical categories with zero count
%!test
%! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'});
%! T = table (c, 'VariableNames', {'g'});
%! G = groupcounts (T, 'g', 'IncludeEmptyGroups', true);
%! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'});
%! assert_equal (G.GroupCount, [2; 1; 0]);
%! assert_equal (G.Percent, [100*2/3; 100/3; 0]);
## A GROUPBINS edge vector groups a numeric variable by bin interval
%!test
%! T = table ([1; 3; 5; 7; 9; 11], 'VariableNames', {'g'});
%! G = groupcounts (T, 'g', [0 6 12]);
%! assert_equal (cellstr (G.disc_g), {'[0, 6)'; '[6, 12]'});
%! assert_equal (G.GroupCount, [3; 3]);
%! assert_equal (G.Percent, [50; 50]);
## Binning combined with 'IncludeEmptyGroups' keeps the empty bins
%!test
%! T = table ([1; 2; 3; 20], 'VariableNames', {'g'});
%! G = groupcounts (T, 'g', [0 5 10 25], 'IncludeEmptyGroups', true);
%! assert_equal (cellstr (G.disc_g), {'[0, 5)'; '[5, 10)'; '[10, 25]'});
%! assert_equal (G.GroupCount, [3; 0; 1]);
%!error <table.groupcounts: invalid argument; expected a GROUPBINS binning scheme or a Name-Value option.> ...
%! groupcounts (table ([1; 2], 'VariableNames', {'g'}), 'g', 'bogus')
%!error <table.groupcounts: 'IncludedEdge' must be 'left' or 'right'.> ...
%! groupcounts (table ([1; 2], 'VariableNames', {'g'}), 'g', 2, 'IncludedEdge', 'x')

## 'groupfilter' with a scalar-returning method keeps or drops whole groups
%!test
%! T = table ([1; 1; 1; 2; 2; 3], [3; 1; 2; 9; 8; 5], ...
%!            'VariableNames', {'g', 'x'});
%! G = groupfilter (T, 'g', @(x) numel (x) > 2);
%! assert_equal (G.Properties.VariableNames, {'g', 'x'});
%! assert_equal (G.g, [1; 1; 1]);
%! assert_equal (G.x, [3; 1; 2]);
## A vector-returning method keeps individual rows within each group
%!test
%! T = table ([1; 1; 1; 1; 2; 2; 2; 2], [67; 65; 71; 55; 61; 79; 58; 78], ...
%!            'VariableNames', {'d', 't'});
%! G = groupfilter (T, 'd', @(x) x == max (x));
%! assert_equal (G.d, [1; 2]);
%! assert_equal (G.t, [71; 79]);
## Output rows keep their original order, not group order
%!test
%! T = table ([2; 1; 2; 1], [1; 2; 3; 4], 'VariableNames', {'g', 'x'});
%! G = groupfilter (T, 'g', @(x) numel (x) >= 1);
%! assert_equal (G.x, [1; 2; 3; 4]);
## A row is kept only when the condition holds across all data variables
%!test
%! T = table ([1; 1; 2; 2], [5; 1; 9; 2], [1; 9; 1; 9], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupfilter (T, 'g', @(v) v > 3);
%! assert_equal (G.x, zeros (0, 1));
## 'DataVars' restricts the variables the filter operates on
%!test
%! T = table ([1; 1; 2; 2], [5; 1; 9; 2], [1; 9; 1; 9], ...
%!            'VariableNames', {'g', 'x', 'y'});
%! G = groupfilter (T, 'g', @(v) v > 3, 'x');
%! assert_equal (G.x, [5; 9]);
%! assert_equal (G.y, [1; 1]);
## Rows with a missing grouping value form their own group and are filtered too
%!test
%! T = table ([1; 1; NaN; 2; 2], [10; 20; 30; 40; 50], ...
%!            'VariableNames', {'g', 'x'});
%! G = groupfilter (T, 'g', @(v) mean (v) > 25);
%! assert_equal (G.g, [NaN; 2; 2]);
%! assert_equal (G.x, [30; 40; 50]);
## A categorical grouping variable groups by category
%!test
%! T = table (categorical ({'a'; 'b'; 'a'; 'b'; 'a'}), [1; 5; 2; 6; 9], ...
%!            'VariableNames', {'g', 'x'});
%! G = groupfilter (T, 'g', @(x) x == max (x));
%! assert_equal (cellstr (G.g), {'b'; 'a'});
%! assert_equal (G.x, [6; 9]);
## A GROUPBINS edge vector filters within bins of a numeric grouping variable
%!test
%! T = table ([1; 3; 5; 7; 9; 11], (1:6)', 'VariableNames', {'g', 'x'});
%! G = groupfilter (T, 'g', [0 6 12], @(x) numel (x) > 2);
%! assert_equal (G.x, (1:6)');
%! G2 = groupfilter (T, 'g', [0 6 12], @(x) mean (x) > 4, 'IncludedEdge', 'right');
%! assert_equal (G2.x, [4; 5; 6]);
%!error <table.groupfilter: METHOD must be a function handle.> ...
%! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'sum')
%!error <table.groupfilter: binning grouping variable 'g' by time unit 'month' is only supported for datetime variables.> ...
%! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'month', @(x) x > 0)
%!error <table.groupfilter: too many positional arguments.> ...
%! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', @(x) x > 0, 'x', 5)
%!error <table.groupfilter: at least one grouping variable is required.> ...
%! groupfilter (table ([1; 2], 'VariableNames', {'g'}), false, @(x) x > 0)
%!error <table.groupfilter: the filter function must return a logical scalar or a logical vector with one element per group row.> ...
%! groupfilter (table ([1; 1], [3; 4], 'VariableNames', {'g', 'x'}), 'g', @(x) [true; true; true])

## 'grouptransform' 'meancenter' subtracts the group mean, in place
%!test
%! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'meancenter');
%! assert_equal (G.Properties.VariableNames, {'g', 'x'});
%! assert_equal (G.g, [1; 1; 1; 2; 2]);
%! assert_equal (G.x, [-10; 0; 10; -10; 10]);
## 'zscore', 'norm', and 'rescale' spot checks
%!test
%! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ...
%!            'VariableNames', {'g', 'x'});
%! assert_equal (grouptransform (T, 'g', 'zscore').x, ...
%!               [-1; 0; 1; -1/sqrt(2); 1/sqrt(2)], 8 * eps);
%! assert_equal (grouptransform (T, 'g', 'norm').x, ...
%!               [[10; 20; 30] ./ norm([10; 20; 30]); ...
%!                [40; 60] ./ norm([40; 60])], 8 * eps);
%! assert_equal (grouptransform (T, 'g', 'rescale').x, [0; 0.5; 1; 0; 1]);
## 'meanfill' replaces missing values with the group mean
%!test
%! T = table ([1; 1; 2; 2], [10; NaN; 30; 40], 'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'meanfill');
%! assert_equal (G.x, [10; 10; 30; 40]);
## 'zscore' omits NaN for the group statistics and leaves NaN in place
%!test
%! T = table ([1; 1; 2; 2], [10; NaN; 30; 40], 'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'zscore');
%! assert_equal (G.x, [NaN; NaN; -1/sqrt(2); 1/sqrt(2)], 8 * eps);
## 'linearfill' interpolates interior missing values, leaving the edges as NaN
%!test
%! T = table ([1; 1; 1; 1; 2; 2; 2], [1; NaN; NaN; 4; NaN; 10; NaN], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'linearfill');
%! assert_equal (G.x, [1; 2; 3; 4; NaN; 10; NaN]);
## A function handle is applied to each group's slice
%!test
%! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', @(v) v - mean (v));
%! assert_equal (G.x, [-10; 0; 10; -10; 10]);
## 'ReplaceValues' false appends a '<method>_<datavar>' column, keeping originals
%!test
%! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'meancenter', 'ReplaceValues', false);
%! assert_equal (G.Properties.VariableNames, {'g', 'x', 'meancenter_x'});
%! assert_equal (G.x, [10; 20; 30; 40; 60]);
%! assert_equal (G.meancenter_x, [-10; 0; 10; -10; 10]);
## 'DataVars' restricts the variables that are transformed
%!test
%! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], [1; 2; 3; 4; 5], ...
%!            'VariableNames', {'g', 'y', 'z'});
%! G = grouptransform (T, 'g', 'meancenter', 'y');
%! assert_equal (G.y, [-10; 0; 10; -10; 10]);
%! assert_equal (G.z, [1; 2; 3; 4; 5]);
## Rows with a missing grouping value form their own group, in original order
%!test
%! T = table ([1; 1; NaN; 2; 2], [10; 20; 30; 40; 60], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', 'meancenter');
%! assert_equal (G.g, [1; 1; NaN; 2; 2]);
%! assert_equal (G.x, [-5; 5; 0; -10; 10]);
## A GROUPBINS edge vector transforms within bins of a numeric grouping variable
%!test
%! T = table ([1; 3; 5; 7; 9; 11], [10; 20; 30; 40; 50; 60], ...
%!            'VariableNames', {'g', 'x'});
%! G = grouptransform (T, 'g', [0 6 12], 'meancenter', 'x');
%! assert_equal (G.x, [-10; 0; 10; -10; 0; 10]);
%!error <table.grouptransform: METHOD must be one of 'zscore', 'norm', 'meancenter', 'rescale', 'meanfill', 'linearfill', or a function handle.> ...
%! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'bogus')
%!error <table.grouptransform: binning grouping variable 'g' by time unit 'month' is only supported for datetime variables.> ...
%! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'month', 'zscore')
%!error <table.grouptransform: 'ReplaceValues' must be a logical scalar.> ...
%! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'zscore', 'ReplaceValues', 'yes')
%!error <table.grouptransform: too many positional arguments.> ...
%! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'zscore', 'x', 5)
%!error <table.grouptransform: grouptransform requires numeric or logical data; got 'cell' \(variable 'x'\).> ...
%! grouptransform (table ([1; 2], {'a'; 'b'}, 'VariableNames', {'g', 'x'}), 'g', 'zscore')

## 'pivot' counts the rows of each row-and-column group by default
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; 'b'; 'b'}), ...
%!            categorical ({'x'; 'y'; 'x'; 'y'; 'y'}), ...
%!            'VariableNames', {'g', 'h'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h');
%! assert_equal (P.Properties.VariableNames, {'g', 'x', 'y'});
%! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'});
%! assert_equal (P{:, {'x', 'y'}}, [1, 1; 1, 2]);
## Column variable names come from the column grouping values, joined with '_'
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ...
%!            [1; 2; 3], 'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Columns', {'g', 'h'}, 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'a_x', 'a_y', 'b_x'});
%! assert_equal (P{:, :}, [1, 2, 3]);
## A logical column grouping variable names the variables 'false' and 'true'
%!test
%! T = table (logical ([0; 0; 1; 1]), [1; 2; 3; 4], ...
%!            'VariableNames', {'b', 'v'});
%! P = pivot (T, 'Columns', 'b', 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'false', 'true'});
%! assert_equal (P{:, :}, [3, 7]);
## 'DataVariable' with 'Method' aggregates that variable; empty mean cells are NaN
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ...
%!            categorical ({'x'; 'y'; 'x'; 'x'}), [10; 20; 30; 50], ...
%!            'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ...
%!            'Method', 'mean');
%! assert_equal (P{:, {'x', 'y'}}, [10, 20; 40, NaN]);
## A numeric data variable defaults to 'sum'; the empty cell sums to zero
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ...
%!            [10; 20; 30], 'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v');
%! assert_equal (P{:, {'x', 'y'}}, [10, 20; 30, 0]);
## A function handle is applied to each cell's data slice
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ...
%!            categorical ({'x'; 'x'; 'y'; 'y'}), [1; 3; 5; 9], ...
%!            'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ...
%!            'Method', @(x) max (x) - min (x));
%! assert_equal (P{:, {'x', 'y'}}, [2, NaN; NaN, 4]);
## Method 'percentage' gives each cell's row count as a percentage of the total
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ...
%!            categorical ({'x'; 'y'; 'x'; 'y'}), ...
%!            'VariableNames', {'g', 'h'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'Method', 'percentage');
%! assert_equal (P{:, {'x', 'y'}}, [25, 25; 25, 25]);
## Method 'none' rearranges the data without aggregating; empty cells are missing
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ...
%!            [10; 20; 30], 'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ...
%!            'Method', 'none');
%! assert_equal (P{:, {'x', 'y'}}, [10, 20; 30, NaN]);
## An omitted 'Rows' dimension collapses to a single row
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ...
%!            'VariableNames', {'g', 'v'});
%! P = pivot (T, 'Columns', 'g', 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'a', 'b'});
%! assert_equal (P{:, :}, [3, 3]);
## With no 'Columns', a single variable named '<method>_<datavar>' (or 'count')
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ...
%!            'VariableNames', {'g', 'v'});
%! P = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'g', 'sum_v'});
%! assert_equal (P{:, 'sum_v'}, [3; 3]);
%! P2 = pivot (T, 'Rows', 'g');
%! assert_equal (P2.Properties.VariableNames, {'g', 'count'});
%! assert_equal (P2{:, 'count'}, [2; 1]);
## 'RowLabelPlacement' 'rownames' moves the row labels to the RowNames property
%!test
%! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ...
%!            'VariableNames', {'g', 'h'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'RowLabelPlacement', 'rownames');
%! assert_equal (P.Properties.VariableNames, {'x', 'y'});
%! assert_equal (P.Properties.RowNames, {'a'; 'b'});
## 'IncludeMissingGroups' controls whether missing grouping values form a group
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; '<undefined>'}, {'a', 'b'}), ...
%!            [1; 2; 3; 4], 'VariableNames', {'g', 'v'});
%! P = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P{:, 'sum_v'}, [3; 3; 4]);
%! Q = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum', ...
%!            'IncludeMissingGroups', false);
%! assert_equal (cellstr (Q{:, 'g'}), {'a'; 'b'});
%! assert_equal (Q{:, 'sum_v'}, [3; 3]);
## A missing column group is named '<missing_<varname>>'
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; '<undefined>'}, {'a', 'b'}), ...
%!            [1; 2; 3; 4], 'VariableNames', {'g', 'v'});
%! P = pivot (T, 'Columns', 'g', 'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'a', 'b', '<missing_g>'});
%! assert_equal (P{:, :}, [3, 3, 4]);
## 'IncludeEmptyGroups' adds unused categorical categories as empty groups
%!test
%! T = table (setcats (categorical ({'a'; 'a'; 'b'}), {'a', 'b', 'c'}), ...
%!            categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'g', 'h'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'IncludeEmptyGroups', true);
%! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'; 'c'});
%! assert_equal (P{:, {'x', 'y'}}, [1, 1; 1, 0; 0, 0]);
## 'IncludeTotals' appends an 'Overall_<method>' marginal row and column
%!test
%! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ...
%!            categorical ({'x'; 'y'; 'x'; 'y'}), [1; 2; 3; 9], ...
%!            'VariableNames', {'g', 'h', 'v'});
%! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ...
%!            'Method', 'sum', 'IncludeTotals', true);
%! assert_equal (P.Properties.VariableNames, {'g', 'x', 'y', 'Overall_sum'});
%! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'; 'Overall_sum'});
%! assert_equal (P{:, {'x', 'y', 'Overall_sum'}}, [1, 2, 3; 3, 9, 12; 4, 11, 15]);
%!error <table.pivot: specify at least one of 'Columns' or 'Rows'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}))
%!error <table.pivot: 'DataVariable' must specify a single variable.> ...
%! pivot (table ([1; 2], [3; 4], [5; 6], 'VariableNames', {'g', 'x', 'y'}), 'Rows', 'g', 'DataVariable', {'x', 'y'})
%!error <table.pivot: unknown Method 'bogus'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'Method', 'bogus')
%!error <table.pivot: Method 'sum' requires a 'DataVariable'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'Method', 'sum')
%!error <table.pivot: Method 'none' allows at most one value per cell, but a group holds several; specify an aggregating method.> ...
%! pivot (table (categorical ({'a'; 'a'}), categorical ({'x'; 'x'}), [1; 2], 'VariableNames', {'g', 'h', 'v'}), 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', 'Method', 'none')
%!error <table.pivot: 'IncludeTotals' is not supported with Method 'none'.> ...
%! pivot (table (categorical ({'a'; 'b'}), [1; 2], 'VariableNames', {'g', 'v'}), 'Rows', 'g', 'DataVariable', 'v', 'Method', 'none', 'IncludeTotals', true)
%!error <table.pivot: 'RowLabelPlacement' must be 'variable' or 'rownames'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'RowLabelPlacement', 'foo')
%!error <table.pivot: 'IncludeMissingGroups' must be a logical scalar.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'IncludeMissingGroups', 'yes')
%!error <table.pivot: 'OutputFormat' must be 'flat' or 'nested'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'OutputFormat', 'bogus')
## 'OutputFormat','nested' nests multiple Columns variables into nested tables
%!test
%! r = [1; 1; 2; 2; 1; 2];
%! c1 = categorical ({'A'; 'A'; 'B'; 'B'; 'A'; 'B'});
%! c2 = categorical ({'X'; 'Y'; 'X'; 'Y'; 'X'; 'Y'});
%! v = [10; 20; 30; 40; 50; 60];
%! T = table (r, c1, c2, v, 'VariableNames', {'r', 'c1', 'c2', 'v'});
%! P = pivot (T, 'Rows', 'r', 'Columns', {'c1', 'c2'}, 'DataVariable', 'v', ...
%!            'Method', 'sum', 'OutputFormat', 'nested');
%! assert_equal (P.Properties.VariableNames, {'r', 'A', 'B'});
%! assert_equal (isa (P.A, 'table'), true);
%! assert_equal (P.A.Properties.VariableNames, {'X', 'Y'});
%! assert_equal (P.A.X, [60; 0]);
%! assert_equal (P.A.Y, [20; 0]);
%! assert_equal (P.B.X, [0; 30]);
%! assert_equal (P.B.Y, [0; 100]);
## Nested with a single Columns variable collapses to flat columns
%!test
%! r = [1; 1; 2];
%! c = categorical ({'A'; 'B'; 'A'});
%! v = [10; 20; 30];
%! T = table (r, c, v, 'VariableNames', {'r', 'c', 'v'});
%! P = pivot (T, 'Rows', 'r', 'Columns', 'c', 'DataVariable', 'v', ...
%!            'Method', 'sum', 'OutputFormat', 'nested');
%! assert_equal (P.Properties.VariableNames, {'r', 'A', 'B'});
%! assert_equal (isa (P.A, 'double'), true);
## Nested output with IncludeTotals keeps a flat Overall_<method> column
%!test
%! r = [1; 1; 2; 2; 1; 2];
%! c1 = categorical ({'A'; 'A'; 'B'; 'B'; 'A'; 'B'});
%! c2 = categorical ({'X'; 'Y'; 'X'; 'Y'; 'X'; 'Y'});
%! v = [10; 20; 30; 40; 50; 60];
%! T = table (r, c1, c2, v, 'VariableNames', {'r', 'c1', 'c2', 'v'});
%! P = pivot (T, 'Rows', 'r', 'Columns', {'c1', 'c2'}, 'DataVariable', 'v', ...
%!            'Method', 'sum', 'OutputFormat', 'nested', 'IncludeTotals', true);
%! assert_equal (any (strcmp (P.Properties.VariableNames, 'Overall_sum')), true);
%! assert_equal (P.Overall_sum, [80; 130; 210]);
%!error <table.pivot: 'IncludedEdge' must be 'left' or 'right'.> ...
%! pivot (table ([1; 2], 'VariableNames', {'g'}), 'Rows', 'g', 'RowsBinMethod', 2, 'IncludedEdge', 'mid')
%!error <table.pivot: binning is not supported for grouping variable 'g' of type 'categorical'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'RowsBinMethod', 5)
%!error <table.pivot: unrecognised option 'Wibble'.> ...
%! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', 'Wibble', 1)

## 'RowsBinMethod' bins a numeric row grouping variable by bin interval
%!test
%! T = table ([1; 5; 1; 9; 5], [1; 1; 2; 2; 3], [10; 20; 30; 40; 50], ...
%!            'VariableNames', {'r', 'c', 'v'});
%! P = pivot (T, 'Rows', 'r', 'Columns', 'c', 'RowsBinMethod', [0 4 10]);
%! assert_equal (iscategorical (P.disc_r), true);
%! assert_equal (cellstr (P.disc_r), {'[0, 4)'; '[4, 10]'});
%! assert_equal (P{:, 2:end}, [1, 1, 0; 1, 1, 1]);
## 'ColumnsBinMethod' names the output variables after the column bins
%!test
%! T = table ([1; 1; 2; 2; 3], [1; 5; 1; 9; 5], [10; 20; 30; 40; 50], ...
%!            'VariableNames', {'c', 'r', 'v'});
%! P = pivot (T, 'Rows', 'c', 'Columns', 'r', 'ColumnsBinMethod', 2, ...
%!            'DataVariable', 'v', 'Method', 'sum');
%! assert_equal (P.Properties.VariableNames, {'c', '[1, 5)', '[5, 9]'});
%! assert_equal (P{:, 2:end}, [10, 20; 30, 40; 0, 50]);

################################################################################
##                       **    Auxiliary Methods    **                        ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'horzcat'          'iscolumn'         'isempty'          'ismatrix'        ##
## 'isrow'            'isscalar'         'istable'          'isvector'        ##
## 'length'           'ndims'            'numel'            'repelem'         ##
## 'repmat'           'size'             'squeeze'          'vertcat'         ##
##                                                                            ##
################################################################################

%!assert_equal (iscolumn (table ([1; 2; 3])), true)
%!assert_equal (iscolumn (table ([1, 2, 3])), true)
%!assert_equal (iscolumn (table (1, 2, 3)), false)
%!assert_equal (isempty (table ()), true)
%!assert_equal (isempty (table ([])), true)
%!assert_equal (isempty (table (1)), false)
%!assert_equal (ismatrix (table ([])), true)
%!assert_equal (ismatrix (table (1)), true)
%!assert_equal (isrow (table ([1, 2, 3])), true)
%!assert_equal (isrow (table (1, 2, 3)), true)
%!assert_equal (isrow (table ([1; 2; 3])), false)
%!assert_equal (isscalar (table (1)), true)
%!assert_equal (isscalar (table ([1, 2, 3])), true)
%!assert_equal (isscalar (table (1, 2, 3)), false)
%!assert_equal (isscalar (table ([1; 2; 3])), false)
%!assert_equal (istable (table ()), true)
%!assert_equal (isvector (table (1)), true)
%!assert_equal (isvector (table (1, 2, 3)), true)
%!assert_equal (isvector (table ([1, 2, 3])), true)
%!assert_equal (isvector (table ([1; 2; 3])), true)
%!assert_equal (isvector (table ([1; 2], {'a'; 'b'})), false)
%!assert_equal (length (table (1)), 1)
%!assert_equal (length (table ([1; 2; 3])), 3)
%!assert_equal (length (table ([1, 2, 3])), 1)
%!assert_equal (length (table ([1; 2; 3], [2; 3; 4])), 3)
%!assert_equal (length (table ([1; 2], [2; 3], [3; 4], [4; 5])), 4)
%!assert_equal (ndims (table ()), 2)
%!assert_equal (ndims (table ([])), 2)
%!assert_equal (ndims (table ([1, 2, 3, 4])), 2)
%!assert_equal (ndims (table ([1; 2], {'a'; 'b'})), 2)
## 'numel' returns rows times variables, i.e. prod (size (tbl))
%!assert_equal (numel (table ()), 0)
%!assert_equal (numel (table (1)), 1)
%!assert_equal (numel (table (1, 2, 3, 4)), 4)
%!assert_equal (numel (table ([1; 2; 3], [4; 5; 6])), 6)
%!assert_equal (numel (table (1, 2, 3, 4)), prod (size (table (1, 2, 3, 4))))

## 'horzcat' combines variables of equal-height tables
%!test
%! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! T2 = table ([10; 20; 30], 'VariableNames', {'z'});
%! H = horzcat (T1, T2);
%! assert_equal (size (H), [3, 3]);
%! assert_equal (H.Properties.VariableNames, {'x', 'y', 'z'});
%! assert_equal (H.x, [1; 2; 3]);
%! assert_equal (H.z, [10; 20; 30]);
%!test  # '[ , ]' operator dispatches to horzcat
%! T1 = table ([1; 2; 3], 'VariableNames', {'x'});
%! T2 = table ([10; 20; 30], 'VariableNames', {'z'});
%! H = [T1, T2];
%! assert_equal (size (H), [3, 2]);
%! assert_equal (H.Properties.VariableNames, {'x', 'z'});
%!test  # row names of an input table are carried over
%! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'});
%! T2 = table ([3; 4], 'VariableNames', {'y'});
%! H = horzcat (T1, T2);
%! assert_equal (H.Properties.VariableNames, {'x', 'y'});
%! assert_equal (H.Properties.RowNames, {'r1'; 'r2'});
%!test  # multiple row-named inputs are aligned by row name, not position
%! T1 = table ([1; 2; 3], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2', 'r3'});
%! T2 = table ([20; 30; 10], 'VariableNames', {'y'}, 'RowNames', {'r2', 'r3', 'r1'});
%! T3 = table ([300; 100; 200], 'VariableNames', {'z'}, 'RowNames', {'r3', 'r1', 'r2'});
%! H = horzcat (T1, T2, T3);
%! assert_equal (H.Properties.RowNames, {'r1'; 'r2'; 'r3'});
%! assert_equal (H.x, [1; 2; 3]);
%! assert_equal (H.y, [10; 20; 30]);
%! assert_equal (H.z, [100; 200; 300]);
## Test input validation for 'horzcat' method
%!error <table.horzcat: all inputs must be tables.> ...
%! horzcat (table (1), 5);
%!error <table.horzcat: all input tables must have unique variable names.> ...
%! horzcat (table (1, 'VariableNames', {'x'}), ...
%!          table (2, 'VariableNames', {'x'}));
%!error <table.horzcat: all input tables must have the same height.> ...
%! horzcat (table ([1; 2], 'VariableNames', {'x'}), ...
%!          table (3, 'VariableNames', {'y'}));
%!error <table.horzcat: input tables must have identical RowNames.> ...
%! horzcat (table (1, 'VariableNames', {'x'}, 'RowNames', {'r1'}), ...
%!          table (2, 'VariableNames', {'y'}, 'RowNames', {'q1'}));

## 'vertcat' stacks rows of tables sharing variable names
%!test
%! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! T2 = table ([4; 5], {'d'; 'e'}, 'VariableNames', {'x', 'y'});
%! V = [T1; T2];
%! assert_equal (size (V), [5, 2]);
%! assert_equal (V.x, [1; 2; 3; 4; 5]);
%! assert_equal (V.y, {'a'; 'b'; 'c'; 'd'; 'e'});
%!test  # variables are matched by name, not position
%! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! T2 = table ({'f'}, 6, 'VariableNames', {'y', 'x'});
%! V = vertcat (T1, T2);
%! assert_equal (V.x, [1; 2; 3; 6]);
%! assert_equal (V.y, {'a'; 'b'; 'c'; 'f'});
%!test  # two row-named inputs concatenate, keeping their unique row names
%! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'});
%! T2 = table ([3; 4], 'VariableNames', {'x'}, 'RowNames', {'r3', 'r4'});
%! V = [T1; T2];
%! assert_equal (V.Properties.RowNames, {'r1'; 'r2'; 'r3'; 'r4'});
%! assert_equal (V.x, [1; 2; 3; 4]);
%!test  # rows from an unnamed input get default 'Row<pos>' names by position
%! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'});
%! T2 = table ([3; 4; 5], 'VariableNames', {'x'});
%! V = [T1; T2];
%! assert_equal (V.Properties.RowNames, {'r1'; 'r2'; 'Row3'; 'Row4'; 'Row5'});
%! assert_equal (V.x, [1; 2; 3; 4; 5]);
%!test  # default 'Row<pos>' numbering tracks the output position
%! T1 = table ([1; 2; 3], 'VariableNames', {'x'});
%! T2 = table ([4; 5], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'});
%! V = [T1; T2];
%! assert_equal (V.Properties.RowNames, {'Row1'; 'Row2'; 'Row3'; 'r1'; 'r2'});
%! assert_equal (V.x, [1; 2; 3; 4; 5]);
%!test  # object-typed variables retain their class through concatenation
%! T1 = table (datetime (2020, 1, [1; 2]), categorical ({'a'; 'b'}), ...
%!             string ({'p'; 'q'}), 'VariableNames', {'d', 'c', 's'});
%! T2 = table (datetime (2020, 1, [3; 4]), categorical ({'b'; 'a'}), ...
%!             string ({'r'; 's'}), 'VariableNames', {'d', 'c', 's'});
%! V = vertcat (T1, T2);
%! assert_equal (height (V), 4);
%! assert_equal (class (V.d), 'datetime');
%! assert_equal (class (V.c), 'categorical');
%! assert_equal (class (V.s), 'string');
%! assert_equal (cellstr (V.c), {'a'; 'b'; 'b'; 'a'});
%! assert_equal (cellstr (V.s), {'p'; 'q'; 'r'; 's'});
## Test input validation for 'vertcat' method
%!error <table.vertcat: all inputs must be tables.> ...
%! vertcat (table (1), 5);
%!error <table.vertcat: input tables must have identical variable names.> ...
%! vertcat (table (1, 'VariableNames', {'x'}), ...
%!          table (2, 'VariableNames', {'y'}));
%!error <table.vertcat: all input tables must have unique row names.> ...
%! vertcat (table (1, 'VariableNames', {'x'}, 'RowNames', {'r1'}), ...
%!          table (2, 'VariableNames', {'x'}, 'RowNames', {'r1'}));

## 'repelem' replicates rows and variables in place
%!test  # rows replicated consecutively (1 1 2 2 3 3)
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! R = repelem (T, 2, 1);
%! assert_equal (size (R), [6, 2]);
%! assert_equal (R.x, [1; 1; 2; 2; 3; 3]);
%! assert_equal (R.y, {'a'; 'a'; 'b'; 'b'; 'c'; 'c'});
%!test  # columns replicated; copied variables get a '_N' suffix
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! R = repelem (T, 1, 2);
%! assert_equal (size (R), [3, 4]);
%! assert_equal (R.Properties.VariableNames, {'x', 'x_1', 'y', 'y_1'});
%! assert_equal (R.x, [1; 2; 3]);
%! assert_equal (R.x_1, [1; 2; 3]);
%!test  # a scalar count applies to both dimensions
%! T = table ([1; 2; 3], 'VariableNames', {'x'});
%! R = repelem (T, 2);
%! assert_equal (size (R), [6, 2]);
%! assert_equal (R.x, [1; 1; 2; 2; 3; 3]);
%! assert_equal (R.Properties.VariableNames, {'x', 'x_1'});
%!test  # repelem (T, 1, 1) leaves the table unchanged
%! T = table ([1; 2; 3], 'VariableNames', {'x'});
%! assert_equal (isequal (repelem (T, 1, 1), T), true);
## Test input validation for 'repelem' method
%!error <table.repelem: too few input arguments for table input.> ...
%! repelem (table (1));
%!error <table.repelem: only 2 dimensions are supported for tables.> ...
%! repelem (table (1), 1, 2, 3);
%!error <table.repelem: SZ must be a positive integer.> ...
%! repelem (table (1), 1.5);
%!error <table.repelem: SZ must be a positive integer.> ...
%! repelem (table (1), 0);
%!error <table.repelem: ROWS must be a positive integer.> ...
%! repelem (table (1), 1.5, 1);
%!error <table.repelem: COLUMNS must be a positive integer.> ...
%! repelem (table (1), 1, 0);
%!error <table.repelem: COLUMNS must be a positive integer.> ...
%! repelem (table (1), 2, 1.5);

## 'repmat' tiles whole rows and variables of a table
%!test  # rows tiled as a block (1 2 3 1 2 3)
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! M = repmat (T, 2, 1);
%! assert_equal (size (M), [6, 2]);
%! assert_equal (M.x, [1; 2; 3; 1; 2; 3]);
%! assert_equal (M.y, {'a'; 'b'; 'c'; 'a'; 'b'; 'c'});
%!test  # columns tiled; the variable block is repeated then suffixed
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! M = repmat (T, 1, 2);
%! assert_equal (size (M), [3, 4]);
%! assert_equal (M.Properties.VariableNames, {'x', 'y', 'x_1', 'y_1'});
%!test  # a scalar count applies to both dimensions
%! T = table ([1; 2], 'VariableNames', {'x'});
%! M = repmat (T, 2);
%! assert_equal (size (M), [4, 2]);
%! assert_equal (M.x, [1; 2; 1; 2]);
%!test  # repmat (T, 1, 1) leaves the table unchanged
%! T = table ([1; 2; 3], 'VariableNames', {'x'});
%! assert_equal (isequal (repmat (T, 1, 1), T), true);
## Test input validation for 'repmat' method
%!error <table.repmat: too few input arguments for table input.> ...
%! repmat (table (1));
%!error <table.repmat: only 2 dimensions are supported for tables.> ...
%! repmat (table (1), 1, 2, 3);
%!error <table.repmat: SZ must be a positive integer.> ...
%! repmat (table (1), 1.5);
%!error <table.repmat: ROWS must be a positive integer.> ...
%! repmat (table (1), 1.5, 1);
%!error <table.repmat: COLUMNS must be a positive integer.> ...
%! repmat (table (1), 1, 0);
%!error <table.repmat: COLUMNS must be a positive integer.> ...
%! repmat (table (1), 2, 1.5);

## 'size' returns [height, width] and supports dimension arguments
%!test
%! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! assert_equal (size (T), [3, 2]);
%! assert_equal (size (T, 1), 3);
%! assert_equal (size (T, 2), 2);
%! assert_equal (size (T, 3), 1);
%! assert_equal (size (T, [1, 2]), [3, 2]);
%! assert_equal (size (T, [2, 1]), [2, 3]);
%! assert_equal (size (T, [2, 3]), [2, 1]);
%!test  # multiple output forms
%! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'});
%! [r, c] = size (T);
%! assert_equal ([r, c], [3, 2]);
%! [r, c, d] = size (T);
%! assert_equal ([r, c, d], [3, 2, 1]);

## 'squeeze' is a no-op for tables (always two-dimensional)
%!test
%! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'});
%! assert_equal (isequal (squeeze (T), T), true);
%!assert_equal (isequal (squeeze (table ()), table ()), true)

################################################################################
##                       **    Forbidden Methods    **                        ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'repelems'         'reshape'          'resize'           'shiftdims'       ##
## 'vec'                                                                      ##
##                                                                            ##
################################################################################

## These methods are unsupported by design and only emit an error
%!error <Function 'repelems' is not supported for tables> ...
%! repelems (table (1));
%!error <Function 'reshape' is not supported for tables> ...
%! reshape (table (1), 1, 1);
%!error <Function 'resize' is not supported for tables> ...
%! resize (table (1), 1);
%!error <Function 'shiftdims' is not supported for tables> ...
%! shiftdims (table (1), 1);
%!error <Function 'vec' is not supported for tables> ...
%! vec (table (1));

################################################################################
##                  ** Reference and Assignment Operations **                 ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'end'              'subsref'          'subsasgn'                           ##
##                                                                            ##
################################################################################

## Test 'subref' and 'subsasgn' methods
%!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA
%! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"};
%! Age = [38; 43; 38; 40; 49];
%! Smoker = logical ([1; 0; 1; 0; 1]);
%! Height = [71; 69; 64; 67; 64];
%! Weight = [176; 163; 131; 133; 119];
%! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80];
%! T = table (Age, Smoker, Height, Weight, BloodPressure);
%!assert_equal (size (T(1,:)), [1, 5]);
%!assert_equal (class (T(1,:)), "table");
%!assert_equal (size (T{1,:}), [1, 6]);
%!assert_equal (T{1,:}, [38, 1, 71, 176, 124, 93]);
%!assert_equal (T{:,2}, logical([1; 0; 1; 0; 1]));
%!assert_equal (T{:,"Smoker"}, logical([1; 0; 1; 0; 1]));
%!assert_equal (T{:,"Height"}, [71; 69; 64; 67; 64]);
%!assert_equal (size(T{:,"Smoker"}), [5, 1]);
%!assert_equal (size(T{:,"Height"}), [5, 1]);
%!test  # brace-indexing all-cell variables yields a cell array
%! Tc = table ({'a'; 'b'}, {'c'; 'd'}, 'VariableNames', {'x', 'y'});
%! assert_equal (Tc{:,:}, {'a', 'c'; 'b', 'd'});
%!error <table.subsref: cannot concatenate the table variables 'x' and 'y', because their types are double and cell.> ...
%! Tm = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); Tm{1,:};
%!error <table.subsref: cannot concatenate the table variables 'x' and 'y', because their types are double and cell.> ...
%! Tm = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); Tm{:,:};
%!assert_equal (T.Variables, [Age, Smoker, Height, Weight, BloodPressure]);
%!assert_equal (isempty (T.Properties.RowNames), true);
%!assert_equal (numel (T.Properties.VariableNames), 5);
%!test
%! T.Properties.Description = "text";
%! assert_equal (T.Properties.Description, "text");
%!assert_equal (size (T(:,{"Age", "Smoker"})), [5, 2]);
%!assert_equal (T{:,{"Age", "Smoker"}}, [Age, Smoker]);
%!test
%! T = table (Age, Smoker);
%! T.("29-May-2019 Blood Pressure Reading") = BloodPressure;
%! assert_equal (T.("29-May-2019 Blood Pressure Reading"), BloodPressure);
%!test
%! T = table (Age, Smoker);
%! T.Height = Height;
%! T.Weight = Weight;
%! assert_equal (size (T), [5, 4]);
%! assert_equal (T.Weight, Weight);
%! assert_equal (T.Weight, T{:,"Weight"});
%! assert_equal (T.Weight, T{:,4});
%!test
%! T.Weight(1) = 25;
%! assert_equal (T.Weight, [25;Weight(2:end)]);
%!test
%! T.Weight([1,3]) = 25;
%! assert_equal (T.Weight, [25;Weight(2);25;Weight(4:end)]);
%!test
%! T.Weight([1,3]) = 25;
%! assert_equal (T.Weight, [25;Weight(2);25;Weight(4:end)]);
%!test
%! T.Weight([1:3,5]) = 25;
%! assert_equal (T.Weight, [25;25;25;Weight(4);25]);
%!test
%! T = table (LastName, Age, Smoker, Height, Weight);
%! T.Properties.RowNames = string (LastName);
%!test
%! T = table (LastName, Age, Smoker, Height, Weight);
%! T.Properties.RowNames = 'LastName';
%!test
%! T = table (LastName, Age, Smoker, Height, Weight);
%! T.Properties.RowNames = {'LastName'};
%!test
%! T = table (LastName, Age, Smoker, Height, Weight);
%! T.Properties.RowNames = ['LastName'];
%!test
%! T = table (LastName, Age, Smoker, Height, Weight);
%! assert_equal (isempty (T.Row), true);
%! T.Properties.DimensionNames(1) = 'Patients';
%! assert_equal (isempty (T.Patients), true);
%!test
%! T.Properties.RowNames = "LastName";
%! assert_equal (isempty (T.Patients), false);
%! T.Properties.DimensionNames(2) = 'Data';
%! assert_equal (T.Data, [Age,Smoker,Height,Weight]);
%!test
%! T = table (Age, Smoker, Height, Weight, BloodPressure);
%! assert_equal (T{1, @isnumeric}, [38, 71, 176, 124, 93]);
%! assert_equal (T{1, vartype ("numeric")}, [38, 71, 176, 124, 93]);
%!test
%! T1 = table (LastName, Age);
%! assert_equal (T1{:, @iscellstr}, LastName);
%! assert_equal (T1{:, @isnumeric}, Age);
%! assert_equal (T1(:, @iscellstr).LastName, LastName)
%! assert_equal (T1(:, @isnumeric).Age, Age)
%!test
%! T1 = table (LastName, Age);
%! assert_equal (T1{:, @iscellstr}, LastName);
%! assert_equal (T1{:, @isnumeric}, Age);
%!test
%! T1 = table (LastName, Age, Height);
%! assert_equal (T1{:, @iscellstr}, LastName);
%! assert_equal (class (T1(:, @isnumeric)), "table");
%! assert_equal (size (T1(:, @isnumeric)), [5, 2]);

## Test "end" keyword
%!test
%! t = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'}, ...
%!                                       'RowNames', {'Row1', 'Row2', 'Row3'});
%! out = t(:,end);
%! assert_equal (out.Properties.VariableNames, {'C'});
%! assert_equal (out.Properties.RowNames, {'Row1'; 'Row2'; 'Row3'});
%! assert_equal (out.C, [7; 8; 9]);
%! out = t{:,end};
%! assert_equal (out, [7; 8; 9]);
%!test
%! t = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'}, ...
%!                                       'RowNames', {'Row1', 'Row2', 'Row3'});
%! out = t(end, :);
%! assert_equal (out.Properties.VariableNames, {'A', 'B', 'C'});
%! assert_equal (out.Properties.RowNames, {'Row3'});
%! assert_equal ([out.A, out.B, out.C], [3, 6, 9]);
%! out = t{end,:};
%! assert_equal (out, [3, 6, 9]);

## 'subsref' with '()' returns a sub-table for every row/variable reference type
%!test
%! T = table ([1;2;3], {'a';'b';'c'}, [10;20;30], 'VariableNames', {'x','y','z'});
%! assert_equal (class (T(2,:)), 'table');
%! assert_equal (T(2,:).x, 2);
%! assert_equal (size (T(1:2,[1 3])), [2, 2]);
%! assert_equal (T(1:2,[1 3]).Properties.VariableNames, {'x','z'});
%! assert_equal (height (T(logical ([1 0 1]),:)), 2);
%! assert_equal (T(:,logical ([1 0 1])).Properties.VariableNames, {'x','z'});
%! assert_equal (T(:,'y').y, {'a';'b';'c'});
%! assert_equal (T(:,{'x','z'}).Properties.VariableNames, {'x','z'});
%! assert_equal (T(:,@isnumeric).Properties.VariableNames, {'x','z'});
%! assert_equal (T(:,vartype ('numeric')).Properties.VariableNames, {'x','z'});
%! assert_equal (size (T(:,:)), [3, 3]);
%!test  # named row references require RowNames
%! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}, ...
%!            'RowNames', {'r1','r2','r3'});
%! assert_equal (T('r2',:).x, 2);
%! assert_equal (T({'r1','r3'},:).x, [1;3]);
%! assert_equal (T('r2',:).Properties.RowNames, {'r2'});

## Test input validation for 'subsref' with '()' indexing
%!error <table.subsref: '\(\)' indexing of table requires exactly two arguments.> ...
%! T = table ([1;2;3]); T(1);
%!error <table: no such variable in table: 'q'.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T(:,'q');
%!error <table: this table has no RowNames.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T('r1',:);
%!error <table: no such named row in table: 'r9'.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}, 'RowNames', {'r1','r2','r3'}); ...
%! T('r9',:);

## 'subsref' with '{}' extracts raw data from concatenable variables
%!test
%! T = table ([1;2;3], [10;20;30], [100;200;300], 'VariableNames', {'x','y','z'});
%! assert_equal (T{2,:}, [2, 20, 200]);
%! assert_equal (T{:,2}, [10;20;30]);
%! assert_equal (T{1:2,[1 3]}, [1, 100; 2, 200]);
%! assert_equal (T{:,'y'}, [10;20;30]);
%! assert_equal (T{:,@isnumeric}, [1 10 100; 2 20 200; 3 30 300]);

## Test input validation for 'subsref' with '{}' indexing
%!error <table.subsref: '{}' indexing of table requires exactly two arguments.> ...
%! T = table ([1;2;3]); T{1};

## 'subsref' with '.' returns variable values, properties, and dimension data
%!test
%! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'});
%! assert_equal (T.x, [1;2;3]);
%! assert_equal (T.y, {'a';'b';'c'});
%! assert_equal (class (T.Properties), 'struct');
%! assert_equal (T.Properties.VariableNames, {'x','y'});
%! assert_equal (isempty (T.Row), true);
%! assert_equal (T.Variables, {1, 'a'; 2, 'b'; 3, 'c'});
%!test  # '.' on the RowNames dimension name returns the row names
%! T = table ([1;2;3], 'VariableNames', {'x'}, 'RowNames', {'r1','r2','r3'});
%! assert_equal (T.Row, {'r1';'r2';'r3'});
%!test  # chained '.' references resolve left to right
%! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'});
%! assert_equal (T.x(2), 2);
%! assert_equal (T.Properties.VariableNames{1}, 'x');
%! assert_equal (T(2:3,:).x, [2;3]);

## Test input validation for 'subsref' with '.' indexing
%!error <table.subsref: '.' index argument must be a character vector.> ...
%! T = table ([1;2;3]); subsref (T, struct ('type', '.', 'subs', 5));
%!error <table: no such variable in table: 'nope'.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T.nope;

## 'subsasgn' with '()' writes numeric, cell, and table data element-wise
%!test  # numeric right-hand side
%! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'});
%! T(1:2,[1 2]) = [70 700; 80 800];
%! assert_equal (T.x, [70;80;3]);
%! assert_equal (T.z, [700;800;30]);
%!test  # cell right-hand side assigns element-wise (not broadcast)
%! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'});
%! T(1:2,1) = {7; 8};
%! assert_equal (T.x, [7;8;3]);
%! T(1:2,[1 2]) = {70 700; 80 800};
%! assert_equal (T.x, [70;80;3]);
%! assert_equal (T.z, [700;800;30]);
%!test  # cellstr variable keeps its contents through cell assignment
%! T = table ({'a';'b';'c'}, 'VariableNames', {'y'});
%! T(1:2,1) = {'P'; 'Q'};
%! assert_equal (T.y, {'P';'Q';'c'});
%!test  # table right-hand side assigns element-wise across variables
%! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'});
%! T2 = table ([77;88], {'P';'Q'}, 'VariableNames', {'x','y'});
%! T(1:2,[1 2]) = T2;
%! assert_equal (T.x, [77;88;3]);
%! assert_equal (T.y, {'P';'Q';'c'});
%!test  # single-element assignment
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T(1,1) = {99};
%! assert_equal (T.x, [99;2;3]);

## Test input validation for 'subsasgn' with '()' indexing
%!error <table.subsasgn: '\(\)' indexing of table requires exactly two arguments.> ...
%! T = table ([1;2;3]); T(1) = 5;
%!error <table.subsasgn: input data mismatch indexed dimensions.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T(1,1) = [9 9];
%!error <table.subsasgn: input data type mismatch indexed variable type.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T(1,1) = {[1 2 3]};

## 'subsasgn' with '{}' is not allowed for assigning values
%!error <table.subsasgn: '{}' invalid indexing for assigning values. Use '\(\)' instead.> ...
%! T = table ([1;2;3]); T{1,1} = 5;

## 'subsasgn' with '.' adds, replaces, and scalar-expands variables
%!test  # add a new variable
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.y = [4;5;6];
%! assert_equal (T.Properties.VariableNames, {'x','y'});
%! assert_equal (T.y, [4;5;6]);
%!test  # replace an existing variable
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.x = [7;8;9];
%! assert_equal (T.x, [7;8;9]);
%!test  # scalar right-hand side is expanded to the table height
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.x = 5;
%! assert_equal (T.x, [5;5;5]);
%!test  # chained assignment into a single variable
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.x(2) = 77;
%! assert_equal (T.x, [1;77;3]);

## Test input validation for 'subsasgn' with '.' indexing
%!error <table.subsasgn: '.' index argument must be a character vector.> ...
%! T = table ([1;2;3]); subsasgn (T, struct ('type', '.', 'subs', 5), 1);
%!error <table.subsasgn: input value and table height mismatch.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); T.y = [4;5];

## 'subsasgn' sets table-level Properties
%!test  # Description and UserData
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.Properties.Description = 'a table';
%! assert_equal (T.Properties.Description, 'a table');
%! T.Properties.UserData = {1, 'two', 3};
%! assert_equal (T.Properties.UserData, {1, 'two', 3});
%!test  # DimensionNames, full and indexed
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.Properties.DimensionNames = {'Rows', 'Vars'};
%! assert_equal (T.Properties.DimensionNames, {'Rows', 'Vars'});
%! T.Properties.DimensionNames(1) = 'Patients';
%! assert_equal (T.Properties.DimensionNames, {'Patients', 'Vars'});
%!test  # VariableNames, full and indexed
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'});
%! T.Properties.VariableNames = {'a', 'b'};
%! assert_equal (T.Properties.VariableNames, {'a', 'b'});
%! T.Properties.VariableNames(2) = {'B'};
%! assert_equal (T.Properties.VariableNames, {'a', 'B'});
%!test  # VariableDescriptions and VariableUnits, full and indexed
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'});
%! T.Properties.VariableDescriptions = {'first', 'second'};
%! assert_equal (T.Properties.VariableDescriptions, {'first', 'second'});
%! T.Properties.VariableDescriptions(1) = {'1st'};
%! assert_equal (T.Properties.VariableDescriptions, {'1st', 'second'});
%! T.Properties.VariableUnits = {'m', 's'};
%! assert_equal (T.Properties.VariableUnits, {'m', 's'});
%! T.Properties.VariableUnits(2) = {'kg'};
%! assert_equal (T.Properties.VariableUnits, {'m', 'kg'});
%!test  # VariableTypes converts the underlying data (whole table)
%! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'});
%! T.Properties.VariableTypes = {'single', 'int32'};
%! assert_equal (class (T.x), 'single');
%! assert_equal (class (T.z), 'int32');
%! assert_equal (T.x, single ([1;2;3]));
%! assert_equal (T.z, int32 ([10;20;30]));
%! assert_equal (T.Properties.VariableTypes, {'single', 'int32'});
%! assert_equal (T.Properties.VariableDescriptions, {'', ''});
%!test  # VariableTypes converts a single indexed variable
%! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'});
%! T.Properties.VariableTypes(2) = {'single'};
%! assert_equal (class (T.x), 'double');
%! assert_equal (class (T.z), 'single');
%! assert_equal (T.Properties.VariableTypes, {'double', 'single'});
%!test  # RowNames, set from an array and removed with []
%! T = table ([1;2;3], 'VariableNames', {'x'});
%! T.Properties.RowNames = {'r1', 'r2', 'r3'};
%! assert_equal (T.Properties.RowNames, {'r1';'r2';'r3'});
%! T.Properties.RowNames = {};
%! assert_equal (isempty (T.Properties.RowNames), true);

## Test input validation for 'subsasgn' Properties assignment
%!error <table.subsasgn: cannot assign new properties.> ...
%! T = table ([1;2;3]); subsasgn (T, substruct ('.', 'Properties'), 5);
%!error <table.subsasgn: Table description must be a character vector or string scalar.> ...
%! T = table ([1;2;3]); T.Properties.Description = 5;
%!error <table.subsasgn: VariableNames must be a cell array of character vectors or a string array matching the number of variables.> ...
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); ...
%! T.Properties.VariableNames = {'only_one'};
%!error <table.subsasgn: out of bound index for VariableNames.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); ...
%! T.Properties.VariableNames(5) = {'a'};
%!error <table.subsasgn: VariableTypes must be a cell array of character vectors or a string array matching the number of variables.> ...
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); ...
%! T.Properties.VariableTypes = {'double'};
%!error <table.subsasgn: the number of 'RowNames' must equal the number of rows.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); ...
%! T.Properties.RowNames = {'r1', 'r2'};
%!error <table.subsasgn: elements in 'RowNames' must be unique.> ...
%! T = table ([1;2;3], 'VariableNames', {'x'}); ...
%! T.Properties.RowNames = {'r1', 'r1', 'r2'};
%!test  # RowNames set from a variable with unique values moves and drops it
%! T = table ({'x'; 'y'; 'z'}, [1; 2; 3], 'VariableNames', {'G', 'V'});
%! T.Properties.RowNames = 'G';
%! assert_equal (T.Properties.RowNames, {'x'; 'y'; 'z'});
%! assert_equal (T.Properties.VariableNames, {'V'});
%!error <table.subsasgn: elements in 'RowNames' must be unique.> ...
%! T = table ({'x'; 'y'; 'x'}, [1; 2; 3], 'VariableNames', {'G', 'V'}); ...
%! T.Properties.RowNames = 'G';

## 'subsasgn' sets custom properties through Properties.CustomProperties
%!test  # table-scoped and variable-scoped custom properties
%! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'});
%! T = addprop (T, {'tp', 'vp'}, {'table', 'variable'});
%! T.Properties.CustomProperties.tp = 'meta';
%! assert_equal (T.Properties.CustomProperties.tp, 'meta');
%! T.Properties.CustomProperties.vp = [10 20];
%! assert_equal (T.Properties.CustomProperties.vp, [10 20]);
%! T.Properties.CustomProperties.vp(2) = 99;
%! assert_equal (T.Properties.CustomProperties.vp, [10 99]);

## Test input validation for 'subsasgn' custom property assignment
%!error <table.subsasgn: custom property 'nope' does not exist, use 'addprop' to add it.> ...
%! T = table ([1;2;3]); T.Properties.CustomProperties.nope = 5;
%!error <table.subsasgn: use 'addprop' to add a new custom property.> ...
%! T = table ([1;2;3]); T.Properties.CustomProperties = 5;

## Restore whatever default display formats the user had before this file ran.
%!test
%! datetimeFormatGuard ('restore');

## Test 'sortrows' -- the DIRECTION value is matched without regard to case
%!assert_equal (sortrows (table ([3; 1; 2]), 1, 'Descend').Var1', [3 2 1])
%!assert_equal (sortrows (table ([3; 1; 2]), 1, 'ASCEND').Var1', [1 2 3])
