classdef string

  ## -*- texinfo -*-
  ## @deftypefn {} {@var{str} =} string (@var{val})
  ## Create an object of the string class.
  ##
  ## @seealso{char, cellstr}
  ## @end deftypefn

  properties (Access = private, Hidden = true)

    str = {""};
    is_missing = false;

  endproperties

  methods (Access = public)

    function this = string (val)

      if (! nargin)
        ## 1x1 string array

      elseif (isstring (val))
        this = val;

      elseif (ischar (val))
        if (isempty (val) && size (val, 2) == 0)
          ## 0x0 string array
          return;
        endif
        ## N-D cellstr without removing trailing spaces
        sz = [size(val), 1];
        sz(2) = [];
        dims = 1:ndims (val);
        dims([1, 2]) = dims([2, 1]);
        val = reshape (permute (val, dims), size (val, 2), []);
        this.str = mat2cell (val', ones (1, size (val, 2)));
        this.str = reshape (this.str, sz);
        this.is_missing = false (size (this.str));

      elseif (iscell (val))
        if (iscellstr (val))
          this.str = val;
          this.is_missing = false (size (this.str));
        else
          sval = cellfun ("string", val, "UniformOutput", false); ## do NOT use @string
          if (all (cellfun (@isStringScalar, sval)(:)))
            this.is_missing = cellfun (@ismissing, sval);
            this.str = repmat ({""}, size (this.is_missing));
            this.str(! this.is_missing) = cellfun (@char, sval(! this.is_missing), "UniformOutput", false);
          else
            error ("string: cannot convert cell array to string");
          endif
        endif

      elseif (isnumeric (val))
        this.str = arrayfun (@num2str, val, "UniformOutput", false);
        this.is_missing = isnan (val);

      elseif (islogical (val))
        this.str = arrayfun (@(x) ifelse (x, "true", "false"), val, "UniformOutput", false);
        this.is_missing = false (size (this.str));

      elseif (isa (val, "missing"))
        this.str = cell (size (val));
        this.is_missing = true (size (val));

      else
        error ("string: cannot convert to string");
      endif

    endfunction

    function sref = subsref (this, s)

      switch (s(1).type)
        case "()"
          sref = this;
          sref.str = subsref (sref.str, s(1));
          sref.is_missing = subsref (sref.is_missing, s(1));

        case "{}"
          if (any (subsref (this.is_missing, substruct ('()', s(1).subs))))
            error ("string: cannot convert from missing to character vector");
          endif
          sref = subsref (this.str, s(1));
          ## Can also return multiple outputs as a cs-list

        case "."
          m = s(1).subs;
          if (numel (s) > 1)
            s = s(2:end);
          else
            s = substruct ("()", {});
          endif
          if (strcmp (s(1).type, "."))
            s = [substruct("()", {}), s];
          endif
          if (! strcmp (s(1).type, "()"))
            error ("string: invalid syntax");
          endif
          switch (m)
            case "empty"
              sref = string.empty (s(1).subs{:});
            case (methods ("string"))
              sref = feval (m, this, s(1).subs{:});
            otherwise
              error ("string: unknown method");
          endswitch
      endswitch

      if (numel (s) > 1)
        sref = subsref (sref, s(2:end));
      endif

    endfunction

    function this = subsasgn (this, s, val)

      switch (s(1).type)
        case "()"
          if (numel (s) > 1)
            error ("string: invalid array indexing");
          endif
          if (isa (val, "double") && isequal (size (val), [0 0]))
            ## s(...) = [] performs deletion
            this.str(s.subs{:}) = [];
            this.is_missing(s.subs{:}) = [];
            return;
          endif
          val = string (val);
          this.str = subsasgn (this.str, s, val.str);
          ## Fill non-allocated values with missing
          this.is_missing = ! subsasgn (! this.is_missing, s, ! val.is_missing);

        case "{}"
          if (numel (s) == 1 && isa (val, "double") && isequal (size (val), [0 0]))
            ## s{...} = [] sets elements to missing
            this.str = subsasgn (this.str, s, "");
            s = substruct ("()", s(1).subs);
            this.is_missing = ! subsasgn (! this.is_missing, s, false);
            return;
          endif
          if (numel (s) == 1 && ! (ischar (val) && isrow (val)))
            error ("string: '{}' assignment requires a char vector");
          endif
          n = numel (this.str);
          this.str = subsasgn (this.str, s, val);
          if (numel (s) > 1 && numel (this.str) > n && ! (ischar (val) && isrow (val)))
            error ("string: '{}' assignment requires a char vector");
          endif
          ## Fill non-allocated values with missing
          s = substruct ("()", s(1).subs);
          this.is_missing = ! subsasgn (! this.is_missing, s, true);

        otherwise
          error ("string: only '()' and '{}' indexing are supported for assigning values");
      endswitch

    endfunction

    function newobj = horzcat (varargin)
      newobj = cat (2, varargin{:});
    endfunction

    function newobj = vertcat (varargin)
      newobj = cat (1, varargin{:});
    endfunction

    function newobj = cat (d, varargin)
      varargin = cellfun ("string", varargin, "UniformOutput", false);
      str = cellfun (@(x) x.str, varargin, "UniformOutput", false);
      ism = cellfun (@(x) x.is_missing, varargin, "UniformOutput", false);
      newobj = string([]);
      newobj.str = cat (d, str{:});
      newobj.is_missing = cat (d, ism{:});
    endfunction

    function this = transpose (this)
      if (! ismatrix (this))
        error ("string: transpose not defined for N-D string arrays");
      endif
      this = permute (this, [2, 1]);
    endfunction

    function this = ctranspose (this)
      if (! ismatrix (this))
        error ("string: ctranspose not defined for N-D string arrays");
      endif
      this = permute (this, [2, 1]);
    endfunction

    function this = permute (this, order)
      this.str = permute (this.str, order);
      this.is_missing = permute (this.is_missing, order);
    endfunction

    function this = ipermute (this, order)
      this.str = ipermute (this.str, order);
      this.is_missing = ipermute (this.is_missing, order);
    endfunction

    function disp (this)
      if (isscalar (this.str))
        if (this.is_missing)
          str = "<missing>";
        else
          str = ["""" this.str{1} """"];
        endif
        printf ("  %s\n", str);
      else
        sz = sprintf ("%dx", size (this.str));
        printf ("  %s string array\n", sz(1:end-1));
      endif
    endfunction

    function sz = size (this, varargin)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.size ()
      ## @end deftypefn

      sz = size (this.str, varargin{:});
    endfunction

    function n = numel (this, varargin)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.numel ()
      ## @end deftypefn

      n = numel (this.str, varargin{:});
    endfunction

    function ind = end (this, k, n)

      sz = size (this.str);
      if (k > numel (sz))
        ind = 1;
      else
        if (n < numel (sz))
          sz = [sz(1:(n-1)) prod(sz(n:end))];
        end
        ind = sz(k);
      end

    endfunction

    function this = reshape (this, varargin)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.reshape ()
      ## @end deftypefn

      this.str = reshape (this.str, varargin{:});
      this.is_missing = reshape (this.is_missing, varargin{:});

    endfunction

##    function append (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.append ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
    function retval = cellstr (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.cellstr ()
      ## @end deftypefn

      retval = this.str;
      retval(this.is_missing) = {""};

    endfunction

    function retval = char (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.char ()
      ## @end deftypefn

      if (any (this.is_missing))
        error ("string: cannot convert from missing to character vector");
      endif
      retval = char (this.str);
      retval = reshape (retval', [size(retval, 2), size(this.str)]);
      retval = permute (retval, [2, 1, 3:ndims(retval)]);
      if (isempty (retval))
        retval = reshape (retval, [0, 0]);
      endif

    endfunction
##
##    function compose (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.compose ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function contains (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.contains ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function count (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.count ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function retval = double (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {@{retval}=} string.double ()
      ## @end deftypefn

      retval = cellfun (@str2double, this.str);
      retval(this.is_missing) = NaN;

    endfunction

##    function endsWith (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.endsWith ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function eq (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.eq ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function erase (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.erase ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function eraseBetween (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.eraseBetween ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function extract (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.extract ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function extractAfter (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.extractAfter ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function extractBefore (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.extractBefore ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function extractBetween (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.extractBetween ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function ge (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.ge ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function gt (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.gt ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function insertAfter (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.insertAfter ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function insertBefore (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.insertBefore ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function tf = ismissing (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.ismissing ()
      ## @end deftypefn

      tf = this.is_missing;

    endfunction

##    function issorted (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.issorted ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function join (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.join ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function le (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.le ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function this = lower (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.lower ()
      ## @end deftypefn

      this.str = tolower (this.str);

    endfunction

##    function lt (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.lt ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function matches (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.matches ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function ne (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.ne ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function or (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.or ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function pad (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.pad ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function C = plus (A, B)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.plus ()
      ## @end deftypefn

      C = string (A);
      B = string (B);
      if (isequal (size (C), size (B)) || isStringScalar (B) || isStringScalar (C))
        C.str = cellfun (@horzcat, C.str, B.str, "UniformOutput", false);
        C.is_missing = arrayfun (@or, C.is_missing, B.is_missing);
      elseif (isvector (B) || isvector (C)) ## To reimplement for N-D
        if (size (C, 1) > size (B, 1))
          B = repmat (B, size (C, 1), 1);
        elseif (size (B, 1) > size (C, 1))
          C = repmat (C, size (B, 1), 1);
        endif
        if (size (C, 2) > size (B, 2))
          B = repmat (B, 1, size (C,2));
        elseif (size (B, 2) > size (C, 2))
          C = repmat (C, 1, size (B, 2));
        endif
        C = plus (C, B);
      else
        error ("string: nonconformant arguments");
      endif

    endfunction

##    function replace (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.replace ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function replaceBetween (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.replaceBetween ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function this = reverse (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.reverse ()
      ## @end deftypefn

      ## use unicode_idx instead
      this.str = cellfun ("fliplr", this.str, "UniformOutput", false);

    endfunction

##    function sort (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.sort ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function split (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.split ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function splitlines (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.splitlines ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function startsWith (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.startsWith ()
##      ## @end deftypefn
##
##
##
##    endfunction
##
##    function strip (this)
##
##      ## -*- texinfo -*-
##      ## @deftypefn {} {} string.strip ()
##      ## @end deftypefn
##
##
##
##    endfunction

    function N = strlength (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{N}=} string.strlength ()
      ## @end deftypefn

      N = NaN (size (this.str));
      N(! this.is_missing) = __unicode_length__ (this.str (! this.is_missing));

    endfunction

    function this = upper (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} string.upper ()
      ## @end deftypefn

      this.str = toupper (this.str);

    endfunction

  endmethods

  methods (Static)

    function s = empty (varargin)

      ## -*- texinfo -*-
      ## @deftypefn {} {@var{s} =} string.empty ()
      ## Create an empty string array.
      ## @end deftypefn

      for i = 1:numel (varargin)
        if (! isnumeric (varargin{i}) && ! islogical (varargin{i}))
          error ("string: input must be a numeric or logical array");
        endif
      endfor

      if (! nargin || isempty (varargin{1}))
        s = string ([]);
      elseif (nargin == 1 && isscalar (varargin{1}))
        if (varargin{1} == 0)
          s = string ([]);
        else
          error ("string: at least one dimension must be zero");
        endif
      else
        s = string (reshape ([], varargin{:}));
      endif

    endfunction

  endmethods

endclassdef

## Test constructor
%!test
%! s = string ();
%! assert (class (s), "string");
%! assert (size (s), [1, 1]);
%! assert (char (s), '');
%! s = string (string ());
%! assert (class (s), "string");
%! assert (size (s), [1, 1]);
%! assert (char (s), "");
%! s = string ("gnu octave");
%! assert (class (s), "string");
%! assert (size (s), [1, 1]);
%! s = string ({"gnu", "octave"});
%! assert (class (s), "string");
%! assert (size (s), [1, 2]);
%! assert (isequal (s(1), string ("gnu")));
%! assert (isequal (s(2), string ("octave")));
%! s = string ({"gnu"; "octave"});
%! assert (class (s), "string");
%! assert (size (s), [2, 1]);
%! assert (isequal (s(1), string ("gnu")));
%! assert (isequal (s(2), string ("octave")));
%! s = string ({"gnu", "octave"; "string", "class"});
%! assert (class (s), "string");
%! assert (size (s), [2, 2]);
%! assert (isequal (s(1, 2), string ("octave")));
%! assert (isequal (s(2), string ("string")));
%! s = string ("");
%! assert (size (s), [1, 1]);
%! s = string (reshape ("", 1, 0));
%! assert (size (s), [1, 1]);
%! s = string (reshape ("", 0, 1));
%! assert (size (s), [0, 1]);
%! s = string (reshape ("", 0, 1, 0));
%! assert (size (s), [0, 0]);
%! s = string ({});
%! assert (size (s), [0, 0]);
%! s = string (cell (1, 0));
%! assert (size (s), [1, 0]);
%! s = string (cell (0, 1));
%! assert (size (s), [0, 1]);
%! s = string ({42, "octave"});
%! assert (size (s), [1, 2]);
%! assert (isequal (s(1), string ("42")));
%! s = string ({1, 2, "a", NaN});
%! assert (size (s), [1, 4]);
%! assert (isequal (s(1), string ("1")));
%! assert (isequal (s(2), string ("2")));
%! assert (isequal (s(3), string ("a")));
%! assert (ismissing (s(4)), true);
%! assert (ismissing (s), [false, false, false, true]);
%! s = string ({1, ""});
%! assert (size (s), [1, 2]);
%! assert (size (s{2}), [0 0]);
%! s = string ([]);
%! assert (size (s), [0 ,0]);
%! s = string (reshape ([], 1, 0));
%! assert (size (s), [1 ,0]);
%! s = string (reshape ([], 0, 1));
%! assert (size (s), [0, 1]);
%! str = ["gnu   "; "octave"];
%! s = string (str);
%! assert (size (s), [2, 1]);
%! str(:,:,2) = str;
%! s = string (str);
%! assert (size (s), [2, 2]);
%! str(:,:,:,2) = str;
%! s = string (str);
%! assert (size (s), [2, 2, 2]);
%! s = string (1);
%! assert (size (s), [1 ,1]);
%! assert (char (s), "1");
%! s = string ([1, 2]);
%! assert (size (s), [1 ,2]);
%! assert (char (s(2)), "2");
%! s = string (rand (2, 3));
%! assert (size (s), [2, 3]);
%! s = string (rand (2, 3, 4));
%! assert (size (s), [2, 3, 4]);
%! s = string (uint16 (eye (3)));
%! assert (size (s), [3, 3]);
%! assert (char (s(1)), "1");
%! assert (char (s(3)), "0");
%! s = string ([1, NaN]);
%! assert (char (s(1)), "1");
%! assert (ismissing (s(2)), true);
%! s = string ([1, Inf, -Inf]);
%! assert (char (s(2)), "Inf");
%! assert (char (s(3)), "-Inf");
%! s = string (1i);
%! assert (char (s), "0+1i");
%! s = string (true);
%! assert (char (s), "true");
%! s = string (false);
%! assert (char (s), "false");
%! s = string ([true, false]);
%! assert (isequal (s, string ({"true", "false"})));
%! s = string (rand (2, 3, 4) > 0.5);
%! assert (size (s), [2, 3, 4]);

## Test dimensions of N-D array
%!test
%! a = []; a(3,3,3) = 1;
%! s = string (a);
%! assert (size (s), size (a));
%! assert (numel (s), numel (a));
%! assert (ndims (s), ndims (a));

## Test subsref
%!test
%! s = string ([1, 2, 3]);
%! assert (numel (s([true, false])), 1);

## Test method call through subsref
%!test
%! s = string ([1, 2, 3]);
%! assert (s.size (), [1, 3]);
%! assert (s.size (2), 3);
%! assert (s.ismissing, false (1, 3));
%! assert (s.ismissing (), false (1, 3));
%! assert (s.cellstr (), {"1", "2", "3"});
%! assert (s.empty.size, [0, 0]);
%! assert (s.empty().size, [0, 0]);
%! assert (s.empty.size(), [0, 0]);
%! assert (s.empty().size(), [0, 0]);
%! assert (s.empty().size()(1), 0);

## Test static method
%!test
%! s = string.empty ();
%! assert (class (s), "string");
%! assert (size (s), [0, 0]);
%! s = string.empty (1, 0);
%! assert (size (s), [1, 0]);
%! assert (size (s, 2), 0);
%! s = string.empty (0, 1, 2);
%! assert (size (s), [0, 1, 2]);
%! assert (size (s, 3), 2);
%! assert (size (s, 5), 1);
%! s = string.empty (0);
%! assert (size (s), [0, 0]);
%! s = string.empty (false);
%!error string.empty ({});
%!error string.empty (1);
%!error string.empty (NaN);
%!error string.empty ([1, 0], 2);

## Test static methods when called from subsref
%!test
%! s = string ("octave");
%! s = s.empty;
%! assert (class (s), "string");
%! assert (size (s), [0, 0]);
%! s = s.empty ([]);
%! assert (size (s), [0, 0]);
%! s = s.empty (0);
%! assert (size (s), [0, 0]);
%! s = s.empty (0, 1);
%! assert (size (s), [0, 1]);
%! s = s.empty ([0, 1]);
%! assert (size (s), [0, 1]);
%!error
%! s = s.empty ([0, 1]);
%! s.empty{1};
%!error
%! s = s.empty ([0, 1]);
%! s.empty({1});
%!error
%! s = s.empty ([0, 1]);
%! s.empty(true);

## Test subsasgn '()'
%!test
%! s = string([1, 2, NaN, 4]);
%! assert (size (s), [1, 4]);
%! s(1) = 2;
%! assert (s{1}, "2");
%! s(1) = "a";
%! assert (s{1}, "a");
%! s(1) = string ("b");
%! assert (s{1}, "b");
%! s([1 3]) = [0, 2];
%! assert (s{1}, "0");
%! assert (s{3}, "2");
%! s(1:2) = 5;
%! assert (s{1}, "5");
%! assert (s{2}, "5");
%! s(1:end) = pi;
%! assert (numel (unique (cellstr (s))), 1);
%! s(6) = 6;
%! assert (size (s), [1 6]);
%! assert (s{6}, "6");
%! assert (s(5).ismissing, true);
%! s(10,10) = NaN;
%! assert (s(10,10).ismissing, true);
%! s(1:5,1:5) = NaN;
%! assert (s(1:5,1:5).ismissing, true (5));
%! s(1)(1);
%! s = string ([1, 2, 3]);
%! s(1) = [];
%! assert (size (s), [1, 2]);
%! assert (s{1}, "2");
%! assert (s{2}, "3");
%! s = string (rand (3));
%! s(5) = [];
%! assert (size (s), [1, 8]);
%! s(1:end) = [];
%! assert (size (s), [1, 0]);
%!error
%! s = string ([1, 2, 3]);
%! s(4) = [];

## Test subsasgn '{}'
%!test
%! s = string ([1, 2, 3]);
%! s{1} = "a";
%! assert (s{1}, "a");
%! s{5} = "Z";
%! assert (s{5}, "Z");
%! assert (s(4).ismissing, true);
%! val = {"a","b"};
#%! [s{9:10}] = val{:}; ## cs-list
#%! assert (size (s), [1, 10]);
#%! assert (s(8).ismissing, true);
#%! assert (s{9}, val{1});
#%! assert (s{10}, val{2});
%!error
%! s = string (1);
%! s{1} = false;
%!error
%! s = string (1);
%! s{1} = 42;
%! s = string (1);
%! s{1} = {"42"};
%!test
%! s = string ({"aaa", "bbb"});
%! s{1}(2) = 'A';
%! assert (s{1}, "aAa");
%! s{2}(4) = 'B';
%! assert (s{2}, "bbbB");
%! s{5}(1) = 'a';
%! assert (s(3).ismissing, true);
%! assert (s{5}, "a");
%! s{5}(1) = 66;
%! assert (s{5}, "B");
%!error
%! s = string ({"aaa", "bbb"});
%! s{5}(1) = 66;
%!test
%! s = string ({"aaa", "bbb"});
%! s{1} = [];
%! assert (size (s), [1, 2]);
%! assert (s(1).ismissing, true);
%! s{2}(1) = [];
%! assert (s{2}, "bb");
%! s{4} = [];
%! assert (ismissing (s), [true false true true]);
%!error
%! s = string ({"aaa", "bbb"});
%! s{3}(1) = [];

## Test permute, transpose, ctranspose
%!test
%! s1 = string ([1, 2, 3]);
%! s2 = s1';
%! assert (size (s2), [3, 1]);
%! s2 = s1.';
%! assert (size (s2), [3, 1]);
%! s2 = permute (s1, [1, 2]);
%! assert (size (s2), [1, 3]);
%! s2 = permute (s1, [2, 1]);
%! assert (size (s2), [3, 1]);
%!error string (rand (3, 3, 3))'
%!error string (rand (3, 4, 5)).'

## Test cat, horzcat, vertcat
%!test
%! s1 = string ([1, 2]);
%! s2 = string ([3, 4, 5]);
%! s3 = string ([6, 7, 8]);
%! s = [s1, s2];
%! assert (size (s), [1, 5]);
%! s = [s1'; s2'];
%! assert (size (s), [5, 1]);
%! s = [s2; s3];
%! assert (size (s), [2, 3]);
%! s = [s1, "A"];
%! assert (size (s), [1, 3]);
%! s = ["B", s1];
%! assert (size (s), [1, 3]);
%! s = ["A", s1, "B"];
%! assert (size (s), [1, 4]);
%! s = [s1, '', false, NaN, []];
%! assert (size (s), [1, 5]);
%! assert (s{4}, "false");
%! assert (ismissing (s(5)), true);
%!error [string(1); string([2 3])];
%!error [string(1), string([2 3]')];
%!error [string([1 2]), string([3 4;5 6])];

## Test double
%!test
%! s = string (3);
%! assert (double (s), 3);
%! s = string ("1+1i");
%! assert (double (s), 1+1i);
%! s = string ({"1", "octave"});
%! assert (double (s), [1, NaN]);
%! s = string ([]);
%! assert (double (s), []);
%! s = string (NaN); ## missing
%! assert (double (s), NaN);

## Test upper, lower
%!test
%! s = string ({"aa", "bB", "", NaN, Inf});
%! s = upper (s);
%! assert (s{1}, "AA");
%! assert (s{2}, "BB");
%! assert (s{3}, "");
%! assert (ismissing (s(4)), true);
%! assert (s{5}, "INF");

## Test strlength
%!test
%! s = string ({'a', 'bb', 'ccc', 'dddd'});
%! assert (strlength (s), [1, 2, 3, 4]);
%! s = string ([]);
%! assert (strlength (s), []);
%! s = string (NaN);
%! assert (strlength (s), NaN);
%! s = string (char ([97 121 204 131 204 149 204 186 98]));
%! assert (strlength (s), 6);
%! s = string (eye(4));
%! assert (strlength (s), ones (4));

## Test plus
%!test
%! s = string ([1, 2]) + string ([3, 4]);
%! assert (cellstr (s), {"13", "24"});
%! s = string ([1, 2]) + "a";
%! assert (cellstr (s), {"1a", "2a"});
%! s = "aa" + string ([1, 2]) + "b";
%! assert (cellstr (s), {"aa1b", "aa2b"});
%! s = string (eye (4)) + NaN;
%! assert (ismissing (s), true (4));
%! s = string ([1, 2]) + string ([3; 4]);
%! assert (cellstr (s), {"13", "23"; "14", "24"});
%! s = string ([1, 2]) + string ([3, 4; 5, 6]);
%! assert (cellstr (s), {"13", "24"; "15", "26"});

## Test reverse
%!test
%! s = string ({"gnu", "octave"});
%! assert (cellstr (reverse (s)), {"ung", "evatco"});
%! assert (ismissing (reverse (string (NaN))), true);

## Test input validator
%!error string (struct ([]));
%!error string ({{}});
