Recognizing Text Patterns

Some text formats are more expressive than a fixed prefix but far smaller than a grammar. A record may begin with a literal marker, require two letters and a digit, and allow an arbitrary payload before a known suffix. Writing a parser for that boundary would be excessive, while a chain of manual searches can hide the shape you meant to validate.

StringPattern fills this narrow space. It combines literals, decoded characters, ranges, and one front/back divider into a reusable recognizer for UTF-8, UTF-16, or UTF-32 text. This page shows both the compact pattern language and the typed construction API, then uses the resulting pattern to match, trim, split, and recover native positions.

Keep the boundary of this tool in mind as you read. A fixed prefix remains clearer as startsWith(), and a sequential grammar with optional branches, values, quoting, or rich diagnostics belongs in StringCharReader. StringPattern is at its best when the complete small shape remains easy to see.

Describe a Small Shape in One Pattern

The parsed constructor accepts the same string-view types as the rest of the text module. For the common UTF-8 case, construct a pattern from a ""_el literal:

const auto pattern = el::StringPattern{"probe-??[0-9]*"_el};

The pattern is parsed and validated during construction. Invalid syntax throws ParseError.

This form is well suited for configuration files and for application code where a compact pattern is easier to read than an equivalent sequence of typed elements.

/// `StringPattern` parses a compact shell-like pattern once and reuses it for many text checks.
///
/// The parsed syntax is useful for small library-level recognizers: a literal prefix, one decoded character with `?`,
/// bracket character sets, and one `*` divider for front/back matching. Backslash escapes keep special characters
/// literal when they are part of the text format itself.
void patternConstruction() {
    // A pattern can describe the small fixed shell around a laboratory record.
    const auto probeRecord = el::StringPattern{"probe-??[0-9]*"_el};

    const auto probeLines = el::StringList{{
        "probe-AB7 temperatur=21.4C"_el,
        "probe-A7 temperatur=21.4C"_el,
    }};
    const auto lineFormat = el::StringFormat{"{:30}: {}"};
    for (const auto &line : probeLines) {
        el::io::printLine(lineFormat.build(line, probeRecord.matches(line)));
    }

    // Escapes make the pattern syntax available for literal diagnostic markers.
    const auto marker = el::StringPattern{"messung\\[\\?\\]\\*"_el};
    el::io::printLine("escaped marker                : "_el, marker.matches("messung[?]* archiviert"_el));
}
probe-AB7 temperatur=21.4C    : true
probe-A7 temperatur=21.4C     : false
escaped marker                : true

Pattern Syntax

A pattern consists of decoded text plus a small set of special characters. It always matches at the boundary of a string.

Without a divider, the pattern matches the prefix. With a divider, the text before the divider matches the prefix and the text after the divider matches the suffix.

The syntax intentionally remains small and predictable.

text

Literal decoded characters.

The pattern messung: matches the prefix of messung:temperatur=21.4.

?

Exactly one decoded Unicode character.

This matches one character, not one byte. The pattern probe-?? matches both probe-A7 and probe-µ7.

[a-z]

A character set using the same compact range syntax as CharSet. It matches one decoded character from the set.

*

The divider.

It does not represent a repeated wildcard. Instead, it separates the prefix and suffix portions of the pattern so both can be validated without scanning the entire string like a regular expression engine.

\?, \*, \[, \], \\

Escaped special characters.

Only these escapes are supported. Restricting the syntax keeps mistakes visible instead of silently accepting unsupported constructs.

A pattern may contain only one divider.

Pattern

Meaning

Example

abc

Match the prefix abc.

abcdef matches.

abc*

Match the same prefix with an explicit divider.

abcdef matches.

*abc

Match the suffix abc.

defabc matches.

abc*xyz

Match prefix abc and suffix xyz.

abcdefxyz matches while leaving def as the middle.

The pattern * is invalid. Patterns containing more than one divider, such as a*b*c, are also invalid.

Build the Same Shape from Typed Elements

For library-internal patterns, the typed element constructor avoids the parsed syntax entirely. It builds immutable compiled pattern data directly, stores literal text as UTF-32, and makes each pattern element explicit.

