## 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/>.

################################################################################
##                    ** Create String and Convert Type **                    ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'string'           'dispstrings'      'cellstr'          'cell'            ##
## 'char'             'double'                                                ##
##                                                                            ##
################################################################################

%!assert_equal (string, string (''))
%!assert_equal (string, string ({''}))
%!assert_equal (string (['a', 'b', 'c']), string ('abc'))
%!assert_equal (string (['a', 'b', 'c']), string ({'abc'}))
%!test
%! str = string (["a";"b";"c"]);
%! assert_equal (cellstr (str), {"a";"b";"c"});
## Constructing from a char vector preserves trailing whitespace (the core
## 'cellstr' would deblank it); MATLAB's 'string' preserves it too.
%!assert_equal (cellstr (string ('z ')), {'z '})
%!assert_equal (cellstr (string (' z ')), {' z '})
## Each row of a padded char matrix keeps its trailing whitespace.
%!test
%! str = string (['ab '; 'c  ']);
%! assert_equal (cellstr (str), {'ab '; 'c  '});
%! assert_equal (size (str), [2, 1]);
## size with no dimension named folds the trailing dimensions into the last
## output when fewer outputs are asked for than the array has dimensions, and
## pads with ones when more are asked for, as core Octave and MATLAB both do.
%!test
%! A = string (repmat ({'a'}, 2, 2, 2));
%! [r, c] = size (A);
%! assert_equal ([r, c], [2, 4]);
%!test
%! A = string (repmat ({'a'}, 2, 2));
%! [a, b, c, d] = size (A);
%! assert_equal ([a, b, c, d], [2, 2, 1, 1]);
%!test
%! str = string ({"a";"b";"c"});
%! assert_equal (cellstr (str), {"a";"b";"c"});
%!test
%! str = string ({"a";"";"c"});
%! tfM = ismissing (str);
%! assert_equal (cell (str), {"a";"";"c"});
%! assert_equal (tfM, logical ([0; 0; 0]));
%!test
%! str = string ([1 2 3 NaN 5]);
%! tfM = ismissing (str);
%! assert_equal (cellstr (str), {"1", "2", "3", "", "5"});
%! assert_equal (tfM, logical ([0 0 0 1 0]));
%!test
%! str = string (duration ([3,4,5; NaN,NaN,NaN]));
%! tfM = ismissing (str);
%! assert_equal (cellstr (str), {"03:04:05"; ""});
%! assert_equal (tfM, logical ([0; 1]));
%!test
%! str = string (calendarDuration ([3,4,5; NaN,NaN,NaN]));
%! tfM = ismissing (str);
%! assert_equal (cellstr (str), {"3y 4mo 5d"; ""});
%! assert_equal (tfM, logical ([0; 1]));
%!test
%! str = string (repmat ('a', 3, 4, 5));
%! assert_equal (str, repmat (string ('aaaa'), 3, 5));
%! assert_equal (size (str), [3, 5]);
%!error<string: cell array must explicitly contain scalar elements.> ...
%! string ({[1 2], false})
%!error<string: cell array contains unsupported types.> string ({"d", @(x)x});
%!error<string: unsupported input type: 'function_handle'> string (@(x)x);
%!error<string: unsupported input type: 'struct'> string (struct ())

%!assert_equal (dispstrings (string ('a')), {'"a"'})
%!assert_equal (dispstrings (string ("a")), {'"a"'})
%!assert_equal (dispstrings (string ('a\na')), {'"a\na"'})
%!assert_equal (dispstrings (string (NaN)), {'<missing>'})

%!assert_equal (cellstr (string ('a')), {'a'})
%!assert_equal (cellstr (string ("a")), {'a'})
%!assert_equal (cellstr (string ('a\na')), {'a\na'})
%!assert_equal (cellstr (string (NaN)), {''})

%!test
%! str = string ({'3', 4, 'asd', [], NaN, hours(3)});
%! out = {3, 4, 'asd', [], [], '3 hr'};
%! assert_equal (cell (str), out);
%!assert_equal (cell ([missing, string('')]), {[], ''})
%!assert_equal (cell ([NaN, string('')]), {[], ''})
%!assert_equal (cell (['', string('')]), {'', ''})
%!assert_equal (cell ([string, string('')]), {'', ''})

%!assert_equal (char (string), '');
%!assert_equal (char (string ('')), '');
%!assert_equal (char (string ({'a', 's', 'd'})), ['a'; 's'; 'd'])

%!assert_equal (double (string ({'a', 's', 'd'})), [NaN, NaN, NaN])
%!assert_equal (double (string ({'a'; 's'; 'd'})), [NaN; NaN; NaN])
%!assert_equal (double (string ([1, 2, NaN])), [1, 2, NaN])
%!assert_equal (double (string ({1, 2, 'a', missing})), [1, 2, NaN, NaN])
%!assert_equal (double ([string([1, 2]) 'a', missing]), [1, 2, NaN, NaN])

################################################################################
##                         ** Summary Information **                          ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'size'             'ndims'            'numel'            'length'          ##
## 'strlength'        'count'            'keyHash'                            ##
##                                                                            ##
################################################################################

%!assert_equal (size (string), [1, 1])
%!assert_equal (size (char (string)), [0, 0])
%!assert_equal (size (char (string (''))), [0, 0])
%!assert_equal (size (string ('')), [1, 1])
%!assert_equal (size (string ([])), [0, 0])
%!assert_equal (size (string (ones (2, 0, 3, 4))), [2, 0, 3, 4])
%!assert_equal (size (string (ones (2, 4))), [2, 4])
%!assert_equal (size (string (ones (2, 4, 1, 1))), [2, 4])
%!assert_equal (size (string (ones (2, 4, 1, 0))), [2, 4, 1, 0])
%!assert_equal (size (string ('a')), [1, 1])
%!assert_equal (size (string ('abcd')), [1, 1])
%!assert_equal (size (string (char (0, 1))), [2, 1])
%!assert_equal (size (string (char (1, 0))), [2, 1])
%!assert_equal (size (string (repmat ('a', 3, 4))), [3, 1])

%!assert_equal (ndims (string), 2)
%!assert_equal (ndims (string ([])), 2)
%!assert_equal (ndims (string (ones (2, 0, 3, 4))), 4)
%!assert_equal (ndims (string (ones (2, 4))), 2)
%!assert_equal (ndims (string (zeros (6, 3))), 2)

%!assert_equal (numel (string), 1)
%!assert_equal (numel (string ([])), 0)
%!assert_equal (numel (string (ones (2, 0, 3, 4))), 0)
%!assert_equal (numel (string (ones (2, 4))), 8)
%!assert_equal (numel (string (zeros (6, 3))), 18)

%!assert_equal (length (string ('1')), 1)
%!assert_equal (length (string ('asd')), 1)
%!assert_equal (length (string (1:5)), 5)
%!assert_equal (length (string (repmat ('a', 2, 3, 4))), 4)
%!assert_equal (length (string (ones (5, 3, 4), 0, 0)), 5)
%!assert_equal (length (string (ones (2, 10, 4), 0, 0)), 10)

%!test
%! str = string ({'asd', '', missing, 'a\na', "a\na"});
%! assert_equal (strlength (str), [3, 0, NaN, 4, 3]);
%!assert_equal (strlength (string (char (0, 1))), [1; 1])
%!assert_equal (strlength (string (char (1, 0))), [1; 1])
%!test
%! str = string ('Hello, World');
%! assert_equal (strlength (str), 12);
%!test
%! str = string ({'Amis', 'Chekhov', 'Joyce'; 'Stein', '', 'Proust'});
%! assert_equal (strlength (str), [4, 7, 5; 5, 0, 6]);

%!assert_equal (count (string ({'paired with red shoes'}), 'red'), 2)
%!test
%! str = string ({'red green red red blue blue green'; ...
%!                'green red blue green green blue'});
%! assert_equal (count (str, 'red'), [3; 1]);
%!test
%! str = string ({'red green red red blue blue green'; ...
%!                'green red blue green green blue'});
%! assert_equal (count (str, string ('blue')), [2; 2]);
%!test
%! str = string ({'red green blue'; 'green red blue green blue'});
%! assert_equal (count (str, {'red', 'blue'}), [2; 3]);
%!test
%! str = string ({'Edgar Allan Poe', 'Louisa May Alcott'});
%! assert_equal (count (str, 'E', 'IgnoreCase',true), [2, 0]);
%!test
%! str = [string({'Andreas', 'Thomas'}), missing, string('Collin')];
%! assert_equal (count (str, 'a'), [1, 1, 0, 0]);
%!test
%! str = [string({'Andreas', 'Thomas'}), missing, string('Collin')];
%! assert_equal (count (str, 'a', 'IgnoreCase', true), [2, 1, 0, 0]);
%!error<string.count: PATTERN is required.> count (string)
%!error<string.count: PATTERN must be a character vector, a string array, or cell array of character vectors.> ...
%! count (string ('a'), 1)
%!error<string.count: 'IgnoreCase' must be a logical scalar.> ...
%! count (string ('a'), 'a', 'IgnoreCase', 1)
%!error<string.count: 'IgnoreCase' must be a logical scalar.> ...
%! count (string ('a'), 'a', 'IgnoreCase', [true, true])
%!error<string.count: unrecognized input argument.> ...
%! count (string ('a'), 'a', 'whatever')

%!assert_equal (keyHash (string), uint64 (11885290805827245494))
%!assert_equal (keyHash (string (1:5)), uint64 (123959226970788855))
%!assert_equal (keyHash (string ({'A', 'B'})), uint64 (10138706323013565148))
%!assert_equal (keyHash (string ({'a', 'a'})), uint64 (16542844838776277277))
%!assert_equal (keyHash (string (['A', 'B'])), uint64 (15822693213880664227))
%!assert_equal (keyHash (string (['a', 'a'])), uint64 (14201447922909418088))
%!assert_equal (keyHash (string ('AB')), uint64 (15822693213880664227))
%!assert_equal (keyHash (string ('aa')), uint64 (14201447922909418088))
%!assert_equal (keyHash (string ({'A'; 'B'})), uint64 (12731811233575238676))
%!assert_equal (keyHash (string ({'a'; 'a'})), uint64 (16082861040433618837))
%!assert_equal (keyHash (string (['A'; 'B'])), uint64 (12731811233575238676))
%!assert_equal (keyHash (string (['a'; 'a'])), uint64 (16082861040433618837))
%!assert_equal (keyHash (string ('')), uint64 (11885290805827245494))
%!assert_equal (keyHash (string ({''})), uint64 (11885290805827245494))
%!assert_equal (keyHash (string (NaN)), uint64 (11885291905338873705))
%!assert_equal (keyHash (string (missing)), uint64 (11885291905338873705))
%!assert_equal (keyHash (string ([missing, missing])), uint64 (2773158418737685269))
%!assert_equal (keyHash (string ([missing; missing])), uint64 (3067086112843206157))
%!assert_equal (keyHash (string ('1')), uint64 (15098403913664737211))
%!test
%! base_key = uint64 (14695981039346656037);
%! key1 = keyHash (string ('1'), base_key);
%! key2 = keyHash (string ('1'));
%! assert_equal (key1, key2);
%!test
%! base_key = uint64 (2342124352342344234);
%! key = keyHash (string ('1'), base_key);
%! assert_equal (key, uint64 (325711939436065646));
%!error<string.keyHash: BASE must be a UINT64 scalar.> ...
%! keyHash (string (1:5), uint64 ([1, 2]))
%!error<string.keyHash: BASE must be a UINT64 scalar.> ...
%! keyHash (string (1:5), 2231107818551636405)

################################################################################
##                          ** Query Operations **                            ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'contains'         'endsWith'         'startsWith'       'matches'         ##
## 'iscolumn'         'isempty'          'ismatrix'         'ismember'        ##
## 'ismissing'        'isrow'            'isscalar'         'issorted'        ##
## 'issortedrows'     'isstring'         'isvector'                           ##
##                                                                            ##
################################################################################

%!test
%! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'});
%! TF = contains (str, 'Paul');
%! assert_equal (TF, [false, true, true]);
%!test
%! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'});
%! TF = contains (str, string ('Paul'));
%! assert_equal (TF, [false, true, true]);
%!test
%! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'});
%! TF = contains (str, 'paul');
%! assert_equal (TF, [false, false, false]);
%!test
%! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'});
%! TF = contains (str, 'paul', 'IgnoreCase', true);
%! assert_equal (TF, [false, true, true]);
%!test
%! str = string ({'Anne', 'Elizabeth', 'Marianne', '"Tracy'});
%! TF = contains (str, 'anne', 'IgnoreCase', true);
%! assert_equal (TF, [true, false, true, false]);
%!test
%! str = string ({'Anne', 'Elizabeth', 'Marianne', '"Tracy'});
%! TF = contains (str, {'anne', 'Anne'}, 'IgnoreCase', false);
%! assert_equal (TF, [true, false, true, false]);
%!error<string.contains: PATTERN is required.> contains (string)
%!error<string.contains: PATTERN must be a character vector, a string array, or cell array of character vectors.> ...
%! contains (string ('a'), 1)
%!error<string.contains: 'IgnoreCase' must be a logical scalar.> ...
%! contains (string ('a'), 'a', 'IgnoreCase', 1)
%!error<string.contains: 'IgnoreCase' must be a logical scalar.> ...
%! contains (string ('a'), 'a', 'IgnoreCase', [true, true])
%!error<string.contains: unrecognized input argument.> ...
%! contains (string ('a'), 'a', 'whatever')

%!test
%! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'});
%! TF = endsWith (str, 'odt');
%! assert_equal (TF, [false, true, false, false]);
%!test
%! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'});
%! TF = endsWith (str, string ('odt'));
%! assert_equal (TF, [false, true, false, false]);
%!test
%! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'});
%! TF = endsWith (str, {'odt', 'ods'});
%! assert_equal (TF, [false, true, false, true]);
%!test
%! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'});
%! TF = endsWith (str, {'odt', 'ODS'});
%! assert_equal (TF, [false, true, false, false]);
%!test
%! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'});
%! TF = endsWith (str, {'odt', 'ODS'}, 'IgnoreCase', true);
%! assert_equal (TF, [false, true, false, true]);
%!error<string.endsWith: PATTERN is required.> endsWith (string)
%!error<string.endsWith: PATTERN must be a character vector, a string array, or cell array of character vectors.> ...
%! endsWith (string ('a'), 1)
%!error<string.endsWith: 'IgnoreCase' must be a logical scalar.> ...
%! endsWith (string ('a'), 'a', 'IgnoreCase', 1)
%!error<string.endsWith: 'IgnoreCase' must be a logical scalar.> ...
%! endsWith (string ('a'), 'a', 'IgnoreCase', [true, true])
%!error<string.endsWith: unrecognized input argument.> ...
%! endsWith (string ('a'), 'a', 'whatever')

