Text Matching and Splitting

String Pattern

Introduction

StringPattern is a lightweight pattern matcher for decoded string characters. It is intended for small internal checks such as protocol or prefix/suffix matching. StringPattern::fromDivider() parses a syntax supporting literal text, ? for one decoded character, [a-z] character sets, and one * divider. A bracket set may start with ^ to invert its characters, such as [^a-z]. Subsequent carets are literal; [^^] excludes caret and [^] accepts every Unicode scalar value. This changes the meaning of bracket sets that previously began with a literal caret. The divider separates front and back matching parts; it is not a repeated wildcard.

Parsed patterns support backslash escapes for ?, *, [, ] and \. The typed fromDivider() overload uses the erbsland::text::pattern element namespace to build immutable compiled pattern data directly without parsing pattern syntax.

Full-string Globs

StringPattern::fromGlob() compiles a case-sensitive Erbsland Core glob. It uses the same character-set syntax and backslash escapes as fromDivider(), including [^a-z] for negation. Unlike a Divider pattern, it matches the complete candidate and permits any number of * wildcards. Malformed brackets, invalid ranges, and unsupported escapes raise ParseError.

Both glob kinds operate on decoded characters across UTF-8, UTF-16, and UTF-32 strings without allocating during matching. Stars match zero or more characters, including directory separators, leading dots, and newlines. Adjacent stars have the same meaning as one star, and ? matches exactly one decoded character. An empty glob matches only empty text; a standalone star matches any text.

The typed fromGlob() factory accepts the existing literal, character, range, and set elements plus Star. Star may appear multiple times. Divider is rejected at compile time by fromGlob(), and Star is rejected by fromDivider(). No arguments creates an empty Glob. Typed patterns retain the existing fixed-capacity limits: 16 compiled elements, 16 total ranges, and eight ranges per set. Each star occupies one element after adjacent stars are coalesced. Parsed patterns use dynamic storage, limited to 65535 pooled ranges.

const auto sources = el::StringPattern::fromGlob("src/*/file?.[ch]pp"_el);
const auto matches = sources.matches("src/text/impl/file1.hpp"_el);

using namespace el::text::pattern;
static const auto logName = el::StringPattern::fromGlob(
    Text{U"log-"}, Star{}, Text{U"-"}, Range{U'0', U'9'}, Star{});

Python-style Globs

