====== std.string — Zeichenketten ====== → [[lyx_-_programmiersprache:units|Zurück zur Unit-Übersicht]] Die meistgenutzte Unit der Standardbibliothek: Suchen, Vergleichen, Ersetzen, Teilen, Verbinden, Auffüllen, Umwandeln — alles auf ''pchar'', also auf rohen, mit einem Nullbyte abgeschlossenen Bytes. **Autor:** Andreas Röne\\ **Copyright:** 2024–2025 Andreas Röne\\ **Quelle:** ''std/string.lyx'' Die Such-, Vergleichs-, Ersetz- und Aufteilfunktionen sind vollständig geprüft und arbeiten korrekt. **Die beiden früher hier verzeichneten Parser-Mängel sind seit 1.0.20F behoben** (#1517, #1518): StrToF64("0.5") = 0.500000 ok=1 StrToF64("3.75") = 3.750000 ok=1 StrToInt64("-") = 0 ok=0 ''StringBuilder'' ist eine **Klasse** und muss mit ''new'' erzeugt werden: var sb: StringBuilder := new StringBuilder(); ''var sb: StringBuilder;'' wird seit [[https://github.com/SEOLizer/LyX-Compiler/issues/1570|#1570]] abgewiesen — die Prüfung aus #1519 greift jetzt auch bei **importierten** Klassen: ''sema error: Variable hat Klassentyp ohne Startwert — `new` oder `null` angeben 'StringBuilder'''. Vorher übersetzte die Zeile fehlerfrei und stürzte beim ersten Aufruf ab. ===== Import ===== import std.string; ''StrLen'', ''StrCharAt'', ''StrSetChar'', ''StrConcat'', ''StrSub'', ''StrNew'', ''StrFree'', ''StrStartsWith'' und ''StrEndsWith'' sind **Builtins** — sie stehen ohne Import zur Verfügung und sind nicht Teil dieser Unit. ---- ===== Die drei Speicher-Konventionen ===== Das ist der wichtigste Abschnitt dieser Seite. Die Unit ist über Jahre gewachsen und verwendet **drei verschiedene Konventionen** dafür, wohin das Ergebnis geschrieben wird. Wer sie verwechselt, bekommt entweder keinen Effekt oder einen Absturz. ^ Konvention ^ Erkennbar an ^ Funktionen ^ | **Ziel vom Aufrufer** | erster Parameter heißt ''dest'' | ''StrToLower'', ''StrToUpper'', ''StrTrimWhitespace'', ''StrSubstring'', ''StrReplace'', ''Int64ToStr'' | | **Ergebnis alloziert** | nur der Eingabestring als Parameter, Rückgabe ''pchar'' | ''StrTrim'', ''StrFirstCharToUpper'', ''StrFirstCharToLower'', ''StrLastCharToUpper'', ''StrLastCharToLower'', ''StrJoin'', ''StrRepeat'', ''StrFormat'', ''StrPadLeft''/''StrPadRight'' (siehe unten) | | **an Ort und Stelle** | verändert das Argument selbst | ''StrReverse'' | Bei der ersten Konvention rechnet der Aufrufer die nötige Größe aus und legt den Puffer an — es findet **keine** Prüfung statt. Bei der zweiten gehört der zurückgegebene Speicher dem Aufrufer. ---- ===== Funktionen ===== ==== Suchen ==== ^ Signatur ^ Beschreibung ^ | ''StrFind(haystack: pchar, needle: pchar): int64'' | Position des ersten Vorkommens, sonst ''-1'' | | ''StrIndexOf(s: pchar, needle: pchar, startIndex: int64): int64'' | wie ''StrFind'', aber ab einer Startposition | | ''StrContains(s: pchar, needle: pchar): bool'' | ob der Teilstring vorkommt | | ''StrIndexOfChar(s: pchar, c: int64, startIndex: int64): int64'' | erstes Vorkommen eines Zeichens ab ''startIndex'' | | ''StrLastIndexOfChar(s: pchar, c: int64): int64'' | letztes Vorkommen eines Zeichens | | ''StrAllIndicesOfCharCount(s: pchar, c: int64): int64'' | wie oft ein Zeichen vorkommt | | ''StrCount(s: pchar, needle: pchar): int64'' | wie oft ein Teilstring vorkommt — **überlappungsfrei** | | ''StrSafeCharAt(s: pchar, index: int64): int64'' | Zeichen an einer Stelle, ''-1'' außerhalb der Länge | Eine **leere Nadel** liefert bei ''StrFind'' die Position 0 (sie gilt als am Anfang gefunden), bei ''StrCount'' dagegen 0. ''StrCount("aaaa", "aa")'' ergibt **2**, nicht 3 — nach einem Treffer wird hinter dem Fund weitergesucht. ==== Vergleichen ==== ^ Signatur ^ Beschreibung ^ | ''StrCmp(a: pchar, b: pchar): int64'' | ''-1'', ''0'' oder ''1'' — lexikografisch nach Bytewert | StrCmp("abc", "abc") // 0 StrCmp("abc", "abd") // -1 StrCmp("b", "a") // 1 ==== Groß- und Kleinschreibung ==== ^ Signatur ^ Beschreibung ^ | ''CharToLower(c: int64): int64'' | einzelnes Zeichen | | ''CharToUpper(c: int64): int64'' | einzelnes Zeichen | | ''StrToLower(dest: pchar, src: pchar): pchar'' | ganzer String **nach ''dest''** | | ''StrToUpper(dest: pchar, src: pchar): pchar'' | ganzer String **nach ''dest''** | | ''StrFirstCharToUpper(s: pchar): pchar'' | erstes Zeichen groß, Ergebnis **alloziert** | | ''StrFirstCharToLower(s: pchar): pchar'' | erstes Zeichen klein, Ergebnis **alloziert** | | ''StrLastCharToUpper(s: pchar): pchar'' | letztes Zeichen groß, Ergebnis **alloziert** | | ''StrLastCharToLower(s: pchar): pchar'' | letztes Zeichen klein, Ergebnis **alloziert** | Alle wandeln **nur ASCII**. ''StrToUpper'' auf ''"straße"'' ergibt ''"STRAßE"'' — das ß bleibt, weil es in UTF-8 zwei Bytes belegt, die keine ASCII-Buchstaben sind. Für Umlaute und Sonderzeichen ist [[lyx_-_programmiersprache:units:text|std.text]] zuständig. ==== Zuschneiden und Teilstrings ==== ^ Signatur ^ Beschreibung ^ | ''StrTrim(s: pchar): pchar'' | Leerraum an beiden Enden weg, Ergebnis **alloziert** | | ''StrTrimWhitespace(dest: pchar, src: pchar): pchar'' | dasselbe, aber **nach ''dest''** | | ''StrSubstring(dest: pchar, src: pchar, start: int64, len: int64): pchar'' | Teilstring nach ''dest''; ''start'' und ''len'' werden auf die Länge begrenzt | | ''IsWhitespace(c: int64): bool'' | Leerzeichen, Tabulator, Zeilenumbruch … | Die beiden Trim-Funktionen unterscheiden sich **nur** in der Speicherkonvention — die Namen verraten das nicht. ''StrTrim'' alloziert, ''StrTrimWhitespace'' schreibt in einen mitgegebenen Puffer. ==== Verändern ==== ^ Signatur ^ Beschreibung ^ | ''StrReverse(s: pchar): pchar'' | dreht **das Argument selbst** um und gibt es zurück | | ''StrReplace(dest: pchar, src: pchar, old: pchar, replacement: pchar): pchar'' | ersetzt **alle** Vorkommen, Ergebnis nach ''dest'' | | ''StrPadLeft(s: pchar, width: int64, padChar: int64): pchar'' | links auffüllen bis ''width'' | | ''StrPadRight(s: pchar, width: int64, padChar: int64): pchar'' | rechts auffüllen bis ''width'' | | ''StrRepeat(s: pchar, n: int64): pchar'' | ''n''-fache Wiederholung, Ergebnis alloziert | ==== Teilen und Verbinden ==== ^ Signatur ^ Beschreibung ^ | ''StrSplit(s: pchar, delim: pchar, out: int64, maxParts: int64): int64'' | schreibt Zeiger auf die Teile in ''out'', liefert deren Anzahl | | ''StrJoin(parts: int64, count: int64, delim: pchar): pchar'' | fügt ''count'' Strings mit Trenner zusammen, Ergebnis alloziert | ''out'' ist die Adresse eines Feldes aus ''int64''-Plätzen zu je 8 Byte. Jeder Teil wird **einzeln alloziert**; der Aufrufer gibt sie einzeln frei. Sind mehr Teile vorhanden als ''maxParts'' zulässt, landet der **gesamte Rest** ungeteilt im letzten Platz: StrSplit("a,b,c,d", ",", out, 2) // 2 Teile: "a" und "b,c,d" ==== Zahlen ==== ^ Signatur ^ Beschreibung ^ | ''StrToInt(s: pchar): int64'' | überliest führenden Leerraum, bricht beim ersten Nicht-Ziffernzeichen ab, **meldet keinen Fehler** | | ''StrToInt64(s: pchar, ok: int64): int64'' | strenger Parser; ''ok'' zeigt auf ein ''int64'', das auf 1 oder 0 gesetzt wird | | ''Int64ToStr(n: int64, buf: int64): pchar'' | Zahl nach ''buf'' — mindestens **22 Byte** bereitstellen | | ''StrToF64(s: pchar, ok: int64): f64'' | Dezimalzahl mit Nachkommateil; ''ok'' zeigt Erfolg an | Die beiden Ganzzahlparser verhalten sich **verschieden**, was leicht zu verwechseln ist: ^ Eingabe ^ ''StrToInt'' ^ ''StrToInt64'' ^ | ''"42"'' | 42 | 42, ok=1 | | ''" 42"'' | 42 | 0, **ok=0** | | ''"12x"'' | 12 | 0, **ok=0** | | ''"abc"'' | 0 | 0, ok=0 | ''StrToInt'' kann eine fehlgeschlagene Umwandlung nicht anzeigen: das Ergebnis 0 ist von einer echten ''"0"'' nicht zu unterscheiden. Wo die Eingabe von außen kommt, gehört ''StrToInt64'' mit ausgewertetem ''ok'' hin. Einen Überlaufschutz hat keiner von beiden. ==== Formatieren ==== ^ Signatur ^ Beschreibung ^ | ''StrFormat(fmt: pchar, a0, a1, a2, a3, a4: int64): pchar'' | printf-artig, **genau 5 Argumente**, Ergebnis alloziert | ^ Platzhalter ^ Bedeutung ^ | ''%s'' | Zeichenkette (''pchar'' als ''int64'' übergeben) | | ''%d'' | Dezimalzahl | | ''%x'' | Hexadezimal, Kleinbuchstaben | | ''%08x'' | Hexadezimal, auf 8 Stellen mit Nullen aufgefüllt | | ''%%'' | ein Prozentzeichen | Die fünf Argumente sind **Pflicht**, auch wenn weniger Platzhalter vorkommen — nicht benutzte mit 0 auffüllen. Die Grenze rührt daher, dass eine Lyx-Funktion höchstens sechs Parameter annimmt. StrFormat("%s hat %d Punkte, hex %x, breit %08x, Prozent %%", "Anna" as int64, 42, 255, 255, 0) Anna hat 42 Punkte, hex ff, breit 000000ff, Prozent % ---- ===== StringBuilder ===== Ein wachsender Puffer zum stückweisen Zusammensetzen. Er verdoppelt seine Kapazität selbständig, wenn es eng wird. ^ Methode ^ Beschreibung ^ | ''Init(initialCap: int64)'' | anlegen; Werte unter 16 werden auf 16 angehoben | | ''Append(s: pchar)'' | Zeichenkette anhängen; ein Nullzeiger wird ignoriert | | ''AppendChar(c: int64)'' | einzelnes Byte anhängen | | ''AppendInt(n: int64)'' | Zahl als Dezimalstring anhängen | | ''Length(): int64'' | belegte Länge ohne die abschließende Null | | ''Capacity(): int64'' | derzeit belegter Puffer | | ''ToString(): pchar'' | Kopie des Inhalts, **alloziert** | | ''Clear()'' | Inhalt verwerfen, Puffer behalten | | ''FreeBuffer()'' | Puffer freigeben | import std.string; fn main(): int64 { var sb: StringBuilder := new StringBuilder(); // new ist Pflicht sb.Init(16); PrintLn("nach Init: len=", IntToStr(sb.Length()), " cap=", IntToStr(sb.Capacity())); sb.Append("Messwerte:"); var i: int64 := 0; while (i < 8) { sb.AppendChar(32); sb.AppendInt(i * 111); i += 1; } PrintLn("nach Append: len=", IntToStr(sb.Length()), " cap=", IntToStr(sb.Capacity())); PrintLn("Inhalt: '", sb.ToString(), "'"); sb.Clear(); PrintLn("nach Clear: len=", IntToStr(sb.Length()), " cap=", IntToStr(sb.Capacity())); sb.FreeBuffer(); return 0; } nach Init: len=0 cap=16 nach Append: len=40 cap=64 Inhalt: 'Messwerte: 0 111 222 333 444 555 666 777' nach Clear: len=0 cap=64 Die Kapazität wandert von 16 über 32 auf 64, ohne dass der Aufrufer etwas tun muss. ''Clear'' setzt die Länge zurück und **behält** den gewachsenen Puffer — wer denselben Builder in einer Schleife wiederverwendet, alloziert also nur einmal. ---- ===== Beispiel: eine Messzeile zerlegen ===== import std.string; import std.alloc; fn main(): int64 { var zeile: pchar := " sensor-07 ; 23.5 ; OK "; // 1. In Felder zerlegen var felder: int64 := alloc(8 * 8); var n: int64 := StrSplit(zeile, ";", felder, 8); PrintLn("Felder: ", IntToStr(n)); var i: int64 := 0; while (i < n) { var roh: pchar := peek64(felder + i * 8) as pchar; PrintLn(" [", IntToStr(i), "] '", roh, "' -> '", StrTrim(roh), "'"); i += 1; } // 2. Namen zerlegen: alles vor und nach dem Bindestrich var name: pchar := StrTrim(peek64(felder) as pchar); var strich: int64 := StrIndexOfChar(name, 45, 0); var klasse: int64 := alloc(64); var nummer: int64 := alloc(64); StrSubstring(klasse as pchar, name, 0, strich); StrSubstring(nummer as pchar, name, strich + 1, StrLen(name) - strich - 1); PrintLn("Geraeteklasse: ", klasse as pchar); PrintLn("Nummer: ", IntToStr(StrToInt(nummer as pchar))); // 3. Bericht zusammensetzen var gross: int64 := alloc(64); var sb: StringBuilder := new StringBuilder(); sb.Init(16); sb.Append(StrToUpper(gross as pchar, name)); sb.Append(" | Status "); sb.Append(StrTrim(peek64(felder + 16) as pchar)); PrintLn(sb.ToString()); PrintLn("Laenge ", IntToStr(sb.Length()), ", Kapazitaet ", IntToStr(sb.Capacity())); sb.FreeBuffer(); return 0; } Felder: 3 [0] ' sensor-07 ' -> 'sensor-07' [1] ' 23.5 ' -> '23.5' [2] ' OK ' -> 'OK' Geraeteklasse: sensor Nummer: 7 SENSOR-07 | Status OK Laenge 21, Kapazitaet 32 Das Beispiel zeigt alle drei Speicherkonventionen nebeneinander: ''StrSplit'' alloziert die Teile selbst, ''StrTrim'' gibt neuen Speicher zurück, ''StrSubstring'' und ''StrToUpper'' schreiben in mitgebrachte Puffer. ---- ===== Worauf zu achten ist ===== ==== StrToF64 kennt keinen Exponenten ==== Der Nachkommateil wird seit 1.0.20F ausgewertet (#1517). Was bleibt, ist die Exponentialschreibweise: ^ Eingabe ^ Ergebnis ^ ''ok'' ^ | ''"3.75"'' | 3.750000 | 1 | | ''"0.5"'' | 0.500000 | 1 | | ''"-2.25"'' | -2.250000 | 1 | | ''"1e3"'' | 0.000000 | **0** | ''"1e3"'' wird also nicht etwa als 1000 gelesen, sondern sauber als ungültig gemeldet — der Rückgabewert ist 0 **und** ''ok'' ist 0. Wer solche Eingaben verarbeiten muss, zerlegt sie am ''e'' selbst. ==== StrPadLeft und StrPadRight allozieren nicht immer ==== Ist die Eingabe bereits mindestens ''width'' lang, geben beide **den Eingabezeiger unverändert** zurück: var kurz: pchar := "7"; var p1: pchar := StrPadLeft(kurz, 4, 48); // "0007" — neuer Speicher var lang: pchar := "12345"; var p2: pchar := StrPadLeft(lang, 4, 48); // "12345" — derselbe Zeiger! Der Rückgabewert darf also **nicht** blind freigegeben werden — im zweiten Fall würde die Freigabe den Speicher des Aufrufers treffen. Dasselbe gilt für ''StrRepeat(s, 0)'' und ''StrJoin(…, 0, …)'', die ein leeres Literal zurückgeben. Wer freigeben will, vergleicht vorher die Zeiger: if ((p2 as int64) != (lang as int64)) { free(p2 as int64, 5); } ==== StrReverse verändert das Original ==== Die Funktion dreht den übergebenen Speicher selbst um und gibt denselben Zeiger zurück. Nach dem Aufruf ist die Eingabe **weg**: var s: pchar := "abcdef"; var r: pchar := StrReverse(s); // r == s, und s lautet jetzt "fedcba" Wird das Original weiter gebraucht, vorher mit ''StrSub'' oder ''StrSubstring'' kopieren. Ein Stringliteral wird bei jeder Zuweisung frisch angelegt, der Schaden bleibt also auf die eine Variable begrenzt — ein Puffer, der an mehreren Stellen benutzt wird, ist dagegen für alle verändert. ==== Zielpuffer werden nicht geprüft ==== Keine der ''dest''-Funktionen kennt die Größe des Ziels. Die nötigen Größen: ^ Funktion ^ Mindestgröße von ''dest'' ^ | ''StrToLower'', ''StrToUpper'', ''StrTrimWhitespace'' | ''StrLen(src) + 1'' | | ''StrSubstring'' | ''len + 1'' | | ''StrReplace'' | Länge nach dem Ersetzen + 1 — bei längerem Ersatztext deutlich mehr als die Quelle | | ''Int64ToStr'' | 22 | ''StrReplace'' ist der gefährlichste Fall: ''"aaa"'' mit ''"a"'' → ''"lang"'' ergibt zwölf Zeichen aus dreien. ==== Nur ASCII ==== Sämtliche Groß-/Kleinschreibung arbeitet auf Bytes im Bereich A–Z und a–z. Umlaute, ß und alles außerhalb von ASCII bleiben unverändert. Auch die Positionsangaben aller Such- und Teilstringfunktionen sind **Byte**-Positionen, keine Zeichenpositionen — bei UTF-8-Text kann ''StrSubstring'' mitten in ein Mehrbytezeichen schneiden. Für Text mit Codepoint-Semantik [[lyx_-_programmiersprache:units:text|std.text]] verwenden. ---- ===== Verwandte Units ===== * [[lyx_-_programmiersprache:units:text|std.text]] — UTF-8 mit Codepoint-Semantik, echte Groß-/Kleinschreibung * [[lyx_-_programmiersprache:units:strtype|std.strtype]] — ''String'' trägt seine Länge mit und darf Nullbytes enthalten * [[lyx_-_programmiersprache:units:buffer|std.buffer]] — binärsichere Operationen auf rohen Bytes * [[lyx_-_programmiersprache:units:conv|std.conv]] — Zahlen, Basen, bitweise Umwandlungen * [[lyx_-_programmiersprache:units:regex|std.regex]] — Mustersuche