Cómo ordenar cadenas con números incrustados correctamente

Usa la ordenación numérica para ordenar nombres de archivos, números de versión y otras cadenas que contienen números en orden natural

Introducción

Cuando ordenas cadenas que contienen números, esperas que file1.txt, file2.txt y file10.txt aparezcan en ese orden. Sin embargo, la comparación de cadenas predeterminada de JavaScript produce file1.txt, file10.txt, file2.txt en su lugar. Esto ocurre porque las cadenas se comparan carácter por carácter, y el carácter 1 en 10 viene antes que el carácter 2.

Este problema aparece siempre que ordenas nombres de archivos, números de versión, direcciones de calles, códigos de productos o cualquier otra cadena con números incrustados. El orden incorrecto confunde a los usuarios y dificulta la navegación de los datos.

JavaScript proporciona la API Intl.Collator con una opción numérica que resuelve este problema. Esta lección explica cómo funciona la comparación numérica, por qué falla la comparación de cadenas predeterminada y cómo ordenar cadenas con números incrustados en orden numérico natural.

Qué es la ordenación numérica

La ordenación numérica es un método de comparación que trata las secuencias de dígitos como números en lugar de caracteres individuales. Al comparar cadenas, el ordenador identifica secuencias de dígitos y las compara por su valor numérico.

Con la comparación numérica deshabilitada, la cadena file10.txt viene antes que file2.txt porque la comparación carácter por carácter encuentra que 1 viene antes que 2 en la primera posición diferente. El comparador nunca considera que 10 representa un número mayor que 2.

Con la comparación numérica habilitada, el comparador reconoce 10 y 2 como números completos y los compara numéricamente. Dado que 10 es mayor que 2, file2.txt viene correctamente antes que file10.txt.

Este comportamiento produce lo que la gente llama ordenación natural u orden natural, donde las cadenas que contienen números se ordenan de la manera en que los humanos esperan en lugar de estrictamente alfabéticamente.

Por qué falla la comparación de cadenas predeterminada para números

La comparación de cadenas predeterminada de JavaScript utiliza ordenación lexicográfica, que compara cadenas carácter por carácter de izquierda a derecha usando valores de puntos de código Unicode. Esto funciona correctamente para texto alfabético pero produce resultados inesperados para números.

Considera cómo la comparación lexicográfica maneja estas cadenas:

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

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

La comparación examina cada posición de carácter de forma independiente. En la primera posición diferente después de file, compara 1 contra 2. Dado que 1 tiene un valor Unicode menor que 2, cualquier cadena que comience con file1 viene antes que cualquier cadena que comience con file2, independientemente de lo que siga.

Esto produce la secuencia file1.txt, file10.txt, file2.txt, file20.txt, lo que viola las expectativas humanas sobre el orden de los números.

Uso de Intl.Collator con la opción numeric

El constructor Intl.Collator acepta un objeto de opciones con una propiedad numeric. Establecer numeric: true habilita la intercalación numérica, haciendo que el intercalador compare secuencias de dígitos por su valor numérico.

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']

El método compare del intercalador devuelve un número negativo cuando el primer argumento debe ir antes del segundo, cero cuando son iguales y un número positivo cuando el primero debe ir después del segundo. Esto coincide con la firma esperada por el método Array.sort() de JavaScript.

El resultado ordenado coloca los archivos en orden numérico natural. El comparador reconoce que 1 < 2 < 10 < 20, produciendo la secuencia que los humanos esperan.

Ordenación de cadenas alfanuméricas mixtas

La ordenación numérica maneja cadenas donde los números aparecen en cualquier posición, no solo al final. El comparador compara las porciones alfabéticas normalmente y las porciones numéricas numéricamente.

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']

El comparador identifica las secuencias de dígitos al principio de cada cadena y las compara numéricamente. Reconoce que 5 < 45 < 123 < 1234, aunque la comparación lexicográfica produciría un orden diferente.

Ordenar números de versión

Los números de versión son un caso de uso común para la intercalación numérica. Las versiones de software como 1.2.10 deben ir después de 1.2.2, pero la comparación lexicográfica produce el orden incorrecto.

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']

El intercalador compara cada componente numérico correctamente. En la secuencia 1.2.2, 1.2.5, 1.2.10, reconoce que el tercer componente aumenta numéricamente. En 1.10.5, reconoce que el segundo componente es 10, que es mayor que 2.

Trabajar con códigos de producto e identificadores

Los códigos de producto, números de factura y otros identificadores a menudo mezclan letras con números. La ordenación numérica garantiza que estos se ordenen de manera lógica.

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']

El prefijo alfabético PROD- coincide en todas las cadenas, por lo que el intercalador compara el sufijo numérico. El resultado refleja un orden numérico creciente en lugar de un orden lexicográfico.

Ordenar con diferentes configuraciones regionales

La opción numeric funciona con cualquier configuración regional. Aunque diferentes configuraciones regionales pueden tener diferentes reglas de ordenación para caracteres alfabéticos, el comportamiento de comparación numérica permanece consistente.

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']

Ambas configuraciones regionales producen el mismo resultado porque las cadenas contienen solo caracteres ASCII y números. Cuando las cadenas incluyen caracteres específicos de la configuración regional, la comparación alfabética sigue las reglas regionales mientras que la comparación numérica permanece consistente.

Comparar cadenas sin ordenar

Puedes usar el método compare del intercalador directamente para determinar la relación entre dos cadenas sin ordenar un array completo.

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)

Esto es útil cuando necesitas verificar el orden sin modificar un array, como al insertar un elemento en una lista ordenada o verificar si un valor se encuentra dentro de un rango.

Comprender la limitación con números decimales

La ordenación numérica compara secuencias de dígitos, pero no reconoce los puntos decimales como parte de los números. El carácter de punto se trata como un separador, no como un separador decimal.

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']

El intercalador trata cada medida como tres componentes numéricos separados: la parte antes del punto, el punto en sí y la parte después del punto. Compara 0 contra 0 (igual), luego compara las partes después del punto como números separados: 5, 5 y 5 (igual). Después compara el segundo lugar decimal: nada, 5 y nada. Esto produce una ordenación incorrecta para números decimales.

Para ordenar números decimales, conviértelos a números reales y ordena numéricamente, o usa relleno de cadenas para asegurar el orden lexicográfico correcto.

Combinación de ordenación numérica con otras opciones

La opción numeric funciona junto con otras opciones de ordenación como sensitivity y caseFirst. Puedes controlar cómo el ordenador maneja mayúsculas y acentos mientras mantiene el comportamiento de comparación numérica.

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']

La opción sensitivity: 'base' hace que la comparación no distinga entre mayúsculas y minúsculas. El ordenador trata Item1, item1 y ITEM1 como equivalentes mientras sigue comparando las porciones numéricas correctamente.

Reutilización de comparadores para mejorar el rendimiento

Crear una nueva instancia de Intl.Collator implica cargar datos de configuración regional y procesar opciones. Cuando necesites ordenar múltiples arrays o realizar muchas comparaciones, crea el ordenador una vez y reutilízalo.

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

Este enfoque es más eficiente que crear un nuevo comparador para cada operación de ordenación. La diferencia de rendimiento se vuelve significativa al ordenar muchos arrays o realizar comparaciones frecuentes.