Los programadores de verdad de caso-caso deben saber

En la Conferencia North Bay Python en 2018, di una charla sobre nombres de usuario. La mayor parte de la información de la charla fue recopilada por mí durante 12 años de apoyo al registro de django . Esta experiencia me dio mucho más conocimiento de lo que había planeado adquirir sobre lo complejas que pueden ser las cosas “simples”.



Sin embargo, al comienzo de mi charla, mencioné que esta no será otra exposición de la serie de "conceptos erróneos sobre X, en los que creen los programadores". Puede encontrar muchas de estas revelaciones. Sin embargo, no me gustan estos artículos. Enumeran varias cosas que supuestamente son falsas, pero rara vez explican por qué y qué se debe hacer en su lugar. Sospecho que la gente simplemente leerá estos artículos, se felicitará por este logro y luego buscará nuevas formas interesantes de cometer errores que no se mencionan en estos artículos. Esto se debe a que realmente no entendieron los problemas que causan estos errores.



Por lo tanto, en mi informe, traté de explicar algunos problemas de la mejor manera posible y explicar cómo resolverlos; me gusta mucho más este enfoque . Uno de los temas que solo mencioné de pasada (fue solo una diapositiva y un par de referencias en otras diapositivas) son las complejidades que se pueden asociar con el caso de los personajes. Hay una Respuesta Correcta ™ oficial para el problema que discutí (comparación de identificadores que no distinguen entre mayúsculas y minúsculas) y en la charla di la mejor solución que conozco de usar solo la biblioteca estándar de Python.



Sin embargo, mencioné brevemente las complejidades más profundas del caso de los caracteres Unicode y quiero dedicar algo de tiempo a describir los detalles. Es interesante y comprenderlo puede ayudarlo a tomar decisiones al diseñar y escribir código de procesamiento de texto. Por lo tanto, le ofrezco lo opuesto a los artículos "conceptos erróneos sobre X que los programadores creen" - "verdad que los programadores deberían saber".



Y una cosa más: Unicode está lleno de terminología. En este artículo, utilizaré principalmente las definiciones "mayúscula" y "minúscula", ya que el estándar Unicodeutiliza estos términos. Si le gustan otros términos como letras minúsculas / mayúsculas, está bien. Además, a menudo utilizaré el término "símbolo", que algunos pueden encontrar incorrecto. Sí, en Unicode el concepto de "carácter" no siempre es lo que la gente espera, por lo que a menudo es mejor evitarlo utilizando otros términos. Sin embargo, en este artículo, usaré el término como se usa en Unicode, para describir una entidad abstracta que se puede reclamar. Siempre que sea importante, usaré términos más específicos como punto de código para aclarar.



Hay más de dos registros



Los hablantes nativos de lenguas europeas están acostumbrados al hecho de que sus lenguas usan letras mayúsculas para denotar cosas específicas. Por ejemplo, en los idiomas inglés [y ruso], generalmente comenzamos las oraciones con una letra mayúscula y continuamos con mayor frecuencia con letras minúsculas. Además, los nombres propios comienzan con letras mayúsculas y muchos acrónimos y abreviaturas se escriben en mayúsculas.



Y solemos pensar que solo hay dos registros. Está la letra "A" y está la letra "a". Uno en mayúsculas, uno en minúsculas, ¿no es así?



Sin embargo, hay tres registros en Unicode. Hay mayúsculas, minúsculas y título en mayúsculas [titlecase]. En inglés, los nombres se escriben de esta manera. Por ejemplo, "Avengers: Infinity War". Por lo general, para esto, la primera letra de cada palabra se escribe simplemente en mayúsculas (y dependiendo de las diferentes reglas y estilos, algunas palabras, como los artículos, no se escriben en mayúscula).



El estándar Unicode da un ejemplo de un carácter en mayúsculas: U + 01F2 LETRA D MAYÚSCULA LATINA CON Z MINÚSCULA. Se ve así: Dz.



