Wie man Zeichenketten mit eingebetteten Zahlen korrekt sortiert

Verwenden Sie numerische Kollation, um Dateinamen, Versionsnummern und andere Zeichenketten mit Zahlen in natürlicher Reihenfolge zu sortieren

Einführung

Wenn Sie Zeichenketten sortieren, die Zahlen enthalten, erwarten Sie, dass file1.txt, file2.txt und file10.txt in dieser Reihenfolge erscheinen. Allerdings erzeugt der Standard-Zeichenkettenvergleich in JavaScript stattdessen file1.txt, file10.txt, file2.txt. Dies geschieht, weil Zeichenketten Zeichen für Zeichen verglichen werden, und das Zeichen 1 in 10 vor dem Zeichen 2 kommt.

Dieses Problem tritt auf, wann immer Sie Dateinamen, Versionsnummern, Straßenadressen, Produktcodes oder andere Zeichenketten mit eingebetteten Zahlen sortieren. Die falsche Reihenfolge verwirrt Benutzer und macht Daten schwer navigierbar.

JavaScript bietet die Intl.Collator-API mit einer numerischen Option, die dieses Problem löst. Diese Lektion erklärt, wie numerische Kollation funktioniert, warum der Standard-Zeichenkettenvergleich fehlschlägt und wie man Zeichenketten mit eingebetteten Zahlen in natürlicher numerischer Reihenfolge sortiert.

Was numerische Kollation ist

Numerische Kollation ist eine Vergleichsmethode, die Ziffernfolgen als Zahlen und nicht als einzelne Zeichen behandelt. Beim Vergleich von Zeichenketten identifiziert der Kollator Ziffernfolgen und vergleicht sie nach ihrem numerischen Wert.

Wenn die numerische Kollation deaktiviert ist, kommt die Zeichenkette file10.txt vor file2.txt, weil der zeichenweise Vergleich feststellt, dass 1 vor 2 an der ersten unterschiedlichen Position kommt. Der Kollator berücksichtigt nie, dass 10 eine größere Zahl als 2 darstellt.

Wenn die numerische Kollation aktiviert ist, erkennt der Kollator 10 und 2 als vollständige Zahlen und vergleicht sie numerisch. Da 10 größer als 2 ist, kommt file2.txt korrekterweise vor file10.txt.

Dieses Verhalten erzeugt das, was man natürliche Sortierung oder natürliche Reihenfolge nennt, bei der Zeichenketten, die Zahlen enthalten, so sortiert werden, wie Menschen es erwarten, und nicht streng alphabetisch.

Warum der Standard-Zeichenkettenvergleich bei Zahlen versagt

Der standardmäßige String-Vergleich in JavaScript verwendet eine lexikographische Sortierung, die Strings zeichenweise von links nach rechts anhand der Unicode-Codepunktwerte vergleicht. Dies funktioniert korrekt für alphabetischen Text, führt jedoch bei Zahlen zu unerwarteten Ergebnissen.

Betrachten wir, wie der lexikographische Vergleich diese Strings behandelt:

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort();

console.log(files);
// Ausgabe: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

Der Vergleich untersucht jede Zeichenposition unabhängig voneinander. An der ersten abweichenden Position nach file vergleicht er 1 mit 2. Da 1 einen niedrigeren Unicode-Wert als 2 hat, kommt jeder String, der mit file1 beginnt, vor jedem String, der mit file2 beginnt, unabhängig davon, was folgt.

Dies erzeugt die Reihenfolge file1.txt, file10.txt, file2.txt, file20.txt, was den menschlichen Erwartungen an die Zahlenreihenfolge widerspricht.

Verwendung von Intl.Collator mit der numeric-Option

Der Konstruktor Intl.Collator akzeptiert ein Options-Objekt mit einer numeric-Eigenschaft. Wenn numeric: true gesetzt wird, wird die numerische Sortierung aktiviert, wodurch der Collator Ziffernfolgen nach ihrem numerischen Wert vergleicht.

const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

files.sort(collator.compare);

console.log(files);
// Ausgabe: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

Die compare-Methode des Collators gibt eine negative Zahl zurück, wenn das erste Argument vor dem zweiten stehen sollte, null, wenn sie gleich sind, und eine positive Zahl, wenn das erste nach dem zweiten stehen sollte. Dies entspricht der Signatur, die von JavaScripts Array.sort()-Methode erwartet wird.

Das sortierte Ergebnis ordnet die Dateien in natürlicher numerischer Reihenfolge an. Der Collator erkennt, dass 1 < 2 < 10 < 20, und erzeugt die Reihenfolge, die Menschen erwarten.

Sortierung gemischter alphanumerischer Strings

Numerische Kollation verarbeitet Zeichenketten, bei denen Zahlen an beliebigen Positionen auftreten, nicht nur am Ende. Der Kollator vergleicht alphabetische Teile normal und numerische Teile numerisch.

const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];

addresses.sort(collator.compare);

console.log(addresses);
// Output: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']

Der Kollator identifiziert die Ziffernfolgen am Anfang jeder Zeichenkette und vergleicht sie numerisch. Er erkennt, dass 5 < 45 < 123 < 1234, obwohl ein lexikografischer Vergleich eine andere Reihenfolge ergeben würde.

Sortieren von Versionsnummern

Versionsnummern sind ein häufiger Anwendungsfall für numerische Kollation. Softwareversionen wie 1.2.10 sollten nach 1.2.2 kommen, aber lexikografischer Vergleich erzeugt die falsche Reihenfolge.

