
(********************************************************************)
(*                                                                  *)
(*  string.s7i    String support library                            *)
(*  Copyright (C) 1989 - 2013  Thomas Mertes                        *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library 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 Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


const proc: (ref string: dest) ::= (in string: source)           is action "STR_CREATE";

const string: (attr string) . value is "";

const proc: (inout string: dest) := (in string: source)          is action "STR_CPY";


(**
 *  String multiplication.
 *  The string ''stri'' is concatenated to itself such that in total
 *  ''factor'' strings are concatenated.
 *   "LA" mult 3     returns "LALALA"
 *   "WORD" mult 0   returns ""
 *  @return the result of the string multiplication.
 *  @exception RANGE_ERROR If the factor is negative.
 *)
const func string: (in string: stri) mult (in integer: factor)   is action "STR_MULT";


(**
 *  Pad a ''string'' with spaces at the left side up to a given length.
 *  @return the ''string'' left padded with spaces.
 *)
const func string: (in string: stri) lpad (in integer: length)   is action "STR_LPAD";


(**
 *  Pad a ''string'' with zeroes at the left side up to a given length.
 *  @return the ''string'' left padded with zeroes.
 *)
const func string: (in string: stri) lpad0 (in integer: length)  is action "STR_LPAD0";


(**
 *  Pad a ''string'' with spaces at the right side up to a given length.
 *  @return the ''string'' right padded with spaces.
 *)
const func string: (in string: stri) rpad (in integer: length)   is action "STR_RPAD";


(**
 *  Concatenate two strings.
 *  This operator is intended for normal expressions. Its parameters
 *  are not converted to ''string''.
 *  @return the result of the concatenation.
 *)
const func string: (in string: stri1) & (in string: stri2)       is action "STR_CAT";


(**
 *  Concatenate two strings.
 *  This operator is intended for write statements. The functions
 *  [[enable_io#enable_io(in_type)|enable_io]] respectively
 *  [[enable_output#enable_output(in_type)|enable_output]] overload
 *  the <& operator for many types. This overloaded operators
 *  optionally convert parameters to ''string''.
 *  @return the result of the concatenation.
 *)
const func string: (in string: stri1) <& (in string: stri2)      is action "STR_CAT";


(**
 *  Get a character, identified by an index, from a ''string''.
 *  The first character has the index 1.
 *  @return the character specified with the index.
 *  @exception INDEX_ERROR If the index is less than 1 or
 *             greater than the length of the ''string''.
 *)
const func char: (in string: stri) [ (in integer: index) ]       is action "STR_IDX";


(**
 *  Get a substring beginning at a start position.
 *  The first character in a ''string'' has the position 1.
 *  @return the substring beginning at the start position.
 *  @exception INDEX_ERROR The start position is negative or zero.
 *  @exception MEMORY_ERROR Not enough memory to represent the result.
 *)
const func string: (in string: stri) [ (in integer: start) .. ]  is action "STR_TAIL";


(**
 *  Get a substring ending at a stop position.
 *  The first character in a ''string'' has the position 1.
 *  @return the substring ending at the stop position.
 *  @exception INDEX_ERROR The stop position is negative.
 *  @exception MEMORY_ERROR Not enough memory to represent the result.
 *)
const func string: (in string: stri) [ .. (in integer: stop) ]   is action "STR_HEAD";


(**
 *  Get a substring from a start position to a stop position.
 *  The first character in a ''string'' has the position 1.
 *  @return the substring from position start to stop.
 *  @exception INDEX_ERROR The start position is negative or zero, or
 *                         the stop position is less than pred(start).
 *  @exception MEMORY_ERROR Not enough memory to represent the result.
 *)
const func string: (in string: stri) [ (in integer: start) ..
                                       (in integer: stop) ]      is action "STR_RANGE";


(**
 *  Get a substring from a start position with a given length.
 *  The first character in a ''string'' has the position 1.
 *  @return the substring from the start position with a given length.
 *  @exception INDEX_ERROR The start position is negative or zero, or
 *                         the length is negative.
 *  @exception MEMORY_ERROR Not enough memory to represent the result.
 *)
const func string: (in string: stri) [ (in integer: start) len
                                       (in integer: length) ]    is action "STR_SUBSTR";


(**
 *  Append the string ''extension'' to ''destination''.
 *  @exception MEMORY_ERROR Not enough memory for the concatenated
 *             string.
 *)
const proc: (inout string: destination) &:= (in string: extension)   is action "STR_APPEND";


(**
 *  Append the char ''extension'' to ''destination''.
 *  @exception MEMORY_ERROR Not enough memory for the concatenated
 *             string.
 *)
const proc: (inout string: destination) &:= (in char: extension)     is action "STR_PUSH";


(**
 *  Assign char ''source'' to the ''position'' of the ''destination''.
 *   A @:= [B] C;
 *  is equivalent to
 *   A := A[..pred(B)] & str(C) & A[succ(B)..];
 *  @exception INDEX_ERROR If ''position'' is negative or zero, or
 *             a character beyond ''destination'' would be overwritten
 *             (''position'' > length(''destination'') holds).
 *)
const proc: (inout string: destination) @:= [ (in integer: position) ] (in char: source) is action "STR_ELEMCPY";


(**
 *  Assign string ''source'' to the ''position'' of the ''destination''.
 *   A @:= [B] C;
 *  is equivalent to
 *   A := A[..pred(B)] & C & A[B+length(C)..];
 *  @exception INDEX_ERROR If ''position'' is negative or zero, or
 *             if ''destination'' is smaller than ''source'', or
 *             characters beyond ''destination'' would be overwritten
 *             (''position'' + length(''source'') > succ(length(''destination''))
 *             holds).
 *)
const proc: (inout string: destination) @:= [ (in integer: position) ] (in string: source) is action "STR_POSCPY";


(**
 *  Check if two strings are equal.
 *  @return TRUE if both strings are equal,
 *          FALSE otherwise.
 *)
const func boolean: (in string: stri1) = (in string: stri2)      is action "STR_EQ";


(**
 *  Check if two strings are not equal.
 *  @return FALSE if both strings are equal,
 *          TRUE otherwise.
 *)
const func boolean: (in string: stri1) <> (in string: stri2)     is action "STR_NE";


(**
 *  Check if stri1 is less than stri2.
 *  @return TRUE if stri1 is less than stri2,
 *          FALSE otherwise.
 *)
const func boolean: (in string: stri1) < (in string: stri2)      is action "STR_LT";


(**
 *  Check if stri1 is greater than stri2.
 *  @return TRUE if stri1 is greater than stri2,
 *          FALSE otherwise.
 *)
const func boolean: (in string: stri1) > (in string: stri2)      is action "STR_GT";


(**
 *  Check if stri1 is less than or equal to stri2.
 *  @return TRUE if stri1 is less than or equal to stri2,
 *          FALSE otherwise.
 *)
const func boolean: (in string: stri1) <= (in string: stri2)     is action "STR_LE";


(**
 *  Check if stri1 is greater than or equal to stri2.
 *  @return TRUE if stri1 is greater than or equal to stri2,
 *          FALSE otherwise.
 *)
const func boolean: (in string: stri1) >= (in string: stri2)     is action "STR_GE";


(**
 *  Compare two strings.
 *  @return -1, 0 or 1 if the first argument is considered to be
 *          respectively less than, equal to, or greater than the
 *          second.
 *)
const func integer: compare (in string: stri1, in string: stri2) is action "STR_CMP";


(**
 *  Compute the hash value of a ''string''.
 *  @return the hash value.
 *)
const func integer: hashCode (in string: stri)                   is action "STR_HASHCODE";


(**
 *  Determine the length of a ''string''.
 *  @return the length of the ''string''.
 *)
const func integer: length (in string: stri)                     is action "STR_LNG";


(**
 *  Determine if a string starts with a ''prefix''.
 *   startsWith("tmp_s7c.c", "tmp_")  returns TRUE
 *   startsWith("example", "E")       returns FALSE
 *  @return TRUE if ''stri'' starts with ''prefix'',
 *          FALSE otherwise.
 *)
const func boolean: startsWith (in string: stri, in string: prefix) is
  return stri[.. length(prefix)] = prefix;


(**
 *  Determine if a string ends with a ''suffix''.
 *   endsWith("hello.sd7", ".sd7")  returns TRUE
 *   endsWith("A string", "\0;")    returns FALSE
 *  @return TRUE if ''stri'' ends with ''suffix'',
 *          FALSE otherwise.
 *)
const func boolean: endsWith (in string: stri, in string: suffix) is
  return length(stri) >= length(suffix) and
         stri[succ(length(stri) - length(suffix)) ..] = suffix;


(**
 *  Check if ''stri'' has the ''searched'' characters starting from ''index''.
 *   equalAtIndex("The quick brown fox", "quick", 5)  returns TRUE
 *   equalAtIndex("axis", "xi", 3)                    returns FALSE
 *  @return TRUE if ''stri'' has the ''searched'' characters starting from ''index'',
 *          FALSE otherwise.
 *)
const func boolean: equalAtIndex (in string: stri, in string: searched, in integer: index) is
  return stri[index len length(searched)] = searched;


(**
 *  Determine leftmost position of string ''searched'' in ''mainStri''.
 *  If the string is found the position of its first character
 *  is the result. The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched''.
 *)
const func integer: pos (in string: mainStri, in string: searched)  is action "STR_POS";


(**
 *  Determine leftmost position of char ''searched'' in ''mainStri''.
 *  The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched''.
 *)
const func integer: pos (in string: mainStri, in char: searched)    is action "STR_CHPOS";


(**
 *  Search string ''searched'' in ''mainStri'' at or after ''fromIndex''.
 *  The search starts at ''fromIndex'' and proceeds to the right.
 *  If the string is found the position of its first character
 *  is the result. The first character in a string has the position 1.
 *  The pos function is designed to allow loops like:
 *
 *   index := pos(stri, searchedStri);
 *   while index <> 0 do
 *     # Do something with index
 *     index := pos(stri, searchedStri, succ(index));
 *   end while;
 *
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched'' at or after ''fromIndex''.
 *  @exception RANGE_ERROR If ''fromIndex'' <= 0 holds.
 *)
const func integer: pos (in string: mainStri, in string: searched,
                         in integer: fromIndex)                     is action "STR_IPOS";


(**
 *  Search char ''searched'' in ''mainStri'' at or after ''fromIndex''.
 *  The search starts at ''fromIndex'' and proceeds to the right.
 *  The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched'' at or after ''fromIndex''.
 *  @exception RANGE_ERROR If ''fromIndex'' <= 0 holds.
 *)
const func integer: pos (in string: mainStri, in char: searched,
                         in integer: fromIndex)                     is action "STR_CHIPOS";


(**
 *  Determine rightmost position of string ''searched'' in ''mainStri''.
 *  If the string is found the position of its first character
 *  is the result. The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched''.
 *)
const func integer: rpos (in string: mainStri, in string: searched) is action "STR_RPOS";


(**
 *  Determine rightmost position of char ''searched'' in ''mainStri''.
 *  The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched''.
 *)
const func integer: rpos (in string: mainStri, in char: searched)   is action "STR_RCHPOS";


(**
 *  Search string ''searched'' in ''mainStri'' at or before ''fromIndex''.
 *  The search starts at ''fromIndex'' and proceeds to the left.
 *  The first character in a string has the position 1.
 *  The rpos function is designed to allow loops like:
 *
 *   index := rpos(stri, searchedStri);
 *   while index <> 0 do
 *     # Do something with index
 *     index := rpos(stri, searchedStri, pred(index));
 *   end while;
 *
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched'' at or before ''fromIndex''.
 *  @exception RANGE_ERROR If ''fromIndex'' > length(stri) holds.
 *)
const func integer: rpos (in string: mainStri, in string: searched,
                          in integer: fromIndex)                    is action "STR_RIPOS";


(**
 *  Search char ''searched'' in ''mainStri'' at or before ''fromIndex''.
 *  The search starts at ''fromIndex'' and proceeds to the left.
 *  The first character in a string has the position 1.
 *  @return the position of ''searched'' or 0 if ''mainStri''
 *          does not contain ''searched'' at or before ''fromIndex''.
 *  @exception RANGE_ERROR If ''fromIndex'' > length(stri) holds.
 *)
const func integer: rpos (in string: mainStri, in char: searched,
                          in integer: fromIndex)                    is action "STR_RCHIPOS";


(* const proc: count (in string: mainStri, in string: searched)       is action "STR_CNT"; *)


(**
 *  Replace occurrences of ''searched'' in ''mainStri'' by ''replacement''.
 *  The function processes ''mainStri'' from left to right and replaces
 *  ''searched'' by ''replacement''. After a ''searched'' has been replaced
 *  the search for the next ''searched'' starts after the ''replacement''.
 *  If a replacement creates new occurrences of ''searched'' they are left
 *  intact.
 *   replace("old gold", "old", "one")        returns "one gone"
 *   replace("it   is very  low", "  ", " ")  returns "it  is very low"
 *   replace("balll", "all", "al")            returns "ball"
 *   replace("faaaaceeees", "aacee", "ace")   returns "faaaceees"
 *  @return the result of the replacement.
 *)
const func string: replace (in string: mainStri, in string: searched,
                            in string: replacement)                 is action "STR_REPL";


(**
 *  Replace one occurrence of ''searched'' in ''mainStri'' by ''replacement''.
 *  If there is no occurrence of ''searched'' return the unchanged ''mainStri''.
 *  @return the result of the replacement.
 *)
const func string: replace1 (in string: mainStri, in string: searched,
    in string: replacement) is func
  result
    var string: replaced is "";
  local
    var integer: pos is 0;
  begin
    pos := pos(mainStri, searched);
    if pos <> 0 then
      replaced := mainStri[.. pred(pos)] & replacement & mainStri[pos + length(searched) ..];
    else
      replaced := mainStri;
    end if;
  end func;


(**
 *  Replace all occurrences of ''searched'' in ''mainStri'' by ''replacement''.
 *  The function processes ''mainStri'' from left to right and replaces
 *  ''searched'' by ''replacement''. If a replacement creates new occurrences
 *  of ''searched'' they are replaced also.  This can be used to replace
 *  multiple occurrences of a character by one occurrence
 *   replaceN("//path///file", "//", "/")      returns "/path/file"
 *   replaceN("it   is very  low", "  ", " ")  returns "it is very low"
 *   replaceN("balll", "all", "al")            returns "bal"
 *   replaceN("faaaaceeees", "aacee", "ace")   returns "faces"
 *  @return the result of the replacement.
 *  @exception MEMORY_ERROR If ''searched'' is a substring of ''replacement''.
 *)
const func string: replaceN (in string: mainStri, in string: searched,
                             in string: replacement) is func
  result
    var string: replaced is "";
  local
    var integer: length is 0;
  begin
    if length(replacement) < length(searched) then
      length := length(mainStri);
      replaced := replace(mainStri, searched, replacement);
      while length(replaced) <> length do
        length := length(replaced);
        replaced := replace(replaced, searched, replacement);
      end while;
    elsif length(replacement) > length(searched) then
      if pos(replacement, searched) <> 0 then
        if pos(mainStri, searched) <> 0 then
          raise MEMORY_ERROR;
        else
          replaced := mainStri;
        end if;
      else
        length := length(mainStri);
        replaced := replace(mainStri, searched, replacement);
        while length(replaced) <> length do
          length := length(replaced);
          replaced := replace(replaced, searched, replacement);
        end while;
      end if;
    elsif searched = replacement then
      replaced := mainStri;
    else
      replaced := replace(mainStri, searched, replacement);
      while pos(replaced, searched) <> 0 do
        replaced := replace(replaced, searched, replacement);
      end while;
    end if;
  end func;


(**
 *  Replace occurrences of ''search1'' followed by ''search2'' with ''replacement''.
 *  Searches ''mainStri'' for ''search1'' followed by ''search2''. The
 *  characters from the beginning of ''search1'' to the end of ''search2''
 *  are replaced by ''replacement''. There can be zero or more characters
 *  between ''search1'' and ''search2''. With ''replace2'' unnested comments
 *  can be removed:
 *   replace2("x := (*ord*) y;", "(*", "*)", "")  returns "x :=  y;"
 *  @return the result of the replacement.
 *)
const func string: replace2 (in string: mainStri, in string: search1,
    in string: search2, in string: replacement) is func
  result
    var string: replaced is "";
  local
    var integer: startPos is 0;
    var integer: endPos is 1;
  begin
    startPos := pos(mainStri, search1);
    while startPos <> 0 do
      replaced &:= mainStri[endPos .. pred(startPos)];
      endPos := pos(mainStri, search2, startPos + length(search1));
      if endPos <> 0 then
        replaced &:= replacement;
        endPos +:= length(search2);
        startPos := pos(mainStri, search1, endPos);
      else
        endPos := startPos;
        startPos := 0;
      end if;
    end while;
    replaced &:= mainStri[endPos ..];
  end func;


(**
 *  Convert a string to upper case.
 *  The conversion uses the default Unicode case mapping,
 *  where each character is considered in isolation.
 *  Characters without case mapping are left unchanged.
 *  The mapping is independent from the locale. Individual
 *  character case mappings cannot be reversed, because some
 *  characters have multiple characters that map to them.
 *  @return the string converted to upper case.
 *)
const func string: upper (in string: stri)                     is action "STR_UP";


(**
 *  Convert a string to lower case.
 *  The conversion uses the default Unicode case mapping,
 *  where each character is considered in isolation.
 *  Characters without case mapping are left unchanged.
 *  The mapping is independent from the locale. Individual
 *  character case mappings cannot be reversed, because some
 *  characters have multiple characters that map to them.
 *  @return the string converted to lower case.
 *)
const func string: lower (in string: stri)                     is action "STR_LOW";


(**
 *  Number of screen columns occupied by the Unicode string ''stri''.
 *  The width of single characters can be 0,1 or 2 depending on the
 *  width occupied on a terminal. Non-spacing characters and control
 *  characters have width of 0. Kanji and other full width characters
 *  have a width of 2.
 *  @return the sum of the character widths in ''stri''.
 *)
const func integer: width (in string: stri) is forward;


(**
 *  Reverse the characters in a string.
 *   reverse("ABC")  returns "CBA"
 *  @return the reversed string.
 *)
const func string: reverse (in string: stri) is forward;


(**
 *  Return string with leading and trailing whitespace omitted.
 *  All characters less than or equal to ' ' (space) count as whitespace.
 *   trim(" /n xyz /r")  returns "xyz"
 *  @return string with leading and trailing whitespace omitted.
 *)
const func string: trim (in string: stri)                      is action "STR_TRIM";


(**
 *  Return string with leading whitespace omitted.
 *  All characters less than or equal to ' ' (space) count as whitespace.
 *   ltrim(" /n xyz /r")  returns "xyz /r"
 *  @return string with leading whitespace omitted.
 *)
const func string: ltrim (in string: stri)                     is action "STR_LTRIM";


(**
 *  Return string with trailing whitespace omitted.
 *  All characters less than or equal to ' ' (space) count as whitespace.
 *   rtrim(" /n xyz /r")  returns " /n xyz"
 *  @return string with trailing whitespace omitted.
 *)
const func string: rtrim (in string: stri)                     is action "STR_RTRIM";


(**
 *  Trim a string such that it can be converted to ''aType''.
 *  Removes leading and trailing spaces from ''stri''.
 *  This function is overloaded for types where removing leading
 *  or trailing whitespace would change the value.
 *   integer(trimValue(integer, " 1 "))        returns 1
 *   integer parse trimValue(integer, " 1 ")   returns 1
 *  @return the trimmed string.
 *)
const func string: trimValue (in type: aType, in string: stri) is
  return trim(stri);


(**
 *  Trim a string such that it can be converted to ''string''.
 *  Leaves ''stri'' unchanged, since that would change the value.
 *   string(trimValue(string, " 1 "))        returns " 1 "
 *   string parse trimValue(string, " 1 ")   returns " 1 "
 *  @return the unchanged string.
 *)
const func string: trimValue (attr string, in string: stri) is
  return stri;


(**
 *  Convert to a string.
 *  @return its parameter unchanged.
 *)
const func string: str (in string: stri)                       is action "STR_STR";


const func string: literal (in string: stri)                   is action "STR_LIT";
const func string: c_literal (in string: stri)                 is action "STR_CLIT";


const func string: getint (inout string: stri) is func
  result
    var string: digits is "";
  local
    var integer: leng is 0;
    var integer: pos is 1;
  begin
    leng := length(stri);
    while pos <= leng and stri[pos] >= '0' and stri[pos] <= '9' do
      incr(pos);
    end while;
    digits := stri[.. pred(pos)];
    stri := stri[pos ..];
  end func;


const func string: gets (inout string: stri, in integer: maxLength) is func
  result
    var string: striRead is "";
  begin
    striRead := stri[.. maxLength];
    stri := stri[succ(length(striRead)) ..];
  end func;


(**
 *  Convert to a string.
 *  @return its parameter unchanged.
 *)
const func string: string (in string: stri) is
  return stri;


(**
 *  Convert to a string.
 *  @return its parameter unchanged.
 *)
const func string: (attr string) parse (in string: stri) is
  return stri;


DECLARE_TERNARY(string);
