St

Strings in Julia

7 Übungen

Über Strings

Ein String ist in Julia eine geordnete Folge von Zeichen. Wie das Handbuch anmerkt: „Natürlich fängt der eigentliche Ärger an, wenn man fragt, was ein Zeichen ist.“

Ein Großteil dieser Komplexität wird im Chars-Konzept ausführlicher behandelt, aber kurz gesagt:

  • Alle Strings in Julia sind Unicode.
  • Strings, die zufällig auch ASCII sind (englische Buchstaben A bis Z in Groß- und Kleinschreibung, Ziffern und ein paar Satzzeichen), sind einfach zu handhaben.
  • Die meisten anderen Schriftsysteme der Welt sind zwar verwendbar, erfordern aber mehr Sorgfalt.

String-Literale

Im Allgemeinen werden Strings in doppelte Anführungszeichen " " gesetzt. Einfache Anführungszeichen ' ' werden nur für Chars verwendet.

Man kann auch dreifache doppelte Anführungszeichen """ """ verwenden, wodurch " innerhalb des Strings ohne Escaping möglich ist. Wichtiger noch: Bei mehrzeiligen Strings wird unerwünschte zusätzliche Einrückung entfernt.

julia> """
       Multiline
       String
       """
"Multiline\nString\n"

Zeichen escapen

Wie die meisten Sprachen seit C verwendet Julia einen Backslash \ zum Escapen:

  1. Um nicht druckbare Zeichen einzugeben, etwa den Zeilenumbruch \n im vorigen Beispiel.
  2. Um Zeichen zu schützen, die sonst eine besondere Bedeutung hätten, etwa \$ (siehe unten).

Iteration

Ein String ist eine iterierbare Sammlung, deshalb funktioniert eine for ... in-Schleife, ohne dass man ihn in einen Vektor von Zeichen umwandeln muss.

julia> s = "Julia"
"Julia"

# admittedly, this is a silly example
julia> for c in s; print(c, "; "); end
J; u; l; i; a; 

Das Iterieren auf diese Weise hat den Vorteil, dass Julia Zeichengrenzen korrekt behandelt, auch bei Nicht-ASCII-Strings. Wie wichtig das ist, wird im nächsten Abschnitt klarer.

Indizierung

Grundsätzlich lassen sich Strings genau wie Vektoren indizieren:

julia> s[1], s[5]
('J', 'a')

Bei einem ASCII-String wie „Julia“ funktioniert das offensichtlich, aber damit ignoriert man die meisten Sprachen der Welt.

Betrachte eines der Zeichen in Beowulf, das vor etwa 15 Jahrhunderten im Altenglischen geschrieben wurde.

„Hrōðgār“ besteht eindeutig aus 7 Zeichen, aber nicht alle davon sind ASCII.

julia> king = "Hrōðgār"
"Hrōðgār"

julia> length(king)
7

julia> isascii(king)
false

# collect() converts to a Char vector:
julia> collect(king)
7-element Vector{Char}:
 'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
 'ō': Unicode U+014D (category Ll: Letter, lowercase)
 'ð': Unicode U+00F0 (category Ll: Letter, lowercase)
 'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
 'ā': Unicode U+0101 (category Ll: Letter, lowercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)

Die Indizierung eines solchen Strings beginnt gut, bricht aber irgendwann zusammen:

julia> king[3]
'ō': Unicode U+014D (category Ll: Letter, lowercase)

julia> king[4]
ERROR: StringIndexError: invalid index [4], valid nearby indices [3]=>'ō', [5]=>'ð'

Das Problem ist, dass eine solche Indizierung Bytes zählt, die bei Nicht-ASCII-Strings keine 1:1-Beziehung zu den Zeichen haben. Ein Unicode/UTF-8-Zeichen kann bis zu 4 Bytes zu seiner Darstellung benötigen, und das Chars-Konzept geht näher darauf ein.

Strings aufbauen

Verkettung

Viele Sprachen verwenden + als Operator zum Verketten von Strings, Julia aber nicht. Immerhin ist die Fehlermeldung hilfreich.

julia> "s1 " + "s2"
ERROR: MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with * 

julia> "s1 " * "s2"
"s1 s2"

Die Funktion string() verkettet eine beliebige Anzahl von Strings.

julia> string("lots ", "of ", "bits")
"lots of bits"