const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];

versions.sort(collator.compare);

console.log(versions);
// Output: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']

Der Kollator vergleicht jede numerische Komponente korrekt. In der Sequenz 1.2.2, 1.2.5, 1.2.10 erkennt er, dass die dritte Komponente numerisch zunimmt. Bei 1.10.5 erkennt er, dass die zweite Komponente 10 ist, was größer als 2 ist.

Arbeiten mit Produktcodes und Kennungen

Produktcodes, Rechnungsnummern und andere Kennungen kombinieren oft Buchstaben mit Zahlen. Numerische Kollation stellt sicher, dass diese in einer logischen Reihenfolge sortiert werden.

const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];

products.sort(collator.compare);

console.log(products);
// Output: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']

Das alphabetische Präfix PROD- stimmt in allen Zeichenketten überein, daher vergleicht der Kollator das numerische Suffix. Das Ergebnis spiegelt die aufsteigende numerische Reihenfolge wider, nicht die lexikografische Reihenfolge.

Sortieren mit verschiedenen Locales

Die Option numeric funktioniert mit jeder Locale. Während verschiedene Locales unterschiedliche Sortierregeln für alphabetische Zeichen haben können, bleibt das numerische Vergleichsverhalten konsistent.

const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });

const items = ['item1', 'item10', 'item2'];

console.log(items.sort(enCollator.compare));
// Output: ['item1', 'item2', 'item10']

console.log(items.sort(deCollator.compare));
// Output: ['item1', 'item2', 'item10']

Beide Locales produzieren das gleiche Ergebnis, da die Strings nur ASCII-Zeichen und Zahlen enthalten. Wenn Strings locale-spezifische Zeichen enthalten, folgt der alphabetische Vergleich den Locale-Regeln, während der numerische Vergleich konsistent bleibt.

Strings vergleichen ohne zu sortieren

Sie können die compare-Methode des Collators direkt verwenden, um die Beziehung zwischen zwei Strings zu bestimmen, ohne ein ganzes Array zu sortieren.

const collator = new Intl.Collator('en-US', { numeric: true });

console.log(collator.compare('file2.txt', 'file10.txt'));
// Output: -1 (negative Zahl bedeutet, dass das erste Argument vor dem zweiten kommt)

console.log(collator.compare('file10.txt', 'file2.txt'));
// Output: 1 (positive Zahl bedeutet, dass das erste Argument nach dem zweiten kommt)

console.log(collator.compare('file2.txt', 'file2.txt'));
// Output: 0 (Null bedeutet, dass die Argumente gleich sind)

Dies ist nützlich, wenn Sie die Reihenfolge überprüfen müssen, ohne ein Array zu modifizieren, zum Beispiel beim Einfügen eines Elements in eine sortierte Liste oder beim Überprüfen, ob ein Wert innerhalb eines Bereichs liegt.

Verständnis der Einschränkung bei Dezimalzahlen

Die numerische Kollation vergleicht Ziffernfolgen, erkennt aber Dezimalpunkte nicht als Teil von Zahlen. Das Punktzeichen wird als Trennzeichen behandelt, nicht als Dezimaltrennzeichen.

const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];

measurements.sort(collator.compare);

console.log(measurements);
// Output: ['0.005', '0.05', '0.5']

Der Collator behandelt jede Messung als drei separate numerische Komponenten: den Teil vor dem Punkt, den Punkt selbst und den Teil nach dem Punkt. Er vergleicht 0 mit 0 (gleich), dann vergleicht er die Teile nach dem Punkt als separate Zahlen: 5, 5 und 5 (gleich). Dann vergleicht er die zweite Dezimalstelle: nichts, 5 und nichts. Dies führt zu einer falschen Reihenfolge für Dezimalzahlen.

Um Dezimalzahlen zu sortieren, konvertieren Sie sie in tatsächliche Zahlen und sortieren Sie numerisch, oder verwenden Sie String-Padding, um die korrekte lexikographische Reihenfolge sicherzustellen.

Kombination numerischer Kollation mit anderen Optionen

Die Option numeric funktioniert zusammen mit anderen Kollationsoptionen wie sensitivity und caseFirst. Sie können steuern, wie der Kollator Groß- und Kleinschreibung sowie Akzente behandelt, während das numerische Vergleichsverhalten beibehalten wird.

const collator = new Intl.Collator('en-US', {
  numeric: true,
  sensitivity: 'base'
});

const items = ['Item1', 'item10', 'ITEM2'];

items.sort(collator.compare);

console.log(items);
// Output: ['Item1', 'ITEM2', 'item10']

Die Option sensitivity: 'base' macht den Vergleich unempfindlich gegenüber Groß- und Kleinschreibung. Der Kollator behandelt Item1, item1 und ITEM1 als gleichwertig, während numerische Teile weiterhin korrekt verglichen werden.

Wiederverwendung von Kollatoren für bessere Leistung

Das Erstellen einer neuen Intl.Collator-Instanz beinhaltet das Laden von Gebietsschema-Daten und die Verarbeitung von Optionen. Wenn Sie mehrere Arrays sortieren oder viele Vergleiche durchführen müssen, erstellen Sie den Kollator einmal und verwenden ihn wieder.

const collator = new Intl.Collator('en-US', { numeric: true });

const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];

files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);

Dieser Ansatz ist effizienter als das Erstellen eines neuen Kollators für jeden Sortiervorgang. Der Leistungsunterschied wird signifikant, wenn viele Arrays sortiert oder häufige Vergleiche durchgeführt werden.