Сравнение строк

Для сравнения строк в MQL5 можно использовать стандартные операторы сравнения, в частности '==', '!=', '>', '<'. Все такие операторы производят сравнение посимвольно, с учетом регистра.

У каждого символа имеется код Unicode, представляющий собой целое типа ushort. Соответственно, сперва сравниваются коды первых символов двух строк, потом коды вторых, и так далее до первого несовпадения или конца одной из строк.

Например, строка "ABC" меньше чем "abc", потому что в таблице символов коды заглавных букв меньше, чем коды соответствующих строчных букв (уже на первом символе получим, что "A" < "a"). Если строки имеют совпадающие символы в начале, но одна из них длиннее другой, то более длинная считается большей ("ABCD" > "ABC").

Подобные отношения строк образуют лексикографический порядок, когда строка "A" меньше строки "Б" ("А" < "Б"), говорят что "А" предшествует "Б".

Для ознакомления с кодами символов вы можете использовать стандартное приложение Windows "Таблица символов". В ней символы расположены в порядке увеличения кодов. Помимо общей таблицы Unicode, включающей множество национальных языков, существуют кодовые страницы: таблицы стандарта ANSI с однобайтными кодами символов — они различаются для каждого языка или группы языков. Мы более подробно изучим данный вопрос в разделе Работа с символами и кодовыми страницами.

Начальная часть таблиц символов с кодами от 0 до 127 является одинаковой для всех языков и приведена в следующей таблице.

Таблица кодов символов ASCII

Таблица кодов символов ASCII

Для получения кода символа возьмите шестнадцатеричную цифру слева (номер строки, в которой расположен символ) и дополните её цифрой сверху (номер колонки, в которой расположен символ): в результате получится шестнадцатеричное число. Например, для '!' слева стоит 2, а сверху 1, значит код символа 0x21, или в десятичной системе 33.

Коды до 32 являются управляющими. Среди них, в частности, табуляция (код 0x9), перевод строки (line feed, 0xA) и возврат каретки (carriage return, 0xD).

Пара символов 0xD 0xA, следующих один за другим, используется в текстовых файлах Windows для перехода на новую строку. Мы знакомились с соответствующими литералами MQL5 в разделе Символьные типы: 0xA можно обозначать как '\n', а 0xD — как '\r'. Табуляция 0x9 также имеет своё представление: '\t'.

MQL5 API предоставляет функцию StringCompare, которая позволяет отключать учет регистра при сравнении строк.

int StringCompare(const string &string1, const string &string2, const bool case_sensitive = true)

Функция сравнивает две строки и возвращает одно из трех значений: +1, если первая строка "больше" второй; 0 — если строки "равны"; -1 — если первая строка "меньше" второй. Понятия "больше", "меньше" и "равно" зависят от параметра case_sensitive.

Когда параметр case_sensitive равен true (что соответствует умолчанию), сравнение производится с учетом регистра, причем заглавные буквы считаются больше аналогичных строчных. Это является обратным порядком по отношению к стандартному лексикографическому порядку согласно кодам символов.

При учете регистра функция StringCompare использует порядок заглавных и строчных букв, отличный от лексикографического. Например, мы знаем, что истинно отношение "A" < "a", в котором оператор '<' руководствуется кодами символов. Поэтому слова с заглавной буквы должны идти в гипотетическом словаре (массиве) раньше, чем слова с той же строчной буквы. Однако при сравнении "A" и "a" с помощью функции StringCompare("A", "a") мы получим +1, что означает "A" больше "a". Таким образом, в отсортированном словаре спереди будут идти слова, начинающиеся со строчных букв, и только после них — с заглавных.

Иными словами, функция ранжирует строки в алфавитном порядке. Но кроме того, в режиме включенной чувствительности к регистру, дополнительно действует правило: при наличии строк, отличающихся лишь регистром, те из них, что имеют заглавные буквы, следуют после своих аналогов со строчными буквами (на тех же позициях в слове).

Когда параметр case_sensitive равен false, регистр букв не учитывается, поэтому строки "A" и "a" равны, а функция возвращает 0.

Убедиться в разных результатах сравнения оператором и функцией StringCompare можно в помощью скрипта StringCompare.mq5.

