Au début de la conférence, cependant, j'ai mentionné que ce ne serait pas une autre exposition de la série des "idées fausses sur X auxquelles les programmeurs croient". Vous pouvez trouver un certain nombre de ces révélations. Cependant, je n'aime pas ces articles. Ils énumèrent diverses choses qui sont censées être fausses, mais expliquent rarement pourquoi et ce qui devrait être fait à la place. Je soupçonne que les gens liront simplement des articles comme ceux-ci, se féliciteront de cette réussite, puis trouveront de nouvelles façons intéressantes de faire des erreurs non mentionnées dans ces articles. C'est parce qu'ils n'ont pas vraiment compris les problèmes à l'origine de ces erreurs.
Par conséquent, dans mon rapport, j'ai essayé d'expliquer au mieux certains problèmes et d'expliquer comment les résoudre - j'aime beaucoup plus cette approche . L'un des sujets que j'ai abordé seulement en passant (c'était juste une diapositive et quelques mentions sur d'autres diapositives) est la complexité qui peut être associée au cas des personnages. Il existe une Correct Answer ™ officielle pour le problème dont j'ai parlé - la comparaison des identifiants insensible à la casse, et dans mon exposé, j'ai donné la meilleure solution que je connaisse en n'utilisant que la bibliothèque standard Python.
Cependant, j'ai brièvement mentionné les complexités plus profondes de la casse des caractères Unicode, et je souhaite consacrer du temps à la description des détails. C'est intéressant et le comprendre peut vous aider à prendre des décisions lors de la conception et de l'écriture de code de traitement de texte. Je vous propose donc le contraire des articles "idées fausses sur X auxquelles les programmeurs croient" - "vérités que les programmeurs devraient connaître".
Une dernière chose: Unicode regorge de terminologie. Dans cet article, j'utiliserai principalement les définitions "majuscules" et "minuscules", puisque le standard Unicodeutilise ces termes. Si vous aimez d'autres termes comme les lettres minuscules / majuscules, c'est très bien. Aussi, j'utiliserai souvent le terme «symbole», que certains trouveront peut-être incorrect. Oui, en Unicode, le concept de «caractère» n'est pas toujours ce à quoi les gens s'attendent, il est donc souvent préférable de l'éviter en utilisant d'autres termes. Cependant, dans cet article, j'utiliserai le terme tel qu'il est utilisé en Unicode - pour décrire une entité abstraite qui peut être revendiquée. Chaque fois que c'est important, j'utiliserai des termes plus spécifiques comme point de code pour clarifier.
Il y a plus de deux registres
Les locuteurs natifs des langues européennes sont habitués au fait que leurs langues utilisent des lettres majuscules pour désigner des choses spécifiques. Par exemple, en anglais [et en russe], nous commençons généralement les phrases par une lettre majuscule et continuons le plus souvent avec des lettres minuscules. De plus, les noms propres commencent par des lettres majuscules et de nombreux acronymes et abréviations sont écrits en majuscules.
Et nous pensons généralement qu'il n'y a que deux registres. Il y a la lettre «A» et il y a la lettre «a». Un en majuscules, un en minuscules - n'est-ce pas?
Cependant, il existe trois registres en Unicode. Il y a une majuscule, une minuscule et une casse de titre [titlecase]. En anglais, les noms sont écrits de cette façon. Par exemple, "Avengers: Infinity War". Habituellement, la première lettre de chaque mot est simplement en majuscule pour cela (et selon les règles et les styles différents, certains mots, tels que les articles, ne sont pas en majuscules).
Le standard Unicode donne un exemple de caractère en majuscule: U + 01F2 LETTRE MAJUSCULE LATINE D AVEC MINUSCULE Z. Cela ressemble à ceci: Dz.
De tels caractères sont parfois nécessaires pour gérer les conséquences négatives de l'une des premières solutions au développement du standard Unicode: la rétrocompatibilité avec les encodages de texte existants. Il serait plus pratique pour Unicode de composer des séquences en utilisant la combinaison de caractères standard. Cependant, dans de nombreux systèmes existants, de l'espace a déjà été alloué pour des séquences toutes faites. Par exemple, dans ISO-8859-1 ("latin-1"), le caractère "é" a une forme prête à l'emploi numérotée 0xe9. En Unicode, il serait préférable d'écrire cette lettre avec un «e» séparé et une marque d'accent. Mais pour assurer une compatibilité descendante totale avec les encodages existants tels que latin-1, Unicode attribue également des points de code pour les caractères prêts à l'emploi. Par exemple, U + 00E9 LETTRE MINUSCULE LATINE E ACCENT AIGU.
Bien que la position du code de ce caractère soit la même que sa valeur d'octet latin-1, vous ne devez pas vous y fier. Il est peu probable que l'encodage de caractères en Unicode conserve ces positions. Par exemple, en UTF-8, la position de code U + 00E9 est écrite sous la forme de la séquence d'octets 0xc3 0xa9.
Et, bien sûr, il y a des caractères dans les encodages existants qui nécessitaient un traitement spécial lors de l'utilisation des majuscules, c'est pourquoi ils ont été inclus dans Unicode «tels quels». Si vous souhaitez les consulter, recherchez dans votre base de données Unicode préférée les caractères de la catégorie Lt ("Letter, titlecase").
Il existe plusieurs façons de définir le cas
La norme Unicode (§4.2) répertorie trois définitions de cas différentes. Peut-être que le choix de l'un des trois est fait pour vous par votre langage de programmation; sinon, votre choix dépendra de votre objectif spécifique. Ces définitions sont:
- Le caractère est en majuscule s'il est dans la catégorie Lu («Lettre, majuscule»), et en minuscule s'il est dans la catégorie Ll («Lettre, minuscule»). La norme reconnaît les limites de cette définition: chaque symbole spécifique doit être attribué à une seule des catégories. Pour cette raison, de nombreux caractères qui «doivent être» en majuscules ou minuscules ne répondront pas à cette exigence car ils appartiennent à une autre catégorie.
- Le caractère est en majuscules s'il hérite de la propriété Uppercase et en minuscules s'il hérite de la propriété Lowercase. C'est une combinaison de la définition une avec d'autres propriétés de caractère, parmi lesquelles le cas peut être.
- Un caractère est en majuscules s'il ne change pas après avoir été mappé en majuscules. Un caractère est en minuscules s'il ne change pas après avoir été mappé en minuscules. C'est une définition assez générale, mais elle peut aussi se comporter de manière non intuitive.
Si vous travaillez avec un sous-ensemble limité de symboles (en particulier, avec des lettres), une définition peut vous suffire. Si votre répertoire est plus large - il comprend des symboles en forme de lettre qui ne sont pas des lettres, la deuxième définition peut vous convenir. Il est recommandé par le standard Unicode, §4.2:
Les programmeurs manipulant des chaînes Unicode doivent travailler avec des fonctions de chaîne telles que isLowerCase (et son cousin fonctionnel toLowerCase) si elles ne fonctionnent pas directement avec les propriétés de caractère.
La fonction mentionnée ici est définie au §3.13 du standard Unicode. Formellement, la définition 3 utilise les fonctions isLowerCase et isUpperCase du §3.13, définies en termes de positions fixes dans toLowerCase et toUpperCase, respectivement.
Si votre langage de programmation a des fonctions pour vérifier ou convertir la casse de chaînes ou de caractères individuels, il vaut la peine de rechercher les définitions ci-dessus utilisées dans l'implémentation. Si vous êtes intéressé, les méthodes isupper () et islower () en Python utilisent la 2ème définition.
Il est impossible de comprendre le cas d'un personnage par son apparence ou son nom
Par l'apparition de nombreux personnages, vous pouvez dire dans quel cas ils sont. Par exemple, "A" est en majuscules. Cela ressort également du nom du symbole: "LATIN CAPITAL LETTER A". Cependant, parfois, cette méthode ne fonctionne pas. Prenez le point de code U + 1D34. Cela ressemble à ceci: ᴴ. En Unicode, on lui attribue le nom: MODIFIER LETTER CAPITAL H. Donc c'est en majuscule, non?
En fait, il hérite de la propriété Minuscules, donc par définition n ° 2, il est en minuscules, malgré le fait qu'il ressemble visuellement à un H majuscule, et le nom contient le mot "CAPITAL".
Certains personnages n'ont aucun cas du tout
La définition 135 au §3.13 du standard Unicode déclare:
C est sensible à la casse si et seulement si C a une propriété Minuscule ou Majuscule, ou si General_Category est Titlecase_Letter.
Cela signifie que beaucoup de caractères Unicode - en fait, la plupart d'entre eux - sont sans casse. Les questions sur leur cas n'ont pas de sens et les changements de cas ne les affectent pas. Cependant, nous pouvons obtenir la réponse à cette question par la définition # 3.
Certains personnages se comportent comme s'ils avaient plusieurs registres
L'implication est que si vous utilisez la définition n ° 3 et demandez si un caractère sans casse est en majuscule ou en minuscule, vous obtenez la réponse «oui».
Le standard Unicode donne un exemple (Tableau 4-1, ligne 7) du caractère U + 02BD MODIFIER LETTER REVERSED COMMA (qui ressemble à ceci: ʽ). Il n'a pas les propriétés héritées des minuscules ou des majuscules, il n'appartient pas à la catégorie Lt, donc il n'y a pas de casse. En même temps, la conversion en majuscules ne la change pas, et la conversion en minuscules ne la change pas, donc, selon la 3ème définition, elle répond "oui" aux deux questions: "appartenez-vous aux majuscules?" et "êtes-vous en minuscules?"
Il semble que cela puisse causer une confusion inutile, mais le fait est que la définition n ° 3 fonctionne avec n'importe quelle séquence de caractères Unicode et vous permet de simplifier les algorithmes de conversion de casse (les caractères sans casse se transforment simplement en eux-mêmes).
La casse est sensible au contexte
Vous pourriez penser que si les tables de conversion de casse Unicode couvrent tous les caractères, cette conversion consiste simplement à trouver le bon endroit dans la table. Par exemple, la base de données Unicode indique que U + 0041 LATIN MAJUSCULE A est une minuscule U + 0061 LATIN MINUSCULE A. Simple, n'est-ce pas?
Un exemple où cette approche ne fonctionne pas est le grec. Le caractère Σ - c'est-à-dire U + 03A3 GREC CAPITAL LETTER SIGMA - est mappé à deux caractères différents lorsqu'il est converti en minuscules, selon l'endroit où il se trouve dans le mot. S'il est à la fin d'un mot, il sera alors en minuscule ς (U + 03C2 GREC MINUSCULE LETTRE FINALE SIGMA). Ailleurs, ce sera σ (U + 03C3 LETTRE MINUSCULE GRECQUE SIGMA).
Cela signifie que le registre n'est pas un-à-un ou transitif. Un autre exemple est ß (U + 00DF LATIN SMALL LETTER SHARP S, ou escet ). Ce sera "SS" en majuscules, bien qu'il existe maintenant une autre forme majuscule (ẞ, U + 1E9E LATIN CAPITAL LETTER SHARP S). Et la conversion de «SS» en minuscules entraîne «ss», donc (en utilisant la terminologie Unicode pour la conversion de cas): toLowerCase (toUpperCase (ß))! = Ss.
La casse dépend des paramètres régionaux
Différentes langues ont des règles de conversion de cas différentes. L'exemple le plus courant: i (U + 0069 LETTRE MINUSCULE LATINE I) et I (U + 0049 LETTRE MAJUSCULE LATINE I) sont convertis l'un à l'autre dans la plupart des paramètres régionaux - la plupart, mais pas tous. Dans les locales az et tr (langues turques), la majuscule i sera İ (U + 0130 LATIN MAJUSCULE LETTRE I POINT EN CHEF) et la minuscule I sera ı (U + 0131 LATIN MINUSCULE LETTRE DOTLESS I). Parfois, bien faire les choses signifie vraiment la différence entre la vie et la mort.
Unicode lui-même ne gère pas toutes les règles de conversion de cas possibles pour tous les paramètres régionaux. La base de données Unicode n'a que des règles générales pour la conversion de tous les caractères, non spécifiques aux paramètres régionaux. Il existe également des règles spéciales pour certaines langues et formes composées - lituanien, langues turques, certaines caractéristiques du grec. Tout le reste n'est pas là. Le §3.13 de la norme le mentionne et recommande l'introduction de règles de traduction spécifiques aux paramètres régionaux si nécessaire.
Un exemple serait un signe anglophone - c'est la casse du titre de certains noms. «O'brian» doit être converti en «O'Brian» (et non «O'brian»). Cependant, ce faisant, «c'est» doit être converti en «c'est» et non en «c'est». Un autre exemple qui n'est pas géré en Unicode est la combinaison de lettres hollandaises "ij", qui, une fois convertie en casse de titre, doit être convertie en majuscules si elle apparaît au début d'un mot. Ainsi, la plus grande baie des Pays-Bas dans le registre des titres sera "IJsselmeer" et non "Ijsselmeer". Unicode a les caractères IJ U + 0132 LATIN CAPITAL LIGATURE IJ et ij U + 0133 LATIN SMALL LIGATURE IJ si vous en avez besoin. Par défaut, la conversion de casse les convertit les uns aux autres (bien que les formulaires de normalisation Unicode utilisant l'équivalence de compatibilité les divisent en deux caractères distincts).
Revenant au matériel présenté dans le rapport. La complexité de la gestion des cas Unicode signifie que les comparaisons insensibles à la casse ne peuvent pas être effectuées à l'aide des fonctions de conversion standard en minuscules ou en majuscules présentes dans de nombreux langages de programmation. Pour de telles comparaisons, Unicode a le concept de pliage de casse, et le §3.13 de la norme définit les fonctions toCaseFold et isCaseFolded.
Vous pensez peut-être que la diffusion en cas plié est similaire à la diffusion en minuscules - mais ce n'est pas le cas. La norme Unicode avertit qu'une chaîne en majuscules n'a pas à être en minuscules. À titre d'exemple, la langue Cherokee est donnée - là, dans une chaîne en majuscules, les caractères en majuscules apparaîtront également.
Dans l'une des diapositives de mon exposé, le rapport technique Unicode n ° 36 est implémenté aussi complètement que possible en Python. La normalisation NFKC est effectuée, puis la méthode casefold () (disponible uniquement dans Python 3+) est appelée pour la chaîne résultante. Même ainsi, certains cas extrêmes tombent, et ce n'est pas vraiment ce qui est recommandé pour la comparaison d'identifiants. La mauvaise nouvelle d'abord: Python n'expose pas suffisamment de propriétés Unicode pour filtrer les caractères qui ne sont pas dans XID_Start ou XID_Continue, ou les caractères qui ont une propriété Default_Ignorable_Code_Point. Autant que je sache, il ne prend pas en charge le mappage NFKC_Casefold. Il n'y a pas non plus de moyen simple d'utiliser le NFKC UAX # 31§5.1 modifié.
La bonne nouvelle est que la plupart de ces cas de pointe n'impliquent aucun risque réel de sécurité posé par les symboles en question. Et le pliage de caisse n'est en principe pas défini comme une opération préservant la normalisation (d'où le mappage NFKC_Casefold, qui est re-normalisé en NFC après le pliage de caisse). Généralement, lors de la comparaison, vous ne vous souciez pas de savoir si les deux chaînes sont normalisées après le prétraitement. Vous vous souciez si le prétraitement n'est pas incohérent et garantit que seules les lignes qui "devraient" différer plus tard seront différentes par la suite. Si cela vous inquiète, vous pouvez manuellement re-normaliser après l'ajout du registre.
Assez pour le moment
Cet article, comme le précédent rapport, n'est pas exhaustif et il n'est guère possible de regrouper tout ce matériel dans un seul article. J'espère que cela a été un aperçu utile de la complexité de ce sujet et fournit suffisamment de points de départ pour rechercher de plus amples informations. Par conséquent, en principe, vous pouvez vous arrêter ici.
Ne serait-il pas naïf d'espérer que d'autres personnes arrêteront d'écrire des expositions à partir de la série «d'idées fausses sur X auxquelles les programmeurs croient» et commenceront à écrire des articles comme «la vérité que les programmeurs devraient savoir»?