%!test
%! str = string ({'ball', 'base', 'Balkan', 'bison'});
%! TF = startsWith (str, 'ba');
%! assert_equal (TF, [true, true, false, false]);
%!test
%! str = string ({'ball', 'base', 'Balkan', 'bison'});
%! TF = startsWith (str, string ('ba'));
%! assert_equal (TF, [true, true, false, false]);
%!test
%! str = string ({'ball', 'base', 'Balkan', 'bison'});
%! TF = startsWith (str, {'ba', 'bi'});
%! assert_equal (TF, [true, true, false, true]);
%!test
%! str = string ({'ball', 'base', 'Balkan', 'bison'});
%! TF = startsWith (str, {'ba', 'Bi'});
%! assert_equal (TF, [true, true, false, false]);
%!test
%! str = string ({'ball', 'base', 'Balkan', 'bison'});
%! TF = startsWith (str, {'ba', 'Bi'}, 'IgnoreCase', true);
%! assert_equal (TF, [true, true, true, true]);
%!error<string.startsWith: PATTERN is required.> startsWith (string)
%!error<string.startsWith: PATTERN must be a character vector, a string array, or cell array of character vectors.> ...
%! startsWith (string ('a'), 1)
%!error<string.startsWith: 'IgnoreCase' must be a logical scalar.> ...
%! startsWith (string ('a'), 'a', 'IgnoreCase', 1)
%!error<string.startsWith: 'IgnoreCase' must be a logical scalar.> ...
%! startsWith (string ('a'), 'a', 'IgnoreCase', [true, true])
%!error<string.startsWith: unrecognized input argument.> ...
%! startsWith (string ('a'), 'a', 'whatever')