Die Funktion join() macht dasselbe und nimmt außerdem einen Vektor von Strings und verkettet sie mit einem gegebenen Trennzeichen (standardmäßig "").

julia> join(["lots", "of", "bits"], "_")
"lots_of_bits"

Beachte, dass Strings unveränderlich sind und dass Verketten ein Kopieren bedeutet. Deshalb ist wiederholtes Verketten innerhalb einer Schleife ineffizient; besser sammelst du die Fragmente in einem Vektor und fügst sie erst am Ende zusammen.

Wiederholung

Ganz einfach:

julia> repeat("Abc", 5)
"AbcAbcAbcAbcAbc"

Interpolation

Viele Sprachen haben eine Möglichkeit, berechnete Werte in Strings einzufügen, doch darüber, welche Syntax dafür am besten ist, herrscht erstaunliche Uneinigkeit.

In Julia steht vor interpolierten Werten ein $.

julia> myvar = 42
42

julia> "myvar evaluates to $myvar"
"myvar evaluates to 42"

julia> r = 5.3
5.3

julia> "A circle of radius $r has area $(π * r^2)"
"A circle of radius 5.3 has area 88.24733763933729"

Runde Klammern sind erforderlich, wenn das Weglassen zu Mehrdeutigkeiten führen würde, bei einem einzelnen Bezeichner, auf den ein Leerzeichen folgt, sind sie optional.

Um ein $ in den String aufzunehmen, escape es als \$.

Beachte die Einschränkung, dass die Interpolation derzeit keine Formatierung erlaubt, etwa die Anzahl der anzuzeigenden Nachkommastellen.

Mögliche Workarounds:

  • Die Berechnung in eine Funktion round() packen.
  • Außerhalb des Exercism-Test-Runners emuliert das Paket PyFormattedStrings die f-Strings von Python.
  • Das Makro @sprintf() wird weiter unten beschrieben.

@sprintf()

Für eine feinere Kontrolle über den Zusammenbau von Strings übernimmt Julia die Funktion sprintf aus C (und aus mehreren späteren Sprachen: Wikipedia listet etwa 30 auf).

In Julia ist das als macro im Modul Printf implementiert, daher das Präfix @.

julia> using Printf

julia> r = 5.3
5.3

julia> @sprintf("A circle of radius %.1f has area %.2f", r, π * r^2)
"A circle of radius 5.3 has area 88.25"

String-Funktionen

Der Einfachheit halber konzentriert sich dieser Abschnitt auf Funktionen, die einen neuen String zurückgeben und die Eingabe unverändert lassen. Viele haben ein Gegenstück mit dem Suffix !, das die Eingabe direkt verändert.

Länge

Wie lang ist ein String? Manchmal hängt das davon ab, wie du zählst.

Der einfache Fall sind ASCII-Strings, bei denen jedes Zeichen 1 Byte belegt.

julia> string_asc = "Julia"
"Julia"

julia> length(string_asc)
5

julia> sizeof(string_asc)
5

Die Funktion length() gibt die Anzahl der Zeichen zurück, die Funktion sizeof() die Anzahl der Bytes.

Bei anderen Zeichensätzen wird dieser Unterschied wichtig:

julia> king = "Hrōðgār"
"Hrōðgār"

julia> length(king)
7

julia> sizeof(king)
10

Aufteilen

Eine häufige Operation beim Programmieren besteht darin, einen langen String anhand eines Trennstrings aus einem oder mehreren Zeichen in Stücke zu zerlegen.

Am allgemeinsten verwenden wir die Funktion split(). Das Trennzeichen ist standardmäßig (beliebiger) Whitespace, aber es können auch andere angegeben werden.

julia> split("my little string")
3-element Vector{SubString{String}}:
 "my"
 "little"
 "string"

julia> split("My_snake_case_string", "_")
4-element Vector{SubString{String}}:
 "My"
 "snake"
 "case"
 "string"

Diese Beispiele zerlegen einen String in kleinere Strings. Einen String in Zeichen zu zerlegen ist eine andere Operation.

julia> collect("input string")
12-element Vector{Char}:
 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
 'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
 'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
 't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
 ' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
 's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
 't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
 'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)

Suchen und ersetzen

