Zum Inhalt springen
OneKitly

Text sortieren ist nicht eine Operation: vier Reihenfolgen, die sich alle alphabetisch nennen

Veröffentlicht am 29.5.2025 · 11 Min. Lesezeit · Text- & Sprach-Tools

Daniel Okonkwo

Daniel OkonkwoFront-end-Entwickler und Tech-Redakteur bei OneKitly

Web-Performance · Dateiformate

Anhand von 5 Quellen geprüft

Profil ansehen
Kurz gesagt

„Alphabetisch“ benennt mindestens vier verschiedene Reihenfolgen, und sie widersprechen einander schon bei gewöhnlichen Eingaben. Das voreingestellte .sort() von JavaScript vergleicht UTF-16-Codeeinheiten: ['Öl','Ohr','Zebra','Ähre'] kommt als Ohr, Zebra, Ähre, Öl heraus — die Umlaute hinter dem Z, weil ihre Codepunkte größer sind — und ['Kapitel10','Kapitel9','Kapitel2'] kommt als Kapitel10, Kapitel2, Kapitel9 heraus, weil „1“ ein kleineres Zeichen ist als „9“. Verlangt man stattdessen einen Intl.Collator mit numeric: true, ergeben dieselben Zeichenketten Kapitel2, Kapitel9, Kapitel10. Die Locale-Kollation ist eine dritte Reihenfolge: unter de sortiert dieselbe Liste als Ähre, Ohr, Öl, Zebra, denn ä und ö stehen neben a und o. Unter sv erhält man wieder Ohr, Zebra, Ähre, Öl - das voreingestellte .sort() liefert für eine deutsche Liste also zufällig die schwedische Antwort. Eine vierte Ordnung sind Kollationsvarianten: de-u-co-phonebk behandelt ö als oe und stellt Öl vor Ohr. Array.prototype.sort muss seit ES2019 stabil sein, gleiche Schlüssel behalten ihre Eingabereihenfolge. Sichtbaren Text ohne genannte Locale zu sortieren ist ein Fehler, keine Abkürzung.

Codeeinheiten-Ordnung, natürliche Ordnung und Locale-Kollation an derselben Liste in Node ausgeführt, Ausgaben abgedruckt. Warum Zebra vor apple steht, warum item10 vor item9 steht und warum ä im Deutschen neben a liegt, im Schwedischen aber hinter z.

Vier Dinge, die alphabetisch heißen

Eine Sortier-Schaltfläche bietet eine Option an und legt nahe, es gebe eine Antwort. Es gibt mindestens vier, und sie sind keine Verfeinerungen voneinander: Es sind verschiedene Reihenfolgen, die aus derselben Eingabe verschiedene Listen zurückgeben. Die Codeeinheiten-Ordnung vergleicht die Zahlenwerte der UTF-16-Einheiten, aus denen eine Zeichenkette besteht. Die natürliche Ordnung liest Ziffernfolgen als Zahlen. Die Locale-Kollation wendet die Regeln einer Sprache darauf an, welche Buchstaben als derselbe Buchstabe zählen. Kollationsvarianten spalten eine einzelne Sprache dann noch in mehrere vertretbare Ordnungen, denn deutsche Wörterbücher und deutsche Telefonbücher waren sich nie einig.

Alles Folgende wurde ausgeführt, nicht erinnert. Jede Liste ist genau so abgedruckt, wie Node 26.3 sie zurückgegeben hat, und die Skripte sind kurz genug zum Abtippen: ein Array, ein sort, ein console.log.

Codeeinheiten-Ordnung: was .sort() tatsächlich tut

Ohne Vergleichsfunktion aufgerufen, wandelt Array.prototype.sort jedes Element in eine Zeichenkette um und vergleicht diese nach UTF-16-Codeeinheiten. Das ist eine dokumentierte Regel, kein Zufall, und sie erzeugt zwei sichtbare Symptome. Großbuchstaben liegen im Bereich 0x41–0x5A, Kleinbuchstaben in 0x61–0x7A, also steht jeder Großbuchstabe vor jedem Kleinbuchstaben. Und Ziffern werden als Zeichen verglichen: ['Kapitel10','Kapitel9','Kapitel2'] liefert Kapitel10, Kapitel2, Kapitel9, denn „1“ ist 0x31 und „9“ ist 0x39, und der Vergleich endet bei der ersten Abweichung.

