14  Zeichen, Strings und Unicode

14.1 Zeichencodierungen (Frühgeschichte)

Es gab - abhängig von Hersteller, Land, Programmiersprache, Betriebsssystem,… - eine große Vielzahl von Codierungen.

Bis heute relevant sind:

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

14.1.2 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:
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

14.2 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)

14.2.1 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)

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

'\U21b4'
'↴': Unicode U+21B4 (category So: Symbol, other)

14.2.3 Eine Auswahl an Schriften

Hinweis

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:

function printuc(c, n)
    for i in 0:n-1
        print(c + i)
    end
end
printuc (generic function with 1 method)

Kyrillisch

printuc('\U0400', 100)
ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣ

Tamilisch

printuc('\U0be7',20)
௧௨௩௪௫௬௭௮௯௰௱௲௳௴௵௶௷௸௹௺

Schach

printuc('\U2654', 12)
♔♕♖♗♘♙♚♛♜♝♞♟

Mathematische Operatoren

printuc('\U2200', 255)
∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾

Runen

printuc('\U16a0', 40)
ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩᚪᚫᚬᚭᚮᚯᚰᚱᚲᚳᚴᚵᚶᚷᚸᚹᚺᚻᚼᚽᚾᚿᛀᛁᛂᛃᛄᛅᛆᛇ

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
printuc('\U101D0', 46 )
𐇐𐇑𐇒𐇓𐇔𐇕𐇖𐇗𐇘𐇙𐇚𐇛𐇜𐇝𐇞𐇟𐇠𐇡𐇢𐇣𐇤𐇥𐇦𐇧𐇨𐇩𐇪𐇫𐇬𐇭𐇮𐇯𐇰𐇱𐇲𐇳𐇴𐇵𐇶𐇷𐇸𐇹𐇺𐇻𐇼𐇽

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

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

14.3 Zeichen und Zeichenketten in Julia

14.3.1 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.
  • Chars können von/zu UInts umgewandelt werden und
  • der Integer-Wert ist gleich dem Unicode-codepoint.

Chars können von/zu UInts umgewandelt werden.

UInt('a')
0x0000000000000061
b = Char(0x2656)
'♖': Unicode U+2656 (category So: Symbol, other)

14.3.2 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.
@show typeof('a') sizeof('a') typeof("a") sizeof("a");
typeof('a') = Char
sizeof('a') = 4
typeof("a") = String
sizeof("a") = 1

Bei einem Nicht-ASCII-String unterscheiden sich Anzahl der Bytes und Anzahl der Zeichen:

asciistr = "Hello World!"
@show length(asciistr) ncodeunits(asciistr);
length(asciistr) = 12
ncodeunits(asciistr) = 12

(Das Leerzeichen zählt natürlich auch.)

str = "😄 Hellö 🎶"
@show length(str) ncodeunits(str);
length(str) = 9
ncodeunits(str) = 16

Iteration über einen String iteriert über die Zeichen:

for i in str
    println(i, "  ", typeof(i))
end
😄  Char
   Char
H  Char
e  Char
l  Char
l  Char
ö  Char
   Char
🎶  Char

14.3.3 Verkettung von Strings

“Strings mit Verkettung bilden ein nichtkommutatives Monoid.”

Deshalb wird in Julia die Verkettung multiplikativ geschrieben.

 str * asciistr * str
"😄 Hellö 🎶Hello World!😄 Hellö 🎶"

Damit sind auch Potenzen mit natürlichem Exponenten definiert.

str^3,  str^0
("😄 Hellö 🎶😄 Hellö 🎶😄 Hellö 🎶", "")

14.3.4 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:

a = 33.4
b = "x"

s = "Das Ergebnis für $b ist gleich $a und die verdoppelte Wurzel daraus ist $(2sqrt(a))\n"
"Das Ergebnis für x ist gleich 33.4 und die verdoppelte Wurzel daraus ist 11.55854662143991\n"

14.3.5 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:

s = "So bekommt man \'Anführungszeichen\" und ein \$-Zeichen und einen\nZeilenumbruch und ein \\ usw... "
print(s)
So bekommt man 'Anführungszeichen" und ein $-Zeichen und einen
Zeilenumbruch und ein \ usw... 

14.3.6 Triple-Quotes

