Работа с символами и кодовыми страницами

Поскольку строки состоят из символов, иногда нужно или просто удобнее манипулировать отдельными символами или группами символов в строке на уровне их целочисленных кодов. Например, требуется читать или заменять символы по одиночке, преобразовывать в массивы целочисленных кодов для передачи по коммуникационным протоколам или в программные интерфейсы сторонних динамических библиотек DLL. Во всех таких случаях передача строк в виде текста может быть сопряжена с различными сложностями:

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

Использование массивов с целочисленными кодами (что дает, фактически, двоичное, а не текстовое представление строки) упрощает решение этих проблем.

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

Напомним, что строки в MQL5 содержат символы в двухбайтовой кодировке Unicode. Это обеспечивает универсальную поддержку всего разнообразия национальных алфавитов в единой (но очень большой) таблице символов. Два байта позволяют закодировать 65535 элементов.

По умолчанию для символов используется тип ushort. Однако при необходимости строку можно конвертировать в последовательность однобайтовых символов uchar в конкретной языковой кодировке. Данная конвертация может сопровождаться потерей части информации (в частности, буквы, отсутствующие в локализованной таблице символов, могут "потерять" умляуты или вовсе "превратиться" в некий символ-заменитель: в зависимости от контекста он может отображаться по-разному, но обычно как '?' или "квадратик").

Во избежание проблем с текстами, в которых могут встречаться произвольные символы, рекомендуется всегда использовать Unicode. Исключение можно сделать, если некие внешние сервисы или программы, с которыми требуется интегрировать вашу MQL-программу, не поддерживают Unicode, или если текст заведомо предназначен для хранения ограниченного набора символов (например, только числа и латинские буквы).

При конвертации в/из однобайтовые символы MQL5 API по умолчанию применяет ANSI-кодировку, зависящую от текущих настроек Windows. Однако разработчик может задать другую кодовую таблицу (см. далее функции CharArrayToString, StringToCharArray).

Примеры использования описываемых далее функций приведены в файле StringSymbols.mq5.

bool StringSetCharacter(string &variable, int position, ushort character)

Функция изменяет в переданной строке variable символ с номером position на значение character. Номер должен быть в диапазоне от 0 до длины строки (StringLen) минус 1.

Если записываемый символ равен 0, он задает новое окончание строки (выступает в роли терминального нуля), то есть длина строки становится равной position. Размер буфера, выделенный под строку, при этом не меняется.

Если параметр position равен длине строки и записываемый символ не равен 0, то символ добавляется к строке и её длина увеличивается на 1. Это эквивалентно выражению: variable += ShortToString(character).

Функция возвращает true в случае успешного выполнения или false в случае ошибки.

void OnStart()
{
   string numbers = "0123456789";
   PRT(numbers);
   PRT(StringSetCharacter(numbers70));   // обрезаем на 7-м символе
   PRT(numbers);                             // 0123456
   PRT(StringSetCharacter(numbersStringLen(numbers), '*')); // добавляем '*'
   PRT(numbers);                             // 0123456*
   ...
}

 

ushort StringGetCharacter(string value, int position)

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

Функция эквивалентна записи с использованием оператора '[]': value[position].

   string numbers = "0123456789";
   PRT(StringGetCharacter(numbers5));      // 53 = код '5'
   PRT(numbers[5]);                          // 53 - то же самое

 

string CharToString(uchar code)

Функция преобразует ANSI-код символа в односимвольную строку. В зависимости от установленной кодовой страницы Windows, верхняя половина кодов (старше 127) может генерировать отличные буквы (отличается начертание символа, код остается одним и тем же). Например, символ с кодом 0xB8 (184 в десятичной системе) обозначает седиль (нижний крючок) в западноевропейских языках, а в русском здесь располагается буква 'ё'. Или вот еще пример:

   PRT(CharToString(0xA9));   // "©"
   PRT(CharToString(0xE6));   // "æ", "ж", или другой знак
                              //     в зависимости от вашей локали Windows

 

string ShortToString(ushort code)

Функция преобразует Unicode-код символа в односимвольную строку. В качестве параметра code можно использовать литерал или целое число. Например, греческая заглавная буква "сигма" (знак суммы в математических формулах) может быть указана как 0x3A3 или 'Σ'.

   PRT(ShortToString(0x3A3)); // "Σ"
   PRT(ShortToString('Σ'));   // "Σ"

 

int StringToShortArray(const string text, ushort &array[], int start = 0, int count = -1)

Функция преобразует строку в последовательность ushort-кодов символов, которая копируется в указанное место массива: начиная с элемента под номером start (по умолчанию — 0, то есть начало массива) и в количестве count.

Обратите внимание: параметр start относится к позиции в массиве, а не в строке. Если требуется сконвертировать часть строки, её необходимо предварительно извлечь с помощью функции StringSubstr.

