====== 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.
→ [[lyx_-_programmiersprache:units:string|std.string]] · [[lyx_-_programmiersprache:units:text|std.text]] · [[lyx_-_programmiersprache:units:unicode|std.unicode]] · [[lyx_-_programmiersprache:units:grapheme|std.grapheme]] · [[lyx_-_programmiersprache:units:regex|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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1311|#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 Lower** — ''TextEqualsFold'' 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 ====
- **Byteweise** (''TextEquals'') — für Schlüssel, IDs, Protokollfelder. Schnell, exakt.
- **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.
- **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''.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1313|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.
→ [[lyx_-_programmiersprache:guides:validate|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.string'' — ''StrSplit'' | ✅ ab lyxc 1.0.16F |
| ''std.string'' — ''StrToUpper''/''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 =====
* [[lyx_-_programmiersprache:units:string|std.string]] · [[lyx_-_programmiersprache:units:text|std.text]] · [[lyx_-_programmiersprache:units:unicode|std.unicode]] · [[lyx_-_programmiersprache:units:grapheme|std.grapheme]] · [[lyx_-_programmiersprache:units:regex|std.regex]] · [[lyx_-_programmiersprache:units:strtype|std.strtype]]
* [[lyx_-_programmiersprache:guides:validate|Validierung (std.validate)]] — die bessere Antwort auf „Format prüfen"
* [[lyx_-_programmiersprache:guides:daten-serialisierung|Daten & Serialisierung]] — Text in Formate und zurück
* [[lyx_-_programmiersprache:sprache:datentypen|Datentypen: pchar im Typsystem]]
* [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher: alloc, peek & poke]]
Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt und ausgeführt worden; die angegebenen Ausgaben sind die tatsächlichen.