## Copyright (C) 2009-2011 Eric Chassande-Mottin, CNRS (France)
##
## This file is part of Octave.
##
## Octave 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.
##
## Octave 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 Octave; see the file COPYING.  If not, see
## <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {Function File} {[@var{a}, @dots{}] =} strread (@var{str})
## @deftypefnx {Function File} {[@var{a}, @dots{}] =} strread (@var{str}, @var{format})
## @deftypefnx {Function File} {[@var{a}, @dots{}] =} strread (@var{str}, @var{format}, @var{format_repeat})
## @deftypefnx {Function File} {[@var{a}, @dots{}] =} strread (@var{str}, @var{format}, @var{prop1}, @var{value1}, @dots{})
## @deftypefnx {Function File} {[@var{a}, @dots{}] =} strread (@var{str}, @var{format}, @var{format_repeat}, @var{prop1}, @var{value1}, @dots{})
## Read data from a string.
##
## The string @var{str} is split into words that are repeatedly matched to the
## specifiers in @var{format}.  The first word is matched to the first
## specifier,
## the second to the second specifier and so forth.  If there are more words
## than
## specifiers, the process is repeated until all words have been processed.
##
## The string @var{format} describes how the words in @var{str} should be
## parsed.
## It may contain any combination of the following specifiers:
## @table @code
## @item %s
## The word is parsed as a string.
##
## @item %d
## @itemx %f
## The word is parsed as a number.
##
## @item %*
## The word is skipped.
## @end table
##
## Parsed word corresponding to the first specifier are returned in the first
## output argument and likewise for the rest of the specifiers.
##
## By default, @var{format} is @t{"%f"}, meaning that numbers are read from
## @var{str}.
##
## For example, the string
##
## @example
## @group
## @var{str} = "\
## Bunny Bugs   5.5\n\
## Duck Daffy  -7.5e-5\n\
## Penguin Tux   6"
## @end group
## @end example
##
## @noindent
## can be read using
##
## @example
## [@var{a}, @var{b}, @var{c}] = strread (@var{str}, "%s %s %f");
## @end example
##
## Optional numeric argument @var{format_repeat} can be used for
## limiting the number of items read:
## @table @asis
## @item -1
## (default) read all of the string until the end.
##
## @item N
## Read N times @var{nargout} items. 0 (zero) is an acceptable
## value for @var{format_repeat}.
##
## @end table
##
## The behavior of @code{strread} can be changed via property-value
## pairs.  The following properties are recognized:
##
## @table @asis
## @item "commentstyle"
## Parts of @var{str} are considered comments and will be skipped.
## @var{value} is the comment style and can be any of the following.
## @itemize
## @item "shell"
## Everything from @code{#} characters to the nearest end-line is skipped.
##
## @item "c"
## Everything between @code{/*} and @code{*/} is skipped.
##
## @item "c++"
## Everything from @code{//} characters to the nearest end-line is skipped.
##
## @item "matlab"
## Everything from @code{%} characters to the nearest end-line is skipped.
## @end itemize
##
## @item "delimiter"
## Any character in @var{value} will be used to split @var{str} into words 
## (default value = any whitespace).
##
## @item "whitespace"
## Any character in @var{value} will be interpreted as whitespace and
## trimmed; the string defining whitespace must be enclosed in double
## quotes for proper processing of special characters like \t.
## The default value for whitespace = " \b\r\n\t" (note the space).
##
## @item "emptyvalue"
## Parts of the output where no word is available is filled with @var{value}.
## @end table
##
## @seealso{textread, load, dlmread, fscanf}
## @end deftypefn

## Author: Eric Chassande-Mottin
## Created:
## Updates:
## Philip Nienhuis <prnienhuis@users.sf.net>
## 2011-06-22 Fixed & implemented proper whitespace, delimiter and endofline processing
##      ''    (the latter for calls by textread.m); 
##      ''    Fixed default whitespace setting;
##      ''    Improved token parsing
##      ''    Changed delimiter default from "\n" to any whitespace (ML compatible);
##      ''    Fixed distribution over nargout for default format (or single '%f') (ML comp);
##      ''    Fixed possibility for output arrays to be of uneven length (ML comp);
##      ''    Fixed default format setting (empty format allowed) (ML compatible);
##      ''    Implemented format string repeat count (ML compatible);
##      ''    Several test cases added