%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, 'Earth');
%! assert_equal (TF, [false, false, true, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, string ('Earth'));
%! assert_equal (TF, [false, false, true, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, 'earth');
%! assert_equal (TF, [false, false, false, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, 'earth', 'IgnoreCase', true);
%! assert_equal (TF, [false, false, true, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, {'Earth', 'Mercury'});
%! assert_equal (TF, [true, false, true, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, {'earth', 'mercury'});
%! assert_equal (TF, [false, false, false, false]);
%!test
%! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'});
%! TF = matches (str, {'earth', 'mercury'}, 'IgnoreCase', true);
%! assert_equal (TF, [true, false, true, false]);
%!error<string.matches: PATTERN is required.> matches (string)
%!error<string.matches: PATTERN must be a character vector, a string array, or cell array of character vectors.> ...
%! matches (string ('a'), 1)
%!error<string.matches: 'IgnoreCase' must be a logical scalar.> ...
%! matches (string ('a'), 'a', 'IgnoreCase', 1)
%!error<string.matches: 'IgnoreCase' must be a logical scalar.> ...
%! matches (string ('a'), 'a', 'IgnoreCase', [true, true])
%!error<string.matches: unrecognized input argument.> ...
%! matches (string ('a'), 'a', 'whatever')

%!assert_equal (iscolumn (string ([1, 2, 3, 4])), false)
%!assert_equal (iscolumn (string ([1; 2; 3; 4])), true)
%!assert_equal (iscolumn (string ([1, 2; 3, 4])), false)
%!assert_equal (iscolumn (string ({'1', '2', '3', '4'})), false)
%!assert_equal (iscolumn (string ({'1'; '2'; '3'; '4'})), true)
%!assert_equal (iscolumn (string ({'1', '2'; '3', '4'})), false)
%!assert_equal (iscolumn (string ([])), false)
%!assert_equal (iscolumn (string (ones (1, 0))), false)
%!assert_equal (iscolumn (string (ones (0, 1))), true)
%!assert_equal (iscolumn (string (1)), true)
%!assert_equal (iscolumn (string (ones (2, 3, 4))), false)

%!assert_equal (isempty (string ([])), true)
%!assert_equal (isempty (string ('')), false)
%!assert_equal (isempty (string (ones (2, 0, 3))), true)
%!assert_equal (isempty (string (ones (2, 1, 3))), false)
%!assert_equal (isempty (string (1)), false)
%!assert_equal (isempty (string), false)

%!assert_equal (ismatrix (string), true)
%!assert_equal (ismatrix (string ([])), true)
%!assert_equal (ismatrix (string (ones (2, 4))), true)
%!assert_equal (ismatrix (string (ones (2, 4, 1))), true)
%!assert_equal (ismatrix (string (ones (2, 1, 4))), false)
%!assert_equal (ismatrix (string (ones (2, 3, 4))), false)
%!assert_equal (ismatrix (string (ones (0, 1))), true)
%!assert_equal (ismatrix (string (ones (1, 0))), true)
%!assert_equal (ismatrix (string (char (0, 1))), true)
%!assert_equal (ismatrix (string (char (1, 0))), true)

%!test
%! A = string ({'dog', 'cat', 'fish', 'horse'});
%! B = string ({'dog ', 'cat', 'fish ', 'horse'});
%! [TF, index] = ismember (A, B);
%! assert_equal (TF, [false, true, false, true]);
%! assert_equal (index, [0, 2, 0, 4]);
%!test
%! A = string ({'dog', 'cat', 'fish', 'horse'});
%! B = ['dog';'cat'];
%! [TF, index] = ismember (A, B);
%! assert_equal (TF, [true, true, false, false]);
%! assert_equal (index, [1, 2, 0, 0]);
%! [TF, index] = ismember (B, A);
%! assert_equal (TF, [true; true]);
%! assert_equal (index, [1; 2]);
%!test
%! A = string ({'dog', 'cat'; 'fish', 'horse'});
%! B = string ({'dog ', 'cat'});
%! [TF, index] = ismember (A, B);
%! assert_equal (TF, [false, true; false, false]);
%! assert_equal (index, [0, 2; 0, 0]);
%! [TF, index] = ismember (A, B, 'rows');
%! assert_equal (TF, [false; false]);
%! assert_equal (index, [0; 0]);
%!test
%! A = string ({'dog', 'cat'; 'fish', 'horse'});
%! B = string ({'dog', 'cat'});
%! [TF, index] = ismember (A, B);
%! assert_equal (TF, [true, true; false, false]);
%! assert_equal (index, [1, 2; 0, 0]);
%! [TF, index] = ismember (A, B, 'rows');
%! assert_equal (TF, [true; false]);
%! assert_equal (index, [1; 0]);
%!error<string.ismember: first input argument must be text.> ismember (1, string)
%!error<string.ismember: second input argument must be text.> ismember (string, 1)
%!error<string.ismember: A and B must have the same number of columns with the 'rows' option.> ...
%! ismember (string (1), string ([1, 2]), 'rows')

%!assert_equal (ismissing (string ('a'), 'a'), true)
%!assert_equal (ismissing (string ('a'), string ({'a', 'c'})), true)
%!assert_equal (ismissing (string ({'a', 'b'}), string ({'a', 'c'})), [true, false])
%!test
%! TF = ismissing (string ({'a', 'b', ''}), string ({'a', 'c'}));
%! assert_equal (TF, logical ([1, 0, 0]));
%!test
%! TF = ismissing (string ({'a', 'b', ''}), string ({'a'; 'c'}));
%! assert_equal (TF, logical ([1, 0, 0]));
%!test
%! TF = ismissing (string ({'a'; 'b'; ''}), string ({'a', 'c'}));
%! assert_equal (TF, logical ([1; 0; 0]));
%!test
%! TF = ismissing ([string({'a', 'b', ''}), missing], string ({'a', 'c'}));
%! assert_equal (TF, logical ([1, 0, 0, 0]));
%!error<string.ismissing: too many input arguments.> ...
%! ismissing (string, string, string)
%!error<string.ismissing: INDICATOR must be a text array.> ...
%! ismissing (string, 1)
%!error<string.ismissing: INDICATOR must be a text array.> ...
%! ismissing (string, {1})
%!error<string.ismissing: INDICATOR must be a vector.> ...
%! ismissing (string, string (ones (2)))

%!assert_equal (isrow (string ([1, 2, 3, 4])), true)
%!assert_equal (isrow (string ([1; 2; 3; 4])), false)
%!assert_equal (isrow (string ([1, 2; 3, 4])), false)
%!assert_equal (isrow (string ([])), false)
%!assert_equal (isrow (string ()), true)
%!assert_equal (isrow (string (ones (1, 0))), true)
%!assert_equal (isrow (string (ones (0, 1))), false)
%!assert_equal (isrow (string (1)), true)
%!assert_equal (isrow (string (ones (2, 3, 4))), false)

%!assert_equal (isscalar (string ([])), false)
%!assert_equal (isscalar (string ()), true)
%!assert_equal (isscalar (string (ones (1, 0))), false)
%!assert_equal (isscalar (string (ones (0, 1))), false)
%!assert_equal (isscalar (string (1)), true)
%!assert_equal (isscalar (string (NaN)), true)
%!assert_equal (isscalar (string (missing)), true)
%!assert_equal (isscalar (string ([1, 2])), false)

%!test  ## issorted, verified against MATLAB R2026a
%! assert_equal (issorted (string ({'a', 'b', 'c'})), true);
%! assert_equal (issorted (string ({'a', 'b', 'c'}), 'strictascend'), true);
%! assert_equal (issorted (string ({'a', 'a', 'b'}), 'strictascend'), false);
%! assert_equal (issorted (string ({'c', 'b', 'a'}), 'descend'), true);
%! assert_equal (issorted (string ({'a', 'b', 'c'}), 'monotonic'), true);
%! assert_equal (issorted (string ({'b', 'a', 'c'}), 'monotonic'), false);
%! assert_equal (issorted ([string('a') string('b') missing], 'ascend'), true);
%! assert_equal (issorted ([string('a') string('b') missing], 'strictascend'), false);
%!error<string.issorted: invalid value for 'MissingPlacement'.> ...
%! issorted (string ({'a', 'b'}), 'MissingPlacement', 'bad')
%!error<string.issorted: invalid DIRECTION value.> ...
%! issorted (string ({'a', 'b'}), 'asd')

%!test  ## strict issortedrows semantics, verified against MATLAB R2026a
%! assert_equal (issortedrows (string ({'a', 'x'; 'a', 'y'}), 'strictascend'), false);
%! assert_equal (issortedrows (string ({'a', 'x'; 'a', 'x'}), 'strictascend'), false);
%! assert_equal (issortedrows (string ({'a', 'p'; 'a', 'q'}), ...
%!                       {'strictascend', 'ascend'}), false);
%! assert_equal (issortedrows (string ({'a', 'p'; 'b', 'p'}), ...
%!                       {'ascend', 'strictascend'}), true);
%! assert_equal (issortedrows (string ({'b', 'z'; 'a', 'y'}), 'monotonic'), true);
%! assert_equal (issortedrows (string ({'b', 'z'; 'a', 'y'}), 'descend'), true);
%! assert_equal (issortedrows (string ({'a', 'q'; 'b', 'p'}), 2, ...
%!                       'strictascend'), false);
%! assert_equal (issortedrows (string ({'b', 'p'; 'a', 'q'}), 2, ...
%!                       'strictascend'), true);
%!test  ## strict issortedrows with missing values (vs MATLAB R2026a)
%! assert_equal (issortedrows ([string('a') string('p'); missing string('q')], ...
%!                       {'strictascend', 'ascend'}), false);
%! assert_equal (issortedrows ([string('a') string('p'); missing string('q')], ...
%!                       {'ascend', 'strictascend'}), true);
%! assert_equal (issortedrows ([missing string('p') string('z'); ...
%!                        string('x') string('q') string('y')], ...
%!                       2, 'strictascend'), true);
%! assert_equal (issortedrows ([string('a') missing; string('b') string('y')], ...
%!                       'strictascend'), true);
%!error<string.issortedrows: invalid DIRECTION value.> ...
%! issortedrows (string ({'a', 'b'}), {'asd'})
%!error<string.issortedrows: COL and DIRECTION mismatch.> ...
%! issortedrows (string ({'a', 'b'; 'c', 'd'}), [1, 2], ...
%!               {'strictascend', 'ascend', 'descend'})

%!assert_equal (isstring (string), true)
%!assert_equal (isstring (cellstr (string)), false)

%!assert_equal (isvector (string ([1, 2, 3, 4])), true)
%!assert_equal (isvector (string ([1; 2; 3; 4])), true)
%!assert_equal (isvector (string ([1, 2; 3, 4])), false)
%!assert_equal (isvector (string ([])), false)
%!assert_equal (isvector (string ()), true)
%!assert_equal (isvector (string ()), true)
%!assert_equal (isvector (string (ones (1, 0))), true)
%!assert_equal (isvector (string (ones (0, 1))), true)
%!assert_equal (isvector (string (1)), true)
%!assert_equal (isvector (string (ones (2, 3, 4))), false)

################################################################################
##                        ** Relational Operations **                         ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'eq'               'ge'               'gt'               'le'              ##
## 'lt'               'ne'               'strcmp'           'strcmpi'         ##
## 'strncmp'          'strncmpi'                                              ##
##                                                                            ##
################################################################################

%!assert_equal (eq (string ('A'), string ('A')), true);
%!assert_equal (eq (string ('A'), string ('b')), false);
%!assert_equal (eq (string ('A'), {'A', 'b'}), [true, false]);
%!assert_equal (eq ({'A', 'b'}, string ('A')), [true, false]);
%!assert_equal (eq (string ({'A', 'b'}), 'A'), [true, false]);
%!assert_equal (eq ('A', string ({"A", "b"})), [true, false]);
%!assert_equal (eq (string ({'A', 'B'}), string ({'A'; 'B'})), logical (eye (2)))
%!error <string.eq: comparison between 'string' and 'double' is not supported.> ...
%! eq (string ('A'), 2)
%!error <string.eq: comparison between 'double' and 'string' is not supported.> ...
%! NaN == string ('A')
%!error <string.eq: incompatible dimensions.> ...
%! eq (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (ge (string ('A'), string ('A')), true);
%!assert_equal (ge (string ('A'), string ('b')), false);
%!assert_equal (ge (string ('b'), {'A', 'b'}), [true, true]);
%!assert_equal (ge ({'A', 'b'}, string ('b')), [false, true]);
%!assert_equal (ge (string ({'A', 'b'}), 'A'), [true, true]);
%!assert_equal (ge ('A', string ({'A', 'b'})), [true, false]);
%!assert_equal (ge (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([1, 1; 0, 1]))
%!error <string.ge: comparison between 'string' and 'double' is not supported.> ...
%! ge (string ('A'), 2)
%!error <string.ge: comparison between 'double' and 'string' is not supported.> ...
%! NaN >= string ('A')
%!error <string.ge: incompatible dimensions.> ...
%! ge (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (gt (string ('A'), string ('A')), false);
%!assert_equal (gt (string ('A'), string ('b')), false);
%!assert_equal (gt (string ('b'), {'A', 'b'}), [true, false]);
%!assert_equal (gt ({'A', 'b'}, string ('b')), [false, false]);
%!assert_equal (gt (string ({'A', 'b'}), 'A'), [false, true]);
%!assert_equal (gt ('A', string ({'A', 'b'})), [false, false]);
%!assert_equal (gt (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, 1; 0, 0]))
%!error <string.gt: comparison between 'string' and 'double' is not supported.> ...
%! gt (string ('A'), 2)
%!error <string.gt: comparison between 'double' and 'string' is not supported.> ...
%! NaN > string ('A')
%!error <string.gt: incompatible dimensions.> ...
%! gt (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (le (string ('A'), string ('A')), true);
%!assert_equal (le (string ('A'), string ('b')), true);
%!assert_equal (le (string ('b'), {'A', 'b'}), [false, true]);
%!assert_equal (le ({'A', 'b'}, string ('b')), [true, true]);
%!assert_equal (le (string ({'A', 'b'}), 'A'), [true, false]);
%!assert_equal (le ('A', string ({'A', 'b'})), [true, true]);
%!assert_equal (le (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([1, 0; 1, 1]))
%!error <string.le: comparison between 'string' and 'double' is not supported.> ...
%! le (string ('A'), 2)
%!error <string.le: comparison between 'double' and 'string' is not supported.> ...
%! NaN <= string ('A')
%!error <string.le: incompatible dimensions.> ...
%! le (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (lt (string ('A'), string ('A')), false);
%!assert_equal (lt (string ('A'), string ('b')), true);
%!assert_equal (lt (string ('b'), {'A', 'b'}), [false, false]);
%!assert_equal (lt ({'A', 'b'}, string ("b")), [true, false]);
%!assert_equal (lt (string ({'A', 'b'}), 'A'), [false, false]);
%!assert_equal (lt ('A', string ({'A', 'b'})), [false, true]);
%!assert_equal (lt (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, 0; 1, 0]))
%!error <string.lt: comparison between 'string' and 'double' is not supported.> ...
%! lt (string ('A'), 2)
%!error <string.lt: comparison between 'double' and 'string' is not supported.> ...
%! NaN < string ('A')
%!error <string.lt: incompatible dimensions.> ...
%! lt (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (ne (string ('A'), string ('A')), false);
%!assert_equal (ne (string ('A'), string ('b')), true);
%!assert_equal (ne (string ('A'), {'A', 'b'}), [false, true]);
%!assert_equal (ne ({'A', 'b'}, string ('A')), [false, true]);
%!assert_equal (ne (string ({'A', 'b'}), 'A'), [false, true]);
%!assert_equal (ne ('A', string ({'A', 'b'})), [false, true]);
%!assert_equal (ne (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, 1; 1, 0]))
%!error <string.ne: comparison between 'string' and 'double' is not supported.> ...
%! ne (string ('A'), 2)
%!error <string.ne: comparison between 'double' and 'string' is not supported.> ...
%! NaN != string ('A')
%!error <string.ne: incompatible dimensions.> ...
%! ne (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'}))

%!assert_equal (strcmp (string ("A"), string ("A")), true);
%!assert_equal (strcmp (string ("A"), string ("b")), false);
%!assert_equal (strcmp (string ("A"), {"A", "b"}), [true, false]);
%!assert_equal (strcmp ({"A", "b"}, string ("A")), [true, false]);
%!assert_equal (strcmp (string ({'A', 'b'}), 'A'), [true, false]);
%!assert_equal (strcmp ('A', string ({"A", "b"})), [true, false]);
%!error <string.strcmp: comparison between 'string' and 'double' is not supported.> ...
%! strcmp (string ("A"), 2)
%!error <string.strcmp: comparison between 'double' and 'string' is not supported.> ...
%! strcmp (2, string ("A"))
%!error <string.strcmp: inconsistent dimensions.> ...
%! strcmp (string ({"A","B"}), string ({"A";"B"}))

%!assert_equal (strcmpi (string ("A"), string ("a")), true);
%!assert_equal (strcmpi (string ("A"), string ("b")), false);
%!assert_equal (strcmpi (string ("a"), {"A", "b"}), [true, false]);
%!assert_equal (strcmpi ({"A", "b"}, string ("a")), [true, false]);
%!assert_equal (strcmpi (string ({'a', 'b'}), 'A'), [true, false]);
%!assert_equal (strcmpi ('A', string ({"a", "b"})), [true, false]);
%!error <string.strcmpi: comparison between 'string' and 'double' is not supported.> ...
%! strcmpi (string ("A"), 2)
%!error <string.strcmpi: comparison between 'double' and 'string' is not supported.> ...
%! strcmpi (2, string ("A"))
%!error <string.strcmpi: inconsistent dimensions.> ...
%! strcmpi (string ({"A","B"}), string ({"a";"b"}))

%!assert_equal (strncmp (string ("ASDFG"), "ASDER", 3), true);
%!assert_equal (strncmp (string ("ASDFG"), "ASDER", 4), false);
%!assert_equal (strncmp (string ("ASDFG"), {"ASDER","ASFGH"}, 3), [true, false]);
%!assert_equal (strncmp (string ("ASDFG"), {"ASDER","ASFGH"}, 2), [true, true]);
%!error <string.strncmp: comparison between 'string' and 'double' is not supported.> ...
%! strncmp (string ("A"), 2)
%!error <string.strncmp: comparison between 'double' and 'string' is not supported.> ...
%! strncmp (2, string ("A"))
%!error <string.strncmp: inconsistent dimensions.> ...
%! strncmp (string ({"A","B"}), string ({"a";"b"}), 1)
%!assert_equal (strncmpi (string ("asDFG"), "ASDER", 3), true);
%!assert_equal (strncmpi (string ("ASDFG"), "asDER", 4), false);
%!assert_equal (strncmpi (string ("asdfg"), {"ASDER","ASFGH"}, 3), [true, false]);
%!assert_equal (strncmpi (string ("asdfg"), {"ASDER","ASFGH"}, 2), [true, true]);
%!error <string.strncmpi: comparison between 'string' and 'double' is not supported.> ...
%! strncmpi (string ("A"), 2)
%!error <string.strncmpi: comparison between 'double' and 'string' is not supported.> ...
%! strncmpi (2, string ("A"))
%!error <string.strncmpi: inconsistent dimensions.> ...
%! strncmpi (string ({"A","B"}), string ({"a";"b"}), 1)

################################################################################
##                          ** String Operations **                           ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'append'           'compose'          'erase'            'eraseBetween'    ##
## 'extract'          'extractAfter'     'extractBefore'    'extractBetween'  ##
## 'insertAfter'      'insertBefore'     'replace'          'replaceBetween'  ##
## 'reverse'          'sort'             'sortrows'         'split'           ##
## 'splitlines'       'strcat'           'strip'            'pad'             ##
## 'join'             'plus'             'lower'            'upper'           ##
## 'unique'                                                                   ##
##                                                                            ##
################################################################################

## append: concatenates corresponding elements of its inputs
%!assert_equal (append (string ('Hello'), string (' World')), string ('Hello World'))
%!assert_equal (append (string ('a'), string ('b'), string ('c')), string ('abc'))
## append: preserves trailing whitespace (unlike 'strcat')
%!assert_equal (append (string ('a '), string ('b')), string ('a b'))
## append: char vectors are one element, cellstr arguments are accepted
%!assert_equal (append (string ('x'), '_', string ('y')), string ('x_y'))
%!assert_equal (append (string ('x'), {'_'}, string ('y')), string ('x_y'))
## append: char matrices contribute one element per row (whitespace preserved)
%!assert_equal (append (string ({'1'; '2'}), char ('a', 'bb')), string ({'1a '; '2bb'}))
## append: element-wise over equal-sized arrays
%!assert_equal (append (string ({'a'; 'b'}), string ({'1'; '2'})), string ({'a1'; 'b2'}))
## append: scalar expands against a non-scalar argument
%!assert_equal (append (string ('p_'), string ({'a'; 'b'; 'c'})), ...
%!              string ({'p_a'; 'p_b'; 'p_c'}))
## append: implicit expansion of a row and a column yields a matrix
%!assert_equal (append (string ({'a', 'b'}), string ({'1'; '2'})), ...
%!              string ({'a1', 'b1'; 'a2', 'b2'}))
## append: a single argument is returned unchanged
%!assert_equal (append (string ({'a'; 'b'})), string ({'a'; 'b'}))
## append: a missing value propagates like NaN (any missing input -> missing),
## and a missing element carries no text
%!test
%! str = append (string ('a'), string (NaN));
%! assert_equal (ismissing (str), true);
%! assert_equal (cellstr (str), {''});
%!test
%! str = append (string ({'a'; 'b'}), string ({missing; 'z'}));
%! assert_equal (ismissing (str), [true; false]);
%! assert_equal (cellstr (str), {''; 'bz'});
## append: error branches
%!error <string.append: input arguments must be string arrays, cell arrays of character vectors, or character matrices.> ...
%! append (string ('a'), 5)
%!error <string.append: inputs have incompatible sizes.> ...
%! append (string ({'a'; 'b'; 'c'}), string ({'x'; 'y'}))

## compose: formatting syntax -- single operator
%!assert_equal (compose (string ('%d apples'), 3), string ('3 apples'))
%!assert_equal (compose (string ('%d'), [1 2 3]), string ({'1', '2', '3'}))
%!assert_equal (compose (string ('%d'), [1; 2; 3]), string ({'1'; '2'; '3'}))
%!assert_equal (compose (string ('%d apples'), [1; 2; 3]), ...
%!              string ({'1 apples'; '2 apples'; '3 apples'}))
## compose: formatting syntax -- multiple operators per row
%!assert_equal (compose (string ('%d + %d = %d'), [1 2 3]), string ('1 + 2 = 3'))
%!assert_equal (compose (string ('%d %d %d'), [1 2 3; 4 5 6]), ...
%!              string ({'1 2 3'; '4 5 6'}))
## compose: FORMATSPEC repeats when A has more columns than operators
%!assert_equal (compose (string ('%d-%d '), [1 2 3 4]), string ({'1-2 ', '3-4 '}))
## compose: leftover operators are emitted unchanged
%!assert_equal (compose (string ('%d and %d'), 5), string ('5 and %d'))
## compose: multiple input arrays consumed left to right
%!assert_equal (compose (string ('%s = %d'), string ('a'), 1), string ('a = 1'))
%!assert_equal (compose (string ('%s=%d'), string ({'a'; 'b'}), [1; 2]), ...
%!              string ({'a=1'; 'b=2'}))
## compose: scalar input expands against a column vector
%!assert_equal (compose (string ('%s%d'), string ('row'), [1; 2; 3]), ...
%!              string ({'row1'; 'row2'; 'row3'}))
## compose: accepted input types
%!assert_equal (compose (string ('%s!'), 'hi'), string ('hi!'))
%!assert_equal (compose (string ('%s!'), string ('hi')), string ('hi!'))
%!assert_equal (compose (string ('%d'), true), string ('1'))
%!assert_equal (compose (string ('%5.2f'), pi), string (' 3.14'))
## compose: '*' dynamic field width consumes an extra value
%!assert_equal (compose (string ('%*d'), [5 42]), string ('   42'))
## compose: literal '%%' becomes a single '%'
%!assert_equal (compose (string ('%d%% done'), 50), string ('50% done'))
## compose: escape sequences in literal text of FORMATSPEC are translated
%!assert_equal (compose (string ('Line %d\n'), [1; 2]), ...
%!              string ({sprintf('Line 1\n'); sprintf('Line 2\n')}))
## compose: zero-column input preserves size and applies no formatting
%!test
%! out = compose (string ('%d'), zeros (2, 0));
%! assert_equal (class (out), 'string');
%! assert_equal (size (out), [2, 0]);
## compose: escape-translation syntax -- compose (TXT)
%!assert_equal (compose (string ('a\tb\nc')), string (sprintf ('a\tb\nc')))
%!assert_equal (compose (string ('100%% done')), string ('100% done'))
## compose (TXT) leaves formatting operators unchanged
%!assert_equal (compose (string ('val=%d\n')), string (sprintf ('val=%%d\n')))
## compose (TXT) preserves size over a multi-element array
%!test
%! out = compose (string ({'a\tb'; 'c'; 'd\ne'}));
%! assert_equal (size (out), [3, 1]);
%! assert_equal (cellstr (out), {sprintf('a\tb'); 'c'; sprintf('d\ne')});
## compose: error branches
%!error <string.compose: FORMATSPEC must be a string scalar or a character vector.> ...
%! compose (string ({'%d', '%d'}), 1)
%!error <string.compose: FORMATSPEC cannot be a missing value.> ...
%! compose (string (NaN), 1)
%!error <string.compose: unsupported input type: 'cell'.> ...
%! compose (string ('%d'), {1, 2})
%!error <string.compose: input arrays must be of compatible sizes.> ...
%! compose (string ('%d%d'), [1; 2; 3], [1; 2])

## erase: removes every occurrence of a single match from each element
%!assert_equal (erase (string ('the quick brown fox'), 'o'), ...
%!              string ('the quick brwn fx'))
%!assert_equal (erase (string ('Hello World'), ' World'), string ('Hello'))
## erase: a match that does not occur leaves the text unchanged
%!assert_equal (erase (string ('abc'), 'z'), string ('abc'))
## erase: an empty match removes nothing
%!assert_equal (erase (string ('abc'), ''), string ('abc'))
## erase: a char-vector match keeps trailing whitespace (bare cellstr deblanks)
%!assert_equal (erase (string ('the quick brown'), 'quick '), string ('the brown'))
## erase: accepts char vectors, cell arrays of char vectors, and string scalars
%!assert_equal (erase (string ('abcabc'), string ('bc')), string ('aa'))
%!assert_equal (erase (string ('aXa'), {'X'}), string ('aa'))
## erase: applied element-wise across an array, preserving its size
%!assert_equal (erase (string ({'aXa', 'bXb'}), 'X'), string ({'aa', 'bb'}))
%!assert_equal (erase (string ({'a.b'; 'c.d'}), '.'), string ({'ab'; 'cd'}))
## erase: a multi-element match deletes every occurrence of every element
%!assert_equal (erase (string ('The quick brown fox'), string ({'The ', 'quick '})), ...
%!              string ('brown fox'))
%!assert_equal (erase (string ('aXbYc'), {'X', 'Y'}), string ('abc'))
## erase: MATCH size need not match STR size (every element is removed from all)
%!assert_equal (erase (string ({'aXbYc', 'XYZ'}), string ({'X', 'Y'})), ...
%!              string ({'abc', 'Z'}))
## erase: erasing the entire content yields an empty, non-missing string
%!test
%! str = erase (string ('abc'), 'abc');
%! assert_equal (str, string (''));
%! assert_equal (ismissing (str), false);
## erase: missing values are preserved and carry no text
%!test
%! str = erase (string ({'a.b.c', missing}), '.');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'abc', ''});
## erase: error branch
%!error <string.erase: MATCH must be a string array, a character vector, or a cell array of character vectors.> ...
%! erase (string ('ab'), 5)

## eraseBetween: pattern syntax erases between boundaries (default exclusive)
%!assert_equal (eraseBetween (string ('The quick brown fox'), 'quick', ' fox'), ...
%!              string ('The quick fox'))
## eraseBetween: pattern syntax with explicit exclusive boundaries
%!assert_equal (eraseBetween (string ('a[xyz]b'), '[', ']', 'Boundaries', 'exclusive'), ...
%!              string ('a[]b'))
## eraseBetween: pattern syntax with inclusive boundaries erases them too
%!assert_equal (eraseBetween (string ('The quick brown fox jumps over the lazy dog'), ...
%!                            ' brown', 'lazy', 'Boundaries', 'inclusive'), ...
%!              string ('The quick dog'))
%!assert_equal (eraseBetween (string ('a[xyz]b'), '[', ']', 'Boundaries', 'inclusive'), ...
%!              string ('ab'))
## eraseBetween: char boundaries keep trailing whitespace (cellstr deblanks)
%!assert_equal (eraseBetween (string ('a quick b'), 'a ', ' b', ...
%!                            'Boundaries', 'inclusive'), string (''))
## eraseBetween: boundary substrings accepted as char, cellstr, or string
%!assert_equal (eraseBetween (string ('a<x>b'), '<', '>'), string ('a<>b'))
%!assert_equal (eraseBetween (string ('a<x>b'), {'<'}, {'>'}), string ('a<>b'))
%!assert_equal (eraseBetween (string ('a<x>b'), string ('<'), string ('>')), ...
%!              string ('a<>b'))
## eraseBetween: a missing boundary leaves the element unchanged
%!assert_equal (eraseBetween (string ('hello'), 'X', 'o'), string ('hello'))
%!assert_equal (eraseBetween (string ('hello'), 'h', 'X'), string ('hello'))
## eraseBetween: the end boundary must follow the start match
%!assert_equal (eraseBetween (string ('a|b|c'), 'b', 'a'), string ('a|b|c'))
## eraseBetween: a scalar boundary pair broadcasts over an array
%!assert_equal (eraseBetween (string ({'<a>1', '<bb>2'}), '<', '>', ...
%!                            'Boundaries', 'inclusive'), string ({'1', '2'}))
## eraseBetween: boundary pairs given element-wise, preserving size
%!assert_equal (eraseBetween (string ({'aXbYc'; '1P2Q3'}), string ({'X'; 'P'}), ...
%!                            string ({'Y'; 'Q'})), string ({'aXYc'; '1PQ3'}))
## eraseBetween: position syntax erases between positions (default inclusive)
%!assert_equal (eraseBetween (string ('Edgar Allen Poe'), 6, 11), string ('Edgar Poe'))
%!assert_equal (eraseBetween (string ('mushrooms, peppers, and onions'), 10, 19), ...
%!              string ('mushrooms and onions'))
## eraseBetween: position syntax with exclusive boundaries keeps the endpoints
%!assert_equal (eraseBetween (string ('small|medium|large'), 6, 13, ...
%!                            'Boundaries', 'exclusive'), string ('small||large'))
## eraseBetween: equal positions erase a single character (inclusive) or none
%!assert_equal (eraseBetween (string ('abcde'), 3, 3), string ('abde'))
%!assert_equal (eraseBetween (string ('abcde'), 3, 3, 'Boundaries', 'exclusive'), ...
%!              string ('abcde'))
## eraseBetween: scalar positions broadcast; array positions map by element
%!assert_equal (eraseBetween (string ({'abcdef'; 'ABCDEF'}), 2, 4), ...
%!              string ({'aef'; 'AEF'}))
%!assert_equal (eraseBetween (string ({'abcdef'; 'ABCDEF'}), [2; 3], [4; 5]), ...
%!              string ({'aef'; 'ABF'}))
## eraseBetween: 'Boundaries' name and string option are case-insensitive
%!assert_equal (eraseBetween (string ('small|medium|large'), 6, 13, ...
%!                            'boundaries', string ('exclusive')), string ('small||large'))
## eraseBetween: missing values are preserved and carry no text
%!test
%! str = eraseBetween (string ({'a[x]b', missing}), '[', ']', 'Boundaries', 'inclusive');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'ab', ''});
## eraseBetween: error branches
%!error <string.eraseBetween: not enough input arguments.> ...
%! eraseBetween (string ('ab'), 1)
%!error <string.eraseBetween: START and STOP must be either both numeric positions or both text patterns.> ...
%! eraseBetween (string ('ab'), 1, 'x')
%!error <string.eraseBetween: BOUNDARIES must be 'inclusive' or 'exclusive'.> ...
%! eraseBetween (string ('ab'), 'a', 'b', 'Boundaries', 'foo')
%!error <string.eraseBetween: invalid optional arguments.> ...
%! eraseBetween (string ('ab'), 1, 2, 'Foo', 'bar')
%!error <string.eraseBetween: position indices out of range.> ...
%! eraseBetween (string ('abc'), 2, 9)
%!error <string.eraseBetween: position indices out of range.> ...
%! eraseBetween (string ('abc'), 0, 2)
%!error <string.eraseBetween: position indices out of range.> ...
%! eraseBetween (string ('abc'), 1.5, 2)
%!error <string.eraseBetween: position indices out of range.> ...
%! eraseBetween (string ('abcde'), 4, 2)
%!error <string.eraseBetween: START must be scalar or the same size as STR.> ...
%! eraseBetween (string ({'a', 'b', 'c'}), [1, 2], [1, 2])
%!error <string.eraseBetween: STOP must be scalar or the same size as STR.> ...
%! eraseBetween (string ({'a', 'b', 'c'}), 1, [1, 2])

## extract: pattern form returns each match of PAT along the columns
%!assert_equal (extract (string ('xax ax'), 'ax'), string ({'ax', 'ax'}))
%!assert_equal (extract (string ('hello world'), 'o'), string ({'o', 'o'}))
## extract: a multi-element PAT matches any alternative, first listed winning
%!assert_equal (extract (string ('cabbage'), {'a', 'b'}), ...
%!              string ({'a', 'b', 'b', 'a'}))
%!assert_equal (extract (string ('ababab'), string ({'ab', 'a'})), ...
%!              string ({'ab', 'ab', 'ab'}))
## extract: matches do not overlap and are taken left to right
%!assert_equal (extract (string ('aaaa'), 'aa'), string ({'aa', 'aa'}))
## extract: a scalar with one match yields a 1x1 string
%!assert_equal (extract (string ('a-b'), '-'), string ('-'))
## extract: no match yields an empty string array
%!test
%! str = extract (string ('hello'), 'z');
%! assert_equal (class (str), 'string');
%! assert_equal (size (str), [1, 0]);
## extract: a non-scalar STR gives one row per element and one column per match
%!assert_equal (extract (string ({'p.q.r'; 'x.y.z'}), '.'), ...
%!              string ({'.', '.'; '.', '.'}))
## extract: elements that do not match are treated as having zero matches
%!test
%! str = extract (string ({'ab', missing}), '.');
%! assert_equal (class (str), 'string');
%! assert_equal (size (str), [2, 0]);
## extract: position form returns the single character at POS
%!assert_equal (extract (string ("All's well that ends well"), 1), string ('A'))
%!assert_equal (extract (string ("All's well that ends well"), 25), string ('l'))
## extract: position form is element-wise and preserves the size of STR
%!assert_equal (extract (string ({'abc'; 'xyz'}), [1; 3]), string ({'a'; 'z'}))
## extract: a scalar position broadcasts over the array
%!assert_equal (extract (string ({'abc', 'xyz'}), 2), string ({'b', 'y'}))
## extract: position form preserves missing values
%!test
%! str = extract (string ({'abc', missing}), 1);
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'a', ''});
## extract: error branches
%!error <string.extract: not enough input arguments.> ...
%! extract (string ('ab'))
%!error <string.extract: PAT must be a string array, a character vector, or a cell array of character vectors.> ...
%! extract (string ('ab'), {1, 2})
%!error <string.extract: all elements of STR must have the same number of matches.> ...
%! extract (string ({'a.b'; 'c.d.e'}), '.')
%!error <string.extract: POS must be scalar or the same size as STR.> ...
%! extract (string ({'a', 'b', 'c'}), [1, 2])
%!error <string.extract: POS must be a positive integer.> ...
%! extract (string ('ab'), 0)
%!error <string.extract: POS must be a positive integer.> ...
%! extract (string ('ab'), 1.5)
%!error <string.extract: POS exceeds the length of the string.> ...
%! extract (string ('ab'), 5)

## extractAfter: pattern form returns the text after the first match of PAT
%!assert_equal (extractAfter (string ('The quick brown fox'), 'quick '), ...
%!              string ('brown fox'))
%!assert_equal (extractAfter (string ('a-b-c'), '-'), string ('b-c'))
## extractAfter: boundary pattern given per element
%!assert_equal (extractAfter (string ({'The quick brown fox jumps'; 'over the lazy dog'}), ...
%!                            string ({'quick '; 'the '})), ...
%!              string ({'brown fox jumps'; 'lazy dog'}))
## extractAfter: a scalar pattern broadcasts over the array
%!assert_equal (extractAfter (string ({'a:1', 'a:2'}), 'a:'), string ({'1', '2'}))
## extractAfter: a pattern that is not found yields a missing value
%!test
%! str = extractAfter (string ('hello'), 'z');
%! assert_equal (ismissing (str), true);
## extractAfter: position form returns the text after POS
%!assert_equal (extractAfter (string ('Edgar Allen Poe'), 12), string ('Poe'))
%!assert_equal (extractAfter (string ({'abcd'; 'wxyz'}), [1; 2]), string ({'bcd'; 'yz'}))
## extractAfter: POS at the last character yields an empty string
%!assert_equal (extractAfter (string ('abc'), 3), string (''))
## extractAfter: missing values are preserved
%!test
%! str = extractAfter (string ({'a-b', missing}), '-');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'b', ''});
## extractAfter: error branches
%!error <string.extractAfter: not enough input arguments.> ...
%! extractAfter (string ('ab'))
%!error <string.extractAfter: PAT must be a string array, a character vector, or a cell array of character vectors.> ...
%! extractAfter (string ('ab'), {1, 2})
%!error <string.extractAfter: PAT must be scalar or the same size as STR.> ...
%! extractAfter (string ('ab'), string ({'a', 'b'}))
%!error <string.extractAfter: POS must be a positive integer.> ...
%! extractAfter (string ('ab'), 0)
%!error <string.extractAfter: POS exceeds the length of the string.> ...
%! extractAfter (string ('ab'), 5)

## extractBefore: pattern form returns the text before the first match of PAT
%!assert_equal (extractBefore (string ('The quick brown fox'), ' brown'), ...
%!              string ('The quick'))
%!assert_equal (extractBefore (string ('a-b-c'), '-'), string ('a'))
## extractBefore: a scalar pattern broadcasts over the array
%!assert_equal (extractBefore (string ({'1:a', '2:a'}), ':a'), string ({'1', '2'}))
## extractBefore: a pattern that is not found yields a missing value
%!test
%! str = extractBefore (string ('hello'), 'z');
%! assert_equal (ismissing (str), true);
## extractBefore: position form returns the text before POS
%!assert_equal (extractBefore (string ('Edgar Allen Poe'), 6), string ('Edgar'))
%!assert_equal (extractBefore (string ({'abcd'; 'wxyz'}), [3; 2]), string ({'ab'; 'w'}))
## extractBefore: POS at the first character yields an empty string
%!assert_equal (extractBefore (string ('abc'), 1), string (''))
## extractBefore: missing values are preserved
%!test
%! str = extractBefore (string ({'a-b', missing}), '-');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'a', ''});
## extractBefore: error branches
%!error <string.extractBefore: not enough input arguments.> ...
%! extractBefore (string ('ab'))
%!error <string.extractBefore: PAT must be a string array, a character vector, or a cell array of character vectors.> ...
%! extractBefore (string ('ab'), {1, 2})
%!error <string.extractBefore: POS exceeds the length of the string.> ...
%! extractBefore (string ('ab'), 5)

