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 mit Zahlen sortieren, erwarten Sie, dass file1.txt, file2.txt und file10.txt in dieser Reihenfolge erscheinen. Der standardmäßige Zeichenkettenvergleich von JavaScript erzeugt jedoch 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 immer dann auf, wenn Sie Dateinamen, Versionsnummern, Straßenadressen, Produktcodes oder andere Zeichenketten mit eingebetteten Zahlen sortieren. Die falsche Reihenfolge verwirrt Benutzer und erschwert die Navigation in Daten.

JavaScript bietet die API Intl.Collator mit einer numerischen Option, die dieses Problem löst. Diese Lektion erklärt, wie numerische Kollation funktioniert, warum der standardmäßige 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 Collator Ziffernfolgen und vergleicht sie nach ihrem numerischen Wert.

Bei deaktivierter numerischer Kollation kommt die Zeichenkette file10.txt vor file2.txt, weil der zeichenweise Vergleich feststellt, dass 1 an der ersten unterschiedlichen Position vor 2 kommt. Der Collator berücksichtigt nie, dass 10 eine Zahl darstellt, die größer ist als 2.

Bei aktivierter numerischer Kollation erkennt der Collator 10 und 2 als vollständige Zahlen und vergleicht sie numerisch. Da 10 größer ist als 2, kommt file2.txt korrekterweise vor file10.txt.

Dieses Verhalten erzeugt das, was als natürliche Sortierung oder natürliche Reihenfolge bezeichnet wird, bei der Zeichenketten mit Zahlen so sortiert werden, wie Menschen es erwarten, anstatt streng alphabetisch.

Warum der Standard-Zeichenkettenvergleich bei Zahlen versagt

Der Standard-Zeichenkettenvergleich von JavaScript verwendet lexikografische Sortierung, die Zeichenketten Zeichen für Zeichen von links nach rechts anhand von Unicode-Codepunktwerten vergleicht. Dies funktioniert korrekt für alphabetischen Text, führt jedoch bei Zahlen zu unerwarteten Ergebnissen.

Betrachten Sie, wie der lexikografische Vergleich diese Zeichenketten behandelt:

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

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

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

Dies erzeugt die Sequenz 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 Intl.Collator-Konstruktor akzeptiert ein Options-Objekt mit einer numeric-Eigenschaft. Das Setzen von numeric: true aktiviert die numerische Kollation, wodurch der Collator Ziffernsequenzen 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);
// Output: ['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 kommen soll, null, wenn sie gleich sind, und eine positive Zahl, wenn das erste nach dem zweiten kommen soll. Dies entspricht der Signatur, die von JavaScripts Array.sort()-Methode erwartet wird.

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

Sortierung gemischter alphanumerischer Zeichenketten

Numerische Kollation verarbeitet Zeichenketten, bei denen Zahlen an beliebiger Position erscheinen, nicht nur am Ende. Der Collator 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 Collator 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.

Sortierung 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 Collator 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. In 1.10.5 erkennt er, dass die zweite Komponente 10 ist, was größer als 2 ist.

Arbeiten mit Produktcodes und Identifikatoren

Produktcodes, Rechnungsnummern und andere Identifikatoren mischen 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 Collator das numerische Suffix. Das Ergebnis spiegelt aufsteigende numerische Reihenfolge wider und nicht lexikografische Reihenfolge.

Sortierung mit verschiedenen Locales

Die Option numeric funktioniert mit jedem 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 erzeugen dasselbe Ergebnis, da die Zeichenketten nur ASCII-Zeichen und Zahlen enthalten. Wenn Zeichenketten locale-spezifische Zeichen enthalten, folgt der alphabetische Vergleich den Locale-Regeln, während der numerische Vergleich konsistent bleibt.

Zeichenketten vergleichen ohne Sortierung

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

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

console.log(collator.compare('file2.txt', 'file10.txt'));
// Output: -1 (negative number means first argument comes before second)

console.log(collator.compare('file10.txt', 'file2.txt'));
// Output: 1 (positive number means first argument comes after second)

console.log(collator.compare('file2.txt', 'file2.txt'));
// Output: 0 (zero means arguments are equal)

Dies ist nützlich, wenn Sie die Reihenfolge überprüfen müssen, ohne ein Array zu ändern, beispielsweise beim Einfügen eines Elements in eine sortierte Liste oder beim Prüfen, ob ein Wert in einen Bereich fällt.

Die Einschränkung bei Dezimalzahlen verstehen

Numerische Kollation vergleicht Ziffernfolgen, erkennt jedoch 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), vergleicht dann 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 Sortierung bei Dezimalzahlen.

Zum Sortieren von Dezimalzahlen konvertieren Sie diese in tatsächliche Zahlen und sortieren numerisch, oder verwenden Sie String-Padding, um eine korrekte lexikografische Reihenfolge sicherzustellen.

Numerische Kollation mit anderen Optionen kombinieren

Die numeric-Option funktioniert zusammen mit anderen Kollationsoptionen wie sensitivity und caseFirst. Sie können steuern, wie der Collator Groß-/Kleinschreibung und 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 sensitivity: 'base'-Option macht den Vergleich unabhängig von Groß-/Kleinschreibung. Der Collator behandelt Item1, item1 und ITEM1 als gleichwertig, während numerische Teile weiterhin korrekt verglichen werden.

Wiederverwendung von Kollatoren für bessere Performance

Das Erstellen einer neuen Intl.Collator-Instanz erfordert das Laden von Locale-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 Sie 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 Performance-Unterschied wird signifikant, wenn viele Arrays sortiert oder häufige Vergleiche durchgeführt werden.