A veces, estos caracteres se requieren para manejar las consecuencias negativas de una de las primeras soluciones al estándar Unicode: compatibilidad con versiones anteriores de codificaciones de texto existentes. Sería más conveniente para Unicode componer secuencias usando la combinación de caracteres estándar. Sin embargo, en muchos sistemas existentes, ya se ha asignado espacio para secuencias listas para usar. Por ejemplo, en ISO-8859-1 ("latin-1"), el carácter "é" tiene una forma predefinida numerada 0xe9. En Unicode, sería preferible escribir esta letra con una "e" separada y una tilde. Pero para garantizar la compatibilidad total con las codificaciones existentes, como latin-1, Unicode también asigna puntos de código para caracteres prefabricados. Por ejemplo, U + 00E9 LETRA E MINÚSCULA LATINA CON AGUDO.



Aunque la posición del código de este carácter es la misma que su valor de byte latin-1, no debe confiar en esto. Es poco probable que la codificación de caracteres en Unicode conserve estas posiciones. Por ejemplo, en UTF-8, la posición del código U + 00E9 se escribe como la secuencia de bytes 0xc3 0xa9.



Y, por supuesto, hay caracteres en las codificaciones existentes que necesitaban un manejo especial cuando se usaban mayúsculas, por lo que se incluyeron en Unicode "tal cual". Si desea verlos, busque en su base de datos Unicode favorita los caracteres de la categoría Lt ("Letra, mayúscula").



Hay varias formas de definir caso



El estándar Unicode (§4.2) enumera tres definiciones de casos diferentes. Quizás la elección de uno de los tres la haga su lenguaje de programación; de lo contrario, su elección dependerá de su objetivo específico. Estas definiciones son:

  1. El carácter está en mayúsculas si está en la categoría Lu ("Letra, mayúscula") y en minúsculas si está en la categoría Ll ("Letra, minúscula"). La norma reconoce las limitaciones de esta definición: cada símbolo específico debe atribuirse a una sola de las categorías. Debido a esto, muchos caracteres que “deben estar” en mayúsculas o minúsculas no cumplirán con este requisito porque pertenecen a alguna otra categoría.
  2. El carácter está en mayúsculas si hereda la propiedad en mayúsculas y en minúsculas si hereda la propiedad en minúsculas. Es una combinación de una definición con otras propiedades de carácter, que pueden incluir mayúsculas y minúsculas.
  3. Un carácter está en mayúsculas si no cambia después de asignarse a mayúsculas. Un carácter está en minúsculas si no cambia después de haber sido asignado a minúsculas. Esta es una definición bastante general, pero también puede comportarse de forma no intuitiva.




Si está trabajando con un subconjunto limitado de símbolos (específicamente, con letras), entonces una definición puede ser suficiente para usted. Si su repertorio es más amplio, incluye símbolos en forma de letras que no son letras, la segunda definición puede ser adecuada para usted. Es recomendado por el estándar Unicode, §4.2:

Los programadores que manipulan cadenas Unicode deberían trabajar con funciones de cadena como isLowerCase (y su primo funcional toLowerCase) si no trabajan directamente con las propiedades de los caracteres.




La función mencionada aquí se define en §3.13 del estándar Unicode. Formalmente, la definición 3 usa las funciones isLowerCase e isUpperCase de §3.13, definidas en términos de las posiciones fijas en toLowerCase y toUpperCase, respectivamente.



Si su lenguaje de programación tiene funciones para verificar o convertir el caso de cadenas o caracteres individuales, vale la pena investigar cuáles de las definiciones mencionadas se utilizan en la implementación. Si está interesado, los métodos isupper () e islower () en Python usan la segunda definición.



Es imposible entender el caso de un personaje por su apariencia o nombre.



Por la apariencia de muchos personajes, se puede saber en qué caso se encuentran. Por ejemplo, "A" está en mayúsculas. Esto también se desprende del nombre del símbolo: "LETRA A MAYÚSCULA LATINA". Sin embargo, a veces este método no funciona. Tome el punto de código U + 1D34. Se ve así: ᴴ. En Unicode, se le asigna el nombre: MODIFICADOR LETRA MAYÚSCULA H. Entonces está en mayúsculas, ¿verdad?