## extractBetween: pattern form returns the text between boundaries (exclusive)
%!assert_equal (extractBetween (string ('The quick brown fox'), 'quick ', ' fox'), ...
%!              string ('brown'))
## extractBetween: pattern form with inclusive boundaries keeps the boundaries
%!assert_equal (extractBetween (string ('The quick brown fox jumps'), ' brown', ...
%!                              'fox', 'Boundaries', 'inclusive'), string (' brown fox'))
## extractBetween: position form returns the span (inclusive by default)
%!assert_equal (extractBetween (string ('Edgar Allen Poe'), 7, 11), string ('Allen'))
## extractBetween: position form with exclusive boundaries drops the endpoints
%!assert_equal (extractBetween (string ('small|medium|large'), 6, 13, ...
%!                              'Boundaries', 'exclusive'), string ('medium'))
## extractBetween: equal positions extract a single character (inclusive)
%!assert_equal (extractBetween (string ('abcde'), 3, 3), string ('c'))
## extractBetween: a scalar string with several matches gives a 1xN row
%!assert_equal (extractBetween (string ('a[1][2][3]b'), '[', ']'), ...
%!              string ({'1', '2', '3'}))
## extractBetween: one match per element preserves the size of STR
%!assert_equal (extractBetween (string ({'<a>'; '<bb>'}), '<', '>'), ...
%!              string ({'a'; 'bb'}))
%!assert_equal (extractBetween (string ({'(x)'; '(yy)'}), '(', ')'), ...
%!              string ({'x'; 'yy'}))
## extractBetween: no match yields an empty string array
%!test
%! str = extractBetween (string ('hello'), '[', ']');
%! assert_equal (class (str), 'string');
%! assert_equal (size (str), [1, 0]);
## extractBetween: 'Boundaries' name and string option are case-insensitive
%!assert_equal (extractBetween (string ('small|medium|large'), 6, 13, ...
%!                              'boundaries', string ('exclusive')), string ('medium'))
## extractBetween: missing values are preserved (position form)
%!test
%! str = extractBetween (string ({'abcd', missing}), 2, 3);
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'bc', ''});
## extractBetween: error branches
%!error <string.extractBetween: not enough input arguments.> ...
%! extractBetween (string ('ab'), 1)
%!error <string.extractBetween: START and STOP must be either both numeric positions or both text patterns.> ...
%! extractBetween (string ('ab'), 1, 'x')
%!error <string.extractBetween: BOUNDARIES must be 'inclusive' or 'exclusive'.> ...
%! extractBetween (string ('ab'), 'a', 'b', 'Boundaries', 'foo')
%!error <string.extractBetween: invalid optional arguments.> ...
%! extractBetween (string ('ab'), 1, 2, 'Foo', 'bar')
%!error <string.extractBetween: all elements of STR must have the same number of matches.> ...
%! extractBetween (string ({'[1]'; '[1][2]'}), '[', ']')
%!error <string.extractBetween: position indices out of range.> ...
%! extractBetween (string ('abc'), 2, 9)
%!error <string.extractBetween: STOP must be scalar or the same size as STR.> ...
%! extractBetween (string ({'a', 'b', 'c'}), 1, [1, 2])

## Code-point positions: numeric indexing counts whole characters, not UTF-8
## bytes.  The test text "a<euro>b<grinning-face>c" has 5 code points spanning
## 1-, 3-, and 4-byte UTF-8 sequences (built from raw bytes so this file stays
## ASCII): a(1) euro(2) b(3) emoji(4) c(5).
%!test
%! s = string (native2unicode (uint8 ([97 226 130 172 98 240 159 152 128 99]), 'UTF-8'));
%! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8'));
%! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8'));
%! ## extract: the 4th character is the whole emoji, not a stray byte
%! assert_equal (extract (s, 4), emoji);
%! assert_equal (extract (s, 2), euro);
%! ## extractBefore / extractAfter split on character boundaries
%! assert_equal (extractBefore (s, 4), append (string ('a'), euro, string ('b')));
%! assert_equal (extractAfter (s, 2), append (string ('b'), emoji, string ('c')));
%! ## eraseBetween (inclusive removes chars 2..4; exclusive removes char 3)
%! assert_equal (eraseBetween (s, 2, 4), string ('ac'));
%! assert_equal (eraseBetween (s, 2, 4, 'Boundaries', 'exclusive'), ...
%!               append (string ('a'), euro, emoji, string ('c')));
%! ## extractBetween (inclusive keeps chars 2..4; exclusive keeps char 3)
%! assert_equal (extractBetween (s, 2, 4), append (euro, string ('b'), emoji));
%! assert_equal (extractBetween (s, 2, 4, 'Boundaries', 'exclusive'), string ('b'));

