Text & Unicode

Zeichenketten in Lyx gibt es auf zwei Ebenen: pchar — eine nullterminierte Bytefolge, schnell und roh — und Text aus std.text, das UTF-8 versteht und Codepoints, Grapheme und Normalisierung kennt. Diese Seite zeigt, wann welche Ebene richtig ist, welche Funktionen tragen und wo die Fallen liegen.

std.string · std.text · std.unicode · std.grapheme · std.regex


1. Zwei Ebenen, eine Entscheidung

pchar + std.string Text + std.text
Einheit Byte Codepoint / Graphem
Speicher Aufrufer stellt Zielpuffer Unit verwaltet, TextFree gibt frei
Länge Bytes bis zur 0 TextByteLength und TextCodepointCount
Umlaute, Emoji zählt Bytes, schneidet mitten hinein korrekt
Groß-/Kleinschreibung nur ASCII voll (std.unicode_case)
Geschwindigkeit maximal eine Indirektion mehr

Faustregel: Protokolle und Dateiformate mit pchar, Benutzertexte mit Text. Ein HTTP-Header, ein Dateiname, ein Schlüssel in einer Konfiguration — das sind Bytefolgen. Ein Name, eine Adresse, eine Eingabe aus einem Formular — das ist Text, und dort kostet Byte-Denken Datenverlust.

var s: pchar := "hallo"c;              // Literal mit c-Suffix: pchar
var t: Text := TextFromPchar(s as int64);   // dieselbe Folge als Text


2. pchar — std.string

import std.io;
import std.alloc;
import std.string;

fn main(): int64 {
    PrintLn(IntToStr(StrFind("hallo welt"c, "welt"c)));    // 6
    PrintBoolLn(StrContains("hallo welt"c, "wel"c));       // true
    PrintLn(IntToStr(StrCount("a,b,c,d"c, ","c)));         // 3
    PrintLn(StrTrim("  text  "c));                         // text

    var d1: int64 := alloc(256);
    PrintLn(StrSubstring(d1 as pchar, "hallo welt"c, 6, 4));   // welt

    var d2: int64 := alloc(256);
    PrintLn(StrReplace(d2 as pchar, "a-b-c-d"c, "-"c, "+"c));  // a+b+c+d

    PrintLn(StrPadLeft("7"c, 3, 48));    // 007  (48 = '0')
    PrintLn(StrRepeat("ab"c, 3));        // ababab
    return 0;
}

Das Muster: Zielpuffer zuerst. Funktionen, die eine neue Zeichenkette erzeugen (StrSubstring, StrReplace, StrToUpper, StrTrimWhitespace), nehmen den Zielpuffer als erstes Argument und geben ihn auch zurück. Der Aufrufer besorgt den Speicher und muss ihn groß genug wählen — geprüft wird nichts.

Aufgabe Funktion
Suchen StrFind, StrIndexOf, StrLastIndexOfChar, StrContains, StrCount
Ausschneiden StrSubstring
Ersetzen StrReplace
Trimmen StrTrim (in place), StrTrimWhitespace (in Zielpuffer)
Vergleichen StrCmp (0 = gleich)
Umwandeln StrToInt, StrToInt64, StrToF64, Int64ToStr
Aufbereiten StrPadLeft, StrPadRight, StrRepeat, StrReverse
Groß/klein StrToUpper, StrToLower, CharToUpper, StrFirstCharToUpper
 
StrToUpper und StrToLower arbeiten nur auf ASCII. StrToUpper(dest, „straße“) liefert STRAßE — das ß bleibt stehen, ebenso jeder Umlaut, jedes Akzentzeichen, jedes kyrillische Zeichen. Für alles jenseits von A–Z ist TextToUpperFull aus std.unicode_case zuständig (Abschnitt 4).

var parts: int64 := alloc(8 * 8);          // Platz fuer 8 Zeiger
var n: int64 := StrSplit("a,b,c"c, ","c, parts, 8);   // n = 3
var i: int64 := 0;
while (i < n) {
    var teil: pchar := peek64(parts + i * 8) as pchar;
    PrintLn(teil);                          // a, b, c
    i := i + 1;
}