/// Typed construction builds a `StringPattern` from explicit pattern elements.
///
/// This form skips parsed pattern syntax. Literal text is supplied as UTF-32 through `Text`, and character choices are
/// expressed with `OneChar`, `Range`, and `Set`. `Divider` is the single front/back divider that corresponds to `*` in
/// parsed patterns.
void staticConstruction() {
    using namespace el::text::pattern;

    static const auto acceptedRecord = el::StringPattern{
        Text{U"probe-"},
        Range{U'A', U'Z'},
        Set{{U'0', U'9'}},
        OneChar{},
        Divider{},
        Text{U";ok"},
    };

    const auto probeLines = el::StringList{{
        "probe-A7b wert=0.42;ok"_el,
        "probe-a7b wert=0.42;ok"_el,
        "probe-A7b wert=0.42;prüfen"_el,
    }};
    const auto lineFormat = el::StringFormat{"{:30}: {}"};
    for (const auto &line : probeLines) {
        el::io::printLine(lineFormat.build(line, acceptedRecord.matches(line)));
    }
}
probe-A7b wert=0.42;ok        : true
probe-a7b wert=0.42;ok        : false
probe-A7b wert=0.42;prüfen    : false

Pattern Elements

The elements are defined in erbsland::text::pattern.

Most static patterns begin with:

using namespace el::text::pattern;
Text

Literal UTF-32 text.

The type stores a std::u32string_view, so literals use the U prefix.

OneChar

One decoded character. Equivalent to ? in the parsed syntax.

Range

One inclusive CharRange.

Set

One character set built from several ranges or from an existing CharSet.

The API uses the name Set. The public header is named PatternSet.hpp to avoid filename collisions with other public Set headers.

Divider

The prefix/suffix divider. Equivalent to * in the parsed syntax.

The number of pattern elements and character ranges is intentionally limited. A string pattern is designed to describe a small boundary shape rather than a general-purpose grammar.

Ask Whether the Boundary Matches

matches() answers the most common question: does this text have the expected shape?

Use it whenever the surrounding code only needs a yes/no decision.

/// `matches()` answers whether a text has the expected decoded-character shape.
///
/// A pattern without `*` matches the front of the string. A trailing `*` keeps that prefix behavior explicit.
/// A leading `*` checks the end. A pattern with text on both sides of `*` requires both the prefix and suffix without
/// overlap.
void matches() {
    const auto nameOnly = el::StringPattern{"messung:"_el};
    const auto withUnit = el::StringPattern{"messung:*;C"_el};
    const auto finished = el::StringPattern{"*;fertig"_el};

    const auto temperature = el::String{"messung:temperatur=21.4;C"_el};
    const auto completed = el::String{"messung:leitwert=0.42;fertig"_el};

    el::io::printLine("front only ..............: "_el, nameOnly.matches(temperature));
    el::io::printLine("front and back ..........: "_el, withUnit.matches(temperature));
    el::io::printLine("suffix only .............: "_el, finished.matches(completed));
    el::io::printLine("wrong suffix ............: "_el, finished.matches(temperature));
    el::io::printLine("missing suffix ..........: "_el, withUnit.matches("messung:temperatur=21.4"_el));
}
front only ..............: true
front and back ..........: true
suffix only .............: true
wrong suffix ............: false
missing suffix ..........: false

The method does not return captures or explain why matching failed.

If you need diagnostics or want to parse several fields after validating the boundary, continue with StringCharReader or use the reader directly.

Remove a Recognized Boundary

trimmed() returns a view with the matching shell removed.

trim() narrows an owning read-only string to the same range and reports whether the operation succeeded. It changes only the visible range; it does not modify character data.

For a prefix pattern, the prefix is removed. For a suffix pattern, the suffix is removed. For a pattern containing both a prefix and a suffix, both are removed and only the middle remains.

This provides a compact way to unwrap small protocol-like envelopes.

