Поиск, замена и извлечение фрагментов строк

Пожалуй, наиболее востребованными операциями при работе со строками являются поиск и замена фрагментов, а также их извлечение. В данном разделе мы изучим функции MQL5 API, которые помогут решить эти задачи. Примеры их использования сведены в файл StringFindReplace.mq5.

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

Функция выполняет поиск подстроки wanted в строке value, начиная с позиции start. Если подстрока найдена, функция вернет позицию, где она начинается, причем символы в строке нумеруются с 0. В противном случае функция вернет -1. Оба параметра передаются по значению, что позволяет обрабатывать не только переменные, но и промежуточные результаты вычислений (выражения, вызовы функций).

Поиск выполняется на строгое соответствие символов, то есть с учетом регистра. Если требуется искать без учета регистра, необходимо предварительно привести исходную строку к единому регистру посредством StringToLower или StringToUpper.

Попробуем подсчитать количество вхождений искомой подстроки в тексте с помощью StringFind. Для этого напишем вспомогательную функцию CountSubstring, которая будет в цикле вызывать StringFind, постепенно смещая стартовую позицию поиска в последнем параметре start. Цикл продолжается пока находятся новые вхождения подстроки.

int CountSubstring(const string valueconst string wanted)
{
   // делаем отступ назад из-за инкремента в начале цикла
   int cursor = -1;
   int count = -1;
   do
   {
      ++count;
      ++cursor// поиск продолжаем со следующей позиции
      // получаем позицию следующей подстроки или -1, если совпадений нет
      cursor = StringFind(valuewantedcursor);
   }
   while(cursor > -1);
   return count;
}

Важно отметить, что представленная реализация ищет подстроки с возможностью наложения. Это происходит из-за того, что текущая позиция изменяется на 1 (++cursor) перед поиском следующего вхождения. В результате, при поиске, например, подстроки "AAA" в строке "AAAAA" будет найдено 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 все найденные подстроки wanted на другую подстроку replacement.

Функция возвращает количество сделанных замен или -1 в случае ошибки. Код ошибки можно получить, вызвав функцию GetLastError. В частности, возможны ошибки нехватки памяти или использования в качестве аргумента неинициализированной строки (NULL). Параметры variable и wanted должны быть строками ненулевой длины.

Когда в качестве аргумента 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, ошибка
   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); // отладка
   
   StringFill(value0); // результат заменит исходную строку
   
   for(int i = 0i < n; ++i)
   {
      // пустые строки означают разделители, и мы их должны добавить только
      // в том случае, если предыдущая строка не пуста (т.е. не разделитель тоже)
      if(elements[i] == "" && (i == 0 || elements[i - 1] != ""))
      {
         value += single;
      }
      else // все прочие строки соединяются вместе "как есть"
      {
         value += elements[i];
      }
   }
   
   return n;
}

Когда разделители встречаются в исходном тексте один за другим, соответствующий элемент в выходном массиве StringSplit оказывается пустой строкой "". Также пустая строка будет в начале массива, если текст начинается с разделителя, и в конце массива, если текст заканчивается разделителем.

Чтобы получить "очищенный" текст, нужно сложить все непустые строки из массива, "склеив" их одиночными символами-разделителями. Причем в разделитель следует "превращать" только те пустые элементы, у которых предыдущий элемент массива не является также пустым.

Разумеется, это лишь один из возможных вариантов реализации данного функционала. Проверим его в функции OnStart.

   ...
   string copy2 = "-" + abracadabra + "-";        // '--ABRA---ABRA--'
   PRT(NormalizeSeparatorsBySplit(copy2, '-'));   // 8
   // отладочный вывод сплит-массива (внутри функции):
   // ""     ""     "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));         // ''