function varargout = strread (str, format = "", varargin)
  ## Check input
  if (nargin < 1)
    print_usage ();
  endif

  if (!ischar (str) || !ischar (format))
    error ("strread: STR and FORMAT arguments must be strings");
  endif

  ## Check for format string repeat count
  format_repeat_count = -1;
  if (nargin > 2 && isnumeric (varargin{1}))
    if (varargin{1} >= 0); format_repeat_count = varargin{1}; endif
    if (nargin > 3); varargin = varargin (2:end); else; varargin = {}; endif
  endif

  ## Parse options
  comment_flag = false;
  numeric_fill_value = NaN;
  white_spaces = " \b\r\n\t";
  delimiter_str = "";
  eol_char = "";
  for n = 1:2:length (varargin)
    switch (lower (varargin {n}))
      case "commentstyle"
        comment_flag = true;
        switch (lower (varargin {n+1}))
          case "c"
            comment_specif = {"/*", "*/"};
          case "c++"
            comment_specif = {"//", "\n"};
          case "shell"
            comment_specif = {"#", "\n"};
          case "matlab"
            comment_specif = {"%", "\n"};
          otherwise
            warning ("strread: unknown comment style '%s'", val);
        endswitch
      case "delimiter"
        delimiter_str = varargin {n+1};
      case "emptyvalue"
        numeric_fill_value = varargin {n+1};
      case "bufsize"
        ## XXX: We could synthesize this, but that just seems weird...
        warning ("strread: property \"bufsize\" is not implemented");
      case "whitespace"
        white_spaces = varargin {n+1};
      case "expchars"
        warning ("strread: property \"expchars\" is not implemented");
      case "endofline"
        eol_char = varargin {n+1};
      otherwise
        warning ("strread: unknown property \"%s\"", varargin {n});
    endswitch
  endfor

  ## Parse format string
  if (isempty (format)); format = "%f"; endif
  idx = strfind (format, "%")';
  specif = format ([idx, idx+1]);
  nspecif = length (idx);
  idx_star = strfind (format, "%*");
  nfields = length (idx) - length (idx_star);

  if ( (max (nargout, 1) != nfields) && ~strcmp (format, "%f"))
    error ("strread: the number of output variables must match that specified by FORMAT");
  endif

  ## Remove comments
  if (comment_flag)
    cstart = strfind (str, comment_specif{1});
    cstop  = strfind (str, comment_specif{2});
    if (length (cstart) > 0)
      ## Ignore nested openers.
      [idx, cidx] = unique (lookup (cstop, cstart), "first");
      if (idx(end) == length (cstop))
        cidx(end) = []; # Drop the last one if orphaned.
      endif
      cstart = cstart(cidx);
    endif
    if (length (cstop) > 0)
      ## Ignore nested closers.
      [idx, cidx] = unique (lookup (cstart, cstop), "first");
      if (idx(1) == 0)
        cidx(1) = []; # Drop the first one if orphaned.
      endif
      cstop = cstop(cidx);
    endif
    len = length (str);
    c2len = length (comment_specif{2});
    str = cellslices (str, [1, cstop + c2len], [cstart - 1, len]);
    str = [str{:}];
  endif

  ## Determine the number of words per line
  format = strrep (format, "%", " %");
  [~, ~, ~, fmt_words] = regexp (format, "[^ ]+");
  if (strcmpi (format, " %f"))
    num_words_per_line = nargout;
    fmt_words (2:nargout) = fmt_words(1);
  else
    num_words_per_line = numel (fmt_words);
  endif
  for m = 1:numel(fmt_words)
    ## Convert formats such as "%Ns" to "%s"
    if (length (fmt_words{m}) > 2)
      if (strcmp (fmt_words{m}(1:2), "%*"))
        fmt_words{m} = "%*";
