Formatting Text with StringFormat

Manual concatenation becomes difficult to read as soon as output has a stable shape or values appear in a different order. A format pattern keeps that shape in one place and lets every supplied value remain in its natural type.

StringFormat stores a parsed UTF-8 pattern that can be reused. This page explains its lifetime, placeholder selection, supported values, and the choice between building a standalone result and appending to an existing builder. Formatting options are intentionally separate: Named Format Specifications describes the primary named syntax, while Compatibility Format Specifications covers the compact C++-style compatibility layer.

Creating and Reusing a StringFormat Pattern

Constructing a format parses and validates its pattern. Keeping the resulting StringFormat is useful when the same shape is applied repeatedly, because later calls do not need to parse it again. U16Format and U32Format provide the same model when the pattern and result use another string width.

build(...) returns a new string. When a larger result is already being assembled, appendTo(...) writes directly to AnyStringBuilder and avoids a temporary formatted string. Supplying a wrong argument count, an invalid position, or a specification that does not accept the value type raises FormatError.

text/StringFormat/FormattingPatterns.cpp

/// `StringFormat` stores a reusable formatting pattern.
/// Named specifications keep the expected value type and each formatting choice visible.
///
/// Use `build()` to create a new string from formatted values.
/// Use `appendTo()` to add formatted text to an existing `AnyStringBuilder` without
/// creating temporary strings.
void formattingPatterns() {
    // Create a reusable pattern for ISO 8601 date-time values.
    const auto isoDateTime =
        el::StringFormat{"{:number:width=4,zero-fill}-{:number:width=2,zero-fill}-{:number:width=2,zero-fill}T"
                         "{:number:width=2,zero-fill}:{:number:width=2,zero-fill}:{:number:width=2,zero-fill}"_el};

    auto timestamp = isoDateTime.build(2026, 5, 30, 21, 41, 56);
    el::io::printLine("ISO date-time: "_el, timestamp);

    // Create a pattern for simple HTML tags.
    const auto htmlTag = el::StringFormat{"<{0}>{1:text:escape=html}</{0}>\n"_el};

    el::AnyStringBuilder htmlOutput;
    htmlTag.appendTo(htmlOutput, "h1"_el, "Hello World"_el);
    htmlTag.appendTo(htmlOutput, "p"_el, "This paragraph was appended to a string builder."_el);
    htmlTag.appendTo(htmlOutput, "p"_el, "We add another <p> tag with \"useful\" text."_el);

    el::io::printLine("HTML output:"_el);
    el::io::print(htmlOutput);
}

Output

ISO date-time: 2026-05-30T21:41:56
HTML output:
<h1>Hello World</h1>
<p>This paragraph was appended to a string builder.</p>
<p>We add another &lt;p&gt; tag with &quot;useful&quot; text.</p>

Empty Placeholders and Automatic Argument Numbering

Regular text appears unchanged in the result, while {} consumes the next argument and applies its default format. Placeholders are processed from left to right, and every supplied argument must be consumed. Write {{ or }} when a literal brace belongs in the output.

text/StringFormat/EmptyPlaceholder.cpp

/// An empty placeholder accepts any supported type and formats it using the default format.
void emptyPlaceholder() {
    const auto pattern = el::StringFormat("a: {} b: {} c: {} d: {}");
    el::io::printLine(pattern.build(123, false, 76.92, "text"_el));
}

Output

a: 123 b: false c: 76.92 d: text

Positional Placeholders and Argument Reuse

An explicit index such as {1} selects the second argument and can select the same value more than once. Explicit positions must form a consecutive set beginning at zero, which catches missing or accidentally unused values when the pattern is created. Do not mix automatic and explicit selection in one pattern.

text/StringFormat/PositionalPlaceholders.cpp

/// Positional placeholders allow you to specify the order of the arguments in the format string.
void positionalPlaceholders() {
    const auto pattern = el::StringFormat("<{1}>{0}</{1}>");
    el::io::printLine(pattern.build("text"_el, "h1"_el));
}

Output

<h1>text</h1>

Supported Value Types and Custom FormatAs Representations

The formatter accepts Core and standard-library text, Char, Boolean values, signed and unsigned integers, floating-point values, saturated integers, and ByteBlock. Types with toString() const -> String participate automatically. Specialize FormatAs when a custom type needs a different public formatting representation.

Passing the original typed value is clearer and often more efficient than converting it into a temporary string first. A named selector can also lock a placeholder to text, number, Boolean, or bytes so an unexpected type fails instead of silently changing the output.

text/StringFormat/SupportedTypes.cpp

/// The formatter accepts text, integers, floats, booleans, and characters.
void supportedTypes() {
    const auto pattern = el::StringFormat("{} {} {} {} {}"_el);
    el::io::printLine(pattern.build(42, 3.14, true, 'A', "hello"_el));
}

Output

42 3.14 true A hello

Complete demo sources

text/StringFormat/FormattingPatterns.cpp

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

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

namespace demo {

/// `StringFormat` stores a reusable formatting pattern.
/// Named specifications keep the expected value type and each formatting choice visible.
///
/// Use `build()` to create a new string from formatted values.
/// Use `appendTo()` to add formatted text to an existing `AnyStringBuilder` without
/// creating temporary strings.
void formattingPatterns() {
    // Create a reusable pattern for ISO 8601 date-time values.
    const auto isoDateTime =
        el::StringFormat{"{:number:width=4,zero-fill}-{:number:width=2,zero-fill}-{:number:width=2,zero-fill}T"
                         "{:number:width=2,zero-fill}:{:number:width=2,zero-fill}:{:number:width=2,zero-fill}"_el};

    auto timestamp = isoDateTime.build(2026, 5, 30, 21, 41, 56);
    el::io::printLine("ISO date-time: "_el, timestamp);

    // Create a pattern for simple HTML tags.
    const auto htmlTag = el::StringFormat{"<{0}>{1:text:escape=html}</{0}>\n"_el};

    el::AnyStringBuilder htmlOutput;
    htmlTag.appendTo(htmlOutput, "h1"_el, "Hello World"_el);
    htmlTag.appendTo(htmlOutput, "p"_el, "This paragraph was appended to a string builder."_el);
    htmlTag.appendTo(htmlOutput, "p"_el, "We add another <p> tag with \"useful\" text."_el);

    el::io::printLine("HTML output:"_el);
    el::io::print(htmlOutput);
}

}

text/StringFormat/EmptyPlaceholder.cpp

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

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

namespace demo {

/// An empty placeholder accepts any supported type and formats it using the default format.
void emptyPlaceholder() {
    const auto pattern = el::StringFormat("a: {} b: {} c: {} d: {}");
    el::io::printLine(pattern.build(123, false, 76.92, "text"_el));
}

}

text/StringFormat/PositionalPlaceholders.cpp

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

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

namespace demo {

/// Positional placeholders allow you to specify the order of the arguments in the format string.
void positionalPlaceholders() {
    const auto pattern = el::StringFormat("<{1}>{0}</{1}>");
    el::io::printLine(pattern.build("text"_el, "h1"_el));
}

}

text/StringFormat/SupportedTypes.cpp

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

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

namespace demo {

/// The formatter accepts text, integers, floats, booleans, and characters.
void supportedTypes() {
    const auto pattern = el::StringFormat("{} {} {} {} {}"_el);
    el::io::printLine(pattern.build(42, 3.14, true, 'A', "hello"_el));
}

}