Editing Strings in Place

Sometimes a transformation is not one clean step. You may need to locate a sentence, insert an annotation, replace a name, remove an obsolete field, and only then know what the finished value should be. Creating and naming an intermediate String for every one of those dependent changes would obscure the algorithm and may allocate more temporary storage than necessary.

StringEditor is the local mutable value for this kind of work. This page follows one editor through range changes, replacements, truncation, reuse, and normalization, while also showing where search positions and storage ownership need care. Once the edit is complete, expose the result as String and return to the ordinary read-only model.

Follow One Value Through a Sequence of Edits

Search functions return positions in the editor’s current text. Apply positions before an earlier mutation invalidates them, or search again after the mutation. Byte indexes are the efficient choice when they originate from UTF-8 searches because no coordinate conversion is required.

/// `StringEditor` is an owning, mutable working value for multi-step edits.
/// Use `String` for parameters, stored read-only text, and ordinary
/// copy-returning transformations.
void editingText() {
    auto story = el::StringEditor{"The frost lifts from the valley. A pale crocus opens beside the stone. "
                                  "Der Wind trägt Blätter durch die Luft."_el};

    // Ordinary text values use the owning read-only string type.
    const auto intro = el::String{"A short alpine field note:"_el};

    // Find the insertion position after the first sentence.
    auto firstFullStopIndex = story.findFirstOf({U'.'});
    story.advance(firstFullStopIndex, el::CpLength{2});

    // Insert a new sentence after the first sentence.
    story.insert(firstFullStopIndex, "Sunlight reaches the wet limestone. "_el);

    // Replace the main subject.
    story.replaceAll("pale crocus"_el, "violet gentian"_el);

    // Remove one sentence from the story.
    constexpr auto sentenceBeginText = "Der Wind"_el;
    auto sentenceBeginIndex = story.find(sentenceBeginText);
    constexpr auto sentenceEndText = "Luft."_el;
    auto sentenceEndIndex = story.find(sentenceEndText, sentenceBeginIndex) + sentenceEndText.length();
    story.remove({sentenceBeginIndex, sentenceEndIndex});

    // Append a final sentence.
    story.append(" 🌿 The day begins quietly."_el);

    // Add a line break after each mid-sentence.
    story.replaceAll(". "_el, ".\n"_el);

    el::io::printLine(intro);
    el::io::printLine(story);
}
A short alpine field note:
The frost lifts from the valley.
Sunlight reaches the wet limestone.
A violet gentian opens beside the stone.
 🌿 The day begins quietly.

Narrow the Working Text by Range

remove() deletes the selected range from the editor. keep() discards everything outside the selected range. Use keep() when the editor must remain mutable after narrowing; use String::slice() when a read-only shared view is sufficient.

/// Range-based in-place edits work best with byte indexes returned by search
/// functions. Those indexes already point into the native UTF-8 storage and can
/// be passed directly to `ByteRange`.
///
/// Code-point ranges are useful for short, fixed-shape labels where positions
/// are naturally counted as decoded characters. For long UTF-8 text, byte
/// ranges avoid repeated scans from the beginning of the string.
void removeAndKeepRanges() {
    const auto record = el::String{"site=Åsen|weather=klart|note=sol"_el};

    // Search returns byte indexes, so the field can be removed without conversion.
    const auto weatherStart = record.find("weather="_el);
    const auto noteStart = record.find("note="_el);
    auto compactRecord = el::StringEditor{record};
    compactRecord.remove(el::ByteRange{weatherStart, noteStart});

    // Keep only the field value by reusing byte indexes from the same search path.
    const auto siteValueStart = record.find("="_el) + "="_el.length();
    const auto siteValueEnd = record.find("|"_el, siteValueStart);
    auto siteName = el::StringEditor{record};
    siteName.keep(el::ByteRange{siteValueStart, siteValueEnd});

    // Code-point ranges are readable for small labels with fixed structure.
    auto label = el::StringEditor{"🌙Luna-04"_el};
    label.keep(el::CpRange{el::CpIndex{1U}, el::CpLength{4U}});

    el::io::printLine("Original: "_el, record);
    el::io::printLine("After remove: "_el, compactRecord);
    el::io::printLine("Kept site: "_el, siteName);
    el::io::printLine("Kept label text: "_el, label);
}
Original: site=Åsen|weather=klart|note=sol
After remove: site=Åsen|note=sol
Kept site: Åsen
Kept label text: Luna

Remove What the Text Contains

removeFirst() changes the first matching text occurrence. removeAll() removes every matching text occurrence or every character from a CharSet. An optional character comparison function supports operations such as case-folded matching.

/// `removeFirst()` changes only the first matching text occurrence.
/// `removeAll()` removes every matching occurrence or every character from
/// a character set.
///
/// Text matching is decoded Unicode text matching. You can pass a comparison
/// function such as `Char::compareCaseFolded` when case-insensitive matching is
/// required.
void removeFirstAndAll() {
    const auto source = el::String{"mist :: frost :: mist :: aurora"_el};

    auto firstOnly = el::StringEditor{source};
    firstOnly.removeFirst("mist"_el);

    auto allMist = el::StringEditor{source};
    allMist.removeAll("mist"_el);

    auto withoutSeparators = el::StringEditor{source};
    withoutSeparators.removeAll(el::CharSet{": "_el});

    auto folded = el::StringEditor{"Ähre | äHRE | aster"_el};
    folded.removeAll("ähre"_el, el::Char::compareCaseFolded);

    el::io::printLine("Source: "_el, source);
    el::io::printLine("First text removed: "_el, firstOnly);
    el::io::printLine("All text removed: "_el, allMist);
    el::io::printLine("Characters removed: "_el, withoutSeparators);
    el::io::printLine("Case-folded removal: "_el, folded);
}
Source: mist :: frost :: mist :: aurora
First text removed:  :: frost :: mist :: aurora
All text removed:  :: frost ::  :: aurora
Characters removed: mistfrostmistaurora
Case-folded removal:  |  | aster