## insertAfter: pattern form inserts after the match (single occurrence)
%!assert_equal (insertAfter (string ('The quick fox'), 'quick', ' brown'), ...
%!              string ('The quick brown fox'))
## insertAfter: pattern form inserts after EVERY non-overlapping occurrence
%!assert_equal (insertAfter (string ('a-b-c'), '-', 'X'), string ('a-Xb-Xc'))
## insertAfter: boundary pattern and NEW given per element
%!assert_equal (insertAfter (string ({'foo'; 'bar'}), string ({'o'; 'r'}), ...
%!                           string ({'1'; '2'})), string ({'fo1o1'; 'bar2'}))
## insertAfter: a scalar pattern and scalar NEW broadcast over the array
%!assert_equal (insertAfter (string ({'a:1', 'a:2'}), 'a', '_'), ...
%!              string ({'a_:1', 'a_:2'}))
## insertAfter: a pattern that is not found leaves the element unchanged
%!assert_equal (insertAfter (string ('hello'), 'z', 'X'), string ('hello'))
## insertAfter: NEW preserves trailing whitespace (bare cellstr would deblank)
%!assert_equal (insertAfter (string ('ab'), 'a', 'X '), string ('aX b'))
## insertAfter: position form inserts after POS
%!assert_equal (insertAfter (string ('abcde'), 3, '_'), string ('abc_de'))
%!assert_equal (insertAfter (string ({'abcd'; 'wxyz'}), [1; 2], '_'), ...
%!              string ({'a_bcd'; 'wx_yz'}))
## insertAfter: POS at the last character appends NEW
%!assert_equal (insertAfter (string ('abc'), 3, 'X'), string ('abcX'))
## insertAfter: missing values in STR are preserved
%!test
%! str = insertAfter (string ({'a-b', missing}), '-', 'X');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'a-Xb', ''});
## insertAfter: a missing NEW makes the corresponding element missing
%!test
%! str = insertAfter (string ('abc'), 'a', string (missing));
%! assert_equal (ismissing (str), true);
## insertAfter: error branches
%!error <string.insertAfter: not enough input arguments.> ...
%! insertAfter (string ('ab'), 'a')
%!error <string.insertAfter: PAT must be a string array, a character vector, or a cell array of character vectors.> ...
%! insertAfter (string ('ab'), {1, 2}, 'x')
%!error <string.insertAfter: PAT must be scalar or the same size as STR.> ...
%! insertAfter (string ('ab'), string ({'a', 'b'}), 'x')
%!error <string.insertAfter: NEW must be a string array, a character vector, or a cell array of character vectors.> ...
%! insertAfter (string ('ab'), 'a', {1, 2})
%!error <string.insertAfter: NEW must be scalar or the same size as STR.> ...
%! insertAfter (string ({'a', 'b'}), 'x', string ({'1', '2', '3'}))
%!error <string.insertAfter: POS must be a positive integer.> ...
%! insertAfter (string ('ab'), 0, 'x')
%!error <string.insertAfter: POS exceeds the length of the string.> ...
%! insertAfter (string ('ab'), 5, 'x')

## insertBefore: pattern form inserts before the match (single occurrence)
%!assert_equal (insertBefore (string ('The quick fox'), 'fox', 'brown '), ...
%!              string ('The quick brown fox'))
## insertBefore: pattern form inserts before EVERY non-overlapping occurrence
%!assert_equal (insertBefore (string ('a-b-c'), '-', 'X'), string ('aX-bX-c'))
## insertBefore: a scalar pattern and scalar NEW broadcast over the array
%!assert_equal (insertBefore (string ({'cat', 'dog'}), string ({'c', 'd'}), '_'), ...
%!              string ({'_cat', '_dog'}))
## insertBefore: a pattern that is not found leaves the element unchanged
%!assert_equal (insertBefore (string ('hello'), 'z', 'X'), string ('hello'))
## insertBefore: position form inserts before POS
%!assert_equal (insertBefore (string ('abcde'), 3, '_'), string ('ab_cde'))
%!assert_equal (insertBefore (string ({'abcd'; 'wxyz'}), [3; 2], '_'), ...
%!              string ({'ab_cd'; 'w_xyz'}))
## insertBefore: POS at the first character prepends NEW
%!assert_equal (insertBefore (string ('abc'), 1, 'X'), string ('Xabc'))
## insertBefore: missing values in STR are preserved
%!test
%! str = insertBefore (string ({'a-b', missing}), '-', 'X');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'aX-b', ''});
## insertBefore: a missing NEW makes the corresponding element missing
%!test
%! str = insertBefore (string ('abc'), 'c', string (missing));
%! assert_equal (ismissing (str), true);
## insertBefore: error branches
%!error <string.insertBefore: not enough input arguments.> ...
%! insertBefore (string ('ab'), 'a')
%!error <string.insertBefore: PAT must be a string array, a character vector, or a cell array of character vectors.> ...
%! insertBefore (string ('ab'), {1, 2}, 'x')
%!error <string.insertBefore: NEW must be a string array, a character vector, or a cell array of character vectors.> ...
%! insertBefore (string ('ab'), 'a', {1, 2})
%!error <string.insertBefore: POS must be a positive integer.> ...
%! insertBefore (string ('ab'), 0, 'x')
%!error <string.insertBefore: POS exceeds the length of the string.> ...
%! insertBefore (string ('ab'), 5, 'x')

## Code-point positions for insertAfter / insertBefore: numeric indexing counts
## whole characters, not UTF-8 bytes (same multibyte text as above).
%!test
%! s = string (native2unicode (uint8 ([97 226 130 172 98 240 159 152 128 99]), 'UTF-8'));
%! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8'));
%! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8'));
%! ## insertAfter after the euro (position 2) lands on a character boundary
%! assert_equal (insertAfter (s, 2, string ('X')), ...
%!               append (string ('a'), euro, string ('Xb'), emoji, string ('c')));
%! ## insertBefore the emoji (position 4) lands on a character boundary
%! assert_equal (insertBefore (s, 4, string ('X')), ...
%!               append (string ('a'), euro, string ('bX'), emoji, string ('c')));

## replace: replaces every occurrence of a single substring
%!assert_equal (replace (string ('a-b-c'), '-', '_'), string ('a_b_c'))
## replace: a substring that does not occur leaves the text unchanged
%!assert_equal (replace (string ('abc'), 'z', 'Y'), string ('abc'))
## replace: applied element-wise across an array, preserving its size
%!assert_equal (replace (string ({'a.b', 'c.d'}), '.', '/'), ...
%!              string ({'a/b', 'c/d'}))
## replace: OLD/NEW size is independent of STR size
%!assert_equal (replace (string ('a-b_c'), {'-', '_'}, '/'), string ('a/b/c'))
## replace: a multi-element OLD pairs up with a same-size NEW
%!assert_equal (replace (string ('a-b_c'), {'-', '_'}, {'+', '='}), ...
%!              string ('a+b=c'))
## replace: multiple patterns applied simultaneously (a swap, not sequential)
%!assert_equal (replace (string ('ab'), {'a', 'b'}, {'b', 'a'}), string ('ba'))
## replace: at each position the OLD substrings are tried in order, first wins
%!assert_equal (replace (string ('aaa'), {'aa', 'a'}, {'X', 'Y'}), string ('XY'))
## replace: an empty OLD element matches nothing (single and multi-pattern)
%!assert_equal (replace (string ('abc'), '', 'X'), string ('abc'))
%!assert_equal (replace (string ('abc'), {'', 'b'}, {'X', 'Y'}), string ('aYc'))
## replace: trailing whitespace is preserved in both OLD and NEW
%!assert_equal (replace (string ('a b'), 'a ', 'Z'), string ('Zb'))
%!assert_equal (replace (string ('ab'), 'a', 'X '), string ('X b'))
## replace: accepts char vectors, cell arrays of char vectors, and string arrays
%!assert_equal (replace (string ('a-b'), string ('-'), string ('_')), string ('a_b'))
## replace: missing values are preserved and carry no text
%!test
%! str = replace (string ({'a-b', missing}), '-', '_');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'a_b', ''});
## replace: error branches
%!error <string.replace: not enough input arguments.> ...
%! replace (string ('ab'), 'a')
%!error <string.replace: OLD must be a string array, a character vector, or a cell array of character vectors.> ...
%! replace (string ('ab'), {1, 2}, 'x')
%!error <string.replace: NEW must be a string array, a character vector, or a cell array of character vectors.> ...
%! replace (string ('ab'), 'a', {1, 2})
%!error <string.replace: NEW must be a single substring or the same size as OLD.> ...
%! replace (string ('ab'), {'a', 'b'}, {'1', '2', '3'})

## replaceBetween: pattern replaces between boundaries (default exclusive)
%!assert_equal (replaceBetween (string ('The quick brown fox'), 'quick ', ' fox', 'red'), ...
%!              string ('The quick red fox'))
## replaceBetween: pattern syntax with explicit exclusive boundaries
%!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', 'Boundaries', 'exclusive'), ...
%!              string ('a[Y]b'))
## replaceBetween: pattern syntax with inclusive boundaries replaces them too
%!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', 'Boundaries', 'inclusive'), ...
%!              string ('aYb'))
## replaceBetween: an empty exclusive span inserts NEW between boundaries
%!assert_equal (replaceBetween (string ('ab'), 'a', 'b', 'X', 'Boundaries', 'exclusive'), ...
%!              string ('aXb'))
## replaceBetween: a scalar boundary broadcasts over the array
%!assert_equal (replaceBetween (string ({'[1]', '[2]'}), '[', ']', '#'), ...
%!              string ({'[#]', '[#]'}))
## replaceBetween: boundaries and NEW given per element
%!assert_equal (replaceBetween (string ({'(a)'; '<b>'}), string ({'('; '<'}), ...
%!                              string ({')'; '>'}), string ({'1'; '2'})), ...
%!              string ({'(1)'; '<2>'}))
## replaceBetween: an unmatched boundary leaves the element unchanged
%!assert_equal (replaceBetween (string ('abc'), '[', ']', 'X'), string ('abc'))
## replaceBetween: position syntax replaces the span (inclusive by default)
%!assert_equal (replaceBetween (string ('abcde'), 2, 4, 'X'), string ('aXe'))
## replaceBetween: position syntax with exclusive boundaries keeps the endpoints
%!assert_equal (replaceBetween (string ('abcde'), 2, 4, 'X', 'Boundaries', 'exclusive'), ...
%!              string ('abXde'))
## replaceBetween: equal positions replace a single character (inclusive)
%!assert_equal (replaceBetween (string ('abc'), 2, 2, 'XY'), string ('aXYc'))
## replaceBetween: 'Boundaries' name and string option are case-insensitive
%!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', 'boundaries', string ('Inclusive')), ...
%!              string ('aYb'))
## replaceBetween: missing values in STR are preserved
%!test
%! str = replaceBetween (string ({'[x]', missing}), '[', ']', 'Y');
%! assert_equal (ismissing (str), [false, true]);
%! assert_equal (cellstr (str), {'[Y]', ''});
## replaceBetween: a missing NEW makes the corresponding element missing
%!test
%! str = replaceBetween (string ('a[x]b'), '[', ']', string (missing));
%! assert_equal (ismissing (str), true);
## replaceBetween: error branches
%!error <string.replaceBetween: not enough input arguments.> ...
%! replaceBetween (string ('ab'), 'a', 'b')
%!error <string.replaceBetween: START and STOP must be either both numeric positions or both text patterns.> ...
%! replaceBetween (string ('ab'), 1, 'x', 'Y')
%!error <string.replaceBetween: BOUNDARIES must be 'inclusive' or 'exclusive'.> ...
%! replaceBetween (string ('a[x]b'), '[', ']', 'Y', 'Boundaries', 'foo')
%!error <string.replaceBetween: invalid optional arguments.> ...
%! replaceBetween (string ('ab'), 1, 2, 'Y', 'Foo', 'bar')
%!error <string.replaceBetween: position indices out of range.> ...
%! replaceBetween (string ('abc'), 2, 9, 'Y')
%!error <string.replaceBetween: STOP must be scalar or the same size as STR.> ...
%! replaceBetween (string ({'a', 'b', 'c'}), 1, [1, 2], 'Y')
%!error <string.replaceBetween: NEW must be scalar or the same size as STR.> ...
%! replaceBetween (string ({'a', 'b'}), 1, 1, string ({'1', '2', '3'}))
%!error <string.replaceBetween: NEW must be a string array, a character vector, or a cell array of character vectors.> ...
%! replaceBetween (string ('a[x]b'), '[', ']', {1, 2})

## Code-point positions for replaceBetween: numeric indexing counts whole
## characters, not UTF-8 bytes (same multibyte text as above): a euro b emoji c.
%!test
%! s = string (native2unicode (uint8 ([97 226 130 172 98 240 159 152 128 99]), 'UTF-8'));
%! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8'));
%! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8'));
%! ## inclusive default replaces chars 2..4 (euro,b,emoji) with 'X'
%! assert_equal (replaceBetween (s, 2, 4, string ('X')), string ('aXc'));
%! ## exclusive keeps chars 2 and 4, replacing char 3 (b) with 'X'
%! assert_equal (replaceBetween (s, 2, 4, string ('X'), 'Boundaries', 'exclusive'), ...
%!               append (string ('a'), euro, string ('X'), emoji, string ('c')));

## reverse: reverses the order of the characters in a string scalar
%!assert_equal (reverse (string ('hello')), string ('olleh'))
## reverse: the verbatim MATLAB documentation example
%!assert_equal (reverse (string ({'airport', 'control tower', 'radar', 'runway'})), ...
%!              string ({'tropria', 'rewot lortnoc', 'radar', 'yawnur'}))
## reverse: a palindrome is returned unchanged
%!assert_equal (reverse (string ('radar')), string ('radar'))
## reverse: applied element-wise across an array, preserving its size
%!assert_equal (reverse (string ({'ab', 'cd'; 'ef', 'gh'})), ...
%!              string ({'ba', 'dc'; 'fe', 'hg'}))
## reverse: a column vector keeps its shape
%!assert_equal (reverse (string ({'ab'; 'cde'})), string ({'ba'; 'edc'}))
## reverse: output is a string array of the same size as the input
%!test
%! r = reverse (string ({'ab', 'cd', 'ef'}));
%! assert_equal (isa (r, 'string'), true);
%! assert_equal (size (r), [1, 3]);
## reverse: a single character is unchanged
%!assert_equal (reverse (string ('x')), string ('x'))
## reverse: an empty string stays empty
%!assert_equal (reverse (string ('')), string (''))
## reverse: an empty (0x0) string array is returned unchanged
%!test
%! r = reverse (string ([]));
%! assert_equal (isa (r, 'string'), true);
%! assert_equal (size (r), [0, 0]);
## reverse: leading and trailing whitespace is preserved (and moved)
%!assert_equal (reverse (string ('ab ')), string (' ba'))
%!assert_equal (reverse (string (' ab')), string ('ba '))
%!assert_equal (reverse (string ('a b c')), string ('c b a'))
## reverse: applying reverse twice restores the original
%!test
%! s = string ({'hello', 'world'; 'foo bar', 'baz'});
%! assert_equal (reverse (reverse (s)), s);
## reverse: missing values are preserved and carry no text
%!test
%! r = reverse (string ({'ab', missing, 'cd'}));
%! assert_equal (ismissing (r), [false, true, false]);
%! assert_equal (cellstr (r), {'ba', '', 'dc'});
## reverse: an all-missing array stays all missing
%!test
%! r = reverse (string ({missing, missing}));
%! assert_equal (ismissing (r), [true, true]);
## reverse: counts whole characters, not UTF-8 bytes (multibyte text
## a euro b emoji c reverses to c emoji b euro a)
%!test
%! s = string (native2unicode (uint8 ([97 226 130 172 98 240 159 152 128 99]), 'UTF-8'));
%! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8'));
%! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8'));
%! assert_equal (reverse (s), ...
%!               append (string ('c'), emoji, string ('b'), euro, string ('a')));
%! ## reversing twice restores the multibyte original exactly
%! assert_equal (reverse (reverse (s)), s);