Man kann Strings auch mit Triple-Quotes begrenzen. In dieser Form bleiben Zeilenumbrüche und Anführungszeichen erhalten:

s = """
 Das soll
ein "längerer"  
  'Text' sein.
"""

print(s)
 Das soll
ein "längerer"  
  'Text' sein.

14.3.7 Raw strings

In einem raw string sind alle backslash-Codierungen außer \" abgeschaltet:

s = raw"Ein $ und ein \ und zwei \\ und ein 'bla'..."
print(s)
Ein $ und ein \ und zwei \\ und ein 'bla'...

14.4 Weitere Funktionen für Zeichen und Strings (Auswahl)

14.4.1 Tests für Zeichen

@show isdigit('0') isletter('Ψ') isascii('\U2655') islowercase('α') 
@show isnumeric('½') iscntrl('\n') ispunct(';');
isdigit('0') = true
isletter('Ψ') = true
isascii('♕') = false
islowercase('α') = true
isnumeric('½') = true
iscntrl('\n') = true
ispunct(';') = true

14.4.2 Anwendung auf Strings

Diese Tests lassen sich z.B. mit all(), any() oder count() auf Strings anwenden:

all(ispunct, ";.:")
true
any(isdigit, "Es ist 3 Uhr! 🕒" )
true
count(islowercase, "Hello, du!!")
6

14.4.3 Weitere String-Funktionen

@show startswith("Lampenschirm", "Lamp")  occursin("pensch", "Lampenschirm")  
@show endswith("Lampenschirm", "irm");
startswith("Lampenschirm", "Lamp") = true
occursin("pensch", "Lampenschirm") = true
endswith("Lampenschirm", "irm") = true
@show uppercase("Eis") lowercase("Eis")  titlecase("eiSen");
uppercase("Eis") = "EIS"
lowercase("Eis") = "eis"
titlecase("eiSen") = "Eisen"
# remove newline from end of string

@show chomp("Eis\n")  chomp("Eis");
chomp("Eis\n") = "Eis"
chomp("Eis") = "Eis"
split("π ist irrational.")
3-element Vector{SubString{String}}:
 "π"
 "ist"
 "irrational."
replace("π ist irrational.", "ist" => "ist angeblich")
"π ist angeblich irrational."

14.5 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:

str
"😄 Hellö 🎶"

Das erste Zeichen

str[1]
'😄': Unicode U+1F604 (category So: Symbol, other)

Dieses Zeichen ist in UTF8-Kodierung 4 Bytes lang. Damit sind 2,3 und 4 ungültige Indizes.

str[2]
StringIndexError: invalid index [2], valid nearby indices [1]=>'😄', [5]=>' '
Stacktrace:
 [1] string_index_err(s::String, i::Int64)
   @ Base ./strings/string.jl:12
 [2] getindex_continued(s::String, i::Int64, u::UInt32)
   @ Base ./strings/string.jl:472
 [3] getindex(s::String, i::Int64)
   @ Base ./strings/string.jl:464
 [4] top-level scope
   @ ~/Julia/23/Book-ansipatch/chapters/10_Strings.qmd:469

Erst das 5. Byte ist ein neues Zeichen:

str[5]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

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.

str[1:7]
"😄 He"

Die Funktion eachindex() liefert einen Iterator über die gültigen Indizes:

for i in eachindex(str)
    c = str[i]
    println("$i: $c")
end
1: 😄
5:  
6: H
7: e
8: l
9: l
10: ö
12:  
13: 🎶

Wie üblich macht collect() aus einem Iterator einen Vektor.

collect(eachindex(str))
9-element Vector{Int64}:
  1
  5
  6
  7
  8
  9
 10
 12
 13

Die Funktion nextind() liefert den nächsten gültigen Index.

@show nextind(str, 1) nextind(str, 2);
nextind(str, 1) = 5
nextind(str, 2) = 5

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:

findfirst('l', str)
8
findfirst("Hel", str)
6:8
str2 = "αβγδϵ"^3
"αβγδϵαβγδϵαβγδϵ"
n = findfirst('γ', str2)
5

So kann man ab dem nächsten nach n=5 gültigen Index weitersuchen:

findnext('γ', str2, nextind(str2, n))
15