Named Format Specifications

A report may need a centered heading, a hexadecimal packet identifier, a rounded measurement, and a readable status word. Named format specifications describe those choices inside a StringFormat pattern, beside the placeholder they affect. Their names make a longer pattern readable without having to memorize compact format codes.

This page builds those representations from the shared syntax, then explains type checks and invalid specifications. For selecting and reordering arguments before formatting them, start with Formatting Text with StringFormat.

Describing a Value’s Representation

A named specification adds a selector and an option list to an ordinary placeholder:

{[index]:selector:option,option,...}

The optional index selects the argument. The selector describes its value family: text, number, bool, or bytes. For example, {:bool:style=yes,capitalization=capitalized} turns a Boolean into Yes or No. The format chooses both words, so the calling code can continue passing the Boolean value directly.

Options are separated by commas and can appear in any order. Long names make application-owned patterns easy to read; short aliases are available when space is limited. width=12, width12, and w12 all select the same width. Names and predefined values are ASCII case-insensitive, but whitespace inside a specification is significant and cannot be added merely to separate options.

Laying Out and Escaping Text

For a report heading, first decide how much source text to show, then how to position it in the field. maximum limits the number of source code points; width gives the minimum output width. alignment places the value at the left, right, or center, and fill supplies the padding character. The first pattern below limits the heading to twelve code points and centers it in eighteen positions:

text/StringFormat/NamedTextFormat.cpp

/// Named text specifications make truncation, escaping, and layout readable at the call site.
void namedTextFormat() {
    const auto heading = el::StringFormat{"{:text:maximum=12,width=18,alignment=center,fill=·}"_el};
    const auto json = el::StringFormat{"{:text:escape=json,escape-amount=non-ascii}"_el};

    el::io::printLine("|", heading.build("bodemvochtigheid"_el), "|"_el);
    el::io::printLine(json.build("sensor café\n"_el));
}

Output

|···bodemvochtig···|
sensor caf\u00E9\n

The second pattern selects JSON escaping and requests escapes for non-ASCII characters as well. The output is an escaped fragment; the pattern does not add surrounding JSON quotation marks. Truncation happens before escaping, so a source character’s escape sequence remains complete. Width and alignment are applied afterward to the escaped result.

width, alignment, and fill also work for numbers and Booleans. Width is measured in decoded code points, which may differ from terminal columns or user-perceived characters. A minimum width does not truncate a longer value; maximum controls text truncation separately.

The escape option selects the destination syntax, such as json, html, or xml. escape-amount controls how much is escaped within that syntax. Choose these together according to where the fragment will be used. The formatting and parsing reference lists the available escape formats.

Formatting Integer Identifiers and Counts

A numeric identifier may need a base prefix and a fixed minimum width, while a report’s difference column needs a sign and alignment. Named number specifications can express both:

text/StringFormat/NamedIntegerFormat.cpp

/// Named number specifications can lock a field to integers and describe its base and layout explicitly.
void namedIntegerFormat() {
    const auto packetId = el::StringFormat{"{:number:hex,base-prefix,letter-case=uppercase,width=10,zero-fill}"_el};
    const auto signedCount = el::StringFormat{"{:number:base=decimal,sign=always,width=7,alignment=right}"_el};

    el::io::printLine("Packet id ...: "_el, packetId.build(42));
    el::io::printLine("Difference ..: "_el, signedCount.build(17));
}

Output

Packet id ...: 0X0000002A
Difference ..:     +17

The packet identifier uses hex to select hexadecimal digits and base-prefix to include 0x. letter-case=uppercase affects both the digits and the prefix, giving 0X0000002A for that packet identifier. zero-fill pads the numeric field with zeroes to its requested width. Ordinary layout padding instead uses the fill character.

The difference uses decimal output and sign=always so a positive result visibly carries +. sign=negative-only retains the usual spelling, while sign=space reserves a space for a positive value’s sign. When the requirement concerns the number of digits rather than the field width, precision sets a minimum digit count.

base=hexadecimal and base=hex are longer spellings of hex. Decimal, octal, and binary forms are available as well. Selecting a base requires an integer argument, so a later change to a floating-point value is reported instead of silently altering the identifier’s representation.

