16 Min. Lesezeit

Im zweiten Artikel dieses Blogserie schrieb ich, dass wir ein Programm entwickeln wollen, das selbständig erkennt, was es genau berechnen soll, wenn es eine Zeichenkette als Input bekommt. Um diese Forderung zu erfüllen, schrieb ich weiter, müssten wir zunächst zwei Probleme klären. Hier nochmals die zwei Probleme:

  1. Wie kann ein Programm aus einer Zeichenkette erkennen, was es berechnen soll? Anders gesagt: Wenn ich die Zeichenkette “1 + 1” habe, wie erkennt das Programm, dass die “1” eine numerische 1 ist und das “+” Addition bedeutet?
  2. Wie kann ich eine mathematische Operation allgemein halten? Das heißt, ich möchte in der Lage sein a + b zu berechnen wobei a und b irgendeine Zahl ist, und nicht nur 1 + 1.

Punkt 2 haben wir bereits im letzten Artikel behandelt und dabei auch Funktionen kennengelernt. Damit ist in diesem Blog Punkt 1 dran. Wir wollen demnach herausfinden, wie man ein Programm schreibt, das den Inhalt einer Zeichenkette selbständig korrekt versteht. Wir kommen also nicht umhin, uns intensiver mit Zeichenketten zu beschäftigen. Aber, bevor wir weiter einsteigen: Erstmal ab zum Themenbereich Strings und Character.


Einführung in Strings und Characters

In Swift repräsentieren die Typen String und Character jeweils eine Zeichenkette oder ein einzelnes Zeichen. Ein String kann mithilfe eines sogenannten String-Literals definiert werden. Ein String-Literal ist eine Reihe von Zeichen (Characters), die mit Gänsefüßchen umfasst wird. Hier ein Beispiel:

let aString = "This is a string!"


Swift ist also in der Lage aus dem String-Literal “…” (wobei … für beliebige Zeichen steht) den Typ der Variablen abzuleiten, hier also String. Dadurch ist es nicht notwendig, den Typ String anzugeben. Anders verhält es sich beim Typ Character. Hier ist es notwendig, den Typ anzugeben. Es muss also heißen:

let b : Character = "B"


Das ist deswegen so, weil Swift nicht unterscheiden kann, ob “B” ein String mit einem einzelnen Zeichen sein soll oder ein Character. Deswegen nimmt es immer an, dass es sich um einen String handelt, solange kein Typ explizit angegeben wird.

Eine weitere Eigenschaft darfst du nicht außer Acht lassen: Eine Konstante oder Variable vom Typ Character darf nur ein Zeichen enthalten. Folgender Sourcecode führt zu einem Fehler: 

let v : Character = "A character!"


Der Fehler lautet: Cannot convert value of type 'String' to specified type 'Character‘. 

Ok, so definiert man also Strings mithilfe von String-Literalen. Und wie kann ich einen String erzeugen, der kein einziges Zeichen enthält? Ganz einfach: 

let c = ""


Leider gilt das nicht für Characters, denn diese enthalten per Definition immer genau ein einzelnes Zeichen. Auch hier wird folgender Sourcecode zu dem oben schon erwähnten Fehler führen. 

let d : Character = ""

Spezielle Zeichen in Strings

Ein String-Literal kann nicht nur Zeichen enthalten, die wir aus unserem Alphabet kennen. Es kann auch folgende spezielle Zeichen enthalten:

  • >\0
  • \\
  • \t = horizontales Tab
  • \n = neue Zeile
  • \r = carriage return (zurück zum Anfang der Zeile)
  • \” = Gänsefüßchen
  • \’ = einfaches Hochkomma
  • \u{n} = Wobei n eine Hexadezimal-Zahl ist, die einen validen Unicode-Wert darstellt. Unicode wird weiter unten erklärt.

Du kannst mithilfe der print-Funktion einfach ausprobieren, was diese Zeichen alles bewirken. Versuch doch einfach mal folgende Befehle:

print("Hier wird ein Gänsefüßchen ausgegeben: \"")
print("Hier wird ein Hochkomma ausgegeben: \'")
print("Das ist die erste Zeile.\nHier beginnt die zweite Zeile.")
print("Der Unicode 1F496 steht für \u{1F496}")

Veränderbare und unveränderliche Strings

