Skip to content

Text formatting

All text formatting expressions start with the prefix format:: to switch the expression engine into “string interpolation” mode. This only makes sense to use in expressions which are intended to evaluate to text values. All formatting expressions allow multiple lines.

Motivation

Text typed directly into a format expression shows as-is in the output. For example the expression format::Hello world would result in the text “Hello world” with no modification.

In order to make format expressions useful, string interpolation is used to embed expressions within the text. This is done by inserting replacement fields into the text. For example format::Running for {uptime / 60}min would result in “Running for 20.563min” (if the system had been running for approximately 20 minutes.)

Usually, when working with values defined by computers there is a lot of extra detail which the expression writer doesn’t want to include. In the example above, the extra decimal places after the whole number of minutes are probably not wanted. To fix this, the writer can include replacement fields to change how the content is formatted. format::Running for {uptime / 60:.0f}min would result in “Running for 20min”, as desired.

Non-formatted text expressions

Text fields which are driven by an expression which does not include the format:: prefix will simply convert the result to text using the default formatting rules. In some cases, it is not possible to convert a result to text (for example a function name on its own does not evaluate to a value which can be converted to text.) This results in an expression error.

Replacement field syntax

Formatting is using the same language as C++ std::format.

Replacement fields are identified by the surrounding braces ({}.) If the expression writer needs to surround text with a literal {}, double the braces, like so: format::{{ hello, no evaluation happens here }} -> {{ hello, no evaluation happens here }}

Within a replacement field normal expression evaluation syntax is used to call functions, read variables, use operators and everything else available to all expressions.

Additionally, after the expression syntax, an optional : is added to the field. This separates the expression from the format specification, and controls how the value is converted to text.

Format specification

The power of the text formatting system is the degree of control over how the value is converted to text. This is accomplished through a small DSL (domain specific language) which has a focus on a compact representation of common text formatting operations.

Width, fill and align

It is sometimes useful to fill and align the text within a certain number of characters.

The alignment and fill behaviours have a series of defaults - the default fill is space ( ) and the default alignment is to the right. By default, there is no width to apply this fill or alignment to.

For all formatting examples here, we enclose the replacement field in single quote marks, to clarify the exact behaviour of the replacement field. This is not necessary in normal usage.

Therefore all of the following expressions result in the identical text ”’   123’“.

  • format::'{123:6}'
  • format::'{123:>6}'
  • format::'{123: >6}'

The space between the : and > is the fill character. In the final example above, this can be replaced with an arbitrary other single character (e.g. format::'{123:x>6}' results in “‘xxx123’“)

Similarly, the > character is the align operator, which can be any of:

  • > - right alignment (format::'{123:x>6}' -> “‘xxx123’“)
  • < - left alignment (format::'{123:x<6}' -> “‘123xxx’“)
  • ^ - center alignment (format::'{123:x^6}' -> “‘x123xx’“)

Finally, the 6 is the width and can be any integer. If the width is specified to be less than the number of characters needed to represent the value, the width is ignored.

Normally, the order of these operators is extremely important, but there is one exception - when the fill character is a 0, the align operator can be skipped. That means that format::'{123:06}' results in “‘000123’“. Note that there are no other numeric fill characters - an attempt to write format::'{123:16}' results in a 16 character width, rather than a fill using 1 characters within a 6 character space.

While the examples here use integer values, string and floating point values also work with this feature. e.g. format::'{"hello":^10s}' -> ”’  hello   ‘“

Precision

In addition to the width above, for numbers which have decimal places, the precision specifier allows the writer to limit the amount of detail presented.

Precision is a number following a period . after the optional width. It must always be followed by the floating point specifiers f or g (discussed below.) For example:

format::'{123.456:.1f}' -> “‘123.5’” (note the rounding is correct)

Width does not change meaning or in any way affect the number of places before the decimal point.

So for example, format::'{123.456:1.1f}' -> “‘123.5’”, but extending the width to beyond the 5 characters the field replacement rendered to begins to insert padding as discussed above - format::'{123.456:6.1f}' -> ”’ 123.5’“. All fill and alignment rules above apply equally. Precision changes the text which is rendered for the given number, whereas width, fill and align adjust where it is presented within the specified width characters.

Sign

Inserting a + in front of the width (or on its own without a width) ensures the sign of the number is always written. e.g. format::'{123:+6}' results in ”’   +123’”, whereas format::'{-123}' would always result in ”‘-123’“. format::'{123:+}' results in ”‘+123‘“

- is also a valid sign operator, but that simply marks the default behaviour as being in some way expected.

Base prefix

For some types (see below) the base of the integer being formatted is changed. It is often useful to use a standard base prefix to identify the base of the number. This is accomplished by inserting the # character before the width. See integer types for more details.

Type

The type specifier is the final character in the format specification. Based on the type of the value the expression evaluates to, it can change the representation of that value to be useful in different circumstances.

The type specifier is therefore grouped into sections based on the type of the value the expression evaluates to:

Integer types

  • b or B (base prefix: “0b” or “0B”). Formats the integer as a binary number (base 2). e.g. format::'{256:#16b}' -> “‘0b0000000100000000‘“
  • o (base prefix: “0”). Formats the integer as an octal number (base 8). e.g. format::'{256:#o}' -> “‘0400‘“
  • x or X (base prefix: “0x” or “0X”). Formats the integer as a hexadecimal number (base 16, using characters 0-9 and a-f). e.g. format::'{256:#x}' -> “‘0x100‘“
  • d (no base prefix). The default behaviour - does not need to be specified. Formats the integer as a decimal number (base 10). e.g. format::'{256:#d}' -> “‘256‘“

Floating point types

  • g - the default behaviour (used when not specifying a type or format specification at all) Formats the number in a compact representation, up to precision characters. When the number exceeds precision (default 6) characters in decimal notation, it shifts to scientific notation and rounds. e.g. format::'{1.5:g}' -> “‘1.5’“.
  • f - format with “fixed” formatting, without using scientific (exponent) notation. e.g. format::'{1.5:f}' -> “‘1.500000’” precision can help tune the result of this format option.
  • a - format with precise machine readable hexadecimal formatting, used by some computer systems to transfer floating point numbers without loss of precision due to string conversion. e.g. format::'{1.5:a}' -> “‘0x1.8p+0‘“

String types

  • s - the default behaviour. Formats the text by simply rendering the text as-supplied. e.g. format::'{"hi\nthere":s}' -> “‘hi
    there‘“
  • ? - format text with escaping and quoting. Useful if there are strange characters or the string is otherwise unexpected. e.g. format::'{"hi\nthere":?}' -> ”‘“hi\nthere”‘”