StringPattern::fromPosixGlob() preserves the original Python-style glob dialect. The name identifies this dialect; it does not promise complete POSIX conformance or filesystem traversal. Its full-string matching and extraction behavior is identical to Glob, while parsing differs:

  • [abc] and [a-z] match a character or ascending range; [!a-z] negates the set. A caret is literal.

  • A closing bracket immediately after [ or [! is a set member. [[] matches an opening bracket. Unclosed brackets are literal. A leading or trailing hyphen is literal; descending ranges contribute no matches.

  • Backslashes are literal, not escapes.

For example, fromGlob("[^a-c]*"_el) and fromPosixGlob("[!a-c]*"_el) both reject a candidate beginning with b. Use fromPosixGlob() when retaining the original parser’s behavior. kind() reports Divider, Glob, or PosixGlob according to the factory used.

Matching and Extraction

All three kinds support every matching and extraction method. Divider patterns retain prefix/suffix boundaries; both glob kinds match and consume the complete candidate. All comparisons are case-sensitive.

Method

Divider on a match

Glob / PosixGlob on a match

matches()

Accepts a matching prefix without *, or non-overlapping anchored parts around one *.

Requires the complete text to match all wildcard elements.

trim() / trimmed()

Removes the matched prefix and/or suffix, retaining the middle text.

Removes the complete text.

split()

Returns {prefix, remainder} without a suffix, {prefix, middle} with both parts, or {leading text, suffix} for a suffix-only pattern.

Returns {text, empty}.

length()

Returns the prefix length, or the suffix length for a suffix-only pattern.

Returns the complete text length.

index()

Returns the index after the prefix, or the start of the suffix for a suffix-only pattern.

Returns the end index.

On a mismatch, matches() and trim() return false, trimmed() returns the original text, split() returns {empty, text}, length() returns zero, and index() returns no-index. trim() reports whether text was removed, so a successful empty glob match returns false. That empty match has length zero and end index zero, distinguishing it from a mismatch through index() or matches(). trim() accepts only string editors; use trimmed() to obtain a narrowed read-only string. Lengths and indexes use bytes for UTF-8, code units for UTF-16, and code points for UTF-32.

Pattern creation uses named factories, including fromDivider() with no arguments for an empty Divider pattern. Copying and moving retain the compiled kind in the destination; a moved-from value behaves as an empty Divider.

Example

using namespace el::text::literals;

const auto parsed = el::StringPattern::fromDivider("http?://*"_el);
const auto ok = parsed.matches("https://example.test"_el);

using namespace el::text::pattern;

static const auto staticPattern = el::StringPattern::fromDivider(
    Text{U"http"},
    OneChar{},
    Text{U"://"},
    Divider{});

Fuzzy Matching

Introduction

The text::fuzzy namespace ranks candidate strings by Damerau-Levenshtein edit distance. Matching operates on decoded Unicode code points, and an adjacent transposition counts as one edit. It is suitable for short suggestion lists, command names, option names, and other user-entered identifiers.

Matcher

Matcher stores the reference pattern, an optional maximum distance, an optional result limit, and a CharCompareFn. Distance and result limits are unbounded by default, and matching is case-sensitive unless a comparison callback is configured.

findMatches() accepts a StringList and returns a MatchList ordered by distance and then original candidate order. Equivalent candidates are deduplicated using the configured comparison callback.

Match Results

Match exposes the original candidate and its CpCount edit distance. MatchList is an util::List of matches.

Example

auto matcher = el::text::fuzzy::Matcher{"verbsoe"_el};
matcher.setMaximumDistance(el::CpCount{2U}).setMaximumResults(el::ItemCount{3U});
const auto matches = matcher.findMatches(el::StringList{"verbose"_el, "version"_el, "quiet"_el});

String Splitters

For a single first or last separator, use the string methods described in Splitting at a Separator. They return both halves and distinguish an absent separator from empty fields.

String splitters read an owning Core string sequentially and return copy-on-write slices without copying their text. StringSplitter is the common UTF-8 alias. The U8StringSplitter, U16StringSplitter and U32StringSplitter variants provide the same interface for each supported width.

Separators

A splitter accepts either one Char or a CharSet. Each call to next() reads through the next matching separator. Malformed encoded data is handled tolerantly like the underlying string type.

StringSplitMode::DiscardSeparator returns only the text between separators. Consecutive separators therefore return empty parts, and a trailing separator produces a final empty part. StringSplitMode::KeepSeparator includes the separator at the end of each part. In this mode a trailing separator completes the preceding part without producing another empty part, which is useful for line-oriented processing.

Sequential State

isAtEnd() distinguishes an empty part from the end of the sequence. Calling next() after the end safely returns an empty string. Calling skip() consumes the same next part as next() without constructing its shared slice; calling it after the end has no effect. splitCount() counts consumed parts, including empty parts and parts discarded by skip(). splitIndex() is the zero-based index of the next part and equals that count. Calls at the end leave both values unchanged; reset() returns both to zero.

remaining() returns the unread suffix as another shared slice, and reset() restarts the splitter at the beginning.

An empty source has one empty part. This preserves ordinary split semantics and lets a caller observe the source once before isAtEnd() becomes true.

The UTF-8 implementation has a direct byte-search path for a single ASCII separator. ASCII bytes cannot occur inside a UTF-8 multibyte sequence, so this optimization preserves tolerant decoding behavior. Other separator sets use decoded character matching. The width-specific backends can use independent search optimizations while keeping the same public contract.

Callback Processing

forEach(function) processes all remaining parts with the current separator mode. The callback accepts either const String &part or const String &part, ItemIndex index, using the splitter’s string width. The index is the zero-based source part index and continues from splitIndex() after earlier reads or skips. Each part is consumed before its callback runs.

Return LoopStatus::Continue to process the next part, LoopStatus::Stop to return LoopResult::Stopped, or LoopStatus::Error to return LoopResult::Error. Reaching the end returns LoopResult::Success. After a stop, error, or callback exception, processing can resume with the next unread part. Callback exceptions propagate to the caller. A null callback returns success without consuming any parts. Calling forEach() at the end returns success without invoking the callback.

Interface

class Match

One fuzzy text match and its edit distance.

Public Functions

Match() = default

Create an empty match.

inline Match(text::String text, unit::CpCount distance) noexcept

Create a match.

Parameters:
  • text – The matching candidate text.

  • distance – The Damerau-Levenshtein edit distance.

inline const text::String &text() const noexcept

Get the matching candidate text.

inline const text::String &candidate() const noexcept

Get the matching candidate text.

inline unit::CpCount distance() const noexcept

Get the edit distance from the pattern.

using erbsland::text::fuzzy::MatchList = util::List<Match>

A ranked list of fuzzy text matches.

class Matcher

Find text candidates using bounded Damerau-Levenshtein distance.

Matching operates on decoded Unicode code points. Adjacent transpositions count as one edit. Results are ordered by distance and retain candidate order for ties.

Public Functions

Matcher() = default

Create a matcher for an empty pattern.

inline explicit Matcher(text::String pattern) noexcept

Create a matcher for a pattern.

Parameters:

pattern – The text to compare with candidates.

inline const text::String &pattern() const noexcept

Get the pattern.

Matcher &setPattern(text::String pattern) noexcept

Set the pattern.

inline unit::CpCount maximumDistance() const noexcept

Get the maximum accepted edit distance.

Matcher &setMaximumDistance(unit::CpCount maximumDistance) noexcept

Set the maximum accepted edit distance.

inline unit::ItemCount maximumResults() const noexcept

Get the maximum number of returned matches.

Matcher &setMaximumResults(unit::ItemCount maximumResults) noexcept

Set the maximum number of returned matches.

inline CharCompareFn comparisonFn() const noexcept

Get the optional decoded-character comparison function.

Matcher &setComparisonFn(CharCompareFn comparisonFn) noexcept

Set the decoded-character comparison function. Empty selects exact comparison.

MatchList findMatches(const text::StringList &candidates) const

Find matching candidates.

Parameters:

candidates – Candidate text in preferred tie order.

Returns:

Accepted, deduplicated matches ordered by distance and candidate order.

struct Divider

Split the pattern into front and back matching parts.

struct OneChar

Match one decoded character.

class Set

A set element made of one or more ranges.

Public Functions

constexpr Set() noexcept = default

Create an empty set.

inline constexpr Set(std::initializer_list<Range> ranges)

Create a set from a list of ranges.

inline explicit Set(const CharSet &charSet)

Create a set from an existing character set.

inline constexpr std::size_t count() const noexcept

The number of ranges in this set.

inline constexpr CharRange range(const std::size_t index) const noexcept

Access the range at the given index.

class Range

A character range element.

Public Functions

constexpr Range() noexcept = default

Create an empty range.

inline constexpr Range(const Char first, const Char last) noexcept

Create a range from two characters.

inline constexpr Range(const char32_t first, const char32_t last) noexcept

Create a range from two code points.

inline explicit constexpr Range(const CharRange range) noexcept

Create a range from an existing character range.

inline constexpr CharRange charRange() const noexcept

Access the range.

struct Star

Match zero or more decoded characters in a glob pattern.

class Text

Non-owning UTF-32 text element for static pattern construction.

Public Functions

constexpr Text() noexcept = default

Create an empty text element.

template<std::size_t N>
inline explicit constexpr Text(const char32_t (&data)[N]) noexcept

Create a text element from a UTF-32 literal.

inline explicit constexpr Text(const std::u32string_view text) noexcept

Create a text element from a UTF-32 read-only string.

inline constexpr std::u32string_view view() const noexcept

Access the literal text.

class StringPattern

A lightweight decoded-character string pattern.

See: Text Matching and Splitting

Public Functions

StringPatternKind kind() const noexcept

Get the matching and extraction semantics selected by the factory.

Returns:

The factory-selected Divider, Glob, or PosixGlob kind; moved-from patterns report Divider.

bool matches(const U8String &text) const noexcept

Test whether text matches this pattern.

Divider matches a prefix, or non-overlapping anchored prefix/suffix parts around its divider. Both glob kinds require complete text to match; empty globs match empty text. An empty Divider never matches.

Parameters:

text – Decoded candidate text; matching is case-sensitive.

Returns:

Whether the configured pattern matches.

bool matches(const U16String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

bool matches(const U32String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

bool trim(U8StringEditor &text) const

Remove matched text from an editable string.

Divider removes the matching prefix and/or suffix; both glob kinds clear the complete text on a match. A mismatch leaves text unchanged, and a match against empty text removes nothing.

Parameters:

text – Text to modify.

Returns:

Whether any text was removed.

bool trim(U16StringEditor &text) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

bool trim(U32StringEditor &text) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

U8String trimmed(const U8String &text) const noexcept

Return text after removing its matching parts.

Divider removes its prefix and/or suffix, retaining the middle; both glob kinds return empty text on a match.

Parameters:

text – Text to match without modifying it.

Returns:

The remaining text on a match, or the original text on a mismatch.

U16String trimmed(const U16String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

U32String trimmed(const U32String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

std::pair<U8String, U8String> split(const U8String &text) const noexcept

Split text according to this pattern’s matching boundary.

Divider returns {prefix, remainder} without a suffix, {prefix, middle} with both parts, or {leading text, suffix} with only a suffix. Both glob kinds return {text, empty} on a match.

Parameters:

text – Text to match without modifying it.

Returns:

The described pair on a match; {empty, text} on a mismatch.

std::pair<U16String, U16String> split(const U16String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

std::pair<U32String, U32String> split(const U32String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

unit::ByteCount length(const U8String &text) const noexcept

Get the native length selected by a match.

Divider returns the prefix length, or the suffix length for a suffix-only pattern. Both glob kinds return the complete text length. Units are bytes for UTF-8, code units for UTF-16, and code points for UTF-32.

Parameters:

text – Text to match.

Returns:

The selected length, or zero on a mismatch or an empty glob match.

unit::U16Count length(const U16String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

unit::CpCount length(const U32String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

unit::ByteIndex index(const U8String &text) const noexcept

Get the native boundary selected by a match.

Divider returns the index after its prefix, or the start of its suffix for a suffix-only pattern. Both glob kinds return the end index, including zero for an empty match; units follow the text’s native encoding.

Parameters:

text – Text to match.

Returns:

The selected index, or no-index on a mismatch.

unit::U16Index index(const U16String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

unit::CpIndex index(const U32String &text) const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Public Static Functions

static StringPattern fromDivider(const AnyString &pattern)

Parse a case-sensitive prefix/divider pattern from text of any supported width.

Without a divider, matches() accepts a matching prefix; one * divides anchored prefix and suffix parts. An empty pattern never matches. Use fromGlob() for full-string matching with multiple stars.

Parameters:

pattern – Literals, ?, [a-z], [^a-z], and at most one *; backslash escapes ?, *, [, ], and backslash.

Throws:

ParseError – If syntax is invalid, a star stands alone, or compiled range limits are exceeded.

Returns:

A compiled Divider pattern.

template<pattern::AnyElement... Args>
static StringPattern fromDivider(const Args&... elements)

Create a case-sensitive prefix/divider pattern directly from typed elements.

No elements creates a Divider pattern that never matches. Star elements are rejected at compile time.

Template Parameters:

Args – Supported typed pattern element types.

Parameters:

elements – The literal, character, set and optional divider elements.

Throws:

ParseError – If the divider stands alone, repeats, or typed storage limits are exceeded.

Returns:

A compiled Divider pattern.

static StringPattern fromGlob(const AnyString &pattern)

Compile an Erbsland Core full-string glob using Divider’s escaping and character-set syntax.

Stars match zero or more characters, including separators and newlines; empty globs match only empty text.

Parameters:

pattern – Text of any supported width: literals, *, ?, [a-z], and [^a-z], with Divider’s escapes.

Throws:

ParseError – If syntax is invalid or compiled range limits are exceeded.

Returns:

A compiled Glob pattern.

template<pattern::AnyElement... Args>
static StringPattern fromGlob(const Args&... elements)

Create a full-string glob directly from typed elements.

No elements matches only empty text. Divider elements are rejected at compile time.

Template Parameters:

Args – Supported typed pattern element types.

Parameters:

elements – The literal, character, set and any number of Star elements.

Throws:

ParseError – If typed storage limits are exceeded or an element is invalid.

Returns:

A compiled Glob pattern.

static StringPattern fromPosixGlob(const AnyString &pattern)

Compile a full-string glob using the existing Python-style dialect.

Parameters:

pattern – Text of any supported width: *, ?, [abc], [a-z], [!a-z]; backslashes and unmatched brackets are literal.

Throws:

ParseError – If characters are invalid or the compiled representation exceeds 65535 ranges.

Returns:

A compiled PosixGlob; stars include separators/newlines, and an empty glob matches only empty text.

Public Static Attributes

static constexpr auto cMaximumElements = pattern::cMaximumElements

Maximum number of elements in a typed pattern.

static constexpr auto cMaximumRanges = pattern::cMaximumStaticRanges

Maximum number of total ranges in a typed pattern.

static constexpr auto cMaximumSetRanges = pattern::cMaximumSetRanges

Maximum number of ranges in one set element.

enum class erbsland::text::StringPatternKind : std::uint8_t

Select the matching and extraction semantics of a compiled string pattern.

See: Text Matching and Splitting

Values:

enumerator Divider

Match a prefix, or anchored prefix/suffix parts separated by one optional divider.

Trimming removes these parts and preserves the text between them.

enumerator Glob

Match the complete text with Core character-set syntax, escapes, and any number of glob wildcards.

A successful match consumes the complete text for trimming and splitting.

enumerator PosixGlob

Match the complete text using the Python-style glob dialect with exclamation-mark set negation.

A successful match consumes the complete text for trimming and splitting.

enum class erbsland::text::StringSplitMode : uint8_t

Select how a string splitter handles separator characters.

Values:

enumerator DiscardSeparator

Exclude the separator from each returned part.

enumerator KeepSeparator

Include the separator at the end of each returned part.

typedef U8StringSplitter erbsland::text::StringSplitter

The common sequential UTF-8 string splitter.

typedef impl::StringSplitter<U16String> erbsland::text::U16StringSplitter

A sequential UTF-16 string splitter.

typedef impl::StringSplitter<U32String> erbsland::text::U32StringSplitter

A sequential UTF-32 string splitter.

typedef impl::StringSplitter<U8String> erbsland::text::U8StringSplitter

A sequential UTF-8 string splitter.