Ordenar texto no es una sola operación: cuatro órdenes que se llaman todos alfabéticos
Publicado el 29/5/2025 · 12 min de lectura · Herramientas de texto e idioma
Daniel Okonkwo — Desarrollador front-end y redactor de Tecnología en OneKitly
Rendimiento web · Formatos de archivo
Verificado con 5 fuentes
«Alfabético» designa al menos cuatro órdenes distintos, y discrepan con datos corrientes. El .sort() por defecto de JavaScript compara unidades de código UTF-16: ["ñu","nube","niño","zorro"] sale niño, nube, zorro, ñu — la ñ expulsada detrás de la z, porque su código es mayor — y ["capítulo10","capítulo9","capítulo2"] sale capítulo10, capítulo2, capítulo9, porque «1» es un carácter menor que «9». Pide un Intl.Collator con numeric: true y esas mismas cadenas salen capítulo2, capítulo9, capítulo10. La colación por locale es un tercer orden: bajo es la lista sale niño, nube, ñu, zorro, con la ñ como letra propia situada después de la n y antes de la o. Un cuarto son las variantes de colación: de-u-co-phonebk, el orden de guía telefónica alemana, trata ö como oe y adelanta Öl a Ohr, mientras que el de estándar hace lo contrario. Array.prototype.sort está obligado a ser estable desde ES2019: con claves iguales se conserva el orden de entrada. Ordenar texto visible para el usuario sin nombrar un locale es un error, no un atajo.
Orden de unidades de código, orden natural y colación por locale ejecutados sobre la misma lista en Node, con las salidas impresas. Por qué Zebra va antes que apple, por qué item10 va antes que item9 y por qué ä está junto a a en alemán pero después de z en sueco.
Cuatro cosas llamadas alfabético
Un botón de ordenar ofrece una opción y da a entender que hay una respuesta. Hay al menos cuatro, y no son refinamientos unas de otras: son órdenes distintos que devuelven listas distintas a partir de la misma entrada. El orden de unidades de código compara los valores numéricos de las unidades UTF-16 que forman la cadena. El orden natural lee las series de dígitos como números. La colación por locale aplica las reglas propias de una lengua sobre qué letras cuentan como la misma letra. Las variantes de colación dividen luego una misma lengua en varios órdenes defendibles, porque los diccionarios alemanes y las guías telefónicas alemanas nunca se han puesto de acuerdo.
Todo lo que sigue se ejecutó, no se recordó. Cada lista está impresa exactamente como la devolvió Node 26.3, y los scripts son lo bastante cortos para reescribirlos: un array, un sort, un console.log.
Orden de unidades de código: lo que hace de verdad .sort()
Llamado sin comparador, Array.prototype.sort convierte cada elemento en cadena y compara esas cadenas por unidad de código UTF-16. Es una regla documentada, no un accidente, y produce dos síntomas visibles. Las mayúsculas ocupan el rango 0x41–0x5A y las minúsculas 0x61–0x7A, así que toda mayúscula va antes que toda minúscula. Y los dígitos se comparan como caracteres: ["capítulo10","capítulo9","capítulo2"] devuelve capítulo10, capítulo2, capítulo9, porque «1» es 0x31 y «9» es 0x39, y la comparación se detiene en la primera diferencia.
El tercer síntoma es el que llega al usuario. Toda letra con signo diacrítico vive por encima de 0x7A, así que el orden de unidades de código empuja el alfabeto acentuado entero detrás de la z. ["ñu","nube","niño","zorro"] devuelve niño, nube, zorro, ñu. ["Öl","Ohr","Zebra","Ähre"] devuelve Ohr, Zebra, Ähre, Öl. ["ação","acordo","água","avô"] devuelve acordo, avô, ação, água. ["étage","effet","zèbre","Île"] devuelve effet, zèbre, Île, étage. Cuatro lenguas, cuatro respuestas falsas, una línea de código.
El orden de unidades de código no es inútil. Es total, transparente, rápido — 20 000 palabras ordenadas en 6 ms en el banco de pruebas de abajo — e idéntico en todos los motores y todas las locales, lo que lo convierte en la opción correcta para todo lo que lee una máquina: claves de índice, cubos de deduplicación, identificadores de caché, serializaciones canónicas. Solo es incorrecto cuando la salida es para una persona.
Orden natural: leer los dígitos como números
El orden natural — el de un gestor de archivos — trata una serie de dígitos dentro de una cadena como un solo número en vez de como una secuencia de caracteres. En JavaScript es una opción: new Intl.Collator("es", { numeric: true }). Sobre ["capítulo10","capítulo9","capítulo2"] devuelve capítulo2, capítulo9, capítulo10, y sobre una lista más larga ["item2","item9","item10","item100","item20"] devuelve item2, item9, item10, item20, item100, donde el .sort() por defecto devuelve item10, item100, item2, item20, item9.
Conviene conocer dos límites antes de activarla en todas partes. La colación numérica es una comodidad de presentación, no aritmética: compara series de dígitos, así que tiene opiniones sobre «v1.10» frente a «v1.9» que un analizador de versiones semánticas no compartiría, y no dice nada útil sobre signos, separadores decimales o separadores de millares. Y cambia la respuesta para claves que simplemente contienen dígitos, como códigos de producto donde 0090 y 90 son artículos distintos. Actívala para listas que una persona recorre, déjala apagada para los identificadores.
Colación por locale: el alemán contra el sueco
La colación por locale es el caso clásico, y el alemán contra el sueco es la pareja clásica. Toma ["Öl","Ohr","Zebra","Ähre"]. Bajo new Intl.Collator("de") sale Ähre, Ohr, Öl, Zebra: ä es una variante de a, ö una variante de o, y el diacrítico solo desempata. Bajo new Intl.Collator("sv") esas mismas cuatro cadenas salen Ohr, Zebra, Ähre, Öl: en sueco, å, ä y ö son las tres últimas letras del alfabeto, después de la z. Ninguno de los dos es un error. Son dos lenguas con dos alfabetos, y la lista tiene que elegir uno.
Ahora la parte incómoda: para esta lista, el .sort() por defecto devuelve Ohr, Zebra, Ähre, Öl — carácter por carácter la respuesta sueca. Un programa que se saltó el locale no produjo «ningún orden en particular». Produjo un orden extranjero concreto, en silencio, y lo seguirá produciendo para cada lista alemana, española, portuguesa o francesa que toque.
Los demás locales de este artículo se comportan igual. El español hace de la ñ una letra propia después de la n: ["ñu","nube","niño","zorro"] da niño, nube, ñu, zorro bajo es, mientras que el orden de unidades de código destierra ñu detrás de zorro. El portugués trata los acentos como desempates: ["ação","acordo","água","avô"] sale en orden de diccionario bajo pt y revuelto bajo .sort(). El italiano da ancora, Àncora, elite, zucchero bajo it, donde el acento es una diferencia secundaria y la mayúscula una terciaria. Y el francés tiene un orden auténticamente regional: sobre ["cote","coté","côte","côté"], fr devuelve cote, coté, côte, côté, mientras que fr-CA devuelve cote, côte, coté, côté, porque el francés de Canadá compara los acentos desde el final de la palabra.
Una lengua, varios órdenes: las variantes de colación
Incluso dentro de una lengua hay más de una respuesta correcta, y el CLDR de Unicode las entrega como variantes con nombre, elegidas mediante la cadena de locale. El alemán tiene dos de uso diario. La colación de diccionario, de, ordena ["Göbel","Goethe","Godel","Gözde","Gott"] como Göbel, Godel, Goethe, Gott, Gözde: ö es una variante de o. La colación de guía telefónica, de-u-co-phonebk, ordena esas mismas cinco como Godel, Göbel, Goethe, Gözde, Gott, porque ö se expande a oe — lo que coloca Göbel entre Godel y Goethe, justo donde buscaría alguien que busca «Goebel».
Dos mandos acompañan a la variante y cambian la respuesta tanto como ella. sensitivity decide qué diferencias cuentan: sobre «cote» frente a «côte» y «cote» frente a «Cote», la sensibilidad «base» declara ambas parejas iguales, «accent» separa el acento pero no la caja, «case» separa la caja pero no el acento, y «variant» — el valor por defecto — separa ambas. caseFirst decide quién gana un empate: sobre ["apple","Apple","APPLE"], el defecto devuelve apple, Apple, APPLE, y caseFirst: "upper" devuelve APPLE, Apple, apple. Ninguno de los dos mandos es cosmético. sensitivity es además lo que convierte a un colador en herramienta de búsqueda: con «base», compare devuelve 0 para cadenas que un lector llamaría la misma palabra.
Estabilidad: qué pasa con los empates
Una ordenación es estable cuando los elementos que comparan iguales conservan el orden relativo que tenían en la entrada. Esto importa en cuanto ordenas por una clave parcial — una inicial, una categoría, una fecha sin hora — porque los empates no son casos límite raros: son la mayor parte de la lista. Al ordenar diez nombres solo por su inicial, los elementos con inicial a salieron en las posiciones de entrada 1, 2, 4, 6, 8 y los de inicial b en las posiciones 0, 3, 5, 7, 9: cada grupo conservado, en orden.
La especificación exige que Array.prototype.sort sea estable desde ES2019. Antes, los motores podían usar un algoritmo inestable por encima de cierto tamaño de array, y varios lo hacían: por eso los consejos antiguos recomiendan un comparador compuesto o llevar el índice a cuestas. La exigencia vale sea cual sea el tamaño: un array de mil elementos ordenado por una clave con solo tres valores distintos volvió con cada grupo en orden de entrada. Puedes por tanto construir una ordenación multiclave ordenando varias veces, de la clave menos significativa a la más significativa — primero el nombre, luego el apellido — y contar con que las pasadas anteriores sobrevivan.
Lo que cuesta, y la regla que se deduce
Ordenar teniendo en cuenta el locale cuesta más que ordenar por unidades de código, pero no lo que la gente teme, y el error caro es otro. Ordenando 20 000 palabras en Node 26.3: el .sort() por defecto tardó 6 ms, a.localeCompare(b, "de") 12 ms, un colador construido una vez y reutilizado 28 ms — y construir un new Intl.Collator dentro del comparador tardó 1 771 ms, sesenta veces más lento que reutilizarlo. El coste no es la colación. El coste es construir el colador cientos de miles de veces.
Queda una regla lo bastante corta para aplicarla. Si una máquina lee la salida, ordena por unidad de código y déjalo escrito. Si la lee una persona, nombra un locale — la lengua en que está escrito el documento, no la del navegador que lo muestra —, construye un solo Intl.Collator, decide numeric y sensitivity a propósito, y reutilízalo. Lo único nunca defendible es llamar a .sort() sobre texto visible para el usuario y llamar alfabético al resultado.
| Orden | Cómo pedirlo | Lista alemana | Lista numerada |
|---|---|---|---|
| Orden de unidades de código | arr.sort() | Ohr, Zebra, Ähre, Öl | item10, item2, item9 |
| Colación alemana | new Intl.Collator("de") | Ähre, Ohr, Öl, Zebra | item10, item2, item9 |
| Variante de guía telefónica alemana | new Intl.Collator("de-u-co-phonebk") | Ähre, Öl, Ohr, Zebra | item10, item2, item9 |
| Colación sueca | new Intl.Collator("sv") | Ohr, Zebra, Ähre, Öl | item10, item2, item9 |
| Colación con conciencia numérica | new Intl.Collator("de", { numeric: true }) | Ähre, Ohr, Öl, Zebra | item2, item9, item10 |
Preguntas frecuentes
- ¿Por qué «Zebra» se ordena antes que «apple»?
- Porque .sort() sin comparador compara unidades de código UTF-16, y toda mayúscula ASCII (0x41–0x5A) tiene un valor menor que toda minúscula ASCII (0x61–0x7A). No ordena letras, ordena números que representan letras. Cualquier colador de locale lo arregla: new Intl.Collator("en").compare da apple, Banana, zebra, Zebra en ese orden, con la caja como último desempate y no como primer criterio.
- ¿Es estable la ordenación de JavaScript?
- Sí, y está obligada a serlo. ES2019 convirtió la estabilidad en un requisito de la especificación para Array.prototype.sort, y Array.prototype.toSorted sigue la misma regla. Verificado aquí sobre un array de mil elementos ordenado por una clave con tres valores distintos: cada grupo volvió en orden de entrada. Esa garantía es lo que permite implementar una ordenación multicolumna como una secuencia de ordenaciones de una columna, empezando por la menos significativa.
- ¿Qué locale uso si no conozco el del lector?
- Usa la lengua del contenido, no la del dispositivo del lector. Una lista de nombres de productos alemanes corresponde a la colación alemana la mire quien la mire, exactamente como un catálogo alemán impreso. Recurrir al valor por defecto del motor es la peor opción, porque es invisible: new Intl.Collator() sin argumento se resolvió como en-US en la máquina en que se escribió este artículo, y se resolvería de otro modo en la siguiente, de forma que la misma lista se ordenaría distinto en dos servidores sin cambiar una línea de código.
- ¿Por qué mi gestor de archivos pone item2 antes que item10 y mi código no?
- El gestor de archivos usa orden natural: reconoce la serie de dígitos como un número. Tu código compara caracteres, así que se detiene en «1» frente a «9» y nunca lee el resto. Añade { numeric: true } a un Intl.Collator, o pasa la misma opción a localeCompare, y los dos coinciden. No intentes emularlo rellenando con ceros las cadenas mostradas: eso arregla la ordenación y estropea las etiquetas.
- ¿Es localeCompare demasiado lento para una lista larga?
- Por sí solo no. Sobre 20 000 palabras en Node 26.3, a.localeCompare(b, "de") tardó 12 ms y un Intl.Collator reutilizado 28 ms, frente a 6 ms del .sort() por defecto: una diferencia que nadie notará. Lo verdaderamente lento es construir un colador dentro del comparador: así, la misma ordenación tardó 1 771 ms, porque se fabrica un colador nuevo para cada una de los cientos de miles de comparaciones. Constrúyelo una vez, fuera de la ordenación, y pasa su .compare.
- ¿Cómo ordeno nombres alemanes como una guía telefónica?
- Pide la variante de colación por su nombre: new Intl.Collator("de-u-co-phonebk"). La parte -u-co- de un identificador de locale selecciona una colación, y phonebk es la adaptación de guía telefónica alemana, en la que ö se comporta como oe, ä como ae y ü como ue. Sobre ["Göbel","Goethe","Godel","Gözde","Gott"], de a secas da Göbel, Godel, Goethe, Gott, Gözde y de-u-co-phonebk da Godel, Göbel, Goethe, Gözde, Gott. Otras lenguas tienen sus variantes: una lista china puede ordenarse por pinyin o por número de trazos del mismo modo.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- Unicode Consortium — Unicode Technical Standard #10: Unicode Collation Algorithm
- Unicode Consortium — CLDR — Common Locale Data Repository, collation charts and locale tailorings
- Ecma International — ECMAScript Language Specification — Array.prototype.sort (stability) and the Intl.Collator constructor
- Ecma International — ECMAScript Internationalization API Specification (ECMA-402) — Intl.Collator options: usage, sensitivity, numeric, caseFirst
- MDN Web Docs — Intl.Collator and String.prototype.localeCompare
¿Has detectado un error en este artículo?