Swift bietet dir die Möglichkeit an, einen String aus anderen Strings zusammenzusetzen. Dazu muss man lediglich den Operator + benutzen. Betrachten wir also folgendes Beispiel:

var name     = "Stephen"
var surname  = "Hawking"
var fullName = name + " " + surname


Hier wird die Variable aus den Werten der Variablen name, surname und dem String-Literal “ “ zusammengestellt. Man könnte aber auch Folgendes programmieren:

var fullName = "Stephen"
var surname  = "Hawking"

fullName += " "
fullName += surname

Der hier benutzte Operator += ist genau genommen kein Operator, sondern eine Mischung aus dem Operator + und dem Zuweisungszeichen =. Folgende Zuweisungen sind in Swift gleichwertig:

var name      = "Stephen"
var surname   = "Hawking"
var fullName1 = name + " "
var fullName2 = name + " "

fullName1 = fullName1 + surname
fullName2 += surname

print("1) Full name = \(fullName1)")
print("2) Full name = \(fullName2)")


Swift bietet dir also die Möglichkeit an, einen String nachträglich zu erweitern. Dazu muss jedoch eine Variable genutzt werden und nicht eine Konstante, die ja bekanntlich ihren Wert per Definition nicht ändern darf.

Unicode

Unicode ist ein internationaler Standard für die Kodierung, Darstellung und Verarbeitung von Text in verschiedenen Schriftsystemen. Aber was heißt das eigentlich?

Nun, zunächst sollte klar sein, was ich mit Kodierung von Zeichen meine. Bei uns Menschen ist es so, das wir ein Zeichen sehen, es erkennen und somit wissen, was es ist. Wir sehen ein “A” und wissen, es handelt sich um das Zeichen “A”. Für uns ist das relativ unspektakulär. Ein Programm hingegen, speichert intern jedes Zeichen als eine Zahl ab, eben als Code. Das Zeichen “A” wird somit als hexadezimale Zahl 41 gespeichert. Andere Zeichen haben andere Codes. Du kannst dir Unicode als Tabelle vorstellen, wo jedem bekannten Zeichen ein Code zugewiesen wird.

Unicode ermöglicht somit, nahezu jedes Zeichen aus jeder Sprache in einer standardisierten Form darzustellen, zu lesen und zu schreiben. Die String- und Zeichentypen Strings und Characters) von Swift sind vollständig Unicode-kompatibel.

Es gibt zwei Begriffe, die im Zusammenhang mit Swifts Implementierung des Typs String erläutert werden müssen. Die Begriffe sind Unicode Scalar und Extended Grapheme Cluster.

In Swift basiert die Implementierung des String-Typs auf so genannten Unicode-Skalare (Unicode Scalar). Ein Unicode-Skalar ist eine 21 Bit lange Nummer, sprich eine Ganzzahl, die eines der Millionen Zeichen identifiziert, die im Unicode-Standard definiert sind. Mit Zeichen sind nicht nur Buchstaben und Zahlen gemeint, sondern schlicht alle Zeichen, die in allen Alphabeten der Welt existieren. Dazu gehören auch so genannte modifizierende Zeichen; das sind Zeichen, wie beispielsweise ein Akzentzeichen, die zusammen mit einem anderen Zeichen eine neues Zeichen ergeben. Z.B.: “e” + “´” = “é”.

Das Spiel kann man aber noch weiter treiben, denn tatsächlich wird das Zeichen “é” auch durch den Unicode-Skalar \u{E9} repräsentiert und nicht nur durch die Kombination aus “e” und “´” also \u{65}\u{301}. Betrachten wir folgendes Beispiel:

let eAcute: Character = "\u{E9}"
let combinedEAcute: Character = "\u{65}\u{301}"

print(eAcute)
print(combinedEAcute)


Wir haben hier also zwei Variablen vom Typ Character und beide speichern das Zeichen “é”, obwohl sie aus verschiedenen Unicode-Skalaren zusammengesetzt wurden. Defakto sind die Werte der beiden Variablen gleich. Zusammengefasst heißt das:

  1. Ein Swift-Character repräsentiert immer genau ein Zeichen
  2. Ein Swift-Character kann durch verschiedene Unicode-Skalare erzeugt werden
  3. Die Menge der Unicode-Skalare, die ein und dasselbe Zeichen repräsentieren, nennt man schließlich Extended Grapheme Cluster.

