查找、替换和提取字符串片断

处理字符串时,或许最常见的操作就是查找、替换以及提取片断。在本节中,我们将学习帮助解决这些问题的 MQL5 API 函数。这些函数的使用示例在 StringFindReplace.mq5 文件中进行了汇总。

int StringFind(string value, string wanted, int start = 0)

该函数在value 字符串中搜索 wanted 子串,从 start 位置开始搜索。如果找到该子串,则该函数将返回该子串的开始位置,并将该字符串中的字符从 0 开始编号。如果找不到,则该函数将返回 -1。两个参数均通过值传递,不仅允许处理变量,而且允许处理计算的中间结果(表达式、函数调用)。

搜索基于严格字符匹配执行,即区分大小写。如果要以区分大小写的方式搜索,必须首先使用 StringToLowerStringToUpper将源字符串转换为单大写或单小写。

我们尝试使用 StringFind 来统计文本中找到的目标子串的数量。为此,我们编写 CountSubstring 辅助函数,其将循环调用 StringFind 逐渐偏移最后参数 start 中的搜索起始位置。只要找到新的子串,循环就继续。

int CountSubstring(const string valueconst string wanted)
{
   // indent back because of the increment at the beginning of the loop
   int cursor = -1;
   int count = -1;
   do
   {
      ++count;
      ++cursor// search continues from the next position
      // get the position of the next substring, or -1 if there are no matches
      cursor = StringFind(valuewantedcursor);
   }
   while(cursor > -1);
   return count;
}

务必要注意,所提供的实现是查找能够重叠的子串。这是因为在开始查找下一个匹配结果之前会将当前位置加 1 (++cursor)。因此,当在字符串 "AAAAA" 中查找子串 "AAA" 时,将找到 3 个匹配结果。但实践中的搜索技术要求可能与此行为存在差异。特别是有一种实践,是在之前找到的片断结束的位置之后继续搜索。这种情况下就需要修改算法,使得游标以等于 StringLen(wanted) 的步长移动。

我们为 OnStart 函数中的不同自变量调用 CountSubstring

void OnStart()
{
   string abracadabra = "ABRACADABRA";
   PRT(CountSubstring(abracadabra"A"));    // 5
   PRT(CountSubstring(abracadabra"D"));    // 1
   PRT(CountSubstring(abracadabra"E"));    // 0
   PRT(CountSubstring(abracadabra"ABRA")); // 2
   ...
}

 

int StringReplace(string &variable, const string wanted, const string replacement)

该函数在 variable 字符串中,用 replacement 子串替换找到的所有 wanted 子串。

该函数返回完成的替换数,如果出错,则返回 -1。错误代码可通过调用函数 GetLastError来获得。这些错误尤其可能是内存不足错误,或者是使用了未初始化的字符串 (NULL) 作为自变量。variableswanted 参数必须是非零长度的字符串。

若为 replacement 自变量提供了空字符串 "",所有找到的 wanted 结果直接从原始字符串剪切。

如果没有进行替换,则函数结果为 0。

