数据到字符串的通用格式化输出

如果生成字符串以向用户显示,将字符串保存到文件或是通过互联网发送,可能需要将不同类型的若干个变量的值包括在其中。要解决这个问题,可以将所有变量显式强制转换为 (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 字符
  • C – ANSI 字符
  • d, i – 有符号十进制数
  • o – 无符号八进制数
  • u – 无符号十进制数
  • x – 无符号十六进制数(小写)
  • X – 无符号十六进制数(大写字母)

别忘了,根据内部数据存储方法,整数类型还包括内置 MQL5 类型 datetimecolorbool 以及枚举。

对于实数,以下符号适用于作为 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 – 如果小于指定宽度,则在输出值前面添加零;
  • (空格)– 如果值有符号并为正值,则在显示值前面添加空格;
  • # – 控制 oxX 格式的八进制和十六进制数字前缀的显示(例如,对于 x 格式,会在显示的数字前面添加前缀 "0x",对于 X 格式则添加前缀 "0X")、带零小数部分的实数中的小数点(eEaA 格式)以及一些其它细微差别。

可以在 文档中深入了解有关格式化输出到字符串的内容。

函数参数总数不能超过 64。

如果传递到函数的自变量数量大于说明符数量,则多余自变量被省略。

如果格式字符串中的说明符数量大于自变量数,则系统将尝试显示零以代替缺失数据,但将为字符串说明符嵌入文本警告(“缺少字符串参数”)。

如果值的类型与对应说明符的类型不匹配,则系统将尝试根据格式从变量读取数据并显示结果值(由于错误解析了真实数据的内部位表示方式,显示的值可能看起来奇怪)。对于字符串,结果中可能嵌入警告(“传递了非字符串”)。

我们使用 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));  // standard
PRT(StringFormat("default: %i"SHORT_MIN));
PRT(StringFormat("space  : % i"SHORT_MAX)); // extra space for positive
PRT(StringFormat("space  : % i"SHORT_MIN));
PRT(StringFormat("sign   : %+i"SHORT_MAX)); // force sign output
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));
   
// default precision = 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 各种格式字符串(事先准备)。