Das dritte Symptom ist jenes, das bei Leserinnen und Lesern ankommt. Jeder Buchstabe mit diakritischem Zeichen liegt oberhalb von 0x7A, also schiebt die Codeeinheiten-Ordnung das gesamte akzentuierte Alphabet hinter das z. ["Öl","Ohr","Zebra","Ähre"] liefert Ohr, Zebra, Ähre, Öl. ["ñu","nube","niño","zorro"] liefert niño, nube, zorro, ñu. ["ação","acordo","água","avô"] liefert acordo, avô, ação, água. ["étage","effet","zèbre","Île"] liefert effet, zèbre, Île, étage. Vier Sprachen, vier falsche Antworten, eine Zeile Code.

Die Codeeinheiten-Ordnung ist nicht nutzlos. Sie ist total, transparent, schnell - 20 000 Wörter in 6 ms im Benchmark weiter unten - und in jeder Laufzeitumgebung und jeder Locale identisch, was sie zur richtigen Wahl für alles macht, was eine Maschine liest: Indexschlüssel, Deduplizierungs-Buckets, Cache-Kennungen, kanonische Serialisierungen. Falsch ist sie nur, wenn die Ausgabe für einen Menschen bestimmt ist.

Natürliche Ordnung: Ziffern als Zahlen lesen

Die natürliche Ordnung - jene, die ein Dateimanager verwendet - behandelt eine Ziffernfolge innerhalb einer Zeichenkette als eine einzige Zahl statt als Zeichenfolge. In JavaScript ist sie eine Option: new Intl.Collator("de", { numeric: true }). Auf ["Kapitel10","Kapitel9","Kapitel2"] liefert sie Kapitel2, Kapitel9, Kapitel10, und auf der längeren Liste ["item2","item9","item10","item100","item20"] liefert sie item2, item9, item10, item20, item100, während das voreingestellte .sort() item10, item100, item2, item20, item9 liefert.

Zwei Grenzen sollte man kennen, bevor man sie überall einschaltet. Numerische Kollation ist eine Darstellungsbequemlichkeit, keine Arithmetik: Sie vergleicht Ziffernfolgen, hat also über „v1.10“ gegen „v1.9“ eine Meinung, die ein Parser für semantische Versionen nicht teilen würde, und sie sagt nichts Brauchbares über Vorzeichen, Dezimal- oder Tausendertrennzeichen. Und sie ändert die Antwort für Schlüssel, die bloß zufällig Ziffern enthalten, etwa Produktcodes, bei denen 0090 und 90 verschiedene Artikel sind. Schalte sie für Listen ein, die ein Mensch überfliegt, und für Kennungen aus.

Locale-Kollation: Deutsch gegen Schwedisch

Die Locale-Kollation ist der klassische Fall, und Deutsch gegen Schwedisch ist das klassische Paar. Nimm ["Öl","Ohr","Zebra","Ähre"]. Unter new Intl.Collator("de") sortiert die Liste als Ähre, Ohr, Öl, Zebra: ä ist eine Variante von a, ö eine Variante von o, und das diakritische Zeichen entscheidet nur Gleichstände. Unter new Intl.Collator("sv") ergeben dieselben vier Zeichenketten Ohr, Zebra, Ähre, Öl: Im Schwedischen sind å, ä und ö die letzten drei Buchstaben des Alphabets, hinter dem z. Keines von beiden ist ein Fehler. Es sind zwei Sprachen mit zwei Alphabeten, und die Liste muss sich für eines entscheiden.

Jetzt der unangenehme Teil: Für diese Liste liefert das voreingestellte .sort() Ohr, Zebra, Ähre, Öl - Zeichen für Zeichen die schwedische Antwort. Ein Programm, das die Locale ausgelassen hat, hat nicht „keine bestimmte Reihenfolge“ erzeugt. Es hat stillschweigend eine bestimmte fremde Reihenfolge erzeugt, und es wird das für jede deutsche, spanische, portugiesische und französische Liste weiter tun, die es anfasst.

Die übrigen Locales dieses Artikels verhalten sich genauso. Spanisch macht ñ zu einem eigenen Buchstaben nach n: ["ñu","nube","niño","zorro"] ergibt unter es niño, nube, ñu, zorro, während die Codeeinheiten-Ordnung ñu hinter zorro verbannt. Portugiesisch behandelt Akzente als Gleichstandsbrecher: ["ação","acordo","água","avô"] kommt unter pt in Wörterbuchreihenfolge heraus und unter .sort() durcheinander. Italienisch ergibt unter it ancora, Àncora, elite, zucchero, wobei der Akzent eine sekundäre und die Großschreibung eine tertiäre Differenz ist. Und Französisch hat eine wirklich regionale Ordnung: Auf ["cote","coté","côte","côté"] liefert fr cote, coté, côte, côté, fr-CA dagegen cote, côte, coté, côté, weil das kanadische Französisch Akzente vom Wortende her vergleicht.