Der Rückgabewert ist die Anzahl der Teile, das Zielarray hält die Zeiger. Bis lyxc 1.0.16F verdeckte ein gleichnamiger Compiler-Builtin diese Funktion und lieferte statt der Anzahl eine Adresse; seit 1.0.16F entscheidet die Argumentzahl (#1311).


3. Text — UTF-8 richtig behandeln

Text kennt den Unterschied zwischen einem Byte, einem Codepoint und einem sichtbaren Zeichen.

import std.io;
import std.text;

fn main(): int64 {
    var t: Text := TextFromPchar("Größe: 42 €"c as int64);

    Print("Bytes: ");      PrintLn(IntToStr(TextByteLength(t)));       // 15
    Print("Codepoints: "); PrintLn(IntToStr(TextCodepointCount(t)));   // 11
    Print("gueltig: ");    PrintLn(IntToStr(TextIsValid(t)));          // 1

    var n: Text := TextFromPchar("42"c as int64);
    Print("enthaelt: ");   PrintLn(IntToStr(TextContains(t, n)));      // 1
    Print("Position: ");   PrintLn(IntToStr(TextFind(t, n)));          // 9 (Bytes!)

    var up: Text := TextAsciiUpper(t);
    var p: pchar := TextToPchar(up) as pchar;
    PrintLn(p);                                                        // GRößE: 42 €

    TextFree(up);
    TextFree(n);
    TextFree(t);
    return 0;
}

15 Bytes, 11 Codepoints: ö kostet zwei Bytes, drei. Wer mit pchar bei Zeichen 5 schneidet, zerteilt womöglich ein Zeichen und erzeugt ungültiges UTF-8.

Aufgabe Funktion
Erzeugen TextFromPchar, TextFromUtf8, TextFromUtf16, TextFromUtf16Bom
Messen TextByteLength, TextCodepointCount, TextIsValid
Zugreifen TextCodepointAt, TextByteAt, TextByteOffsetOfCodepoint
Ausschneiden TextSubstringCp — nach Codepoints, nicht nach Bytes
Suchen TextFind (Byte-Offset), TextFindCp (Codepoint-Index), TextContains
Ändern TextConcat, TextReplace, TextTrim
Zerlegen TextSplit, TextSplitCount, TextPartAt
Vergleichen TextEquals, TextCompare, TextStartsWith
Hinausgeben TextToPchar, TextToUtf16, TextToUtf16Bom
Freigeben TextFree

TextFind liefert einen Byte-Offset, TextFindCp einen Codepoint-Index. Beide zu verwechseln ist der häufigste Fehler in UTF-8-Code. Wer den Fund an TextSubstringCp weitergibt, braucht TextFindCp.

Jeder Text ist eine Allokation. TextFree gehört auf jedes Zwischenergebnis — TextAsciiUpper, TextConcat, TextReplace erzeugen jeweils einen neuen.


4. Unicode: Groß-/Kleinschreibung, Faltung, Normalisierung

import std.io;
import std.text;
import std.unicode;
import std.unicode_case;
import std.grapheme;

fn main(): int64 {
    var t: Text := TextFromPchar("Größe"c as int64);

    var up: Text := TextToUpperFull(t);
    var p: pchar := TextToPchar(up) as pchar;
    PrintLn(p);                                                  // GRÖßE

    var a: Text := TextFromPchar("STRASSE"c as int64);
    var b: Text := TextFromPchar("strasse"c as int64);
    Print("Fold-gleich: "); PrintLn(IntToStr(TextEqualsFold(a, b)));   // 1

    var nfc: Text := TextToNFC(t);
    var nfd: Text := TextToNFD(t);
    Print("NFC Bytes: "); PrintLn(IntToStr(TextByteLength(nfc)));      // 7
    Print("NFD Bytes: "); PrintLn(IntToStr(TextByteLength(nfd)));      // 8
    Print("gleich: ");    PrintLn(IntToStr(TextEqualsNormalized(nfc, nfd)));  // 1

    var emoji: Text := TextFromPchar("a👍🏽b"c as int64);
    Print("Codepoints: "); PrintLn(IntToStr(TextCodepointCount(emoji)));   // 4
    Print("Grapheme: ");   PrintLn(IntToStr(TextGraphemeCount(emoji)));    // 3
    return 0;
}

Was die Zahlen bedeuten

  • NFC 7 Bytes, NFD 8 Bytes — dasselbe Wort. In NFC ist ö ein Codepoint, in NFD sind es zwei (o + Trema). Zwei Zeichenketten, die auf dem Bildschirm identisch aussehen, sind byteweise verschieden. TextEqualsNormalized vergleicht trotzdem richtig.
  • 4 Codepoints, 3 Grapheme👍🏽 ist Daumen plus Hautton-Modifikator: zwei Codepoints, ein sichtbares Zeichen. Wer Eingaben in der Länge begrenzt oder eine Zeile umbricht, muss mit Graphemen rechnen, nicht mit Codepoints.
  • Fold statt LowerTextEqualsFold ist der richtige Vergleich für „gleich, egal wie geschrieben„. TextToLowerFull und danach vergleichen ist es nicht: Case-Folding kennt Sonderfälle, die eine Kleinschreibung nicht abdeckt.
Zweck Funktion
Voll-Unicode Groß/Klein TextToUpperFull, TextToLowerFull, UnicodeToUpperFull
Case-insensitiver Vergleich TextEqualsFold, TextCompareFold, TextContainsFold
Normalisierung TextToNFC, TextToNFD, TextToNFKC, TextToNFKD
Vergleich über Formen hinweg TextEqualsNormalized, TextCompareNormalized
Grapheme TextGraphemeCount, TextGraphemeAt, TextGraphemeByteOffset
Unicode-Leerraum trimmen TextTrimUnicode, TextTrimStartUnicode, TextTrimEndUnicode

TextTrimUnicode entfernt auch geschütztes Leerzeichen und schmales Leerzeichen — TextTrim kennt nur ASCII-Leerraum. Bei Formulareingaben ist das der Unterschied zwischen „Eingabe leer“ und „Eingabe sieht leer aus„.

 
ß wird von TextToUpperFull nicht zu SS. Größe ergibt GRÖßE statt GRÖSSE. Umlaute werden korrekt umgesetzt; nur die Fälle, in denen ein Zeichen zu mehreren wird, fehlen. Für Anzeigezwecke unkritisch, für Sortierschlüssel und Vergleiche TextEqualsFold verwenden statt selbst großzuschreiben.

Empfehlung: Vergleiche in drei Stufen

  1. Byteweise (TextEquals) — für Schlüssel, IDs, Protokollfelder. Schnell, exakt.
  2. Normalisiert (TextEqualsNormalized) — für alles, was aus verschiedenen Quellen kommt: eine Datei vom Mac liegt in NFD vor, dieselbe Eingabe aus einem Webformular in NFC.
  3. Gefaltet (TextEqualsFold) — für Benutzernamen, Suchbegriffe, Anmeldedaten.

5. Reguläre Ausdrücke

std.regex ist ein eigener, kleiner Matcher — kein PCRE. Der abgedeckte Sprachumfang ist begrenzt, und die Grenzen sind nicht offensichtlich, deshalb hier gemessen:

import std.io;
import std.alloc;
import std.regex;

fn main(): int64 {
    PrintBoolLn(RegexMatch("^abc$"c, "abc"c));               // true
    PrintBoolLn(RegexMatch("^abc$"c, "abcd"c));              // false
    PrintLn(IntToStr(RegexSearch("[0-9]+"c, "Preis: 42 Euro"c)));   // 7

    var d: int64 := alloc(256);
    var n: int64 := RegexReplaceInto(d as pchar, "[0-9]+"c, "a1b22c"c, "#"c, 0);
    var s: pchar := d as pchar;
    Print("Ersetzungen: "); PrintLn(IntToStr(n));            // 2
    PrintLn(s);                                              // a#b#c
    return 0;
}

Konstrukt Zustand (1.0.16F)
Zeichenklassen [a-z], [^0-9]
Quantoren +, *, ?, {n}
Anker ^, $
Escapes \d, \w, \s
Gruppen (…)
Alternation a|b
Captures RegexCaptureText, RegexCaptureCount ❌ — siehe unten
Rückwärtsreferenzen, Lookahead
 
Die Gruppen matchen, die Captures nicht. (…) und a|b arbeiten seit dem stdlib-Stand 80479041 korrekt — aber der Inhalt einer Gruppe ist nicht auslesbar: nach einem erfolgreichen RegexMatch("([a-z]+)@([a-z]+)", "user@example") liefert RegexCaptureCount() den Wert 0 und RegexCaptureText eine leere Zeichenkette.

Für „passt das Muster?“ ist das ohne Belang. Wer Teile herausziehen will, sucht die Trennstelle mit StrIndexOfChar und schneidet mit StrSubstring.
(Issue #1313)

Praktische Folge: std.regex trägt für Suchen (RegexSearch), Ersetzen (RegexReplaceInto) und Prüfen (RegexMatch mit ^…$). Für fest umrissene Formate — E-Mail, Datum, IBAN, Kontonummer — ist std.validate trotzdem die bessere Wahl: geprüfte Algorithmen samt Prüfziffer statt eines Musters, das Syntax mit Gültigkeit verwechselt.

Validierungs-Guide für IBAN, EAN, VIN und zwanzig weitere geprüfte Formate.


6. Wiederkehrende Fallstricke

Ergebnisse ausgeben

var s: pchar := puffer as pchar;
PrintLn(s);                  // richtig
PrintLn(satz.name);          // pchar-FELD: gibt die ADRESSE aus

Ein pchar-Feld eines Structs wird als Zahl ausgegeben; über eine Variable geht es richtig. Die Typumwandlung im Argument war bis lyxc 1.0.15G ebenso betroffen und arbeitet seit 1.0.16F korrekt.

Zielpuffer bemessen

Keine der Str-Funktionen prüft die Zielgröße. Bei StrReplace kann das Ergebnis länger als die Eingabe sein — ein Puffer in Eingabegröße reicht nicht. Faustregel: Eingabelänge × Ersatzlänge ÷ Suchlänge, plus Reserve.

Nullterminierung

Was aus dem Netz oder aus einer Datei kommt, ist nicht nullterminiert. Vor jeder Str-Funktion selbst setzen:

poke8(buf + n, 0);

Und deshalb nur bufsize - 1 Byte einlesen.

Index heißt Byte

StrSubstring(dest, src, 6, 4) zählt Bytes. Bei ASCII fällt das nie auf, bei Größe schneidet es mitten in das ö. Sobald Benutzertext im Spiel ist: Text verwenden und TextSubstringCp.

Was freigegeben werden muss

Angelegt mit Freigegeben mit
alloc(n) free(ptr, n)
TextFromPchar, TextFromUtf8, jede Text-Operation TextFree
StrJoin, StrRepeat, StrPadLeft (eigene Allokation) free mit der Ergebnislänge

7. Zustand auf einen Blick (lyxc 1.0.16F)

Bereich Zustand
std.string — Suchen, Schneiden, Ersetzen, Trimmen, Padding
std.stringStrSplit ✅ ab lyxc 1.0.16F
std.stringStrToUpper/StrToLower ⚠️ nur ASCII
std.text — Text, Codepoints, Suchen, Zerlegen, UTF-16
std.unicode — Normalisierung NFC/NFD/NFKC/NFKD
std.unicode_case — Fold, Full-Case ✅ (ßSS fehlt)
std.grapheme — Graphemzählung und -zugriff
std.regex — Klassen, Quantoren, Escapes, Anker, Gruppen, Alternation
std.regex — Captures auslesen (RegexCaptureText) ❌ liefert leer, RegexCaptureCount ist 0

8. Weiterführend

Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt und ausgeführt worden; die angegebenen Ausgaben sind die tatsächlichen.