#     elseif (fmt_words{m}(1) == "%")
#       fmt_words{m} = fmt_words{m}([1, end]);
      endif
    endif
  endfor

  if (~isempty (eol_char))
    white_spaces = unique ([ white_spaces eol_char ]);
  endif

  if (~isempty (white_spaces))
    ## Check for overlapping whitespaces and delimiters & trim whitespace
    if (~isempty (delimiter_str))
      [ovlp, iw, ~] = intersect (white_spaces, delimiter_str);
      if (~isempty (ovlp))
        ## Remove delimiter chars from white_spaces
        white_spaces = cell2mat (strsplit (white_spaces, white_spaces(iw)));
      endif
    endif
  endif

  if (isempty (delimiter_str))
    delimiter_str = " ";
  endif

  pad_out = 0;
  if (~isempty (white_spaces))
    ## Check if trailing "\n" might signal padding output arrays to equal size
    ## before it is trimmed away below
    if ( (int8 (str(end:end)) == 10) && (nargout > 1) ); pad_out = 1; endif
    ## Remove repeated white_space chars. First find white_space positions
    idx = strchr (str, white_spaces);
    ## Find repeated white_spaces
    idx2 = ~(idx(2:end) - idx(1:end-1) - 1);
    ## Set al whitespace chars to spaces
    ## FIXME: this implies real spaces are always part of white_spaces
    str(idx(find (idx))) = ' ';
    ## Set all repeated white_space to \0
    str(idx(find (idx2))) = "\0";
    str = strsplit (str, "\0");
    ## Reconstruct trimmed str
    str = cell2mat (str);
    ## Remove leading & trailing space, but preserve delimiters.
    str = strtrim (str);
  endif

  ## Split 'str' into words
  words = split_by (str, delimiter_str);
  if (~isempty (white_spaces))
    ## Trim leading and trailing white_spaces
    words = strtrim (words);
  endif
  num_words = numel (words);
  num_lines = ceil (num_words / num_words_per_line);

  ## For each specifier
  k = 1;
  for m = 1:num_words_per_line
    if (format_repeat_count < 0)
      data = words (m:num_words_per_line:end);
    elseif (format_repeat_count == 0)
      data = {};
    else
      lastline = min (num_words_per_line * format_repeat_count + m - 1, numel (words));
      data = words (m:num_words_per_line:lastline);
    endif
    ## Map to format
    ## FIXME - add support for formats like "%4s" or "<%s>", "%[a-zA-Z]"
    ##         Someone with regexp experience is needed.
    switch fmt_words{m}(1:2)
      case "%s"
        if (pad_out); data (end+1:num_lines) = {""}; endif
        varargout {k} = data';
        k++;
      case {"%d", "%u", "%f"}
        n = cellfun (@isempty, data);
        data = str2double (data);
        data(n) = numeric_fill_value;
        if (pad_out); data (end+1:num_lines) = numeric_fill_value; endif
        varargout {k} = data.';
        k++;
      case {"%0", "%1", "%2", "%3", "%4", "%5", "%6", "%7", "%8", "%9"}
        nfmt = strsplit (fmt_words{m}(2:end-1), '.');
        swidth = str2num (nfmt{1});
        switch fmt_words{m}(end)
          case {"d", "u", "f"}
            n = cellfun (@isempty, data);
            data = str2double (strtrunc (data, swidth));
            data(n) = numeric_fill_value;
            if (pad_out); data (end+1:num_lines) = numeric_fill_value; endif
            if (numel (nfmt) > 1);
              sprec = str2num (nfmt{2});
              data = 10^-sprec * round (10^sprec * data);
            endif
            varargout {k} = data.';
            k++;
          case "s"
            if (pad_out); data (end+1:num_lines) = {""}; endif
            varargout {k} = strtrunc (data, 3)';
            k++;
          otherwise
        endswitch
      case {"%*", "%*s"}
        ## skip the word
      otherwise
        ## Ensure descriptive content is consistent
        if (numel (unique (data)) > 1
            || ! strcmpi (unique (data), fmt_words{m}))
          error ("strread: FORMAT does not match data");
        endif
    endswitch
  endfor

