JuliaKurs23/chapters/10_Strings.qmd

538 lines
13 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
engine: julia
---
```{julia}
#| error: false
#| echo: false
#| output: false
using InteractiveUtils
import QuartoNotebookWorker
Base.stdout = QuartoNotebookWorker.with_context(stdout)
```
# Zeichen, Strings und Unicode
## Zeichencodierungen (Frühgeschichte)
Es gab - abhängig von Hersteller, Land, Programmiersprache, Betriebsssystem,... - eine große Vielzahl von Codierungen.
Bis heute relevant sind:
### ASCII
Der _American Standard Code for Information Interchange_ wurde 1963 in den USA als Standard veröffentlicht.
- Er definiert $2^7=128$ Zeichen, und zwar:
- 33 Steuerzeichen, wie `newline`, `escape`, `end of transmission/file`, `delete`
- 95 graphisch darstellbare Zeichen:
- 52 lateinische Buchstaben `a-z, A-Z`
- 10 Ziffern `0-9`
- 7 Satzzeichen `.,:;?!"`
- 1 Leerzeichen ` `
- 6 Klammern `[{()}]`
- 7 mathematische Operationen `+-*/<>=`
- 12 Sonderzeichen ``` #$%&'\^_|~`@ ```
- ASCII ist heute noch der "kleinste gemeinsame Nenner" im Codierungs-Chaos.
- Die ersten 128 Unicode-Zeichen sind identisch mit ASCII.
### ISO 8859-Zeichensätze
- ASCII nutzt nur 7 Bits.
- In einem Byte kann man durch Setzen des 8. Bits weitere 128 Zeichen unterbringen.
- 1987/88 wurden im ISO 8859-Standard verschiedene 1-Byte-Codierungen festgelegt, die alle ASCII-kompatibel sind, darunter:
:::{.narrow}
|Codierung | Region | Sprachen|
|:-----------|:----------|:-------|
|ISO 8859-1 (Latin-1) | Westeuropa | Deutsch, Französisch,...,Isländisch
|ISO 8859-2 (Latin-2) | Osteuropa | slawische Sprachen mit lateinischer Schrift
|ISO 8859-3 (Latin-3) | Südeuropa | Türkisch, Maltesisch,...
|ISO 8859-4 (Latin-4) | Nordeuropa | Estnisch, Lettisch, Litauisch, Grönländisch, Sami
|ISO 8859-5 (Latin/Cyrillic) | Osteuropa | slawische Sprachen mit kyrillischer Schrift
|ISO 8859-6 (Latin/Arabic) | |
|ISO 8859-7 (Latin/Greek) | |
|...| |
|ISO 8859-15 (Latin-9)| | 1999: Revision von Latin-1: jetzt u.a. mit Euro-Zeichen
:::
## Unicode
Das Ziel des Unicode-Consortiums ist eine einheitliche Codierung für alle Schriften der Welt.
- Unicode Version 1 erschien 1991
- Unicode Version 15.1 erschien 2023 mit 149 813 Zeichen, darunter:
- 161 Schriften
- mathematische und technische Symbole
- Emojis und andere Symbole, Steuer- und Formatierungszeichen
- davon entfallen über 90 000 Zeichen auf die CJK-Schriften (Chinesisch/Japanisch/Koreanisch)
### Technische Details
- Jedem Zeichen wird ein `codepoint` zugeordnet. Das ist einfach eine fortlaufende Nummer.
- Diese Nummer wird hexadezimal notiert
- entweder 4-stellig als `U+XXXX` (0-te Ebene)
- oder 6-stellig als `U+XXXXXX` (weitere Ebenen)
- Jede Ebene geht von `U+XY0000` bis `U+XYFFFF`, kann also $2^{16}=65\;534$ Zeichen enthalten.
- Vorgesehen sind bisher 17 Ebenen `XY=00` bis `XY=10`, also der Wertebereich von `U+0000` bis `U+10FFFF`.
- Damit sind maximal 21 Bits pro Zeichen nötig.
- Die Gesamtzahl der damit möglichen Codepoints ist etwas kleiner als 0x10FFFF, da aus technischen Gründen gewisse Bereiche nicht verwendet werden. Sie beträgt etwa 1.1 Millionen, es ist also noch viel Platz.
- Bisher wurden nur Codepoints aus den Ebenen
- Ebene 0 = BMP _Basic Multilingual Plane_ `U+0000 - U+FFFF`,
- Ebene 1 = SMP _Supplementary Multilingual Plane_ `U+010000 - U+01FFFF`,
- Ebene 2 = SIP _Supplementary Ideographic Plane_ `U+020000 - U+02FFFF`,
- Ebene 3 = TIP _Tertiary Ideographic Plane_ `U+030000 - U+03FFFF` und
- Ebene 14 = SSP _Supplementary Special-purpose Plane_ `U+0E0000 - U+0EFFFF`
vergeben.
- `U+0000` bis `U+007F` ist identisch mit ASCII
- `U+0000` bis `U+00FF` ist identisch mit ISO 8859-1 (Latin-1)
### Eigenschaften von Unicode-Zeichen
Im Standard wird jedes Zeichen beschrieben duch
- seinen Codepoint (Nummer)
- einen Namen (welcher nur aus ASCII-Großbuchstaben, Ziffern und Minuszeichen besteht) und
- verschiedene Attributen wie
- Laufrichtung der Schrift
- Kategorie: Großbuchstabe, Kleinbuchstabe, modifizierender Buchstabe, Ziffer, Satzzeichen, Symbol, Seperator,....
Im Unicode-Standard sieht das dann so aus (zur Vereinfachung nur Codepoint und Name):
```
...
U+0041 LATIN CAPITAL LETTER A
U+0042 LATIN CAPITAL LETTER B
U+0043 LATIN CAPITAL LETTER C
U+0044 LATIN CAPITAL LETTER D
...
U+00E9 LATIN SMALL LETTER E WITH ACUTE
U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX
...
U+0641 ARABIC LETTER FEH
U+0642 ARABIC LETTER QAF
...
U+21B4 RIGHTWARDS ARROW WITH CORNER DOWNWARDS
...
```
Wie sieht 'RIGHTWARDS ARROW WITH CORNER DOWNWARDS' aus?
Julia verwendet `\U...` zur Eingabe von Unicode Codepoints.
```{julia}
'\U21b4'
```
### Eine Auswahl an Schriften
::: {.content-visible when-format="html"}
:::{.callout-note}
Falls im Folgenden einzelne Zeichen oder Schriften in Ihrem Browser nicht darstellbar sind, müssen Sie geeignete
Fonts auf Ihrem Rechner installieren.
Alternativ können Sie die PDF-Version dieser Seite verwenden. Dort sind alle Fonts eingebunden.
:::
:::
Eine kleine Hilfsfunktion:
```{julia}
function printuc(c, n)
for i in 0:n-1
print(c + i)
end
end
```
__Kyrillisch__
```{julia}
printuc('\U0400', 100)
```
__Tamilisch__
:::{.cellmerge}
```{julia}
#| echo: true
#| output: false
printuc('\U0be7',20)
```
\begingroup\setmonofont{Noto Sans Tamil}
```{julia}
#| echo: false
#| output: true
printuc('\U0be7',20)
```
\endgroup
:::
__Schach__
```{julia}
printuc('\U2654', 12)
```
__Mathematische Operatoren__
```{julia}
printuc('\U2200', 255)
```
__Runen__
```{julia}
printuc('\U16a0', 40)
```
:::{.cellmerge}
__Scheibe (Diskus) von Phaistos__
- Diese Schrift ist nicht entziffert.
- Es ist unklar, welche Sprache dargestellt wird.
- Es gibt nur ein einziges Dokument in dieser Schrift: die Tonscheibe von Phaistos aus der Bronzezeit
```{julia}
#| echo: true
#| output: false
printuc('\U101D0', 46 )
```
\begingroup\setmonofont{Phaistos.otf}
```{julia}
#| echo: false
#| output: true
printuc('\U101D0', 46 )
```
\endgroup
:::
### Unicode transformation formats: UTF-8, UTF-16, UTF-32
_Unicode transformation formats_ legen fest, wie eine Folge von Codepoints als eine Folge von Bytes dargestellt wird.
Da die Codepoints unterschiedlich lang sind, kann man sie nicht einfach hintereinander schreiben. Wo hört einer auf und fängt der nächste an?
- __UTF-32__: Das einfachste, aber auch speicheraufwändigste, ist, sie alle auf gleiche Länge zu bringen. Jeder Codepoint wird in 4 Bytes = 32 Bit kodiert.
- Bei __UTF-16__ wird ein Codepoint entweder mit 2 Bytes oder mit 4 Bytes dargestellt.
- Bei __UTF-8__ wird ein Codepoint mit 1,2,3 oder 4 Bytes dargestellt.
- __UTF-8__ ist das Format mit der höchsten Verbreitung. Es wird auch von Julia verwendet.
### UTF-8
- Für jeden Codepoint werden 1, 2, 3 oder 4 volle Bytes verwendet.
- Bei einer Codierung mit variabler Länge muss man erkennen können, welche Bytefolgen zusammengehören:
- Ein Byte der Form 0xxxxxxx steht für einen ASCII-Codepoint der Länge 1.
- Ein Byte der Form 110xxxxx startet einen 2-Byte-Code.
- Ein Byte der Form 1110xxxx startet einen 3-Byte-Code.
- Ein Byte der Form 11110xxx startet einen 4-Byte-Code.
- Alle weiteren Bytes eines 2-,3- oder 4-Byte-Codes haben die Form 10xxxxxx.
- Damit ist der Platz, der für den Codepoint zur Verfügung steht (Anzahl der x):
- Ein-Byte-Code: 7 Bits
- Zwei-Byte-Code: 5 + 6 = 11 Bits
- Drei-Byte-Code: 4 + 6 + 6 = 16 Bits
- Vier-Byte-Code: 3 + 6 + 6 + 6 = 21 Bits
- Damit ist jeder ASCII-Text automatisch auch ein korrekt codierter UTF-8-Text.
- Sollten die bisher für Unicode festgelegten 17 Ebenen (= 21 Bit = 1.1 Mill. mögliche Zeichen) mal erweitert werden, dann wird UTF-8 auf 5- und 6-Byte-Codes erweitert.
## Zeichen und Zeichenketten in Julia
### Zeichen: `Char`
Der Datentyp `Char` kodiert ein einzelnes Unicode-Zeichen.
- Julia verwendet dafür einfache Anführungszeichen: `'a'`.
- Ein `Char` belegt 4 Bytes Speicher und
- repräsentiert einen Unicode-Codepoint.
- `Char`s können von/zu `UInt`s umgewandelt werden und
- der Integer-Wert ist gleich dem Unicode-codepoint.
`Char`s können von/zu `UInt`s umgewandelt werden.
```{julia}
UInt('a')
```
```{julia}
b = Char(0x2656)
```
### Zeichenketten: `String`
- Für Strings verwendet Julia doppelte Anführungszeichen: `"a"`.
- Sie sind UTF-8-codiert, d.h., ein Zeichen kann zwischen 1 und 4 Bytes lang sein.
```{julia}
@show typeof('a') sizeof('a') typeof("a") sizeof("a");
```
__Bei einem Nicht-ASCII-String unterscheiden sich Anzahl der Bytes und Anzahl der Zeichen:__
```{julia}
asciistr = "Hello World!"
@show length(asciistr) ncodeunits(asciistr);
```
(Das Leerzeichen zählt natürlich auch.)
```{julia}
str = "😄 Hellö 🎶"
@show length(str) ncodeunits(str);
```
__Iteration über einen String iteriert über die Zeichen:__
```{julia}
for i in str
println(i, " ", typeof(i))
end
```
### Verkettung von Strings
"Strings mit Verkettung bilden ein nichtkommutatives Monoid."
Deshalb wird in Julia die Verkettung multiplikativ geschrieben.
```{julia}
str * asciistr * str
```
Damit sind auch Potenzen mit natürlichem Exponenten definiert.
```{julia}
str^3, str^0
```
### Stringinterpolation
Das Dollarzeichen hat in Strings eine Sonderfunktion, die wir schon oft in
`print()`-Anweisungen genutzt haben. MAn kann damit eine Variable oder einen Ausdruck interpolieren:
```{julia}
a = 33.4
b = "x"
s = "Das Ergebnis für $b ist gleich $a und die verdoppelte Wurzel daraus ist $(2sqrt(a))\n"
```
### Backslash escape sequences
Der _backslash_ `\` hat in Stringkonstanten ebenfalls eine Sonderfunktion.
Julia benutzt die von C und anderen Sprachen bekannten _backslash_-Codierungen für Sonderzeichen und für Dollarzeichen und Backslash selbst:
```{julia}
s = "So bekommt man \'Anführungszeichen\" und ein \$-Zeichen und einen\nZeilenumbruch und ein \\ usw... "
print(s)
```
### Triple-Quotes
Man kann Strings auch mit Triple-Quotes begrenzen.
In dieser Form bleiben Zeilenumbrüche und Anführungszeichen erhalten:
```{julia}
s = """
Das soll
ein "längerer"
'Text' sein.
"""
print(s)
```
### Raw strings
In einem `raw string` sind alle backslash-Codierungen außer `\"` abgeschaltet:
```{julia}
s = raw"Ein $ und ein \ und zwei \\ und ein 'bla'..."
print(s)
```
## Weitere Funktionen für Zeichen und Strings (Auswahl)
### Tests für Zeichen
```{julia}
@show isdigit('0') isletter('Ψ') isascii('\U2655') islowercase('α')
@show isnumeric('½') iscntrl('\n') ispunct(';');
```
### Anwendung auf Strings
Diese Tests lassen sich z.B. mit `all()`, `any()` oder `count()` auf Strings anwenden:
```{julia}
all(ispunct, ";.:")
```
```{julia}
any(isdigit, "Es ist 3 Uhr! 🕒" )
```
```{julia}
count(islowercase, "Hello, du!!")
```
### Weitere String-Funktionen
```{julia}
@show startswith("Lampenschirm", "Lamp") occursin("pensch", "Lampenschirm")
@show endswith("Lampenschirm", "irm");
```
```{julia}
@show uppercase("Eis") lowercase("Eis") titlecase("eiSen");
```
```{julia}
# remove newline from end of string
@show chomp("Eis\n") chomp("Eis");
```
```{julia}
split("π ist irrational.")
```
```{julia}
replace("π ist irrational.", "ist" => "ist angeblich")
```
## Indizierung von Strings
Strings sind nicht mutierbar aber indizierbar. Dabei gibt es ein paar Besonderheiten.
- Der Index nummeriert die Bytes des Strings.
- Bei einem nicht-ASCII-String sind nicht alle Indizes gültig, denn
- ein gültiger Index adressiert immer ein Unicode-Zeichen.
Unser Beispielstring:
```{julia}
str
```
Das erste Zeichen
```{julia}
str[1]
```
Dieses Zeichen ist in UTF8-Kodierung 4 Bytes lang. Damit sind 2,3 und 4 ungültige Indizes.
```{julia}
str[2]
```
Erst das 5. Byte ist ein neues Zeichen:
```{julia}
str[5]
```
Auch bei der Adressierung von Substrings müssen Anfang und Ende jeweils gültige Indizes sein, d.h., der Endindex muss ebenfalls das erste Byte eines Zeichens indizieren und dieses Zeichen ist das letzte des Teilstrings.
```{julia}
str[1:7]
```
Die Funktion `eachindex()` liefert einen Iterator über die gültigen Indizes:
```{julia}
for i in eachindex(str)
c = str[i]
println("$i: $c")
end
```
Wie üblich macht collect() aus einem Iterator einen Vektor.
```{julia}
collect(eachindex(str))
```
Die Funktion `nextind()` liefert den nächsten gültigen Index.
```{julia}
@show nextind(str, 1) nextind(str, 2);
```
Warum verwendet Julia einen Byte-Index und keinen Zeichenindex? Der Hauptgrund dürfte die Effizienz der Indizierung sein.
- In einem langen String, z.B. einem Buchtext, ist die Stelle `s[123455]` mit einem Byte-Index schnell zu finden.
- Ein Zeichen-Index müsste in der UTF-8-Codierung den ganzen String durchlaufen, um das n-te Zeichen zu finden, da die Zeichen 1,2,3 oder 4 Bytes lang sein können.
Einige Funktionen liefern Indizes oder Ranges als Resultat. Sie liefern immer gültige Indizes:
```{julia}
findfirst('l', str)
```
```{julia}
findfirst("Hel", str)
```
```{julia}
str2 = "αβγδϵ"^3
```
```{julia}
n = findfirst('γ', str2)
```
So kann man ab dem nächsten nach `n=5` gültigen Index weitersuchen:
```{julia}
findnext('γ', str2, nextind(str2, n))
```