## sort: a vector is sorted in ascending order by default
%!assert_equal (sort (string ({'b', 'c', 'a'})), string ({'a', 'b', 'c'}))
## sort: a column vector keeps its shape
%!assert_equal (sort (string ({'b'; 'a'; 'c'})), string ({'a'; 'b'; 'c'}))
## sort: the second output indexes the original elements (B = A(index))
%!test
%! a = string ({'b', 'c', 'a'});
%! [b, i] = sort (a);
%! assert_equal (i, [3, 1, 2]);
%! assert_equal (b, a(i));
## sort: 'descend' reverses the order
%!assert_equal (sort (string ({'b', 'c', 'a'}), 'descend'), ...
%!              string ({'c', 'b', 'a'}))
## sort: a matrix is sorted column-wise by default
%!assert_equal (sort (string ({'d', 'a'; 'b', 'c'})), ...
%!              string ({'b', 'a'; 'd', 'c'}))
## sort: DIM selects the operating dimension
%!assert_equal (sort (string ({'d', 'a'; 'b', 'c'}), 2), ...
%!              string ({'a', 'd'; 'b', 'c'}))
## sort: DIM and DIRECTION can be combined
%!assert_equal (sort (string ({'d', 'a'; 'b', 'c'}), 2, 'descend'), ...
%!              string ({'d', 'a'; 'c', 'b'}))
## sort: the empty string sorts before any non-empty string
%!assert_equal (sort (string ({'b', '', 'a'})), string ({'', 'a', 'b'}))
## sort: a scalar is returned unchanged
%!assert_equal (sort (string ('x')), string ('x'))
## sort: ties are stable -- equal elements keep their original order
%!test
%! [~, i] = sort (string ({'b', 'a', 'b'}));
%! assert_equal (i, [2, 1, 3]);
## sort: missing values go last for ascending order ('auto' default)
%!test
%! r = sort (string ({'b', missing, 'a'}));
%! assert_equal (ismissing (r), [false, false, true]);
%! assert_equal (cellstr (r), {'a', 'b', ''});
## sort: missing values go first for descending order ('auto' default)
%!test
%! r = sort (string ({'b', missing, 'a'}), 'descend');
%! assert_equal (ismissing (r), [true, false, false]);
%! assert_equal (cellstr (r), {'', 'b', 'a'});
## sort: 'MissingPlacement','first' forces missing to the front
%!test
%! r = sort (string ({'b', missing, 'a'}), 'MissingPlacement', 'first');
%! assert_equal (ismissing (r), [true, false, false]);
%! assert_equal (cellstr (r), {'', 'a', 'b'});
## sort: 'MissingPlacement','last' forces missing to the end (descending)
%!test
%! r = sort (string ({'b', missing, 'a'}), 'descend', ...
%!           'MissingPlacement', 'last');
%! assert_equal (ismissing (r), [false, false, true]);
%! assert_equal (cellstr (r), {'b', 'a', ''});
## sort: error branches
%!error <string.sort: invalid value for 'MissingPlacement'.> ...
%! sort (string ('a'), 'MissingPlacement', 'bogus')

## sortrows: rows are sorted ascending, ties broken by later columns
%!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'})), ...
%!              string ({'a', 'z'; 'b', 'a'; 'b', 'x'}))
## sortrows: the second output indexes the original rows (B = A(index,:))
%!test
%! a = string ({'b', 'x'; 'a', 'z'; 'b', 'a'});
%! [b, i] = sortrows (a);
%! assert_equal (i, [2; 3; 1]);
%! assert_equal (b, a(i, :));
## sortrows: COL selects columns; a negative index sorts that column descending
%!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), [1, -2]), ...
%!              string ({'a', 'z'; 'b', 'x'; 'b', 'a'}))
## sortrows: a single DIRECTION applies to all columns
%!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), 'descend'), ...
%!              string ({'b', 'x'; 'b', 'a'; 'a', 'z'}))
## sortrows: DIRECTION can be a cell array, one entry per column
%!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), ...
%!                        {'ascend', 'descend'}), ...
%!              string ({'a', 'z'; 'b', 'x'; 'b', 'a'}))
## sortrows: COL and DIRECTION together (the sign of COL is ignored)
%!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), ...
%!                        [1, 2], {'ascend', 'descend'}), ...
%!              string ({'a', 'z'; 'b', 'x'; 'b', 'a'}))
## sortrows: a missing element sends its row last for ascending ('auto')
%!test
%! a = string ({'b', 'x'; 'a', 'z'});
%! a(1,1) = string (missing);
%! r = sortrows (a);
%! assert_equal (cellstr (r), {'a', 'z'; '', 'x'});
%! assert_equal (ismissing (r), [false, false; true, false]);
## sortrows: 'MissingPlacement','first' sends the missing row first
%!test
%! a = string ({'b', 'x'; 'a', 'z'});
%! a(1,1) = string (missing);
%! r = sortrows (a, 'MissingPlacement', 'first');
%! assert_equal (cellstr (r), {'', 'x'; 'a', 'z'});
## sortrows: 'MissingPlacement','last' keeps the missing row last (descending)
%!test
%! a = string ({'b', 'x'; 'a', 'z'});
%! a(1,1) = string (missing);
%! r = sortrows (a, 'descend', 'MissingPlacement', 'last');
%! assert_equal (cellstr (r), {'a', 'z'; '', 'x'});
## sortrows: error branches
%!error <string.sortrows: A must be a 2-D matrix.> ...
%! sortrows (string (reshape (1:8, [2, 2, 2])))
%!error <string.sortrows: invalid value for 'MissingPlacement'.> ...
%! sortrows (string ({'a', 'b'}), 'MissingPlacement', 'bogus')
%!error <string.sortrows: COL must be a vector of nonzero integers indexing columns in A.> ...
%! sortrows (string ({'a', 'b'}), [1.5])
%!error <string.sortrows: COL must be a vector of nonzero integers indexing columns in A.> ...
%! sortrows (string ({'a', 'b'}), [0])
%!error <string.sortrows: COL indexes non-existing column.> ...
%! sortrows (string ({'a', 'b'}), [3])
%!error <string.sortrows: DIRECTION input must contain either 'ascend' or 'descend' values.> ...
%! sortrows (string ({'a', 'b'}), {'up', 'down'})
%!error <string.sortrows: too many input arguments.> ...
%! sortrows (string ({'a', 'b'}), [1, 2], {'ascend', 'descend'}, 'x')
%!error <string.sortrows: invalid third input argument.> ...
%! sortrows (string ({'a', 'b'}), {'ascend', 'descend'}, 'ascend')

## split: a scalar splits into an Nx1 column at the delimiter
%!assert_equal (split (string ('a-b-c'), '-'), string ({'a'; 'b'; 'c'}))
## split: the default delimiter is whitespace
%!assert_equal (split (string ('one two three')), string ({'one'; 'two'; 'three'}))
## split: delimiters are not collapsed -- consecutive ones yield empty strings
%!assert_equal (split (string ('Jones,,,68,175'), ','), ...
%!              string ({'Jones'; ''; ''; '68'; '175'}))
## split: a trailing delimiter yields a trailing empty string
%!assert_equal (split (string ('a-'), '-'), string ({'a'; ''}))
## split: an Mx1 column splits into an MxN array
%!assert_equal (split (string ({'a-b'; 'c-d'}), '-'), string ({'a', 'b'; 'c', 'd'}))
## split: a 1xM row splits into a 1xMxN array
%!test
%! r = split (string ({'a-b', 'c-d'}), '-');
%! assert_equal (size (r), [1, 2, 2]);
%! assert_equal (cellstr (r(1,1,:))(:), {'a'; 'b'});
%! assert_equal (cellstr (r(1,2,:))(:), {'c'; 'd'});
## split: multiple delimiters are all used (first match wins at a position)
%!assert_equal (split (string ('a, b and c'), {', ', ' and '}), ...
%!              string ({'a'; 'b'; 'c'}))
## split: the second output returns the matched delimiters (one fewer piece)
%!test
%! [s, m] = split (string ('a, b and c'), {', ', ' and '});
%! assert_equal (s, string ({'a'; 'b'; 'c'}));
%! assert_equal (m, string ({', '; ' and '}));
## split: DIM lays the pieces out along the given dimension
%!test
%! r = split (string ('x.y.z'), '.', 2);
%! assert_equal (size (r), [1, 3]);
%! assert_equal (cellstr (r), {'x', 'y', 'z'});
## split: a string with no delimiter match returns itself (single piece)
%!assert_equal (split (string ('abc'), '-'), string ('abc'))
## split: missing values are preserved and count as a single piece
%!test
%! r = split (string (missing), '-');
%! assert_equal (ismissing (r), true);
%! assert_equal (size (r), [1, 1]);
%!test
%! r = split (string ({'ab', missing}), '-');
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'ab', ''});
## split: error branches
%!error <string.split: too many input arguments.> ...
%! split (string ('a'), '-', 2, 3)
%!error <string.split: DELIMITER must be a string array, a character vector, or a cell array of character vectors.> ...
%! split (string ('a'), {1, 2})
%!error <string.split: DIM must be a positive integer scalar.> ...
%! split (string ('a'), '-', 0)
%!error <string.split: STR must have size 1 along dimension DIM.> ...
%! split (string ({'a-b'; 'c-d'}), '-', 1)
%!error <string.split: each element of STR must split into the same number of substrings.> ...
%! split (string ({'a-b', 'c'}), '-')

## splitlines: a scalar splits into an Nx1 column at line feeds
%!assert_equal (splitlines (string (sprintf ("a\nb\nc"))), string ({'a'; 'b'; 'c'}))
## splitlines: a CR+LF pair is a single boundary (no empty line between)
%!assert_equal (splitlines (string (sprintf ("a\r\nb"))), string ({'a'; 'b'}))
## splitlines: a lone carriage return is also a boundary
%!assert_equal (splitlines (string (sprintf ("a\rb"))), string ({'a'; 'b'}))
## splitlines: consecutive newlines are not collapsed (empty line between)
%!assert_equal (splitlines (string (sprintf ("a\n\nb"))), string ({'a'; ''; 'b'}))
## splitlines: vertical tab and form feed are boundaries too
%!assert_equal (splitlines (string (sprintf ("a\vb\fc"))), string ({'a'; 'b'; 'c'}))
## splitlines: the Unicode line separator (U+2028) is a boundary
%!test
%! ls = native2unicode (uint8 ([226, 128, 168]), "UTF-8");
%! assert_equal (splitlines (string (["a", ls, "b"])), string ({'a'; 'b'}));
## splitlines: an Mx1 column splits into an MxN array
%!assert_equal (splitlines (string ({sprintf("a\nb"); sprintf("c\nd")})), ...
%!              string ({'a', 'b'; 'c', 'd'}))
## splitlines: a string with no newline returns itself (single line)
%!assert_equal (splitlines (string ('abc')), string ('abc'))
## splitlines: missing values are preserved and count as a single line
%!test
%! r = splitlines (string ({'ab', missing}));
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'ab', ''});
## splitlines: error branch -- elements must have the same number of lines
%!error <string.splitlines: each element of STR must split into the same number of substrings.> ...
%! splitlines (string ({sprintf("a\nb"), 'c'}))

## strcat: concatenates corresponding elements (verbatim MATLAB example)
%!assert_equal (strcat (string ({'John ', 'Mary '}), string ({'Smith', 'Jones'})), ...
%!              string ({'John Smith', 'Mary Jones'}))
## strcat: string inputs preserve trailing whitespace (unlike char strcat)
%!assert_equal (strcat (string ('a '), string ('b')), string ('a b'))
## strcat: a scalar argument expands, and char/cellstr may be mixed in
%!assert_equal (strcat (string ({'x', 'y'}), '_', string ('Z')), ...
%!              string ({'x_Z', 'y_Z'}))
%!assert_equal (strcat (string ({'a', 'b'}), {'1', '2'}), string ({'a1', 'b2'}))
## strcat: a single argument is returned as a string
%!assert_equal (strcat (string ('hi')), string ('hi'))
## strcat: output is a string array
%!test
%! r = strcat (string ({'a', 'b'}), string ('!'));
%! assert_equal (isa (r, 'string'), true);
%! assert_equal (cellstr (r), {'a!', 'b!'});
## strcat: error branch
%!error <string.strcat: incompatible data type: 'double'> ...
%! strcat (string ('a'), 5)

## strip: removes leading and trailing whitespace from both sides by default
%!assert_equal (strip (string ('  Ann Marie   ')), string ('Ann Marie'))
## strip: 'right' removes trailing whitespace only
%!assert_equal (strip (string ('   Ann Marie   '), 'right'), string ('   Ann Marie'))
## strip: 'left' removes leading whitespace only
%!assert_equal (strip (string ('   Ann Marie   '), 'left'), string ('Ann Marie   '))
## strip: a custom strip character on a given side (verbatim MATLAB example)
%!assert_equal (strip (string ('0095.36'), 'left', '0'), string ('95.36'))
## strip: a single-character second argument is the strip character
%!assert_equal (strip (string ('xxhixx'), 'x'), string ('hi'))
## strip: 'both' with an explicit strip character
%!assert_equal (strip (string ('--a--'), 'both', '-'), string ('a'))
## strip: the default whitespace set includes tab and newline
%!assert_equal (strip (string (sprintf ("\t a \n"))), string ('a'))
## strip: an all-whitespace string becomes empty
%!assert_equal (strip (string ('   ')), string (''))
## strip: an empty string stays empty
%!assert_equal (strip (string ('')), string (''))
## strip: applied element-wise across an array, preserving its size
%!assert_equal (strip (string ({'  a  ', ' b '})), string ({'a', 'b'}))
## strip: a non-breaking space (char 160) is not treated as whitespace
%!test
%! nbsp = native2unicode (uint8 ([194, 160]), "UTF-8");
%! s = string ([nbsp, 'a', nbsp]);
%! assert_equal (strip (s), s);
## strip: a multibyte strip character is matched as a whole code point
%!test
%! euro = native2unicode (uint8 ([226, 130, 172]), "UTF-8");
%! assert_equal (strip (string ([euro, euro, 'a', euro]), euro), string ('a'));
## strip: missing values are preserved and carry no text
%!test
%! r = strip (string ({'  a  ', missing}));
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'a', ''});
## strip: error branches
%!error <string.strip: too many input arguments.> ...
%! strip (string ('a'), 'left', '-', 'extra')
%!error <string.strip: SIDE must be 'left', 'right', or 'both'.> ...
%! strip (string ('a'), 'middle')
%!error <string.strip: STRIPCHAR must be a single character.> ...
%! strip (string ('a'), 'left', 'xy')

