Универсальный форматированный вывод данных в строку

При формировании строки для показа пользователю, сохранения в файл или передачи в Интернет может потребоваться включить в нее значения нескольких переменных разных типов. Эту задачу можно решить явным приведением всех переменных к типу (string) и сложением получившихся строк, но в этом случае инструкция MQL-кода окажется длинной и трудной для понимания. Вероятно, более удобным было бы применение функции StringConcatenate, но этот способ не решает проблему целиком.

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

Для этой задачи существует специальное решение: функция StringFormat.

По такому же принципу действует другая функция MQL5 API: PrintFormat.

string StringFormat(const string format, ...)

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

Взаимодействие форматной строки и аргументов StringFormat

Взаимодействие форматной строки и аргументов StringFormat

Каждое место вставки переменной в строку отмечается форматным спецификатором: символом '%', после которого может указываться несколько настроек.

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

Шаблон может не содержать ни одного спецификатора, то есть быть простой строкой. В таком случае в функцию нужно передать дополнительный фиктивный аргумент помимо самой строки (аргумент не будет помещен в строку).

Если в шаблоне требуется вывести знак процента, то следует написать его два раза подряд %%. Если знак % не задвоен, то несколько последующих символов после него всегда анализируется как спецификатор.

Обязательным атрибутом спецификатора является некий символ, обозначающий ожидаемый тип и интерпретацию очередного аргумента функции. Назовем этот символ условно T. Тогда в простейшем случае один спецификатор формата выглядит как %T.

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

%[Z][W][.P][M]T

Каждое поле выполняет свою функцию и принимает одно из разрешенных значений. Далее мы постепенно затронем все поля.

Тип T

Для целых чисел в качестве T могут применяться следующие символы, с пояснением как соответствующие им числа отображаются в строке:

  • c — Unicode символ
  • С — ANSI символ
  • d, i — знаковое десятичное
  • o — беззнаковое восьмеричное
  • u — беззнаковое десятичное
  • x — беззнаковое шестнадцатеричное (строчные буквы)
  • X — беззнаковое шестнадцатеричное (заглавные буквы)

Напомним, что по способу внутреннего хранения данных к целым типам также относятся встроенные типы MQL5 datetime, color, bool и перечисления.

Для вещественных чисел в качестве T применимы следующие символы:

  • e — научный формат с показателем (маленькая 'e')
  • E — научный формат с показателем (большая 'E')
  • f — обычный формат
  • g — аналог f или e (выбирается наиболее компактный вид)
  • G — аналог f или E (выбирается наиболее компактный вид)
  • a — научный формат с показателем, шестнадцатеричное (строчные буквы)
  • A — научный формат с показателем, шестнадцатеричное (заглавные буквы)

Наконец, для строк доступен всего один вариант символа T: s.

Размер целых чисел M

Для целых типов дополнительно можно явным образом указать размер переменной в байтах, если перед T поставить один из следующих знаков или их комбинаций (мы обобщили их под буквой M):

  • h — 2 байта (short, ushort)
  • l (маленькая L) — 4 байта (int, uint)
  • I32 (большая i) — 4 байта (int, uint)
  • ll (две мелких L) — 8 байт (long)
  • I64 (большая i) — 8 байт (long, ulong)

Ширина W

Поле W — это неотрицательное десятичное число, которое задает минимальное количество знакомест, выделяемых под отформатированное значение. Если значение переменной укладывается в меньшее количество символов, то слева или справа добавляется соответствующее количество пробелов. Левая или правая сторона выбирается в зависимости от выравнивания (см. далее флаг '–' в поле Z). При наличии флага '0' перед выводимым значением добавляется соответствующее количество нулей. Если число выводимых символов больше заданной ширины, то настройка ширины игнорируется и выводимое значение не усекается.

Если в качестве ширины указана звездочка '*', то в списке передаваемых параметров, на предыдущей позиции перед форматируемой переменной должно находиться значение типа int, которое будет использовано в качестве ширины выводимого значения.

Точность P

Поле P тоже содержит неотрицательное десятичное число и всегда предваряется точкой '.'. Для целочисленных T данное поле задает минимальное количество значащих цифр. Если значение укладывается в меньшее количество цифр, оно предваряется нулями.

Для вещественных чисел P задает количество знаков в дробной части (по умолчанию — 6), за исключением спецификаторов g и G, для которых P — это общее количество значащих цифр (в мантиссе и дробной части).

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

Если в качестве точности указана звездочка '*', она обрабатывается по такому же принципу, как и для ширины, но управляет точностью.

Фиксированная ширина и/или точность, вместе с выравниванием по правому краю, позволяют выводить значения аккуратным столбиком.

Флаги Z