De hecho, hereda la propiedad Minúsculas, por lo que por definición # 2 está en minúsculas, a pesar de que visualmente se parece a una H mayúscula y el nombre contiene la palabra "CAPITAL".



Algunos personajes no tienen ningún caso



La definición 135 en §3.13 del estándar Unicode establece:

C distingue entre mayúsculas y minúsculas si y solo si C tiene una propiedad en minúsculas o mayúsculas, o General_Category es Titlecase_Letter.




Esto significa que muchos caracteres Unicode, de hecho, la mayoría de ellos, no tienen mayúsculas y minúsculas. Las preguntas sobre su caso no tienen sentido y los cambios en el caso no los afectan. Sin embargo, podemos obtener la respuesta a esta pregunta mediante la definición n. ° 3.



Algunos personajes se comportan como si tuvieran varios registros



La implicación es que si usa la definición # 3 y pregunta si un carácter sin mayúsculas está en mayúsculas o minúsculas, obtendrá la respuesta "sí".



El estándar Unicode da un ejemplo (Tabla 4-1, línea 7) del carácter U + 02BD MODIFICADOR LETRA COMA INVERTIDA (que se ve así: ʽ). No tiene las propiedades heredadas de Minúsculas o Mayúsculas, no pertenece a la categoría Lt, por lo que no tiene caso. Al mismo tiempo, convertir a mayúsculas no lo cambia, y convertir a minúsculas no lo cambia, por lo que en la 3ª definición responde "sí" a ambas preguntas: "¿estás en mayúsculas?" y "¿estás en minúsculas?"



Parece que esto puede causar una confusión innecesaria, pero el punto es que la definición n. ° 3 funciona con cualquier secuencia de caracteres Unicode y le permite simplificar los algoritmos de conversión de mayúsculas y minúsculas (los caracteres sin mayúsculas simplemente se convierten en sí mismos).



El caso es sensible al contexto



Podría pensar que si las tablas de conversión de casos Unicode cubren todos los caracteres, entonces esta conversión consiste simplemente en encontrar el lugar correcto en la tabla. Por ejemplo, la base de datos Unicode dice que U + 0041 LETRA LATINA MAYÚSCULA A es minúscula U + 0061 LETRA LATINA MINÚSCULA A. Simple, ¿no?



Un ejemplo en el que este enfoque no funciona es el griego. El carácter Σ, es decir, U + 03A3 LETRA MAYÚSCULA GRIEGA SIGMA, se asigna a dos caracteres diferentes cuando se convierte a minúscula, dependiendo de dónde esté en la palabra. Si está al final de una palabra, entonces en minúsculas será ς (U + 03C2 GRIEGO MINÚSCULA FINAL SIGMA). En otros lugares será σ (U + 03C3 GRIEGO MINÚSCULA SIGMA).



Esto significa que el registro no es uno a uno ni transitivo. Otro ejemplo es ß (U + 00DF LETRA MINÚSCULA LATINA SHARP S o escet ). Será "SS" en mayúsculas, aunque ahora hay otra forma en mayúscula (ẞ, U + 1E9E LETRA MAYÚSCULA LATINA S SHARP). Y convertir "SS" a minúsculas da como resultado "ss", entonces (usando terminología Unicode para conversión de mayúsculas y minúsculas): toLowerCase (toUpperCase (ß))! = Ss.



El caso depende de la configuración regional



Los diferentes idiomas tienen diferentes reglas de conversión de casos. El ejemplo más popular: i (U + 0069 LETRA MINÚSCULA LATINA I) e I (U + 0049 LETRA MAYÚSCULA LATINA I) se convierten entre sí en la mayoría de las configuraciones regionales, la mayoría, pero no todas. En los locales az y tr (idiomas turcos), la i mayúscula será İ (U + 0130 LETRA I MAYÚSCULA LATINA CON PUNTO ARRIBA), y la I minúscula será ı (U + 0131 LETRA MINÚSCULA LATINA SIN PUNTO I). A veces, hacerlo bien significa realmente la diferencia entre la vida y la muerte.