/// `trimmed()` and `trim()` remove the matched front or back shell of a string pattern.
///
/// For a pattern with text on both sides of the divider, the divider represents the part that stays. This is useful
/// when a protocol-like string wraps the meaningful value in a predictable prefix and suffix.
void trimAndTrimmed() {
    const auto envelope = el::StringPattern{"messung:*;ok"_el};
    const auto original = el::String{"messung:temperatur=21.4C;ok"_el};

    el::io::printLine("original ................: "_el, original);
    el::io::printLine("trimmed .................: "_el, envelope.trimmed(original));

    auto narrowed = el::String{"messung:ph=7.1;ok"_el};
    if (envelope.trim(narrowed)) {
        el::io::printLine("narrowed ................: "_el, narrowed);
    }

    auto unchanged = el::String{"messung:ph=7.1;prüfen"_el};
    el::io::printLine("changed .................: "_el, envelope.trim(unchanged));
    el::io::printLine("kept ....................: "_el, unchanged);
}
original ................: messung:temperatur=21.4C;ok
trimmed .................: temperatur=21.4C
narrowed ................: ph=7.1
changed .................: false
kept ....................: messung:ph=7.1;prüfen

Trimming is deliberately conservative.

If the pattern does not match, the original text remains unchanged and trim() returns false.

These methods are intended for removing a single matching shell. To remove repeated occurrences throughout a string, use the normal string editing and search functions instead.

Split Where the Pattern Places Its Divider

split() returns two views.

The first contains the text matched by the prefix portion of the pattern. The second contains everything after that boundary.

This is particularly useful for UTF-8 text. A pattern such as probe-?? may consume different numbers of bytes depending on the matched Unicode characters. split() calculates the correct native split position automatically.

/// `split()` returns the matched text and the remaining text at the pattern boundary.
///
/// The method is especially handy when `?` or bracket sets make the matched prefix variable in bytes. For a pattern
/// with both a prefix and suffix, the suffix is validated and then dropped from the second result.
void split() {
    const auto recordPattern = el::StringPattern{"probe-??*;ok"_el};
    const auto record = el::String{"probe-A7 temperatur=21.4C;ok"_el};

    const auto [recordId, valueText] = recordPattern.split(record);
    el::io::printLine("record id ...............: "_el, recordId);
    el::io::printLine("value ...................: "_el, valueText);

    const auto suffixPattern = el::StringPattern{"*;fehler"_el};
    const auto [payload, suffix] = suffixPattern.split("probe-B8 leitwert=0.41;fehler"_el);
    el::io::printLine("payload .................: "_el, payload);
    el::io::printLine("suffix ..................: "_el, suffix);
}
record id ...............: probe-A7
value ...................:  temperatur=21.4C
payload .................: probe-B8 leitwert=0.41
suffix ..................: ;fehler

With a suffix-only pattern such as *;ok, the first result contains everything before the suffix and the second contains the matched suffix.

With a pattern such as probe-??*;ok, the suffix is validated and omitted from the second result, leaving only the useful middle part.

If the pattern does not match, the first result is empty and the second result contains the original text.

Reuse the Native Match Position

length() and index() expose the native boundary position found by the matcher.

For UTF-8 strings, the result is a ByteLength or ByteIndex.

For UTF-16 strings, the values are UTF-16 code-unit positions.

For UTF-32 strings, they are code-point positions.

/// `length()` and `index()` return native string positions for the matching part.
///
/// For UTF-8 text this means byte positions, for UTF-16 it means data-unit positions, and for UTF-32 it means
/// code-point positions. The values are meant for slicing and diagnostics near string boundaries, not for scanning an
/// entire grammar.
void lengthAndIndex() {
    const auto microSensor = el::StringPattern{u8"µ?*"_el};
    const auto u8Measurement = el::String{u8"µA=0.42"_el};

    el::io::printLine("UTF-8 split index .......: "_el, microSensor.index(u8Measurement));
    el::io::printLine("UTF-8 match bytes .......: "_el, microSensor.length(u8Measurement));

    const auto suffix = el::StringPattern{"*;ok"_el};
    const auto accepted = el::String{"temperatur=21.4;ok"_el};

    el::io::printLine("suffix index ............: "_el, suffix.index(accepted));
    el::io::printLine("suffix length ...........: "_el, suffix.length(accepted));
}
UTF-8 split index .......: 3
UTF-8 match bytes .......: 3
suffix index ............: 15
suffix length ...........: 3

These methods are useful when the next operation performs native string slicing, reports diagnostics near the validated boundary, or stores native string positions.

They are not intended to replace iteration. If your algorithm needs to process every character or token, a search loop or StringCharReader is usually the better tool.