Когда параметр count равен -1 (или WHOLE_ARRAY), копируются все символы до конца строки (включая терминальный ноль) или по размеру массива, если он фиксированного размера.

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

Чтобы скопировать символы без терминального нуля, следует явно указывать вызов StringLen в качестве аргумента count. В противном случае длина массива будет на 1 больше длины строки (и в последнем элементе — 0).

Функция возвращает количество скопированных символов.

   ...
   ushort array1[], array2[]; // динамические массивы
   ushort text[5];            // массив фиксированного размера
   string alphabet = "ABCDEАБВГД";
   // копируем с терминальным '0'
   PRT(StringToShortArray(alphabetarray1)); // 11
   ArrayPrint(array1); // 65   66   67   68   69 1040 1041 1042 1043 1044    0
   // копируем без терминального '0'
   PRT(StringToShortArray(alphabetarray20StringLen(alphabet))); // 10
   ArrayPrint(array2); // 65   66   67   68   69 1040 1041 1042 1043 1044
   // копируем в фиксированный массив
   PRT(StringToShortArray(alphabettext)); // 5
   ArrayPrint(text); // 65 66 67 68 69
   // копируем за прежние пределы массива
   // (элементы [11-19] будут случайными)
   PRT(StringToShortArray(alphabetarray220)); // 11
   ArrayPrint(array2);
   /*
   [ 0]    65    66    67    68    69  1040  1041  1042
         1043  1044     0     0     0     0     0 14245
   [16] 15102 37754 48617 54228    65    66    67    68
           69  1040  1041  1042  1043  1044     0
   */

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

string ShortArrayToString(const ushort &array[], int start = 0, int count = -1)

Функция преобразует часть массива с кодами символов в строку. Диапазон элементов массива задается параметрами start и count: соответственно начальной позицией и количеством. Параметр start должен быть в пределах от 0 до числа элементов в массиве минус 1. Когда count равно -1 (или WHOLE_ARRAY) копируются все элементы до конца массива или до первого нулевого.

Продолжая текущий пример из StringSymbols.mq5, попробуем преобразовать в строку массив array2, который имеет размер 30.

   ...
   string s = ShortArrayToString(array2030);
   PRT(s); // "ABCDEАБВГД", здесь могут появиться дополнительные случайные символы

Поскольку в массив array2 была дважды скопирована строка "ABCDEАБВГД", причем один раз — в самое начало, а второй раз — по смещению 20, промежуточные символы будут случайными и способны сформировать более длинную строку, чем получилось у нас.

int StringToCharArray(const string text, uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)

Функция преобразует строку text в последовательность однобайтовых символов, которая копируется в указанное место массива: начиная с элемента под номером start (по умолчанию — 0, то есть начало массива) и в количестве count. В процессе копирования производится конвертация символов из Unicode в выбранную кодовую страницу codepage — по умолчанию, CP_ACP, что означает язык операционной системы Windows (подробнее об этом — чуть ниже).

Когда параметр count равен -1 (или WHOLE_ARRAY), копируются все символы до конца строки (включая терминальный ноль) или по размеру массива, если он фиксированного размера.

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

Чтобы скопировать символы без терминального нуля, следует явно указывать вызов StringLen в качестве аргумента count.

Функция возвращает количество скопированных символов.

Перечень допустимых кодовых страниц для параметра codepage смотрите в документации. Вот некоторые из широко распространенных кодовых страниц стандарта ANSI:

Язык

Код

Центральноевропейский латинский

1250

Кириллица

1251

Западноевропейский латинский

1252

Греческий

1253

Турецкий

1254

Иврит

1255

Арабский

1256

Прибалтийский

1257

Таким образом, на компьютерах с западноевропейскими языками CP_ACP равна 1252, а, например, на русских — 1251.

В процессе конвертации некоторые символы могут быть преобразованы с потерей информации, поскольку таблица Unicode намного больше ANSI (в каждой таблице ANSI-кодов — 256 символов).

В связи с этим особую важность среди всех констант CP_*** имеет CP_UTF8. Она позволяет правильно сохранить национальные символы за счет кодирования с переменной длиной: результирующий массив по-прежнему хранит байты, но каждый национальный символ может занимать несколько байтов, записанных в особом формате. Из-за этого длина массива может быть существенно больше, чем длина строки. Кодировка UTF-8 широко используется в Интернете и различном программном обеспечении. Кстати говоря, UTF расшифровывается как Unicode Transformation Format, и существуют другие модификации, в частности UTF-16 и UTF-32.

Мы рассмотрим пример для StringToCharArray после того, как познакомимся с "обратной" функцией CharArrayToString: их работу необходимо демонстрировать в связке.

string CharArrayToString(const uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)

Функция преобразует массив с байтами или его часть в строку. Массив должен содержать символы в определенной кодировке. Диапазон элементов массива задается параметрами start и count: соответственно начальной позицией и количеством. Параметр start должен быть в пределах от 0 до числа элементов в массиве. Когда count равно -1 (или WHOLE_ARRAY) копируются все элементы до конца массива или до первого нулевого.