Choosing Floating-Point Notation and Precision

For measurements, choose notation according to how readers will compare the values. Fixed notation is useful when the number of decimal places matters; scientific notation keeps large and small magnitudes in a comparable form:

text/StringFormat/NamedFloatFormat.cpp

/// Named number specifications use notation to select floating-point formatting.
void namedFloatFormat() {
    const auto fixed = el::StringFormat{"{:number:notation=fixed,precision=2,sign=always}"_el};
    const auto scientific = el::StringFormat{"{:number:notation=scientific,precision=3,letter-case=uppercase}"_el};

    el::io::printLine("Temperature ...: "_el, fixed.build(21.375));
    el::io::printLine("Pressure ......: "_el, scientific.build(1013.25));
}

Output

Temperature ...: +21.38
Pressure ......: 1.013E+03

The temperature uses two digits after the decimal point and rounds 21.375 to +21.38. The pressure uses scientific notation with three fractional digits and an uppercase E. Precision follows the selected notation, so changing notation can also change what a precision value means. converting text and scalar values explains the corresponding formatting objects when you need to configure the representation in C++.

The notation option requires a floating-point argument. An integer supplied to the scientific pattern is a type mismatch even if its value could be converted to a float. This lets a format state the kind of value it expects as well as how that value should look.

Choosing Boolean Words and Capitalization

A Boolean often reads better as a status or answer than as true or false. The bool selector’s style chooses a pair of words: true and false, yes and no, on and off, or enabled and disabled. Then capitalization controls the spelling of the selected word:

text/StringFormat/NamedBooleanFormat.cpp

/// Named Boolean specifications select the word pair, capitalization, and layout independently.
void namedBooleanFormat() {
    const auto status =
        el::StringFormat{"{:bool:style=enabled,capitalization=uppercase,width=10,alignment=right,fill=.}"_el};
    const auto answer = el::StringFormat{"{:bool:style=yes,capitalization=capitalized}"_el};

    el::io::printLine("Sensor ...: "_el, status.build(true));
    el::io::printLine("Alarm ....: "_el, answer.build(false));
}

Output

Sensor ...: ...ENABLED
Alarm ....: No

capitalization=capitalized produces an initial capital with the remaining letters lowercase, such as Yes or Disabled. The other choices are lowercase and uppercase; cap=c, cap=l, and cap=u are their compact forms. The shared width, alignment, and fill options are applied after the word has been selected and capitalized.

When a Boolean is already available, keeping these choices in the format avoids creating an intermediate string and then applying a case transformation to it. For a string supplied by another source, the helpers described in Transforming Strings provide those transformations directly.

Keeping Byte Diagnostics Bounded

A byte block needs a representation that makes its individual values visible. The bytes selector renders hexadecimal bytes, with optional spaces for readability. For a long payload, a bounded view can preserve the beginning and end around an ellipsis:

text/StringFormat/NamedByteFormat.cpp

/// Named byte specifications make separators and bounded diagnostic output explicit.
void namedByteFormat() {
    const auto packet = el::mem::ByteBlock::fromVector(
        std::vector<uint8_t>{0x10U, 0x21U, 0x32U, 0x43U, 0x54U, 0x65U, 0x76U, 0x87U, 0x98U});
    const auto full = el::StringFormat{"{:bytes:separator}"_el};
    const auto diagnostic = el::StringFormat{"{:bytes:separator,maximum=6,truncate=middle}"_el};

    el::io::printLine("Complete ......: "_el, full.build(packet));
    el::io::printLine("Diagnostic ....: "_el, diagnostic.build(packet));
}

Output

Complete ......: 10 21 32 43 54 65 76 87 98
Diagnostic ....: 10 21 32 … 87 98

maximum=6 counts output items, including the ellipsis, so the shortened diagnostic contains five bytes and one ellipsis. truncate=middle selects the omitted portion; begin and end are also available. This limit concerns byte items rather than the width of the resulting hexadecimal text.