endfunction

function out = split_by (text, sep)
  out = strsplit (text, sep);
  out(cellfun (@isempty, out)) = {""};
endfunction

%!test
%! [a, b] = strread ("1 2", "%f%f");
%! assert (a == 1 && b == 2);

%!test
%! str = "# comment\n# comment\n1 2 3";
%! [a, b] = strread (str, '%d %s', 'commentstyle', 'shell');
%! assert (a, [1; 3]);
%! assert (b, {"2"});

%!test
%! str = '';
%! a = rand (10, 1);
%! b = char (round (65 + 20 * rand (10, 1)));
%! for k = 1:10
%!   str = sprintf ('%s %.6f %s\n', str, a (k), b (k));
%! endfor
%! [aa, bb] = strread (str, '%f %s');
%! assert (a, aa, 1e-5);
%! assert (cellstr (b), bb);

%!test
%! str = '';
%! a = rand (10, 1);
%! b = char (round (65 + 20 * rand (10, 1)));
%! for k = 1:10
%!   str = sprintf ('%s %.6f %s\n', str, a (k), b (k));
%! endfor
%! aa = strread (str, '%f %*s');
%! assert (a, aa, 1e-5);

%!test
%! str = sprintf ('/* this is\nacomment*/ 1 2 3');
%! a = strread (str, '%f', 'commentstyle', 'c');
%! assert (a, [1; 2; 3]);

%!test
%! str = sprintf ("Tom 100 miles/hr\nDick 90 miles/hr\nHarry 80 miles/hr");
%! fmt = "%s %f miles/hr";
%! c = cell (1, 2);
%! [c{:}] = strread (str, fmt);
%! assert (c{1}, {"Tom"; "Dick"; "Harry"})
%! assert (c{2}, [100; 90; 80])

%!test
%! a = strread ("a b c, d e, , f", "%s", "delimiter", ",");
%! assert (a, {"a b c"; "d e"; ""; "f"});

%!test
%! [a, b, c] = strread ("1,,2", "%s%s%s", "delimiter", ",");
%! assert (a{1}, '1');
%! assert (b{1}, '');
%! assert (c{1}, '2');

%!test
%! a = strread ("[SomeText]", "%s", "delimiter", "]");
%! assert (a{1}, "[SomeText");
%! assert (a{2}, '');

%!test
%! dat = "Data file.\r\n=  =  =  =  =\r\nCOMPANY    : <Company name>\r\n";
%! a = strread (dat, "%s", 'delimiter', "\n", 'whitespace', '', 'endofline', "\r\n");
%! assert (a{2}, "=  =  =  =  =");
%! assert (double (int8 (a{3}(end-5:end))), [32 110 97 109 101 62], 1e-5);

%!test
%! [a, b, c, d] = strread ("1,2,3,,5,6", "%d%d%d%d", 'delimiter', ',');
%! assert (c, 3, 1e-5);
%! assert (d, NaN);

%!test
%! [a, b, c, d] = strread ("1,2,3,,5,6\n", "%d%d%d%d", 'delimiter', ',');
%! assert (c, [3, NaN]', 1e-5);
%! assert (d, [NaN, NaN]');

%!test
%1 [a, b, c] = strread ("0.12 0.234 0.3567");
%1 assert (a, 0.12, 1e-5);
%1 assert (b, 0.234, 1e-5);
%1 assert (a, 0.3567, 1e-5);

%!test
%! [a, b] = strread('0.41 8.24 3.57 6.24 9.27', "%f%f", 2, 'delimiter', ' ');
%1 assert (a, [0.41 3.57]', 1e-5);
