Cadenas de texto y Unicode: comparar, limpiar y componer nombres
Dos plantas se llaman «Planta Ático». En pantalla parecen iguales, pero una comparación devuelve falso. La respuesta habitual consiste en quitar tildes, espacios y cualquier cosa que moleste hasta que el programa deje de protestar. A veces funciona. Otras veces consigue que nombres distintos parezcan el mismo, una contribución bastante creativa al problema que pretendíamos resolver.
Trabajar con texto exige decidir qué diferencias cuentan en cada campo. El nombre de una planta, su identificador y la etiqueta que mostraremos en un informe no tienen por qué compartir reglas. Vamos a distinguirlos con ejemplos de Python y C#, conservar los originales y construir comparaciones explícitas. Unicode no está saboteando tu modelo. Simplemente contiene más información de la que cabe en la idea de que una letra siempre ocupa una posición y cualquier limpieza mejora los datos.
Dos textos que se ven iguales pueden tener representaciones distintas
Una cadena contiene una secuencia de unidades que el lenguaje representa según sus reglas. No conviene confundir esa secuencia con los caracteres que creemos reconocer al mirar la pantalla. Una letra acentuada puede estar representada por un punto de código propio o por una letra seguida de una marca combinante. Ambas formas pueden dibujarse de manera equivalente sin tener exactamente la misma secuencia.
El primer programa construye esas dos versiones del nombre. Los escapes \u00c1 y \u0301 indican puntos de código Unicode: el primero es la Á precompuesta y el segundo es la marca de acento que añadimos a una A. Utilizamos escapes para que la diferencia siga siendo reproducible aunque el editor de texto modifique lo que pegamos.
Con Python 3.12 obtendrás False, 12 13 y True. La normalización NFC permite comparar una representación canónica en este caso. No ha eliminado la tilde ni convertido el nombre en otro nombre sin acento. Ha resuelto una diferencia de representación que nuestra política considera equivalente. Esa distinción importa: normalizar Unicode no significa quitar todo lo que no parezca una letra inglesa.
La documentación de unicodedata describe las formas de normalización. NFC y las formas de compatibilidad, como NFKC, no deben tratarse como botones intercambiables. Las segundas pueden unificar otras representaciones que quizá necesites distinguir. Para estos nombres usaremos NFC y probaremos el resultado; no hemos establecido una regla universal para todos los campos de un proyecto.
Tampoco debes interpretar len como una medida general del número de símbolos visibles. Python cuenta puntos de código de una cadena; en .NET, String.Length cuenta unidades de código UTF-16. Una secuencia que el lector percibe como un carácter puede requerir varias unidades. Aquí no vamos a cortar nombres por longitud, pero esta diferencia explica por qué un límite de interfaz necesita una definición más precisa que «contar letras».
La clave de comparación puede ser distinta del nombre que conservamos
Para un catálogo didáctico de nombres de planta acordaremos tres reglas: normalización NFC, reducción de secuencias de espacios en blanco a un espacio y comparación sin distinguir mayúsculas. Conservaremos las tildes. También mantendremos el nombre original y el identificador, porque una clave calculada para comparar no debe sustituir automáticamente lo que hemos recibido.
La función siguiente recibe texto. El código \u00a0 introduce un espacio de no separación, habitual al copiar contenido de ciertas fuentes. split() sin separador reconoce espacios en blanco y produce las palabras; join las reúne con un espacio ordinario. casefold() prepara una comparación sin distinción de mayúsculas conforme a sus reglas Unicode.
Verás planta ático, True, False y True. La comparación admite las diferencias acordadas y conserva la diferencia entre ático y atico. El registro original sigue intacto: la función devuelve otra cadena. Las cadenas de Python son inmutables; sus operaciones no reescriben el objeto original como ocurriría al modificar un diccionario compartido.
Reducir espacios también es una decisión de contenido. En estos nombres consideramos equivalentes un tabulador, un salto de línea o varios espacios entre palabras. En un texto de varias líneas o un dato cuyo formato sea significativo podríamos necesitar conservarlos. Tampoco estamos eliminando cualquier carácter invisible: por ejemplo, un espacio de ancho cero no desaparece por el mero hecho de llamar a split. Si aparece, habrá que identificarlo y decidir qué significa antes de añadir otra sustitución a la cadena de limpieza.
No aplicaría esta función al identificador P-01. Los identificadores los define el sistema que los emite, y su sensibilidad a mayúsculas o sus caracteres permitidos forman parte de ese contrato. El hecho de que dos nombres de planta nos parezcan equivalentes no autoriza a alterar claves de proyecto, URN o códigos de una API. Conviene que una utilidad se llame clave_nombre y tenga ese alcance, en lugar de presentarse como una lavadora universal de cadenas.
C# permite declarar cómo queremos comparar las cadenas
En C# también podemos normalizar y separar la preparación del texto de su comparación. Este ejemplo utiliza NFC, reduce espacios en blanco y compara con StringComparison.OrdinalIgnoreCase. Para los nombres españoles del ejemplo consigue la equivalencia buscada. No presupone que este modo de comparación y casefold sean idénticos para todos los caracteres de Unicode.
En una consola de .NET 8 produce PLANTA ÁTICO, True, False y True. La primera línea conserva las mayúsculas; ignorarlas es responsabilidad de la comparación, no de PrepararNombre. La referencia nula al separador indica a esa sobrecarga de Split que utilice espacios en blanco. No es un nombre de entrada nulo: la función sigue esperando una cadena válida.
Microsoft documenta tanto String.Normalize como los modos de StringComparison. Una comparación ordinal tiene un propósito distinto de ordenar nombres según reglas lingüísticas para una persona. Si queremos una lista ordenada como espera un lector de una cultura concreta, debemos elegir esa operación expresamente. Ordenar, buscar y comprobar identidad pueden requerir políticas diferentes.
La diferencia entre lenguajes merece una prueba si el catálogo se amplía a otros idiomas. Por ejemplo, casefold puede expandir ciertos caracteres al comparar; no debemos trasladar esa expectativa a cualquier comparación de .NET sin comprobarla. Compartir ejemplos equivalentes ayuda a aprender, pero un contrato que cruce sistemas necesita un conjunto de casos comunes y una política acordada. Mi considerable talento para copiar un método de un lenguaje a otro no hace que ambos interpreten de repente todas las escrituras del planeta igual.
Un patrón de nombre comprueba forma, no existencia
Una expresión regular resulta útil si necesitamos comprobar un formato concreto. Supongamos que un catálogo exige códigos de planta con una P mayúscula, un guion y exactamente dos dígitos ASCII. En Python podemos expresar ese patrón y exigir que coincida con la cadena completa. El programa es independiente de los anteriores y no aplica ninguna normalización previa a los códigos.
Solo P-01 obtiene True. Usamos [0-9] porque el contrato requiere esos diez caracteres; una categoría de dígitos más amplia podría admitir otras escrituras. fullmatch exige la coincidencia del texto entero, de modo que un espacio inicial tampoco pasa inadvertido. Si el requisito admite minúsculas o espacios exteriores, habrá que cambiar el contrato o preparar una propuesta de corrección, no fingir que ya lo decía.
Superar esta prueba no demuestra que la planta exista. P-99 cumple el formato, pero podría no pertenecer al catálogo del proyecto. Son comprobaciones diferentes: primero forma y después pertenencia, si ambas hacen falta. Una expresión regular no conoce los niveles de Revit por llevar dos llaves y un guion; a pesar de lo intimidante que resulte cuando alguien la pega en una reunión.
Tampoco usaría un patrón cada vez más complejo para deducir el significado de nombres libres. Si necesitas distinguir edificio, nivel y disciplina, quizá deban llegar como campos separados en vez de extraerse de un nombre que cada equipo interpreta a su manera. El patrón ayuda cuando existe una convención definida. No fabrica el acuerdo que falta entre quienes rellenan los datos.
Antes de unificar nombres, revisa las coincidencias que has creado
Una clave de comparación puede hacer que dos registros acaben con el mismo resultado. Eso no significa que debamos fusionarlos. Dos plantas de edificios distintos pueden tener el mismo nombre legítimamente; una normalización también puede revelar variantes que requieren revisión. Conserva identificador, contexto y original para mostrar qué se ha considerado equivalente y decidir qué hacer con ello.
Prueba al menos un nombre precompuesto y otro con marca combinante, espacios exteriores, espacios de no separación, cambios de mayúsculas y una diferencia de tilde que deba conservarse. Añade el texto vacío y define cómo tratarlo: nuestra función devuelve una clave vacía para una cadena de espacios, pero eso no la convierte en un nombre válido. Un dato ausente, un tipo incorrecto y un nombre que no cumple la regla requieren decisiones adicionales.
Al componer una etiqueta para un informe, utiliza los campos elegidos y un formato visible, sin reutilizar ciegamente la clave interna como texto de presentación. Si el destino es HTML, CSV o una consulta a otro sistema, tendrá además sus propias reglas de escape o parametrización. Normalizar Unicode no sustituye esas operaciones. Cada capa merece resolver el problema que tiene, sin cargar a una función de nombres con tareas que pertenecen a la salida.
Si un catálogo te ha obligado a decidir si dos nombres eran realmente el mismo, cuenta qué diferencias debían conservarse. Es un ejemplo mucho más interesante que una receta para quitar tildes de todo. Y si compartes esta explicación con quien mantiene el catálogo, mejor: quizá la próxima limpieza empiece acordando qué significa cada campo, antes de conseguir que todos se parezcan con una eficiencia admirable y un criterio bastante discutible.
Respuestas