The selector accepts a ByteBlock, not a string that happens to contain hexadecimal characters. Even an empty option list, {:bytes:}, retains that type check.

Format Errors and Reusable Patterns

A FormatError during pattern construction identifies an invalid specification, such as an unknown option, missing value, or unsupported combination. An option may appear only once: mixing width and w is still a duplicate, as is combining hex with base=hexadecimal. fill requires one safe Unicode code point.

Other checks depend on the argument and happen when the pattern is applied. text accepts Core strings, standard strings, and Char; number accepts integers or floating-point values unless an option narrows that family. bool and bytes accept their corresponding value types. A mismatch raises FormatError at that point.

Keep a configured StringFormat when the same representation will be used repeatedly, then pass the original typed values to each call. The text formatting and parsing reference lists every supported option, alias, and value for less common formatting requirements.

Complete demo sources

text/StringFormat/NamedTextFormat.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "StringFormatDemos.hpp"

#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>

namespace demo {

/// Named text specifications make truncation, escaping, and layout readable at the call site.
void namedTextFormat() {
    const auto heading = el::StringFormat{"{:text:maximum=12,width=18,alignment=center,fill=·}"_el};
    const auto json = el::StringFormat{"{:text:escape=json,escape-amount=non-ascii}"_el};

    el::io::printLine("|", heading.build("bodemvochtigheid"_el), "|"_el);
    el::io::printLine(json.build("sensor café\n"_el));
}

}

text/StringFormat/NamedIntegerFormat.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "StringFormatDemos.hpp"

#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>

namespace demo {

/// Named number specifications can lock a field to integers and describe its base and layout explicitly.
void namedIntegerFormat() {
    const auto packetId = el::StringFormat{"{:number:hex,base-prefix,letter-case=uppercase,width=10,zero-fill}"_el};
    const auto signedCount = el::StringFormat{"{:number:base=decimal,sign=always,width=7,alignment=right}"_el};

    el::io::printLine("Packet id ...: "_el, packetId.build(42));
    el::io::printLine("Difference ..: "_el, signedCount.build(17));
}

}

text/StringFormat/NamedFloatFormat.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "StringFormatDemos.hpp"

#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>

namespace demo {

/// Named number specifications use notation to select floating-point formatting.
void namedFloatFormat() {
    const auto fixed = el::StringFormat{"{:number:notation=fixed,precision=2,sign=always}"_el};
    const auto scientific = el::StringFormat{"{:number:notation=scientific,precision=3,letter-case=uppercase}"_el};

    el::io::printLine("Temperature ...: "_el, fixed.build(21.375));
    el::io::printLine("Pressure ......: "_el, scientific.build(1013.25));
}

}

text/StringFormat/NamedBooleanFormat.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "StringFormatDemos.hpp"

#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>

namespace demo {

/// Named Boolean specifications select the word pair, capitalization, and layout independently.
void namedBooleanFormat() {
    const auto status =
        el::StringFormat{"{:bool:style=enabled,capitalization=uppercase,width=10,alignment=right,fill=.}"_el};
    const auto answer = el::StringFormat{"{:bool:style=yes,capitalization=capitalized}"_el};

    el::io::printLine("Sensor ...: "_el, status.build(true));
    el::io::printLine("Alarm ....: "_el, answer.build(false));
}

}

text/StringFormat/NamedByteFormat.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "StringFormatDemos.hpp"

#include <erbsland/ByteBlock.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>

#include <cstdint>
#include <vector>

namespace demo {

/// Named byte specifications make separators and bounded diagnostic output explicit.
void namedByteFormat() {
    const auto packet = el::mem::ByteBlock::fromVector(
        std::vector<uint8_t>{0x10U, 0x21U, 0x32U, 0x43U, 0x54U, 0x65U, 0x76U, 0x87U, 0x98U});
    const auto full = el::StringFormat{"{:bytes:separator}"_el};
    const auto diagnostic = el::StringFormat{"{:bytes:separator,maximum=6,truncate=middle}"_el};

    el::io::printLine("Complete ......: "_el, full.build(packet));
    el::io::printLine("Diagnostic ....: "_el, diagnostic.build(packet));
}

}