void OnStart()
{
   PRT(StringCompare("A""a"));        // 1, что означает "A" > "a" (!)
   PRT(StringCompare("A""a"false)); // 0, что означает "A" == "a"
   PRT("A" > "a");                      // false,   "A" < "a"
   
   PRT(StringCompare("x","y"));         // -1, что означает "x" < "y"
   PRT("x" > "y");                      // false,    "x" < "y"
   ...
}

В разделе Шаблоны функций мы создали шаблонизированный алгоритм быстрой сортировки. Преобразуем его в шаблонный класс и используем для нескольких вариантов сортировки: с помощью операторов сравнения, а также функции StringCompare как с включенным, так и выключенным режимом учета регистра. Новый класс QuickSortT поместим в заголовочный файл QuickSortT.mqh и подключим его в тестовый скрипт StringCompare.mq5.

Программный интерфейс сортировки остался почти без изменений.

template<typename T>
class QuickSortT
{
public:
   void Swap(T &array[], const int iconst int j)
   {
      ...
   }
   
   virtual int Compare(T &aT &b)
   {
      return a > b ? +1 : (a < b ? -1 : 0);
   }
   
   void QuickSort(T &array[], const int start = 0int end = INT_MAX)
   {
      ...
         for(int i = starti <= endi++)
         {
            //if(!(array[i] > array[end]))
            if(Compare(array[i], array[end]) <= 0)
            {
               Swap(arrayipivot++);
            }
         }
      ...
   }
};

Основное отличие в том, что мы добавили виртуальный метод Compare, который по умолчанию содержит сравнение с помощью операторов '>' и '<', и возвращает +1, -1 или 0 по тому же принципу, что и StringCompare. Метод Compare используется теперь в методе QuickSort вместо простого сравнения и должен быть переопределен в классах-наследниках, для того чтобы задействовать функцию StringCompare или любой другой принцип сравнения величин.

В частности, в файле StringCompare.mq5 мы реализуем следующий класс-"компаратор", производный от QuickSortT<string>:

class SortingStringCompare : public QuickSortT<string>
{
   const bool caseEnabled;
public:
   SortingStringCompare(const bool sensitivity = true) :
      caseEnabled(sensitivity) { }
      
