Transforming Strings

Most text changes are easier to reason about when the original value stays intact. You start with a String, ask for the value you want, and give that result a name. The source remains available for another decision, a diagnostic, or a different transformation.

Erbsland Core expresses this style through past-tense operations such as trimmed(), replacedAll(), removed(), and normalized(). This page shows how these copy-returning operations fit together, what they cost, and when a sequence of changes has become a true editing workflow that belongs in StringEditor.

A New Value or One Mutable Working Copy?

A single transformation is naturally expressed as a returned String. Several independent derived values should also remain String operations because each result has clear value semantics.

Use StringEditor when the algorithm is genuinely a sequence of dependent mutations to one working value. That avoids creating a succession of intermediate results and can reuse unique capacity between steps. See Editing Strings in Place for that workflow.

Where New Character Storage Is Needed

A slice selects a range and shares the original backing store, so it does not copy the selected character data. A transformation that changes character data generally needs result storage and copies or encodes the resulting text. Some no-op cases can return shared data, but code should select an operation for clarity rather than depend on that optimization.

Repeatedly assigning transformed results inside a long edit chain can allocate several intermediate buffers. If all steps form one mutation workflow, an editor is the more direct representation. If the goal is construction from fragments, use the allocation-aware choices in Building Strings instead.

Give Each Meaningful Result a Name

Keep the original as String and name meaningful derived results. This is particularly effective for canonicalization pipelines where each stage has a distinct purpose.

/// `String::transformed()` can create a canonical text form with a single character-mapping function.
///
/// ASCII-only mappings are useful for machine-readable identifiers because they leave non-ASCII characters untouched
/// and avoid the Unicode database.
void canonicalStationName() {
    const auto displayName = el::String{"Module ORBITE-Äther 07"_el};
    auto canonicalName = displayName.transformed(el::Char::toAsciiLowercase);

    el::io::printLine("Display name ..: "_el, displayName);
    el::io::printLine("Canonical .....: "_el, canonicalName);
}
Display name ..: Module ORBITE-Äther 07
Canonical .....: module orbite-Äther 07

Replace or Remove Without Losing the Source

Use byte indexes when positions came from UTF-8 search operations; those indexes already address native storage. Use code-point coordinates when a short, fixed-shape value is naturally described in decoded characters.

replaced(), replacedFirst(), and replacedAll() return changed values. removed(), removedFirst(), and removedAll() return values without the selected range, text, or character set. For a shared selection without character-data copying, use slice() instead of removal.

/// A `String` allows building a new string with text or characters replaced.
/// The replacement happens in one pass, without creating intermediate copies.
/// Also, if no text is replaced, the original string is returned.
void characterReplacement() {
    const auto note = el::String{"⛈️ Gewitter; 🍃 Wind; 🌫 Nebel; ☔️ Regen; 🍃 Wind"_el};
    const auto semicolon = el::CharSet{U';'};
    auto emojisAndSpace = el::CharSet::from(el::UnicodeCategory::OtherSymbol);
    emojisAndSpace.add(U' ');

    // Replace all semicolons with a comma.
    el::io::printLine("Original ....: \""_el, note, "\""_el);
    auto withComma = note.replacedAll(semicolon, U',');
    el::io::printLine("Compact .....: \""_el, withComma, "\""_el);

    // Replace the semicolon + space sequence with a longer ` → ` sequence.
    auto withArrow = note.replacedAll("; "_el, " → "_el);
    el::io::printLine("Arrows ......: \""_el, withArrow, "\""_el);

    // Remove the emojis and space from the text.
    auto plainCompact = note.replacedAll(emojisAndSpace, el::String{});
    el::io::printLine("Plain .......: \""_el, plainCompact, "\""_el);

    // Add a bit of sunshine by replacing all `🍃 Wind` with `☀️ Sun`
    auto sunnySpots = note.replacedAll("🍃 Wind"_el, "☀️ Sun"_el);
    el::io::printLine("Sunny .......: \""_el, sunnySpots, "\""_el);

    // When no text gets replaced, the original string is returned
    el::io::printLine("\nNo change, no copy:");
    el::io::printLine("Original ....: \""_el, note, "\""_el);

    el::io::printLine("Sunny .......: \""_el, sunnySpots, "\""_el);
}
Original ....: "⛈️ Gewitter; 🍃 Wind; 🌫 Nebel; ☔️ Regen; 🍃 Wind"
Compact .....: "⛈️ Gewitter, 🍃 Wind, 🌫 Nebel, ☔️ Regen, 🍃 Wind"
Arrows ......: "⛈️ Gewitter → 🍃 Wind → 🌫 Nebel → ☔️ Regen → 🍃 Wind"
Plain .......: "️Gewitter;Wind;Nebel;️Regen;Wind"
Sunny .......: "⛈️ Gewitter; ☀️ Sun; 🌫 Nebel; ☔️ Regen; ☀️ Sun"

No change, no copy:
Original ....: "⛈️ Gewitter; 🍃 Wind; 🌫 Nebel; ☔️ Regen; 🍃 Wind"
Sunny .......: "⛈️ Gewitter; ☀️ Sun; 🌫 Nebel; ☔️ Regen; ☀️ Sun"

Changes That Depend on Unicode Text

Case conversion and Unicode normalization may change both the number of code points and the native encoded length. They therefore produce new values rather than exposing writable storage. Read Normalizing Unicode Strings before comparing or storing canonically equivalent text.

Trimming and character-set removal are useful for cleanup at boundaries. When a returned range can represent the result, the implementation can retain shared storage instead of copying the kept characters.

/// `String::trimmed()` returns a view with selected characters removed from the front, back, or both sides.
///
/// With a custom `CharSet`, trimming is not limited to whitespace.
void trimCharacterSet() {
    const auto raw = el::String{"*** signal-orbite ;; "_el};
    static const auto border = el::CharSet{" *;"_el};

    auto clean = raw.trimmed(border);
    auto frontOnly = raw.trimmed(border, el::StringSide::Front);
    auto backOnly = raw.trimmed(border, el::StringSide::Back);

    el::io::printLine("Raw .......: \"", raw, "\""_el);
    el::io::printLine("Both sides : \"", clean, "\""_el);
    el::io::printLine("Front only : \"", frontOnly, "\""_el);
    el::io::printLine("Back only .: \"", backOnly, "\""_el);
}
Raw .......: "*** signal-orbite ;; "
Both sides : "signal-orbite"
Front only : "signal-orbite ;; "
Back only .: "*** signal-orbite"