Unicode en sí mismo no maneja todas las posibles reglas de conversión de mayúsculas y minúsculas para todas las configuraciones regionales. La base de datos Unicode solo tiene reglas generales para convertir todos los caracteres, no específicas de la configuración regional. También hay reglas especiales para algunos idiomas y formas compuestas: lituano, idiomas turcos, algunas características del griego. Todo lo demás no está ahí. §3.13 del estándar menciona esto y recomienda la introducción de reglas de traducción específicas de la configuración regional si es necesario.



Un ejemplo sería un letrero de habla inglesa: este es el caso del título de ciertos nombres. "O'brian" debe convertirse en "O'Brian" (no "O'brian"). Sin embargo, al hacerlo, "es" debe convertirse en "Es" y no en "Es". Otro ejemplo que no se maneja en Unicode es la combinación de letras holandesas "ij", que, cuando se convierte a mayúsculas y minúsculas, debe convertirse a mayúsculas si aparece al principio de una palabra. Por tanto, la bahía más grande de los Países Bajos en el registro de títulos será "IJsselmeer" y no "Ijsselmeer". Unicode tiene los caracteres IJ U + 0132 LATIN CAPITAL LIGATURE IJ y ij U + 0133 LATIN SMALL LIGATURE IJ si los necesita. De forma predeterminada, la conversión de mayúsculas y minúsculas los convierte entre sí (aunque los formularios de normalización Unicode que utilizan la equivalencia de compatibilidad los dividirán en dos caracteres separados).





Volviendo al material presentado en el informe. La complejidad de la administración de casos Unicode significa que las comparaciones que no distinguen entre mayúsculas y minúsculas no se pueden realizar utilizando las funciones estándar de conversión de minúsculas o mayúsculas que se encuentran en muchos lenguajes de programación. Para tales comparaciones, Unicode tiene el concepto de plegado de mayúsculas y minúsculas, y §3.13 del estándar define las funciones toCaseFold e isCaseFolded.



Puede pensar que la conversión a una caja doblada es similar a la conversión a una minúscula, pero no lo es. El estándar Unicode advierte que una cadena en mayúsculas y minúsculas no tiene que estar en minúsculas. Como ejemplo, se da el idioma Cherokee: allí, en una cadena que está en mayúsculas, también aparecerán caracteres en mayúsculas.



En una de las diapositivas de mi charla, el Informe técnico de Unicode n. ° 36 se implementa de la manera más completa posible en Python. Se realiza la normalización NFKC y luego se llama al método casefold () (disponible solo en Python 3+) para la cadena resultante. Y aun así, algunos casos extremos fallan, y esto no es realmente lo que se recomienda para la comparación de ID. Primero, las malas noticias: Python no expone suficientes propiedades Unicode para filtrar caracteres que no están en XID_Start o XID_Continue, o caracteres que tienen una propiedad Default_Ignorable_Code_Point. Hasta donde yo sé, no es compatible con el mapeo NFKC_Casefold. Tampoco existe una manera fácil de utilizar el NFKC UAX # 31§5.1 modificado.



La buena noticia es que la mayoría de estos casos extremos no implican ningún riesgo de seguridad real que plantean los símbolos en cuestión. Y el plegado de casos, en principio, no se define como una operación de preservación de la normalización (de ahí el mapeo NFKC_Casefold, que se vuelve a normalizar a NFC después del plegado de casos). Generalmente, al comparar, no le importa si ambas cadenas se normalizan después del preprocesamiento. Le importa si el preprocesamiento no es inconsistente, y si garantiza que solo las líneas que "deberían" diferir posteriormente serán diferentes posteriormente. Si le preocupa esto, puede volver a normalizar manualmente después de agregar el registro.



Suficiente por ahora



Este artículo, al igual que el informe anterior, no es exhaustivo y difícilmente es posible encajar todo este material en un solo artículo. Espero que haya sido una descripción general útil de las complejidades de este tema y proporcione suficientes puntos de partida para buscar más información. Por lo tanto, en principio, puede detenerse aquí.



¿No sería ingenuo esperar que otras personas dejen de escribir exposiciones de las series de "conceptos erróneos acerca de X creen los programadores" y comiencen a escribir artículos como "la verdad que los programadores deberían saber"?



All Articles