   virtual int Compare(string &astring &boverride
   {
      return StringCompare(abcaseEnabled);
   }
};

В конструктор передается 1 параметр, задающий признак сравнения строк с учетом (true) или без учета (false) регистра. Само сравнение строк производится в переопределенном виртуальном методе Compare, который вызывает функцию StringCompare с заданными аргументами и настройкой.

Для проверки работы сортировки нам нужен набор строк, сочетающий заглавные и строчные буквы. Мы можем его сгенерировать сами: достаточно разработать класс, который выполняет перестановки (с повторением) символов из предопределенного набора (алфавита) для заданной длины набора (строки). Например, можно ограничиться маленьким алфавитом "abcABC", то есть три начальных английских буквы в обоих регистрах, и генерировать из них все возможные строки длиной 2 символа.

Класс PermutationGenerator поставляется в файле PermutationGenerator.mqh и оставлен для самостоятельного изучения. Здесь приведем лишь его публичный интерфейс.

class PermutationGenerator
{
public:
   struct Result
   {
      int indices[]; // индексы элементов в каждой позиции набора, т.е.
   };                // например, номера букв "алфавита" в каждой позиции строки
   PermutationGenerator(const int lengthconst int elements);
   SimpleArray<Result> *run();
};

При создании объекта генератора необходимо задать длину генерируемых наборов length (в нашем случае это будет длина строк, то есть 2) и количество разных элементов, из которых будут составляться наборы (в нашем случае это количество уникальных букв, то есть 6). При таких входных данных должно получиться 6*6=36 вариантов строк.

Сам процесс выполняется методом run. Для возврата массива с результатами используется шаблонный класс SimpleArray, который мы рассматривали в разделе Шаблоны методов. В данном случае он параметризуется типом структуры Result.

Вызов генератора и фактическое создание строк в соответствии с полученным от него массивом перестановок (в виде индексов букв на каждой позиции для всех возможных строк) производится во вспомогательной функции GenerateStringList.

void GenerateStringList(const string symbolsconst int lenstring &result[])
{
   const int n = StringLen(symbols); // длина алфавита, уникальные символы
   PermutationGenerator g(lenn);
   SimpleArray<PermutationGenerator::Result> *r = g.run();
   ArrayResize(resultr.size());
   // цикл по всем полученным перестановкам символов
   for(int i = 0i < r.size(); ++i)
   {
      string element;
      // цикл по всем знакоместам в строке
      for(int j = 0j < len; ++j)
      {
         // добавляем букву из алфавита (по её индексу) к строке
         element += ShortToString(symbols[r[i].indices[j]]);
      }
      result[i] = element;
   }
}

Здесь используется несколько ещё незнакомых нам функций (ArrayResize, ShortToString), но скоро мы до них доберемся. Пока достаточно будет знать, что функция ShortToString по коду символа типа ushort возвращает строку, состоящую из этого одного символа. С помощью оператора '+=' мы состыковываем каждую результирующую строку из таких односимвольных строк. Напомним, что для строк определен оператор [], поэтому выражение symbols[k] вернет k-й символ строки symbols. Разумеется, k может быть в свою очередь целочисленным выражением, и здесь оно — r[i].indices[j] — обращается к i-му элементу массива r, из которого читается индекс символа "алфавита" для j-ой позиции строки.

Каждая полученная строка сохраняется в массив-параметр result.

Настало время применить эти наработки в функции OnStart.

void OnStart()
{
   ...
   string messages[];
   GenerateStringList("abcABC"2messages);
   Print("Original data["ArraySize(messages), "]:");
   ArrayPrint(messages);
   
   Print("Default case-sensitive sorting:");
   QuickSortT<stringsorting;
   sorting.QuickSort(messages);
   ArrayPrint(messages);
   
   Print("StringCompare case-insensitive sorting:");
   SortingStringCompare caseOff(false);
   caseOff.QuickSort(messages);
   ArrayPrint(messages);
   
   Print("StringCompare case-sensitive sorting:");
   SortingStringCompare caseOn(true);
   caseOn.QuickSort(messages);
   ArrayPrint(messages);
}

Скрипт сначала получает все варианты строк в массив messages, а затем сортирует его в 3 режимах: с помощью встроенных операторов сравнения, с помощью функции StringCompare без учета регистра, и с помощью неё же, но уже с учетом регистра.

Мы получим следующий вывод в журнал:

Original data[36]:
[ 0] "aa" "ab" "ac" "aA" "aB" "aC" "ba" "bb" "bc" "bA" "bB" "bC" "ca" "cb" "cc" "cA" "cB" "cC"
[18] "Aa" "Ab" "Ac" "AA" "AB" "AC" "Ba" "Bb" "Bc" "BA" "BB" "BC" "Ca" "Cb" "Cc" "CA" "CB" "CC"
Default case-sensitive sorting:
[ 0] "AA" "AB" "AC" "Aa" "Ab" "Ac" "BA" "BB" "BC" "Ba" "Bb" "Bc" "CA" "CB" "CC" "Ca" "Cb" "Cc"
[18] "aA" "aB" "aC" "aa" "ab" "ac" "bA" "bB" "bC" "ba" "bb" "bc" "cA" "cB" "cC" "ca" "cb" "cc"
StringCompare case-insensitive sorting:
[ 0] "AA" "Aa" "aA" "aa" "AB" "aB" "Ab" "ab" "aC" "AC" "Ac" "ac" "BA" "Ba" "bA" "ba" "BB" "bB"
[18] "Bb" "bb" "bC" "BC" "Bc" "bc" "CA" "Ca" "cA" "ca" "CB" "cB" "Cb" "cb" "cC" "CC" "Cc" "cc"
StringCompare case-sensitive sorting:
[ 0] "aa" "aA" "Aa" "AA" "ab" "aB" "Ab" "AB" "ac" "aC" "Ac" "AC" "ba" "bA" "Ba" "BA" "bb" "bB"
[18] "Bb" "BB" "bc" "bC" "Bc" "BC" "ca" "cA" "Ca" "CA" "cb" "cB" "Cb" "CB" "cc" "cC" "Cc" "CC"

Легко увидеть, каким именно образом отличаются все 3 режима.