Eine Sprache, mehrere Ordnungen: Kollationsvarianten

Selbst innerhalb einer Sprache gibt es mehr als eine richtige Antwort, und das CLDR von Unicode liefert sie als benannte Varianten, die über die Locale-Zeichenkette gewählt werden. Deutsch hat zwei im täglichen Gebrauch. Die Wörterbuch-Kollation de sortiert ['Göbel','Goethe','Godel','Gözde','Gott'] als Göbel, Godel, Goethe, Gott, Gözde: ö ist eine Variante von o. Die Telefonbuch-Kollation de-u-co-phonebk sortiert dieselben fünf als Godel, Göbel, Goethe, Gözde, Gott, weil ö zu oe expandiert wird - womit Göbel zwischen Godel und Goethe steht, genau dort, wo jemand nachschlägt, der „Goebel“ sucht.

Neben der Variante stehen zwei Regler, die die Antwort ebenso stark verändern. sensitivity entscheidet, welche Unterschiede überhaupt zählen: Bei „cote“ gegen „côte“ und „cote“ gegen „Cote“ meldet die Sensitivität „base“ beide Paare als gleich, „accent“ trennt den Akzent, aber nicht die Groß- und Kleinschreibung, „case“ trennt die Schreibung, aber nicht den Akzent, und „variant“ - die Voreinstellung - trennt beides. caseFirst entscheidet, wer einen Gleichstand gewinnt: Bei ['apple','Apple','APPLE'] liefert die Voreinstellung apple, Apple, APPLE, und caseFirst: 'upper' liefert APPLE, Apple, apple. Keiner der Regler ist Kosmetik. sensitivity macht einen Kollator außerdem zum Suchwerkzeug: Mit „base“ gibt compare 0 für Zeichenketten zurück, die eine Leserin dasselbe Wort nennen würde.

Stabilität: was mit Gleichständen geschieht

Eine Sortierung ist stabil, wenn Elemente, die als gleich verglichen werden, ihre relative Reihenfolge aus der Eingabe behalten. Das zählt in dem Moment, in dem man nach einem Teilschlüssel sortiert - einem Anfangsbuchstaben, einer Kategorie, einem Datum ohne Uhrzeit -, denn Gleichstände sind keine seltenen Randfälle, sie sind der größte Teil der Liste. Beim Sortieren von zehn Vornamen allein nach ihrem Anfangsbuchstaben kamen die Elemente mit a in den Eingabepositionen 1, 2, 4, 6, 8 heraus und die mit b in den Positionen 0, 3, 5, 7, 9: jede Gruppe erhalten, in Reihenfolge.

Array.prototype.sort muss laut Spezifikation seit ES2019 stabil sein. Davor durften Engines oberhalb einer bestimmten Arraylänge einen instabilen Algorithmus verwenden, und mehrere taten das - deshalb raten ältere Texte zu einem zusammengesetzten Vergleicher oder dazu, den Index mitzuführen. Die Anforderung gilt unabhängig von der Größe: Ein Array mit tausend Elementen, sortiert nach einem Schlüssel mit nur drei verschiedenen Werten, kam mit jeder Gruppe in Eingabereihenfolge zurück. Du kannst deshalb eine Mehrschlüssel-Sortierung bauen, indem du mehrfach sortierst, vom unwichtigsten Schlüssel zum wichtigsten - erst Vorname, dann Nachname - und sich darauf verlassen, dass die früheren Durchgänge überleben.

Was es kostet, und die Regel, die folgt

Locale-bewusstes Sortieren kostet mehr als Codeeinheiten-Sortieren, aber nicht so viel, wie befürchtet wird, und der teure Fehler ist ein anderer. 20 000 Wörter in Node 26.3 sortiert: Das voreingestellte .sort() brauchte 6 ms, a.localeCompare(b, "de") 12 ms, ein einmal gebauter und wiederverwendeter Kollator 28 ms - und ein new Intl.Collator, im Vergleicher selbst konstruiert, brauchte 1 771 ms, sechzigmal langsamer als die Wiederverwendung. Der Kostenpunkt ist nicht die Kollation. Der Kostenpunkt ist, den Kollator hunderttausendfach zu bauen.