Extended Grapheme Cluster sind eine flexible Möglichkeit, viele komplexe Schriftzeichen als einen einzigen Zeichenwert darzustellen. 

Wie man Zeichen zählt

Wenn man mit Strings arbeitet, möchte man oft die Anzahl an Zeichen in einem gegebenen String ermitteln. Dies machst du mithilfe der Property count. Was eine Property ist, werde ich noch in einem späteren Blog erklären. Zunächst kannst du einfach davon ausgehen, dass du die Anzahl an Zeichen in einem String, wie im folgenden Beispiel gezeigt, ermitteln kannst:

let planet = "Earth"

print (planet.count)


Das Ergebnis ist 5. Doch betrachten wird das nächste Beispiel. Hier werden zwei Variablen auf verschiedene Weise definiert, die den Wert “España” haben:

var country1 = "Espan"
country1 += "\u{0303}"
country1 += "a"

var country2 = "Espa"
country2 += "\u{00F1}"
country2 += "a"

print("country1 = \(country1) - count = \(country1.count)")
print("country2 = \(country2) - count = \(country2.count)")


Wir erhalten hier zwei Mal 6 als Anzahl der Zeichen. Das liegt daran, dass das Zeichen “ñ” das Extended Grapheme Cluster repräsentiert, das aus folgenden Zeichen zusammengesetzt ist:

  • \u{00F1} – Unicode-Skalar von “ñ”
  • \u{006E}\u{0303} – Kombination des Zeichens “n” mit dem Unicode-Skalar von “~”

Es ist egal wie viele kombinierte Unicode-Skalare zu einem Zeichen führen. Am Ende wird nur das repräsentierte Zeichen gezählt.

Zugriff auf einzelne Zeichen

Betrachten wir nochmals das Beispiel mit zwei Unicode-Skalaren “n” und “~” für das Zeichen “ñ”.

var country1 = "Espan"
country1 += "\u{0303}"
country1 += "a"


Unicode-Skalare


In diesem Beispiel können wir sehen, dass das Zeichen “ñ” zwei Skalare benötigt, nämlich “n” und “~”. Das bedeutet, dass für diesen String 7 Speicherplätze (von 0 bis 6) benötigt werden. Wir sehen, dass die Anzahl an Skalare nicht gleich der Anzahl an Zeichen ist. Um also die Position des fünften Zeichens zu ermitteln, also das “ñ”, muss man zusammengehörende Skalare zu einer Position zusammenfassen. Das bedeutet: Ich beginne beim ersten Zeichen zu zählen und zähle solange weiter, bis ich das gewünschte Zeichen erreicht habe. Dabei zähle ich für jedes entdeckte extended grapheme cluster (hier “n” und “~”) nur eine Position weiter.