Посмотрим, как функции StringToCharArray и CharArrayToString работают с разными национальными символами при разных настройках кодовых страниц. Для этого подготовлен тестовый скрипт StringCodepages.mq5.

В качестве подопытных будут использованы две строки — на русском и немецком языках:

void OnStart()
{
   Print("Locales");
   uchar bytes1[], bytes2[];
 
   string german = "straßenführung";
   string russian = "Русский Текст";
   ...

Мы будем их копировать в массивы bytes1 и bytes2, а затем восстанавливать в строки.

Для начала преобразуем немецкий текст, применив европейскую кодовую страницу 1252.

   ...
   StringToCharArray(germanbytes10WHOLE_ARRAY1252);
   ArrayPrint(bytes1);
   // 115 116 114  97 223 101 110 102 252 104 114 117 110 103   0

На европейских копиях Windows это эквивалентно более простому вызову функции с параметрами по умолчанию, потому что там CP_ACP = 1252:

   StringToCharArray(germanbytes1);

Затем восстановим текст из массива с помощью следующего вызова и убедимся, что все совпадет с первоисточником:

   ...
   PRT(CharArrayToString(bytes10WHOLE_ARRAY1252));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,1252)='straßenführung'

Теперь попробуем преобразовать русский текст в той же европейской кодировке (или можно вызвать StringToCharArray(russian, bytes2) в среде Windows, где в качестве кодовой страницы по умолчанию CP_ACP стоит 1252):

   ...
   StringToCharArray(russianbytes20WHOLE_ARRAY1252);
   ArrayPrint(bytes2);
   // 63 63 63 63 63 63 63 32 63 63 63 63 63  0

Здесь уже видно, что во время конвертации возникла проблема, потому что 1252 не имеет кириллицы. Восстановление строки из массива наглядно показывает суть:

   ...
   PRT(CharArrayToString(bytes20WHOLE_ARRAY1252));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,1252)='??????? ?????'

Повторим опыт в условной русской среде, то есть преобразуем туда и обратно обе строки с использованием кириллической кодовой страницы 1251.

   ...
   StringToCharArray(russianbytes20WHOLE_ARRAY1251);
   // на русской Windows этот вызов эквивалентен более простому
   // StringToCharArray(russian, bytes2);
   // потому что CP_ACP = 1251
   ArrayPrint(bytes2); // в этот раз коды символов осмысленные
   // 208 243 241 241 234 232 233  32 210 229 234 241 242   0
   
   // восстановим строку и убедимся, что она совпадает с исходной
   PRT(CharArrayToString(bytes20WHOLE_ARRAY1251));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,1251)='Русский Текст'
   
   // и для немецкого текста...
   StringToCharArray(germanbytes10WHOLE_ARRAY1251);
   ArrayPrint(bytes1);
   // 115 116 114  97  63 101 110 102 117 104 114 117 110 103   0
   // если сравнить это содержимое bytes1 с предыдущим вариантом,
   // легко заметить, что пара символов пострадала; вот что было:
   // 115 116 114  97 223 101 110 102 252 104 114 117 110 103   0
   
   // восстановим строку, чтобы увидеть отличия наглядно:
   PRT(CharArrayToString(bytes10WHOLE_ARRAY1251));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,1251)='stra?enfuhrung'
   // испорченными оказались специфические немецкие символы

Таким образом, налицо хрупкость однобайтовых кодировок.

Наконец, задействуем кодировку CP_UTF8 для обеих тестовых строк. Эта часть примера будет стабильно работать вне зависимости от настроек Windows.

   ...
   StringToCharArray(germanbytes10WHOLE_ARRAYCP_UTF8);
   ArrayPrint(bytes1);
   // 115 116 114  97 195 159 101 110 102 195 188 104 114 117 110 103   0
   PRT(CharArrayToString(bytes10WHOLE_ARRAYCP_UTF8));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,CP_UTF8)='straßenführung'
   
   StringToCharArray(russianbytes20WHOLE_ARRAYCP_UTF8);
   ArrayPrint(bytes2);
   // 208 160 209 131 209 129 209 129 208 186 208 184 208 185
   //  32 208 162 208 181 208 186 209 129 209 130   0
   PRT(CharArrayToString(bytes20WHOLE_ARRAYCP_UTF8));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,CP_UTF8)='Русский Текст'

Обратите внимание, что обе строки в кодировке UTF-8 потребовали более длинных массивов, чем в ANSI. Причем массив с русским текстом стал фактически в 2 раза длиннее, потому что все буквы теперь занимают по 2 байта. Желающие могут найти в открытых источниках подробности о том, как именно устроена кодировка UTF-8. В контексте данной книги для нас важно, что MQL5 API предоставляет готовые функции для работы с ней.