## pad: by default pads with trailing spaces to the longest element
%!assert_equal (pad (string ({'a'; 'bbb'; 'cc'})), string ({'a  '; 'bbb'; 'cc '}))
## pad: a single element is returned unchanged (it is the longest)
%!assert_equal (pad (string ('abc')), string ('abc'))
## pad: NCHARS gives an explicit width, padding on the right by default
%!assert_equal (pad (string ('ab'), 5), string ('ab   '))
## pad: 'left' adds the padding before the text
%!assert_equal (pad (string ('ab'), 5, 'left'), string ('   ab'))
## pad: 'both' splits the padding, putting an odd extra on the right
%!assert_equal (pad (string ('ab'), 5, 'both'), string (' ab  '))
%!assert_equal (pad (string ('ab'), 6, 'both'), string ('  ab  '))
## pad: a custom pad character on a given side
%!assert_equal (pad (string ('7'), 4, 'left', '0'), string ('0007'))
## pad: a single-character second argument is the pad character (to longest)
%!assert_equal (pad (string ({'a'; 'bbb'}), '*'), string ({'a**'; 'bbb'}))
## pad: a SIDE-only second argument pads to the longest element
%!assert_equal (pad (string ({'a'; 'bbb'}), 'left'), string ({'  a'; 'bbb'}))
## pad: elements longer than NCHARS are not truncated
%!assert_equal (pad (string ('abcdef'), 3), string ('abcdef'))
## pad: width is measured in characters (code points), not bytes
%!test
%! euro = native2unicode (uint8 ([226, 130, 172]), "UTF-8");
%! assert_equal (pad (string (euro), 3), string ([euro, '  ']));
## pad: missing values are preserved and ignored when finding the longest
%!test
%! r = pad (string ({'a', missing, 'ccc'}));
%! assert_equal (ismissing (r), [false, true, false]);
%! assert_equal (cellstr (r), {'a  ', '', 'ccc'});
## pad: error branches
%!error <string.pad: too many input arguments.> ...
%! pad (string ('a'), 1, 'left', '-', 'extra')
%!error <string.pad: NCHARS must be a nonnegative integer scalar.> ...
%! pad (string ('a'), -1)
%!error <string.pad: SIDE must be 'left', 'right', or 'both'.> ...
%! pad (string ('a'), 'middle')
%!error <string.pad: PADCHAR must be a single character.> ...
%! pad (string ('a'), 5, 'left', 'xy')

## join: combines along the last non-singleton dimension with a space (verbatim)
%!assert_equal (join (string ({'Carlos', 'Sada'; 'Ella', 'Olsen'; 'Diana', 'Lee'})), ...
%!              string ({'Carlos Sada'; 'Ella Olsen'; 'Diana Lee'}))
## join: a single custom delimiter is placed between all elements (verbatim)
%!assert_equal (join (string ({'x', 'y', 'z'; 'a', 'b', 'c'}), '-'), ...
%!              string ({'x-y-z'; 'a-b-c'}))
## join: an array of delimiters goes between consecutive elements (verbatim)
%!assert_equal (join (string ({'x', 'y', 'z'; 'a', 'b', 'c'}), ...
%!                    string ({' + ', ' = '; ' - ', ' = '})), ...
%!              string ({'x + y = z'; 'a - b = c'}))
## join: DIM selects the joined dimension (verbatim example, dim 1)
%!assert_equal (join (string ({'Carlos', 'Sada'; 'Ella', 'Olsen'; 'Diana', 'Lee'}), 1), ...
%!              string ({'Carlos Ella Diana', 'Sada Olsen Lee'}))
## join: a row or column vector collapses to a string scalar
%!assert_equal (join (string ({'a', 'b', 'c'})), string ('a b c'))
%!assert_equal (join (string ({'a'; 'b'; 'c'})), string ('a b c'))
## join: a string scalar is returned unchanged
%!assert_equal (join (string ('solo')), string ('solo'))
## join: a delimiter and a dimension together
%!assert_equal (join (string ({'a', 'b'; 'c', 'd'}), '-', 1), ...
%!              string ({'a-c', 'b-d'}))
## join: join is the inverse of split for a single delimiter
%!assert_equal (join (split (string ('a-b-c'), '-'), '-'), string ('a-b-c'))
## join: a missing element (or delimiter) propagates to the joined result
%!test
%! r = join (string ({'a', 'b'; 'c', missing}));
%! assert_equal (ismissing (r), [false; true]);
%! assert_equal (cellstr (r(1)), {'a b'});
## join: error branches
%!error <string.join: too many input arguments.> ...
%! join (string ('a'), '-', 2, 3)
%!error <string.join: the second argument must be a delimiter or a dimension.> ...
%! join (string ('a'), {1, 2})
%!error <string.join: DELIMITER must have one fewer element than STR along the joined dimension.> ...
%! join (string ({'a', 'b'}), string ({'-', '-'}))
%!error <string.join: DIM must be a positive integer scalar.> ...
%! join (string ('a'), '-', 0)

## plus: appends STR2 to STR1, equivalent to the '+' operator
%!assert_equal (string ('foo') + string ('bar'), string ('foobar'))
%!assert_equal (plus (string ('foo'), string ('bar')), string ('foobar'))
## plus: applied element-wise across equal-sized arrays
%!assert_equal (string ({'a', 'b'}) + string ({'1', '2'}), string ({'a1', 'b2'}))
## plus: a scalar operand broadcasts over the array
%!assert_equal (string ({'a', 'b'}) + string ('!'), string ({'a!', 'b!'}))
## plus: a missing value propagates (any missing operand -> missing)
%!test
%! r = string ({'a', missing}) + string ('X');
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'aX', ''});
## plus: error branch -- both operands must be string arrays
%!error <string.plus: both STR1 and STR2 must be string arrays.> ...
%! string ('a') + 5

## lower: converts uppercase characters to lowercase (verbatim MATLAB example)
%!assert_equal (lower (string ('The SOONER,')), string ('the sooner,'))
## lower: digits, punctuation, and already-lowercase letters are unchanged
%!assert_equal (lower (string ('aB3!Cd')), string ('ab3!cd'))
## lower: applied element-wise across an array, preserving its size
%!assert_equal (lower (string ({'AB', 'Cd'; 'eF', 'GH'})), ...
%!              string ({'ab', 'cd'; 'ef', 'gh'}))
## lower: an empty string stays empty
%!assert_equal (lower (string ('')), string (''))
## lower: missing values are preserved and carry no text
%!test
%! r = lower (string ({'AB', missing}));
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'ab', ''});

## upper: converts lowercase characters to uppercase (verbatim MATLAB example)
%!assert_equal (upper (string ('Hello, World.')), string ('HELLO, WORLD.'))
## upper: digits, punctuation, and already-uppercase letters are unchanged
%!assert_equal (upper (string ('aB3!cD')), string ('AB3!CD'))
## upper: applied element-wise across an array, preserving its size
%!assert_equal (upper (string ({'ab', 'cD'; 'Ef', 'gh'})), ...
%!              string ({'AB', 'CD'; 'EF', 'GH'}))
## upper: an empty string stays empty
%!assert_equal (upper (string ('')), string (''))
## upper: missing values are preserved and carry no text
%!test
%! r = upper (string ({'ab', missing}));
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'AB', ''});

## unique: returns sorted unique elements with index vectors B=A(ixA), A=B(ixB)
%!test
%! A = string ({'b', 'a', 'b', 'c', 'a'});
%! [B, ixA, ixB] = unique (A);
%! assert_equal (B, string ({'a', 'b', 'c'}));
%! assert_equal (ixA, [2; 1; 4]);
%! assert_equal (ixB, [2; 1; 2; 3; 1]);
## unique: 'stable' keeps first-seen order
%!assert_equal (unique (string ({'b', 'a', 'b', 'c', 'a'}), 'stable'), ...
%!              string ({'b', 'a', 'c'}))
## unique: 'first' and 'last' select which occurrence the index points to
%!test
%! [~, iaF] = unique (string ({'b', 'a', 'b', 'c', 'a'}), 'first');
%! [~, iaL] = unique (string ({'b', 'a', 'b', 'c', 'a'}), 'last');
%! assert_equal (iaF, [2; 1; 4]);
%! assert_equal (iaL, [5; 3; 4]);
## unique: 'rows' returns the unique rows of a 2-D string matrix
%!assert_equal (unique (string ({'a', 'b'; 'a', 'b'; 'c', 'd'}), 'rows'), ...
%!              string ({'a', 'b'; 'c', 'd'}))
## unique: missing values are not de-duplicated and are appended at the end
%!test
%! B = unique (string ({'b', missing, 'a', missing}));
%! assert_equal (ismissing (B), [false, false, true, true]);
%! assert_equal (cellstr (B(~ismissing (B))), {'a', 'b'});
## unique: error branches
%!error <string.unique: invalid option 'bogus'.> ...
%! unique (string ('a'), 'bogus')
%!error <string.unique: 'rows' applies only to 2-D matrices.> ...
%! unique (reshape (string ({'a','b','c','d','e','f','g','h'}), 2, 2, 2), 'rows')

################################################################################
##                           ** Array Operations **                           ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'cat'              'horzcat'          'vertcat'          'repmat'          ##
## 'repelem'          'repelems'         'reshape'          'circshift'       ##
## 'permute'          'ipermute'         'transpose'        'ctranspose'      ##
##                                                                            ##
################################################################################

## cat: concatenates string arrays along the first dimension (rows)
%!assert_equal (cat (1, string ({'a', 'b'}), string ({'c', 'd'})), ...
%!              string ({'a', 'b'; 'c', 'd'}))
## cat: concatenates string arrays along the second dimension (columns)
%!assert_equal (cat (2, string ({'a'; 'b'}), string ({'c'; 'd'})), ...
%!              string ({'a', 'c'; 'b', 'd'}))
## cat: a higher dimension stacks the inputs into an N-D array
%!test
%! c = cat (3, string ({'a', 'b'}), string ({'c', 'd'}));
%! assert_equal (size (c), [1, 2, 2]);
%! assert_equal (cellstr (c(:,:,1)), {'a', 'b'});
%! assert_equal (cellstr (c(:,:,2)), {'c', 'd'});
## cat: accepts more than two inputs
%!assert_equal (cat (2, string ('a'), string ('b'), string ('c')), ...
%!              string ({'a', 'b', 'c'}))
## cat: converts char, cellstr, numeric, and logical arguments to string
%!assert_equal (cat (2, string ('x'), 'yy', {'z'}), string ({'x', 'yy', 'z'}))
%!assert_equal (cat (2, string ('n'), 5, true, false), ...
%!              string ({'n', '5', 'true', 'false'}))
## cat: numeric arrays keep their shape when converted
%!assert_equal (cat (1, string ({'a'; 'b'}), [1; 2]), ...
%!              string ({'a'; 'b'; '1'; '2'}))
## cat: a char matrix contributes one element per row, preserving trailing
## whitespace (unlike the core 'cellstr', which would deblank it)
%!assert_equal (cat (1, string ({'1'; '2'}), char ('a', 'bb')), ...
%!              string ({'1'; '2'; 'a '; 'bb'}))
## cat: an empty argument drops out of the concatenation
%!assert_equal (cat (2, string ('a'), string ([])), string ('a'))
%!assert_equal (cat (1, string ([]), string ({'a'; 'b'})), string ({'a'; 'b'}))
## cat: a single argument is returned unchanged
%!assert_equal (cat (2, string ({'a'; 'b'})), string ({'a'; 'b'}))
## cat: missing values are carried through positionally
%!test
%! c = cat (2, string ('a'), string (NaN), string ('b'));
%! assert_equal (ismissing (c), [false, true, false]);
%! assert_equal (cellstr (c), {'a', '', 'b'});
## cat: a categorical, datetime, or duration argument promotes the result to
## that class (the first input is forwarded to its constructor)
%!assert_equal (class (cat (2, string ('a'), categorical ({'b'}))), ...
%!              'categorical')
%!assert_equal (class (cat (2, string ('2020-01-01'), ...
%!                          datetime (2021, 1, 1))), 'datetime')
%!assert_equal (class (cat (2, string ('1:00:00'), duration (2, 0, 0))), ...
%!              'duration')

## horzcat: '[A, B]' equals 'horzcat (A, B)' equals 'cat (2, A, B)'
%!assert_equal ([string({'a', 'b'}), string({'c', 'd'})], ...
%!              horzcat (string ({'a', 'b'}), string ({'c', 'd'})))
%!assert_equal (horzcat (string ({'a', 'b'}), string ({'c', 'd'})), ...
%!              cat (2, string ({'a', 'b'}), string ({'c', 'd'})))
## horzcat: converts and concatenates non-string arguments
%!assert_equal ([string('x'), 'yy', {'z'}, 5, true], ...
%!              string ({'x', 'yy', 'z', '5', 'true'}))
## horzcat: missing values are carried through positionally
%!test
%! h = [string('a'), string(NaN)];
%! assert_equal (ismissing (h), [false, true]);
%! assert_equal (cellstr (h), {'a', ''});

## vertcat: '[A; B]' equals 'vertcat (A, B)' equals 'cat (1, A, B)'
%!assert_equal ([string({'a', 'b'}); string({'c', 'd'})], ...
%!              vertcat (string ({'a', 'b'}), string ({'c', 'd'})))
%!assert_equal (vertcat (string ({'a', 'b'}), string ({'c', 'd'})), ...
%!              cat (1, string ({'a', 'b'}), string ({'c', 'd'})))
## vertcat: converts and concatenates non-string arguments
%!assert_equal ([string('x'); 'yy'; {'z'}; 5; true], ...
%!              string ({'x'; 'yy'; 'z'; '5'; 'true'}))
## vertcat: missing values are carried through positionally
%!test
%! v = [string('a'); string(NaN)];
%! assert_equal (ismissing (v), [false; true]);
%! assert_equal (cellstr (v), {'a'; ''});

## repmat: a scalar count tiles the array along every dimension
%!assert_equal (repmat (string ({'a', 'b'}), 2), ...
%!              string ({'a', 'b', 'a', 'b'; 'a', 'b', 'a', 'b'}))
## repmat: separate row and column counts
%!assert_equal (repmat (string ('x'), 2, 3), ...
%!              string ({'x', 'x', 'x'; 'x', 'x', 'x'}))
## repmat: a dimension vector is equivalent to the scalar-list form
%!assert_equal (repmat (string ('x'), [2, 3]), repmat (string ('x'), 2, 3))
## repmat: a zero count yields an empty string array
%!assert_equal (size (repmat (string ('a'), 0)), [0, 0])
%!assert_equal (class (repmat (string ('a'), 0)), 'string')
## repmat: missing values are preserved
%!test
%! r = repmat (string (NaN), 1, 3);
%! assert_equal (ismissing (r), [true, true, true]);
%! assert_equal (size (r), [1, 3]);

## repelem: a scalar count repeats each element that many times
%!assert_equal (repelem (string ({'a', 'b'}), 2), ...
%!              string ({'a', 'a', 'b', 'b'}))
## repelem: a count vector repeats each element by its own count
%!assert_equal (repelem (string ({'a', 'b', 'c'}), [1, 2, 3]), ...
%!              string ({'a', 'b', 'b', 'c', 'c', 'c'}))
## repelem: a zero count drops the corresponding element
%!assert_equal (repelem (string ({'a', 'b'}), [0, 2]), string ({'b', 'b'}))
## repelem: separate per-dimension counts repeat a matrix block-wise
%!test
%! r = repelem (string ({'a', 'b'; 'c', 'd'}), 2, 3);
%! assert_equal (size (r), [4, 6]);
%! assert_equal (cellstr (r(1,:)), {'a', 'a', 'a', 'b', 'b', 'b'});
%! assert_equal (cellstr (r(3,:)), {'c', 'c', 'c', 'd', 'd', 'd'});
## repelem: missing values are repeated like any other element
%!test
%! r = repelem (string ({'a', missing}), 2);
%! assert_equal (ismissing (r), [false, false, true, true]);
%! assert_equal (cellstr (r), {'a', 'a', '', ''});