Um dieses Hochzählen durchführen zu können, verwendet der Typ String sogenannte Indizes (Index, die mithilfe von Propertys und Funktionen genutzt werden können. Um das alles besser zu erklären, schauen wir uns folgendes Beispiel an:

var country = "Espan"
country += "\u{0303}"
country += "a"

var index1 = country.startIndex

print("Das erste Zeichen ist \(country[index1])”)


Die Property startIndex liefert den Index für das erste Zeichen im String. Die Schreibweise ist wie oben gezeigt. Ich werde auf die Bedeutung von Properties in einem späterem Blog eingehen. Jetzt ist nur wichtig zu wissen, wie diese Properties benutzt werden können.

Im Beispiel wird also der Index des ersten Zeichens der Variablen index zugewiesen. Einen Index kann man sich als ein Pfeil vorstellen, der auf ein Zeichen zeigt. Im print Im Print-Befehl wird dann mithilfe der Klammernotation country[…] das Zeichen ermittelt, das durch den angegebenen Index referenziert wird. Anders: Der Ausdruck country[index] bedeutet “Gib bitte das Zeichen zurück, das durch den übergebenen Index referenziert wird.”.

Der Typ String besitzt eine weitere Property mit dem Namen endIndex. Dieser Index zeigt auf die Position hinter dem letzten Zeichen. Deswegen ist der Aufruf country[country.endIndex] nicht erlaubt. Falls ein String leer ist, sind startIndex und endIndex gleich.

Um die Zeichen zwischen dem ersten und dem letzten Zeichen zu referenzieren, bietet der Typ String verschiedene Funktionen an. Siehe nächstes Beispiel:

var country = "Espan"
country += "\u{0303}"
country += "a"

var index1 = country.startIndex
print("Das erste Zeichen ist \(country[index1])")

var index2 = country.index(after: index1)
print("Das zweite Zeichen ist \(country[index2])")

var indexLast = country.index(before: country.endIndex)
print("Das letzte Zeichen ist \(country[indexLast])")

var index5 = country.index(index1, offsetBy: 4)
print("Das fünfte Zeichen ist \(country[index5])")


Die Funktion
index(after:) liefert den Index nach dem übergebenen Index. Die Funktion index(before:) hingegen, liefert den Index vor dem übergebenen Index. Im Beispiel wird am Ende mit der Funktion index(_: offsetBy:) gezeigt, wie der vierte Index ab den aktuellen Index ermittelt werden kann, ohne vier Mal hintereinander index(after:) aufzurufen.

Substrings

Im vorangegangenen Abschnitt haben wir gelernt, dass mit der Notation country[Index] das Zeichen im String country ermittelt werden kann, das von dem übergebenen Index referenziert wird. Was ist, wenn ich den Index eines Zeichens ermitteln möchte, und zwar das erste Vorkommen eines konkreten Zeichens? Betrachten wir das nächste Beispiel:

let expression = “(4 * 5) + 1"


Swift bietet eine Menge Funktionen an, um mit Strings zu arbeiten. So gibt auch eine Funktion, um den Index des ersten Vorkommens eines Zeichens zu ermitteln. Siehe nächstes Beispiel.

let plusIndex = expression.index(of: "+")!


Hier liefert die Funktion index(of:) den gesuchten Index. An dieser Stelle muss ich ein bisschen vorgreifen und erklären, was es mit dem Ausführungszeichen “!” am Ende der Zeile im Beispiel auf sich hat. Nun, die Funktion index(of:) liefert nicht immer einen Index, und zwar dann nicht, wenn das gesuchte Zeichen schlicht nicht im String enthalten ist. Der Aufruf expression.index(of: “P”) kann niemals einen korrekten Index zurückgeben. Was wird stattdessen zurückgegeben? Die Antwort auf diese Frage hängt von der Definition der Funktion ab. Die hier betrachtete Funktion index(of:) ist so definiert, dass sie entweder einen Index zurückliefert oder nil. Letzteres ist ein Bezeichner, um anzudeuten, dass kein Wert vorhanden ist. Swift erlaubt es standardmäßig nicht, dass einer Variablen oder Konstanten nil zugewiesen wird. Um das dennoch zuzulassen, wird dieses Ausführungszeichen benutzt. Kurz gesagt, hat das Ausführungszeichen die Funktion “weise der Variablen oder Konstanten den Rückgabewert zu. Falls der Rückgabewert nil ist, weise diesen trotzdem zu.” Hinter dem Ausführungszeichen steckt noch einiges mehr, doch das werde ich in einem späteren Blog erklären.

Angenommen, ich möchte nicht das Zeichen an der entsprechenden Position haben, sondern alle Zeichen bis zu dieser Position. Mann spricht dann von einem Substring, also einer Teil-Zeichenkette. Betrachten wir das folgende Beispiel.

let leftOperandUntrimmed = expression[..<plusIndex]


Mithilfe des Mengenoperators ..< ist der gesamte Bereich vor einem Index gemeint, d.h. vom ersten Zeichen bis zu dem letztem Zeichen vor dem von plusIndex referenzierten Zeichen. Der Wert von leftOperandUntrimmed ist dann “(4 * 5) ”.

Der Mengenoperator ..< erlaubt auch folgenden Ausdruck:

let leftOperandUntrimmed = expression[anyIndexBeforePlusIndex..<plusIndex]


Ich habe hier der Einfachheit halber die Variable anyIndexBeforePlusIndex einfach mal erfunden. Aber wie der Name schon sagt, soll es einen Index darstellen, der links von plusIndex ein Zeichen referenziert, so z.B. “*”. Dann wäre das Ergebnis “* 5) ”.

Beachte bitte, dass das Leerzeichen hinter der schließenden Klammer auch Teil der Zeichenkette ist, da es sich links von dem von plusIndex referenzierten Zeichen “+” befindet. Um das Pluszeichen mit in dem Teilstring zu haben, kann man folgenden Ausdruck benutzen:

let leftOperandUntrimmed = expression[...plusIndex]


Während ..< den oberen Index des definierten Bereichs ausschließt, liefert ... eine Zeichenkette, die das von dem oberen Index (plusIndex) referenzierte Zeichen mit enthält. Der Mengenoperator ... kann zudem auch folgendermaßen benutzt werden.

let aString = expression[plusIndex...]


Hier liefert expression[plusIndex...] den String “+ 1” zurück. Von plusIndex ausgehend werden das referenzierte Zeichen sowie alle nachfolgenden bis zum Ende von expression zurückgegeben.

So, dann können wir jetzt weiter machen. Ich hoffe, es war nicht zu trocken. Wir wollen jetzt ausprobieren, wie wir aus einem String, der eine Addition enthält, die einzelnen Bestandteile identifizieren. Nehmen wir ein sehr einfaches Beispiel:

import Foundation

let expression = "1 + 1"

let plusIndex      = expression.index(of: "+")!
let leftUntrimmed  = expression[..<plusIndex]
let left           = leftUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let rightUntrimmed = expression[expression.index(after: plusIndex)..<expression.endIndex]
let right          = rightUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)


