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

Пожалуй, наиболее востребованными операциями при работе со строками являются поиск и замена фрагментов, а также их извлечение. В данном разделе мы изучим функции 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 value, const string wanted)
{
   // делаем отступ назад из-за инкремента в начале цикла
   int cursor = -1;
   int count = -1;
   do
   {
      ++count;
      ++cursor; // поиск продолжаем со следующей позиции
      // получаем позицию следующей подстроки или -1, если совпадений нет
      cursor = StringFind(value, wanted, cursor);
   }
   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 &value, const ushort separator = ' ')
{
   const string single = ShortToString(separator);
   const string twin = single + single;
   int count = 0;
   int replaced = 0;
   do
   {
      replaced = StringReplace(value, twin, single);
      if(replaced > 0) count += 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 &value, const ushort separator = ' ')
{
   const string single = ShortToString(separator);
   
   string elements[];
   const int n = StringSplit(value, separator, elements);
   ArrayPrint(elements); // отладка
   
   StringFill(value, 0); // результат заменит исходную строку
   
   for(int i = 0; i < 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", 4, 3));        // 'CAD'
   PRT(StringSubstr("ABRACADABRA", 4, 100));      // 'CADABRA'
   PRT(StringSubstr("ABRACADABRA", 4));           // 'CADABRA'
   PRT(StringSubstr("ABRACADABRA", 100));         // ''