Enthält ein String einen bestimmten Teilstring? Seltsamerweise gibt es zwei Funktionen, um das zu prüfen: occursin() und contains() unterscheiden sich nur in der Reihenfolge ihrer Argumente.

julia> needle = "Julia"
"Julia"

julia> haystack = "Python, Julia and R"
"Python, Julia and R"

julia> occursin(needle, haystack)
true

julia> contains(haystack, needle)
true

Genauer bezüglich der Position:

julia> startswith(haystack, "Python")
true

julia> endswith(haystack, "Python")
false

Um die Start- und Endindizes eines Teilstrings zu erhalten, gibt es mehrere Funktionen:

julia> findfirst(needle, haystack)
9:13

Um Teilstrings zu ersetzen, liste die Änderungen mit der Syntax x => y auf.

julia> replace("abc", "a"=>"b", "b"=>"c", "c"=>"a")
"bca"

Seit Julia 1.7 kann die Funktion replace eine beliebige Anzahl von Änderungen entgegennehmen, mit der Garantie, dass kein Zeichen während des Ersetzens mehr als einmal geändert wird.

Die Syntax x => y heißt pair, ein wichtiger Typ in Julia, der im Dicts and Pairs-Konzept ausführlicher behandelt wird.

Hinweis: Der Einfachheit halber verwenden alle Beispiele oben String-Literale. Viele der Funktionen sind allgemeiner und funktionieren auch mit regulären Ausdrücken. Wir kommen in einem späteren Konzept darauf zurück.

Whitespace

Oft müssen wir führenden und/oder abschließenden Whitespace aus einem String entfernen.

Dafür gibt es die Funktionen lstrip() (links), rstrip() (rechts) und strip() (beides).

julia> strip("\n\t  useful_bit\n\n")
"useful_bit"

Groß- und Kleinschreibung ändern

Diese Funktionen haben recht selbsterklärende Namen:

julia> s = repeat("aBcD ", 5)
"aBcD aBcD aBcD aBcD aBcD "

julia> uppercase(s)
"ABCD ABCD ABCD ABCD ABCD "

julia> lowercase(s)
"abcd abcd abcd abcd abcd "

julia> titlecase(s)  # Initial letter of each word uppercase
"Abcd Abcd Abcd Abcd Abcd "

Parsen

Zahlen und ihre String-Darstellung ineinander umzuwandeln ist oft nützlich.

Von der Zahl zum String ist einfach, allerdings braucht eine andere Basis als die Standardbasis 10 einen zusätzlichen Parameter:

julia> string(42)
"42"

julia> string(42, base=16)
"2a"

julia> string(42, base=2)
"101010"

Einen String zu parsen, um einen Zahlenwert zu erhalten, ist etwas komplizierter und fehleranfälliger. Die Angabe des Ziel-Zahlentyps ist Pflicht. Die Angabe der Basis ist optional und standardmäßig 10.

julia> parse(Int, "42")
42

julia> parse(Float64, "42")
42.0

julia> parse(Int, "42?")
ERROR: ArgumentError: invalid base 10 digit '?' in "42?"

julia> parse(Int, "42 ")  # whitespace may be tolerated
42

Spezielle Strings

Manchmal ist es nützlich, die übliche Interpolation und das Escaping innerhalb von Strings zu unterdrücken. Dazu stellst du dem öffnenden Anführungszeichen raw voran.

julia> x = 10
10

julia> "x = $x if we use \$ for interpolation"
"x = 10 if we use \$ for interpolation"

julia> raw"x = $x if we use \$ for interpolation"
"x = \$x if we use \\\$ for interpolation"

Reguläre Ausdrücke haben ihre eigene komplizierte Syntax, und Julia bietet zwei hilfreiche String-Typen: r" " für den Regex und s" " für die Arbeit mit erfassten Fragmenten.

Verschiedene andere spezielle Strings sind beim Programmieren im Exercism-Stil weniger verbreitet, werden aber beim Erstellen von Bibliotheken häufig verwendet.

  • Semantische Versions-Strings v"x.y.z" behandeln die Major.Minor.Patch-Nummerierung für Software-Releases.
  • Byte-Array-Literale b" " umgehen die Unicode-Zeichengrenzen und arbeiten auf Byte-Ebene.
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne Strings

Das Üben ist gesperrt

Schalte 2 weitere Übungen frei, um Strings zu üben