Die erste Anweisung ermittelt den Index für das Plus-Zeichen. Die zweite Anweisung liefert den Teil-String “1“ zurück. In der dritten Zeile werden die überflüssigen Leerzeichen entfernt. Überflüssig sind alle Leerzeichen die am Anfang oder am Ende des String enthalten sind. Dieses “trimmen” vollführt die Funktion trimminCharacters(in:). Sie entfernt einfach alle Zeichen, die in der übergebenen Menge CharacterSet.whitespacesAndNewLines enthalten sind, aus dem Anfang bzw. Ende des Strings expression. Was genau eine Menge oder auch Collection ist, werde ich in einem späteren Blog erklären. An dieser stelle soll uns reichen, zu wissen, dass die Konstante left den Wert “1” hat.

Die nächste Anweisung expression[expression.index(after: plusIndex)..<expression.endIndex] liefert den rechten Operanden “1”. Hierbei wird mit expression.index(after: plusIndex) der Index ermittelt, der auf das Leerzeichen rechts vom Plus-Zeichen zeigt. Die rechte Seite des Mengenoperators ..< ist der Index hinter dem letzten Zeichen. Schließlich wird der Konstante right die getrimmte Version des rechten Operanden zugewiesen.

Jetzt haben wir schon fast alle Zutaten, um aus einem String, der eine einfache Summe enthält, das mathematische Ergebnis auszurechnen. Wir sind zwar in der Lage, den linken und den rechten Operanden einer Summe zu identifizieren, wir wissen aber nicht, wie wir aus den Operanden Zahlen machen. Das ist notwendig, da wir Swift für uns rechnen lassen wollen, Swift aber nur mit numerischen Typen rechnen kann. Wir müssen also die Operanden (die ja Swift sind) in Werte vom Typ Double verwandeln.

Um den String “1” in die Fließkommazahl 1,0 zu verwandeln, müssen wir lediglich Folgendes tun:

let left = Double("1")


Double(“1”) ist eine spezielle Funktion (Initializer), die ein Double zurückliefert mit dem Wert des übergebenen Strings als Zahl. Das bedeutet, dass Double(“1”) für uns die Verwandlung von String in ein Double durchführt.

So, jetzt können wir ein kleines Programm schreiben, dass eine einfache Summe ausrechnen kann. Siehe folgendes Beispiel:

import Foundation

func compute(addition : String) -> Double {
    let plusIndex      = addition.index(of: "+")!
    let leftUntrimmed  = addition[..<plusIndex]
    let leftStr        = leftUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
    let left           = Double(leftStr)!
    let rightUntrimmed = addition[addition.index(after: plusIndex)..<addition.endIndex]
    let rightStr       = rightUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
    let right          = Double(rightStr)!
    
    return add(value1: left , value2: right)
}

func add(value1: Double, value2: Double) -> Double {
    return value1 + value2
}

let expression = "20 + 7"
let result = compute(addition: expression)
print("Die Summe \(expression) ist gleich \(result)")


Wir haben es geschafft. Wir habe ein kleines Programm geschrieben, dass eine einfache Summe selbständig ausrechnen kann. Wie wir das Ganze komfortabler gestalten können, werde ich dir im nächsten Blog zeigen. Bis dahin, viel Spaß beim ausprobieren.

Kommentare