Bleibt eine Regel, kurz genug, um sie anzuwenden. Liest eine Maschine die Ausgabe, sortierst du nach Codeeinheiten und schreibst es auf. Liest ein Mensch sie, nennst du eine Locale - die Sprache, in der das Dokument geschrieben ist, nicht die des Browsers, der es zufällig darstellt -, bau einen Intl.Collator, entscheide numeric und sensitivity bewusst und verwende ihn wieder. Nie vertretbar ist nur das eine: .sort() auf sichtbaren Text anzuwenden und das Ergebnis alphabetisch zu nennen.

Dieselben zwei Listen unter fünf Reihenfolgen, in Node 26.3 ausgeführt. Spalte drei ist ["Öl","Ohr","Zebra","Ähre"], Spalte vier ist ["item10","item9","item2"]. Nur der numerische Kollator repariert die zweite Liste, nur ein Locale-Kollator die erste.
ReihenfolgeWie man sie anfordertDeutsche ListeNummerierte Liste
Codeeinheiten-Ordnungarr.sort()Ohr, Zebra, Ähre, Ölitem10, item2, item9
Deutsche Kollationnew Intl.Collator("de")Ähre, Ohr, Öl, Zebraitem10, item2, item9
Deutsche Telefonbuch-Variantenew Intl.Collator("de-u-co-phonebk")Ähre, Öl, Ohr, Zebraitem10, item2, item9
Schwedische Kollationnew Intl.Collator("sv")Ohr, Zebra, Ähre, Ölitem10, item2, item9
Zahlenbewusste Kollationnew Intl.Collator("de", { numeric: true })Ähre, Ohr, Öl, Zebraitem2, item9, item10
Zeilen sortierenSortiere Zeilen alphabetisch, entferne Duplikate und Leerzeichen.Tool ausprobieren

Häufige Fragen

Warum wird „Zebra“ vor „apple“ einsortiert?
Weil .sort() ohne Vergleichsfunktion UTF-16-Codeeinheiten vergleicht und jeder ASCII-Großbuchstabe (0x41–0x5A) einen kleineren Wert hat als jeder ASCII-Kleinbuchstabe (0x61–0x7A). Es sortiert keine Buchstaben, es sortiert Zahlen, die zufällig Buchstaben darstellen. Jeder Locale-Kollator behebt das: new Intl.Collator("en").compare liefert apple, Banana, zebra, Zebra in dieser Reihenfolge und behandelt die Groß- und Kleinschreibung als letzten Gleichstandsbrecher statt als erstes Kriterium.
Ist die Sortierung von JavaScript stabil?
Ja, und sie muss es sein. ES2019 hat Stabilität für Array.prototype.sort zur Spezifikationsanforderung gemacht, und Array.prototype.toSorted folgt derselben Regel. Hier an einem Array mit tausend Elementen überprüft, sortiert nach einem Schlüssel mit drei verschiedenen Werten: Jede Gruppe kam in Eingabereihenfolge zurück. Diese Garantie erlaubt es, eine mehrspaltige Sortierung als Folge einspaltiger Sortierungen umzusetzen, beginnend mit der unwichtigsten Spalte.
Welche Locale nehme ich, wenn ich die des Lesers nicht kenne?
Nimm die Sprache des Inhalts, nicht die des Lesegeräts. Eine Liste deutscher Produktnamen gehört in deutsche Kollation, egal wer sie ansieht - genau wie ein gedruckter deutscher Katalog. Auf die Voreinstellung der Laufzeitumgebung zurückzufallen ist die schlechteste Option, weil sie unsichtbar ist: new Intl.Collator() ohne Argument löste sich auf der Maschine, auf der dieser Artikel entstand, zu en-US auf und würde sich auf der nächsten anders auflösen - dieselbe Liste ordnete sich also auf zwei Servern verschieden, ohne dass eine Zeile Code sich ändert.
Warum ordnet mein Dateimanager item2 vor item10, mein Code aber nicht?
Der Dateimanager verwendet die natürliche Ordnung: Er erkennt die Ziffernfolge als Zahl. Dein Code vergleicht Zeichen, hält also bei „1“ gegen „9“ an und liest den Rest nie. Füge einem Intl.Collator { numeric: true } hinzu oder übergib dieselbe Option an localeCompare, dann stimmen beide überein. Versuch nicht, es durch Auffüllen der angezeigten Zeichenketten mit Nullen nachzubilden - das repariert die Sortierung und zerstört die Beschriftungen.
Ist localeCompare für eine lange Liste zu langsam?
Für sich genommen nicht. Bei 20 000 Wörtern in Node 26.3 brauchte a.localeCompare(b, "de") 12 ms und ein wiederverwendeter Intl.Collator 28 ms gegenüber 6 ms für das voreingestellte .sort() - ein Unterschied, den niemand bemerkt. Wirklich langsam ist es, einen Kollator im Vergleicher zu konstruieren: So brauchte dieselbe Sortierung 1 771 ms, weil für jede der Hunderttausenden Vergleiche ein neuer Kollator gebaut wird. Bau ihn einmal außerhalb der Sortierung und übergib sein .compare.
Wie sortiere ich deutsche Namen wie ein Telefonbuch?
Fordere die Kollationsvariante namentlich an: new Intl.Collator("de-u-co-phonebk"). Der Teil -u-co- einer Locale-Kennung wählt eine Kollation aus, und phonebk ist die deutsche Telefonbuch-Anpassung, in der sich ö wie oe, ä wie ae und ü wie ue verhält. Auf ["Göbel","Goethe","Godel","Gözde","Gott"] liefert einfaches de Göbel, Godel, Goethe, Gott, Gözde und de-u-co-phonebk liefert Godel, Göbel, Goethe, Gözde, Gott. Andere Sprachen haben eigene Varianten - eine chinesische Liste lässt sich genauso nach Pinyin oder nach Strichzahl ordnen.