Наконец поле Z описывает флаги:

  • - (минус) — выравнивание по левому краю в пределах заданной ширины (в отсутствие флага делается выравнивание по правому краю);
  • + (плюс) — безусловный вывод знака '+' или '-' перед значением (без этого флага '-' отображается только для отрицательных значений);
  • 0 — перед выводимым значением добавляются нули, если оно меньше заданной ширины;
  • (пробел) — перед выводимым значением ставится пробел, если оно является знаковым и положительным;
  • # — управляет отображением префиксов восьмеричной и шестнадцатеричной записи чисел в форматах o, x или X (например, для формата x перед выводимым числом добавляется префикс "0x", для формата X — префикс "0X"), десятичной точки в вещественных числах (форматы e, E, a или A) с нулевой дробной частью, и некоторыми другими нюансами.

Более подробно изучить возможности форматированного вывода в строку можно в документации.

Общее количество параметров функции не может превышать 64.

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

Если количество спецификаторов в форматной строке больше аргументов, то система попытается вывести вместо отсутствующих данных нули, однако для строковых спецификаторв будет встроено текстовое предупреждение ("missing string parameter").

Если тип значения не совпадает с типом соответствующего спецификатора, система попытается прочитать данные из переменной в соответствии с форматом и отобразит получившуюся величину (она может выглядеть странно за счет неправильной интерпретации внутреннего битового представления реальных данных). В случае строк в результат может быть встроено предупреждение ("non-string passed").

Протестируем функцию с помощью скрипта StringFormat.mq5.

Сперва попробуем разные варианты спецификатора типов T и данных.

PRT(StringFormat("[Infinity Sign] Unicode (ok): %c; ANSI (overflow): %C",
   '∞', '∞'));
PRT(StringFormat("short (ok): %hi, short (overflow): %hi",
   SHORT_MAXINT_MAX));
PRT(StringFormat("int (ok): %i, int (overflow): %i",
   INT_MAXLONG_MAX));
PRT(StringFormat("long (ok): %lli, long (overflow): %i",
   LONG_MAXLONG_MAX));
PRT(StringFormat("ulong (ok): %llu, long signed (overflow): %lli",
   ULONG_MAXULONG_MAX));

Здесь представлены как правильные, так и неправильные спецификаторы (неправильные идут вторыми в каждой инструкции и помечены словом "overflow", так как передаваемое значение не умещается в типе формата).

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

StringFormat(Plain string,0)='Plain string'
StringFormat([Infinity Sign] Unicode: %c; ANSI: %C,'∞','∞')=
   '[Infinity Sign] Unicode (ok): ∞; ANSI (overflow):  '
StringFormat(short (ok): %hi, short (overflow): %hi,SHORT_MAX,INT_MAX)=
   'short (ok): 32767, short (overflow): -1'
StringFormat(int (ok): %i, int (overflow): %i,INT_MAX,LONG_MAX)=
   'int (ok): 2147483647, int (overflow): -1'
StringFormat(long (ok): %lli, long (overflow): %i,LONG_MAX,LONG_MAX)=
   'long (ok): 9223372036854775807, long (overflow): -1'
StringFormat(ulong (ok): %llu, long signed (overflow): %lli,ULONG_MAX,ULONG_MAX)=
   'ulong (ok): 18446744073709551615, long signed (overflow): -1'

Все следующие инструкции — правильные:

PRT(StringFormat("ulong (ok): %I64u"ULONG_MAX));
PRT(StringFormat("ulong (HEX): %I64X, ulong (hex): %I64x",
   12345678901234561234567890123456));
PRT(StringFormat("double PI: %f"M_PI));
PRT(StringFormat("double PI: %e"M_PI));
PRT(StringFormat("double PI: %g"M_PI));
PRT(StringFormat("double PI: %a"M_PI));
PRT(StringFormat("string: %s""ABCDEFGHIJ"));

Результат их работы представлен ниже:

StringFormat(ulong (ok): %I64u,ULONG_MAX)=
   'ulong (ok): 18446744073709551615'
StringFormat(ulong (HEX): %I64X, ulong (hex): %I64x,1234567890123456,1234567890123456)=
   'ulong (HEX): 462D53C8ABAC0, ulong (hex): 462d53c8abac0'
StringFormat(double PI: %f,M_PI)='double PI: 3.141593'
StringFormat(double PI: %e,M_PI)='double PI: 3.141593e+00'
StringFormat(double PI: %g,M_PI)='double PI: 3.14159'
StringFormat(double PI: %a,M_PI)='double PI: 0x1.921fb54442d18p+1'
StringFormat(string: %s,ABCDEFGHIJ)='string: ABCDEFGHIJ'

Теперь рассмотрим различные модификаторы.

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

PRT(StringFormat("space padding: %10i"SHORT_MAX));
PRT(StringFormat("0-padding: %010i"SHORT_MAX));
PRT(StringFormat("with sign: %+10i"SHORT_MAX));
PRT(StringFormat("precision: %.10i"SHORT_MAX));

Получим в журнале следующее:

StringFormat(space padding: %10i,SHORT_MAX)='space padding:      32767'
StringFormat(0-padding: %010i,SHORT_MAX)='0-padding: 0000032767'
StringFormat(with sign: %+10i,SHORT_MAX)='with sign:     +32767'
StringFormat(precision: %.10i,SHORT_MAX)='precision: 0000032767'

Для выравнивания влево необходимо применить флаг '-' (минус), дополнение строки до заданной ширины при этом происходит справа:

