====== 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.