我们以 StringFindReplace.mq5 为例来观察 StringReplace 的实际效果。

   string abracadabra = "ABRACADABRA";
   ...
   PRT(StringReplace(abracadabra"ABRA""-ABRA-")); // 2
   PRT(StringReplace(abracadabra"CAD""-"));      // 1
   PRT(StringReplace(abracadabra"""XYZ"));      // -1, error
   PRT(GetLastError());      // 5040, ERR_WRONG_STRING_PARAMETER
   PRT(abracadabra);                              // '-ABRA---ABRA-'
   ...

接下来,使用 StringReplace 函数,我们尝试执行在处理任意文本的过程中遇到的其中一项任务。我们将确保某些分隔符始终用作单字符,即若干个连续分隔符须被替换为一个字符。通常这是指单词之间的空格,但在技术数据中可能有其它分隔符。我们在程序中测试分隔符 '-'。

我们用单独 NormalizeSeparatorsByReplace 函数实现该算法:

int NormalizeSeparatorsByReplace(string &valueconst ushort separator = ' ')
{
   const string single = ShortToString(separator);
   const string twin = single + single;
   int count = 0;
   int replaced = 0;
   do
   {
      replaced = StringReplace(valuetwinsingle);
      if(replaced > 0count += replaced;
   }
   while(replaced > 0);
   return count;
}

程序尝试在一个 do-while 循环中用单分隔符序列替换双分隔符序列,并且只要 StringReplace 函数返回大于 0 的值(即仍然有要替换的内容),则循环继续。函数返回进行替换的总次数。

OnStart 函数中,我们从文本中清除多个 '-' 字符。

   ...
   string copy1 = "-" + abracadabra + "-";
   string copy2 = copy1;
   PRT(copy1);                                    // '--ABRA---ABRA--'
   PRT(NormalizeSeparatorsByReplace(copy1, '-')); // 4
   PRT(copy1);                                    // '-ABRA-ABRA-'
   PRT(StringReplace(copy1"-"""));            // 1
   PRT(copy1);                                    // 'ABRAABRA'
   ...

 

int StringSplit(const string value, const ushort separator, string &result[])

该函数基于给定的分隔符将传递的 value 字符串分离为子串,并将它们放入 result 数组。该函数返回接收的子串数,如果出错,则返回 -1。

如果字符串中没有分隔符,则数组将具有一个等于整个字符串的元素。

如果源字符串为空或 NULL,则函数将返回 0。

为演示该函数的运算,我们使用 StringSplit 以新的方式解决前面的问题。为此,我们编写 NormalizeSeparatorsBySplit 函数。

int NormalizeSeparatorsBySplit(string &valueconst ushort separator = ' ')
{
   const string single = ShortToString(separator);
   
   string elements[];
   const int n = StringSplit(valueseparatorelements);
   ArrayPrint(elements); // debug
   
   StringFill(value0); // result will replace original string
   
   for(int i = 0i < n; ++i)
   {
      // empty strings mean delimiters, and we only need to add them
      // if the previous line is not empty (i.e. not a separator either)
      if(elements[i] == "" && (i == 0 || elements[i - 1] != ""))
      {
         value += single;
      }
      else // all other lines are joined together "as is"
      {
         value += elements[i];
      }
   }
   
   return n;
}

当源文本中先后出现分隔符,输出数组 StringSplit 中的对应元素最终为空字符串 ""。此外,如果文本以分隔符开始,则空字符串将位于数组的开头,而如果文本以分隔符结束,则空字符串位于数组末尾。

为获得“清除过的”文本,需要从数组添加所有非空字符串,以单分隔符“粘合”它们。此外,只有当数组中的空元素前一个元素也是非空时,才应将该空元素转换为分隔符

当然,这只是实现此功能的可能选项之一。我们在 OnStart 函数中检验。

   ...
   string copy2 = "-" + abracadabra + "-";        // '--ABRA---ABRA--'
   PRT(NormalizeSeparatorsBySplit(copy2, '-'));   // 8
   // debug output of split array (inside function):
   // ""     ""     "ABRA" ""     ""     "ABRA" ""     ""
   PRT(copy2);                                    // '-ABRA-ABRA-'

 

string StringSubstr(string value, int start, int length = -1)

该函数从传递的文本 value 中提取一个从指定位置 start 开始且长度为 length 的子串。开始位置可以从 0 到字符串长度减 1。如果 length 长度为 -1 或者大于从 start 到字符串结束的字符数,则字符串剩余部分将被完整提取。

该函数返回一个子串,如果参数不正确,则返回空字符串。

我们看看其工作方式。

   PRT(StringSubstr("ABRACADABRA"43));        // 'CAD'
   PRT(StringSubstr("ABRACADABRA"4100));      // 'CADABRA'
   PRT(StringSubstr("ABRACADABRA"4));           // 'CADABRA'
   PRT(StringSubstr("ABRACADABRA"100));         // ''