PRT(StringFormat("no sign (default): %-10i"SHORT_MAX));
PRT(StringFormat("with sign: %+-10i"SHORT_MAX));

Результат:

StringFormat(no sign (default): %-10i,SHORT_MAX)='no sign (default): 32767     '
StringFormat(with sign: %+-10i,SHORT_MAX)='with sign: +32767    '

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

PRT(StringFormat("default: %i"SHORT_MAX));  // стандарт
PRT(StringFormat("default: %i"SHORT_MIN));
PRT(StringFormat("space  : % i"SHORT_MAX)); // доп. пробел для положительного
PRT(StringFormat("space  : % i"SHORT_MIN));
PRT(StringFormat("sign   : %+i"SHORT_MAX)); // принудительно выводим знак
PRT(StringFormat("sign   : %+i"SHORT_MIN));

Вот как это выглядит в журнале:

StringFormat(default: %i,SHORT_MAX)='default: 32767'
StringFormat(default: %i,SHORT_MIN)='default: -32768'
StringFormat(space  : % i,SHORT_MAX)='space  :  32767'
StringFormat(space  : % i,SHORT_MIN)='space  : -32768'
StringFormat(sign   : %+i,SHORT_MAX)='sign   : +32767'
StringFormat(sign   : %+i,SHORT_MIN)='sign   : -32768'

Теперь сравним, как ширина и точность влияют на вещественные числа.

PRT(StringFormat("double PI: %15.10f"M_PI));
PRT(StringFormat("double PI: %15.10e"M_PI));
PRT(StringFormat("double PI: %15.10g"M_PI));
PRT(StringFormat("double PI: %15.10a"M_PI));
   
// точность по умолчанию = 6
PRT(StringFormat("double PI: %15f"M_PI));
PRT(StringFormat("double PI: %15e"M_PI));
PRT(StringFormat("double PI: %15g"M_PI));
PRT(StringFormat("double PI: %15a"M_PI));

Результат:

StringFormat(double PI: %15.10f,M_PI)='double PI:    3.1415926536'
StringFormat(double PI: %15.10e,M_PI)='double PI: 3.1415926536e+00'
StringFormat(double PI: %15.10g,M_PI)='double PI:     3.141592654'
StringFormat(double PI: %15.10a,M_PI)='double PI: 0x1.921fb54443p+1'
StringFormat(double PI: %15f,M_PI)='double PI:        3.141593'
StringFormat(double PI: %15e,M_PI)='double PI:    3.141593e+00'
StringFormat(double PI: %15g,M_PI)='double PI:         3.14159'
StringFormat(double PI: %15a,M_PI)='double PI: 0x1.921fb54442d18p+1'

В отсутствие явно заданной ширины, значения выводятся без дополнения пробелами.

PRT(StringFormat("double PI: %.10f"M_PI));
PRT(StringFormat("double PI: %.10e"M_PI));
PRT(StringFormat("double PI: %.10g"M_PI));
PRT(StringFormat("double PI: %.10a"M_PI));

Результат:

StringFormat(double PI: %.10f,M_PI)='double PI: 3.1415926536'
StringFormat(double PI: %.10e,M_PI)='double PI: 3.1415926536e+00'
StringFormat(double PI: %.10g,M_PI)='double PI: 3.141592654'
StringFormat(double PI: %.10a,M_PI)='double PI: 0x1.921fb54443p+1'

Установление ширины и точности значений с помощью знака '*' и на основании дополнительных аргументов функции выполняется следующим образом:

PRT(StringFormat("double PI: %*.*f"125M_PI));
PRT(StringFormat("string: %*s"15"ABCDEFGHIJ"));
PRT(StringFormat("string: %-*s"15"ABCDEFGHIJ"));

Обратите внимание, что перед выводимым значением передается 1 или 2 значения целого типа — по числу звездочек '*' в спецификаторе: вы можете управлять отдельно точностью, отдельно шириной, или тем и другим одновременно.

StringFormat(double PI: %*.*f,12,5,M_PI)='double PI:      3.14159'
StringFormat(string: %*s,15,ABCDEFGHIJ)='string:      ABCDEFGHIJ'
StringFormat(string: %-*s,15,ABCDEFGHIJ)='string: ABCDEFGHIJ     '

Наконец, рассмотрим несколько типичных ошибок форматирования.

PRT(StringFormat("string: %s %d %f %s""ABCDEFGHIJ"));
PRT(StringFormat("string vs int: %d""ABCDEFGHIJ"));
PRT(StringFormat("double vs int: %d"M_PI));
PRT(StringFormat("string vs double: %s"M_PI));

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

StringFormat(string: %s %d %f %s,ABCDEFGHIJ)=
   'string: ABCDEFGHIJ 0 0.000000 (missed string parameter)'
StringFormat(string vs int: %d,ABCDEFGHIJ)='string vs int: 0'
StringFormat(double vs int: %d,M_PI)='double vs int: 1413754136'
StringFormat(string vs double: %s,M_PI)=
   'string vs double: (non-string passed)'

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