Insert and Replace at Known Positions

insert() adds text at a byte or code-point index. replace() changes a selected range, while replaceFirst() and replaceAll() search and mutate in one operation. These operations may move following data inside the allocation; repeated large insertions near the beginning can therefore be more expensive than appending.

/// `insert()` adds text at a byte or code-point index. `replace()` changes a
/// range, `replaceFirst()` changes the first matching text occurrence, and
/// `replaceAll()` changes every matching occurrence in place.
///
/// Prefer byte indexes when they come from a search operation. Use code-point
/// indexes and ranges when the text is short and the edit position is naturally
/// counted in decoded characters.
void insertAndReplace() {
    auto report = el::StringEditor{"Plot 07 | sky=grey | sky=grey"_el};

    // A code-point index is readable for inserting at the beginning.
    report.insert(el::CpIndex{0U}, "☀ "_el);

    // Search results are byte indexes and can be reused in a byte range.
    const auto plotNumber = report.find("07"_el);
    report.replace(el::ByteRange{plotNumber, plotNumber + "07"_el.length()}, "08"_el);

    // First and all variants make common text substitutions explicit.
    report.replaceFirst("grey"_el, "clear"_el);
    report.replaceAll("sky="_el, "himmel="_el);

    // A character set replacement handles multiple separators in one pass.
    report.replaceAll(el::CharSet{"|="_el}, U'·');

    auto token = el::StringEditor{"AβC"_el};
    token.replace(el::CpRange{el::CpIndex{1U}, el::CpLength{1U}}, "beta"_el);

    el::io::printLine("Edited report: "_el, report);
    el::io::printLine("Edited token: "_el, token);
}
Edited report: ☀ Plot 08 · himmel·clear · himmel·grey
Edited token: AbetaC

Fit Text into a Character Width

truncate() shortens the editor to a decoded code-point width and can retain the beginning, middle, or end. align() pads the value to a requested code-point width. Both mutate the working value; the corresponding past-tense String methods return a new read-only value.

/// `aligned()` returns a padded read-only value, while `truncate()` shortens an
/// editor in place. Both use decoded code-point lengths rather than UTF-8 bytes.
void alignAndTruncate() {
    const auto label = el::String{"Alpenrose"_el};
    el::io::printLine("Aligned: |"_el, label.aligned(el::CpLength{14U}, el::Alignment::HCenter, U'.'), "|"_el);

    auto observation = el::StringEditor{"Observation: Alpenrose beside pale limestone under morning light"_el};
    observation.truncate(el::CpLength{28U}, el::TruncateMode::Middle, "..."_el);
    el::io::printLine("Truncated: "_el, observation);
}
Aligned: |..Alpenrose...|
Truncated: Observation: ...orning light

Reuse an Editor for Another Pass

clear() removes text while keeping capacity for another edit pass. reset() releases the editor state and returns it to a fresh empty value. append() is appropriate during editing and for small construction tasks, but repeated unreserved growth may reallocate and copy the accumulated character data.

/// `clear()` removes the text while keeping the allocated storage available for
/// reuse. `reset()` returns the string to its initial empty state and releases
/// the reserved storage.
///
/// This pattern is useful when one local editor is reused for several editing
/// passes. Reserve once before predictable growth; do not reserve before each
/// append operation.
void clearResetAndAppend() {
    auto draft = el::StringEditor{"ridge log"_el};
    draft.reserve(el::ByteLength{80U});

    // Append text and repeated code points directly to the editable string.
    draft.append(": "_el).append("lichen"_el).append(", "_el).append("moss"_el);
    draft.append(U'·', el::CpLength{3U});
    printDraftState("Draft"_el, draft);

    // Clear keeps the reserved storage for the next edit pass.
    draft.clear();
    draft.append("reused after clear"_el);
    printDraftState("After clear"_el, draft);

    // Reset releases the storage and starts from the default empty state.
    draft.reset();
    draft.append("fresh after reset"_el);
    printDraftState("After reset"_el, draft);
}

void printDraftState(const el::String &label, const el::StringEditor &editor) {
    el::io::printLine(label, ": "_el, editor);
    el::io::printLine("  bytes="_el, editor.length().toSizeT(), " capacity="_el, editor.capacity().toSizeT());
}
Draft: ridge log: lichen, moss···
  bytes=29 capacity=80
After clear: reused after clear
  bytes=18 capacity=80
After reset: fresh after reset
  bytes=17 capacity=47

Normalize as Part of the Edit

normalize() applies a Unicode normalization form to the editor in place. Use it inside a wider mutation workflow; use String::normalized() when normalization is the only transformation and the source should remain read-only.

/// `normalize()` canonicalizes an editor in place. This decomposed Japanese
/// element name uses a combining voiced mark, which NFC composes with the
/// preceding katakana character.
void normalizeText() {
    auto elementName = el::StringEditor{"カ\u3099リウム"_el};

    el::io::printLine("Before NFC: "_el, elementName);
    elementName.normalize(el::NormalizationForm::Nfc);
    el::io::printLine("After NFC : "_el, elementName);
}
Before NFC: カリウム
After NFC : ガリウム

For capacity planning, copy-on-write behavior, and compaction, continue with Managing StringEditor Storage.