Artikel, die dich interessieren könnten

Alle Ratgeber
AnleitungZeilen nummerieren, damit mehrere Leute denselben Text prüfen könnenDie Nummerierung beginnt bei 1 und lässt sich nicht auf 0 setzen, ausgerichtet wird mit Leerzeichen statt Nullen, und das Entfernen macht acht der elf Trennzeichen rückgängig, ohne die Einrückung anzutasten. Was es weiterhin nicht kann: deine Zahlen von seinen unterscheiden.ErklärungDuplikate in einer Liste finden — ohne TabellenkalkulationZwei Zeilen, die identisch aussehen, sind es oft nicht. Groß- und Kleinschreibung, ein Leerzeichen am Ende, ein geschütztes Leerzeichen und zwei verschiedene Kodierungen desselben Akzentbuchstabens liefen je durch den Duplikatfinder — in drei von vier Fällen meldete er keine Duplikate.RatgeberZahlen für sechs Sprachen formatieren: Trennzeichen, Währung und der Weg zurück1.234,56 und 1,234.56 sind dieselbe Zahl, und sie zu verwechseln ändert den Wert, den eine Leserin herausliest. Wir haben Intl.NumberFormat für alle sechs Sprachen der Website laufen lassen und jedes Trennzeichen ausgegeben — auch das unsichtbare, das das Französische verwendet — und dann gemessen, warum parseFloat davon nichts rückgängig machen kann.ErklärungDie Sprache eines Textes erkennen — und warum kurze Texte scheiternGemessen, nicht behauptet: 90 kurze echte Wendungen in sechs Sprachen, keine abgelehnt und 68 richtig — 76 %, unter sechzehn Buchstaben nur noch 64 %. Vier der falschen Antworten kamen mit 100 % Sicherheit zurück.AnleitungUnsauberen Text bereinigen: die Reihenfolge der Schritte, auf die es ankommtTags entfernen, bevor Entitäten dekodiert werden, trimmen vor dem Deduplizieren, Leerraum zuletzt zusammenfassen. Drei Reihenfolgen in Node ausgeführt, eine neunstufige Pipeline in der richtigen Abfolge und die unsichtbaren Zeichen — U+00A0, U+200B, U+FEFF — die jede naive Bereinigung überleben.RatgeberMarkdown entfernen: was der Klartext verliert und was eine Regex falsch machtEin Link wird zu Text, dessen Ziel gelöscht ist, eine verschachtelte Liste verliert ihre Hierarchie, eine Tabelle wird zu einer Reihe von Wörtern. Dann die technische Hälfte: Markdown hat keine einzige Spezifikation, und ein Regex-Entferner verstümmelt einen Dateinamen, ein Malzeichen und das Innere eines Codeblocks - alles gegen einen echten Parser gehalten.

Ähnliche Tools

Quellen

Hast du einen Fehler in diesem Artikel entdeckt?