## repelems: the first row of R indexes elements, the second row repeats them
%!assert_equal (repelems (string ({'a', 'b', 'c'}), [1, 3; 2, 2]), ...
%!              string ({'a', 'a', 'c', 'c'}))
## repelems: a single column selects and repeats one element
%!assert_equal (repelems (string ({'x', 'y', 'z'}), [2; 3]), ...
%!              string ({'y', 'y', 'y'}))
## repelems: a selected missing element stays missing
%!test
%! r = repelems (string ({'a', missing, 'c'}), [2, 3; 1, 2]);
%! assert_equal (ismissing (r), [true, false, false]);
%! assert_equal (cellstr (r), {'', 'c', 'c'});

## reshape: dimensions are filled column-major
%!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3), ...
%!              string ({'a', 'c', 'e'; 'b', 'd', 'f'}))
## reshape: an empty dimension '[]' is inferred from the element count
%!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, []), ...
%!              reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3))
%!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), [], 3), ...
%!              reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3))
## reshape: a dimension vector is equivalent to the scalar-list form
%!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), [3, 2]), ...
%!              reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 3, 2))
## reshape: missing values follow their elements column-major
%!test
%! r = reshape (string ({'a', missing, 'c', missing}), 2, 2);
%! assert_equal (ismissing (r), [false, false; true, true]);

## circshift: a positive scalar shifts along the first non-singleton dimension
%!assert_equal (circshift (string ({'a', 'b', 'c', 'd'}), 1), ...
%!              string ({'d', 'a', 'b', 'c'}))
## circshift: a negative shift moves elements the other way
%!assert_equal (circshift (string ({'a', 'b', 'c', 'd'}), -1), ...
%!              string ({'b', 'c', 'd', 'a'}))
## circshift: a trailing DIM argument selects the dimension to shift
%!assert_equal (circshift (string ({'a', 'b'; 'c', 'd'}), 1, 2), ...
%!              string ({'b', 'a'; 'd', 'c'}))
## circshift: a vector shift applies one value per dimension
%!assert_equal (circshift (string ({'a', 'b'; 'c', 'd'}), [1, 1]), ...
%!              string ({'d', 'c'; 'b', 'a'}))
## circshift: missing values are shifted like any other element
%!test
%! r = circshift (string ({'a', missing, 'c'}), 1);
%! assert_equal (ismissing (r), [false, false, true]);
%! assert_equal (cellstr (r), {'c', 'a', ''});

## permute: rearranges the dimensions (here equivalent to a transpose)
%!assert_equal (permute (string ({'a', 'b'; 'c', 'd'}), [2, 1]), ...
%!              string ({'a', 'c'; 'b', 'd'}))
## permute: missing values move with their elements
%!test
%! p = permute (string ({'a', missing; 'c', 'd'}), [2, 1]);
%! assert_equal (ismissing (p), [false, false; true, false]);

## ipermute: inverts permute, so the round-trip returns the original array
%!test
%! A = string ({'a', 'b', 'c'; 'd', 'e', 'f'});
%! assert_equal (ipermute (permute (A, [2, 1]), [2, 1]), A);
## ipermute: a 2-element permutation is its own inverse (a transpose)
%!assert_equal (ipermute (string ({'a', 'b'; 'c', 'd'}), [2, 1]), ...
%!              string ({'a', 'c'; 'b', 'd'}))

## transpose: turns a row into a column and vice versa
%!assert_equal (transpose (string ({'a', 'b', 'c'})), string ({'a'; 'b'; 'c'}))
## transpose: flips a matrix across its diagonal
%!assert_equal (transpose (string ({'a', 'b'; 'c', 'd'})), ...
%!              string ({'a', 'c'; 'b', 'd'}))
## transpose: the '.'' operator dispatches to this method
%!assert_equal (string ({'a', 'b'}).', string ({'a'; 'b'}))
## transpose: missing values are preserved
%!test
%! t = transpose (string ({'a', missing}));
%! assert_equal (ismissing (t), [false; true]);
%! assert_equal (size (t), [2, 1]);

## ctranspose: identical to transpose for string arrays
%!assert_equal (ctranspose (string ({'a', 'b', 'c'})), string ({'a'; 'b'; 'c'}))
%!assert_equal (ctranspose (string ({'a', 'b'; 'c', 'd'})), ...
%!              transpose (string ({'a', 'b'; 'c', 'd'})))
## ctranspose: the ''' operator dispatches to this method
%!assert_equal (string ({'a', 'b'})', string ({'a'; 'b'}))

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

## end: indexes the last element of a vector (and counts back from it)
%!test
%! a = string ({'a', 'b', 'c', 'd'});
%! assert_equal (a(end), string ('d'));
%! assert_equal (a(end-1), string ('c'));
## end: resolves per-dimension and linearly within a matrix
%!test
%! m = string ({'a', 'b', 'c'; 'd', 'e', 'f'});
%! assert_equal (m(end, end), string ('f'));
%! assert_equal (m(end), string ('f'));
%! assert_equal (m(1, end), string ('c'));

## subsref: '()' returns a string sub-array (class and contents preserved)
%!test
%! a = string ({'a', 'b', 'c', 'd'});
%! assert_equal (class (a(2:3)), 'string');
%! assert_equal (a(2:3), string ({'b', 'c'}));
## subsref: '()' carries the missing mask along with the elements
%!test
%! a = string ({'a', missing, 'c'});
%! r = a([1, 2]);
%! assert_equal (ismissing (r), [false, true]);
%! assert_equal (cellstr (r), {'a', ''});
## subsref: '{}' returns the backing cell-array slice (a cell, not a string)
%!test
%! a = string ({'a', 'b', 'c', 'd'});
%! v = a{2};
%! assert_equal (class (v), 'cell');
%! assert_equal (v, {'b'});
## subsref: chained '()' references resolve left to right
%!assert_equal (string ({'aa', 'bb', 'cc'})([1, 3])(2), string ('cc'))
## subsref: '.' indexing for reference is an error
%!error <string.subsref: '.' invalid indexing for referencing values. Use '\(\)' instead.> ...
%! (string ({'a', 'b'}).foo)

## subsasgn: '()' assignment from another string replaces the elements
%!test
%! a = string ({'a', 'b', 'c', 'd'});
%! a(2) = string ('Z');
%! assert_equal (a, string ({'a', 'Z', 'c', 'd'}));
## subsasgn: '()' converts a char, cellstr, or numeric right-hand side
%!test
%! a = string ({'a', 'b', 'c'});
%! a(1) = 'Y';
%! a(2) = {'CC'};
%! a(3) = 5;
%! assert_equal (a, string ({'Y', 'CC', '5'}));
## subsasgn: '()' assignment of a missing value sets the missing mask
%!test
%! a = string ({'a', 'b', 'c'});
%! a(2) = string (NaN);
%! assert_equal (ismissing (a), [false, true, false]);
%! assert_equal (cellstr (a), {'a', '', 'c'});
## subsasgn: '()' assignment past the end grows the array, filling the gap
## elements with <missing> (MATLAB-confirmed, forum probe)
%!test
%! a = string ({'a', 'b'});
%! a(4) = string ('x');
%! assert_equal (size (a), [1, 4]);
%! assert_equal (ismissing (a), [false, false, true, false]);
%! assert_equal (cellstr (a), {'a', 'b', '', 'x'});
## subsasgn: growing with a char-vector right-hand side also fills <missing>
%!test
%! b = string ({'a', 'b', 'c'});
%! b(5) = 'z';
%! assert_equal (ismissing (b), [false, false, false, true, false]);
%! assert_equal (cellstr (b), {'a', 'b', 'c', '', 'z'});
## subsasgn: a two-dimensional grow fills every new cell with <missing>
%!test
%! c = string ({'a', 'b'; 'c', 'd'});
%! c(3, 3) = string ('z');
%! assert_equal (size (c), [3, 3]);
%! assert_equal (ismissing (c), ...
%!               [false, false, true; false, false, true; true, true, false]);
## subsasgn: '()' assignment of '[]' deletes the indexed elements
%!test
%! a = string ({'a', 'b', 'c', 'd'});
%! a(2) = [];
%! assert_equal (a, string ({'a', 'c', 'd'}));
## subsasgn: '{}' assigns a single element from a character vector
%!test
%! a = string ({'a', 'b', 'c'});
%! a{2} = 'QQ';
%! assert_equal (a, string ({'a', 'QQ', 'c'}));
## subsasgn: '{}' assignment clears a previously-missing element
%!test
%! a = string ({'a', missing});
%! a{2} = 'now';
%! assert_equal (ismissing (a), [false, false]);
%! assert_equal (a, string ({'a', 'now'}));
## subsasgn: '{}' requires a character-vector right-hand side
%!error <string.subsasgn: '{}' indexed assignment requires a character vector.> ...
%! (subsasgn (string ({'a', 'b'}), substruct ('{}', {2}), 5))
## subsasgn: '{}' is restricted to a single (scalar) element
%!error <string.subsasgn: '{}' indexing can only be used for simple assignment.> ...
%! (subsasgn (string ({'a', 'b'}), substruct ('{}', {[1, 2]}), 'x'))
## subsasgn: '.' indexing for assignment is an error
%!error <string.subsasgn: '.' invalid indexing for assigning values. Use '\(\)' or '{}' instead.> ...
%! (subsasgn (string ({'a', 'b'}), substruct ('.', 'foo'), 1))

################################################################################
##        ** Overloaded methods for duration and categorical classes **       ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'colon'            'linspace'         'intersect'        'setdiff'         ##
## 'setxor'           'union'            'isequal'          'isequaln'        ##
##                                                                            ##
################################################################################

## colon: a duration operand makes the string range delegate to duration
%!assert_equal (class (string ('1:00:00'):hours (5)), 'duration')
## colon: without a duration (or categorical) operand it is an error
%!error <string.colon: unsupported input types.> ...
%! colon (string ('a'), string ('b'))

## linspace: a duration endpoint makes the result a duration array
%!assert_equal (class (linspace (string ('0:00:00'), hours (5), 6)), 'duration')
## linspace: a non-duration second argument is an error
%!error <string.linspace: unsupported input types.> ...
%! linspace (string ('a'), string ('b'), 5)

## intersect: with a categorical it returns the common elements (categorical)
%!test
%! C = intersect (string ({'a', 'b', 'c'}), categorical ({'b', 'c', 'd'}));
%! assert_equal (class (C), 'categorical');
%! assert_equal (cellstr (C), {'b', 'c'});
## intersect: a non-duration, non-categorical second argument is an error
%!error <string.intersect: unsupported input types.> ...
%! intersect (string ({'a'}), string ({'b'}))

## setdiff: with a categorical it returns the elements of A that are not in B
%!test
%! C = setdiff (string ({'a', 'b', 'c'}), categorical ({'b'}));
%! assert_equal (class (C), 'categorical');
%! assert_equal (cellstr (C), {'a', 'c'});
## setdiff: a non-duration, non-categorical second argument is an error
%!error <string.setdiff: unsupported input types.> ...
%! setdiff (string ({'a'}), string ({'b'}))

## setxor: with a categorical it returns the symmetric difference
%!test
%! C = setxor (string ({'a', 'b'}), categorical ({'b', 'c'}));
%! assert_equal (class (C), 'categorical');
%! assert_equal (cellstr (C), {'a', 'c'});
## setxor: a non-duration, non-categorical second argument is an error
%!error <string.setxor: unsupported input types.> ...
%! setxor (string ({'a'}), string ({'b'}))

## union: with a categorical it returns the combined unique elements
%!test
%! C = union (string ({'a', 'b'}), categorical ({'b', 'c'}));
%! assert_equal (class (C), 'categorical');
%! assert_equal (cellstr (C), {'a', 'b', 'c'});
## union: a non-duration, non-categorical second argument is an error
%!error <string.union: unsupported input types.> ...
%! union (string ({'a'}), string ({'b'}))

## isequal: true for equal string arrays, false otherwise
%!assert_equal (isequal (string ({'a', 'b'}), string ({'a', 'b'})), true)
%!assert_equal (isequal (string ({'a', 'b'}), string ({'a', 'c'})), false)
## isequal: a missing element is never equal, even to another missing
%!assert_equal (isequal (string ({'a', missing}), ...
%!                       string ({'a', missing})), false)
## isequal: accepts more than two string arrays
%!assert_equal (isequal (string ('a'), string ('a'), string ('a')), true)
## isequal: a categorical operand delegates to categorical isequal
%!assert_equal (isequal (string ({'a', 'b'}), categorical ({'a', 'b'})), true)
## isequal: a non-string, non-categorical operand is an error
%!error <string.isequal: unsupported input types.> ...
%! isequal (string ({'a'}), 5)

## isequaln: like isequal but treats same-position missing values as equal
%!assert_equal (isequaln (string ({'a', missing}), ...
%!                        string ({'a', missing})), true)
%!assert_equal (isequaln (string ({'a', missing}), string ({'a', 'b'})), false)
## isequaln: true for equal string arrays
%!assert_equal (isequaln (string ({'a', 'b'}), string ({'a', 'b'})), true)
## isequaln: a categorical operand delegates to categorical isequaln
%!assert_equal (isequaln (string ({'a', 'b'}), categorical ({'a', 'b'})), true)
## isequaln: a non-string, non-categorical operand is an error
%!error <string.isequaln: unsupported input types.> ...
%! isequaln (string ({'a'}), 5)

## empty: a 0-by-0 empty string array by default
%!test
%! e = string.empty;
%! assert_equal (class (e), 'string');
%! assert_equal (size (e), [0, 0]);
%! assert_equal (isempty (e), true);

## empty: a requested size, which may have any one dimension zero
%!assert_equal (size (string.empty (0, 3)), [0, 3])
%!assert_equal (size (string.empty (3, 0)), [3, 0])
%!assert_equal (size (string.empty ([0, 3])), [0, 3])
%!assert_equal (size (string.empty (2, 0, 3)), [2, 0, 3])

## empty: a lone dimension gives a square size, so only zero is an empty one
%!assert_equal (size (string.empty (0)), [0, 0])
%!error <string.empty: at least one dimension must be zero for an empty array.> ...
%! string.empty (3)

## empty: a negative dimension counts as zero, as it does for 'zeros', and a
## size vector holding no dimensions names no size
%!assert_equal (size (string.empty (-1)), [0, 0])
%!assert_equal (size (string.empty (0, -1)), [0, 0])
%!assert_equal (size (string.empty (-2, -3)), [0, 0])
%!assert_equal (size (string.empty ([])), [0, 0])
%!error <string.empty: dimensions must be integer values.> ...
%! string.empty (1.5)

## empty: the result behaves as an empty string array elsewhere
%!assert_equal (size ([string('a'), string.empty]), [1, 1])
%!assert_equal (size (ismissing (string.empty)), [0, 0])
%!assert_equal (size (cellstr (string.empty)), [0, 0])

## Test 'sort' -- option VALUES are matched without regard to case
%!assert_equal (cellstr (sort (string ({'b', 'a', 'c'}), 'Descend')), ...
%!              {'c', 'b', 'a'})
%!assert_equal (cellstr (sort (string ({'b', 'a', 'c'}), 'ASCEND')), ...
%!              {'a', 'b', 'c'})
