No início da palestra, entretanto, mencionei que esta não seria mais uma exposição da série de "equívocos sobre o X em que os programadores acreditam". Você pode encontrar inúmeras dessas revelações. No entanto, não gosto desses artigos. Eles listam várias coisas que são supostamente falsas, mas raramente explicam por que isso é e o que deveria ser feito. Suspeito que as pessoas irão apenas ler esses artigos, se parabenizar por essa conquista e, em seguida, encontrar novas maneiras interessantes de cometer erros não mencionados nesses artigos. Isso ocorre porque eles não entendiam realmente os problemas que causavam esses erros.
Portanto, em meu relatório, tentei explicar alguns problemas da melhor forma possível e explicar como resolvê-los - gosto muito mais dessa abordagem . Um dos tópicos que só toquei de passagem (foi apenas um slide e algumas referências em outros slides) são as complexidades que podem ser associadas ao caso dos personagens. Há uma Correct Answer ™ oficial para o problema que discuti - comparação de identificadores que não diferencia maiúsculas de minúsculas - e na palestra eu dei a melhor solução que conheço de usar apenas a biblioteca padrão do Python.
No entanto, mencionei brevemente as complexidades mais profundas do caso de caractere Unicode e quero dedicar algum tempo para descrever os detalhes. É interessante e entendê-lo pode ajudá-lo a tomar decisões ao projetar e escrever código de processamento de texto. Portanto, ofereço a você o oposto dos artigos "conceitos errôneos sobre X que os programadores acreditam" - "verdades que os programadores deveriam saber".
Mais uma coisa: o Unicode é cheio de terminologia. Neste artigo, usarei principalmente as definições "maiúsculas" e "minúsculas", já que o padrão Unicodeusa esses termos. Se você gosta de outros termos, como letras minúsculas / maiúsculas, tudo bem. Além disso, freqüentemente usarei o termo "símbolo", que alguns podem achar incorreto. Sim, em Unicode o conceito de "personagem" nem sempre é o que as pessoas esperam, por isso é melhor evitá-lo usando outros termos. No entanto, neste artigo, usarei o termo como é usado no Unicode - para descrever uma entidade abstrata que pode ser reivindicada. Sempre que importante, usarei termos mais específicos, como ponto de código, para esclarecer.
Existem mais de dois registros
Os falantes nativos de línguas europeias estão acostumados ao fato de que seus idiomas usam letras maiúsculas e minúsculas para denotar coisas específicas. Por exemplo, nos idiomas inglês [e russo], geralmente iniciamos as frases com uma letra maiúscula e continuamos na maioria das vezes com letras minúsculas. Além disso, os nomes próprios começam com letras maiúsculas e muitos acrônimos e abreviações são escritos em maiúsculas.
E geralmente pensamos que existem apenas dois registros. Existe a letra "A" e existe a letra "a". Um em maiúsculas, um em minúsculas - não é mesmo?
No entanto, existem três registros no Unicode. Existe uma maiúscula, existe uma minúscula e existe uma caixa de título [titlecase]. Em inglês, os nomes são escritos desta forma. Por exemplo, "Avengers: Infinity War". Normalmente, para isso, a primeira letra de cada palavra é simplesmente escrita em maiúsculas (e dependendo de diferentes regras e estilos, algumas palavras, como artigos, não são maiúsculas).
O padrão Unicode dá um exemplo de um caractere em maiúsculas: U + 01F2 LATIN MAIÚSCULA LETRA D COM PEQUENO Z. Tem a seguinte aparência: Dz.
Esses caracteres às vezes são necessários para lidar com as consequências negativas de uma das primeiras soluções para o desenvolvimento do padrão Unicode: compatibilidade com versões anteriores de codificações de texto existentes. Seria mais conveniente para o Unicode compor sequências usando a combinação de caracteres padrão. No entanto, em muitos sistemas existentes, o espaço já foi alocado para sequências prontas. Por exemplo, em ISO-8859-1 ("latin-1"), o caractere "é" tem uma forma pronta numerada 0xe9. Em Unicode, seria preferível escrever esta letra com um "e" separado e um acento. Mas para garantir compatibilidade total com versões anteriores com codificações existentes, como latin-1, Unicode também atribui pontos de código para caracteres prontos. Por exemplo, U + 00E9 LATIN PEQUENA LETRA E COM AGUDA.
Embora a posição do código desse caractere seja igual ao valor do byte latin-1, você não deve confiar nisso. É improvável que a codificação de caracteres em Unicode preservará essas posições. Por exemplo, em UTF-8, a posição do código U + 00E9 é escrita como a sequência de bytes 0xc3 0xa9.
E, é claro, há caracteres nas codificações existentes que precisam de tratamento especial ao usar maiúsculas, razão pela qual foram incluídos no Unicode "como estão". Se você quiser dar uma olhada neles, procure em seu banco de dados Unicode favorito por caracteres da categoria Lt ("Letter, titlecase").
Existem várias maneiras de definir o caso
O padrão Unicode (§4.2) lista três definições de caso diferentes. Talvez a escolha de um dos três seja feita para você por sua linguagem de programação; caso contrário, sua escolha dependerá de seu objetivo específico. Essas definições são:
- O caractere estará em maiúsculas se estiver na categoria Lu ("Letra, maiúscula") e em minúsculas se estiver na categoria L ("Letra, minúsculas"). A norma reconhece as limitações desta definição: cada símbolo específico deve ser atribuído a apenas uma das categorias. Por causa disso, muitos caracteres que “devem estar” em maiúsculas ou minúsculas não atenderão a esse requisito porque pertencem a alguma outra categoria.
- O caractere estará em maiúsculas se herdar a propriedade Maiúsculas e em minúsculas se herdar a propriedade Minúsculas. É uma combinação da definição de um com outras propriedades do caractere, que podem incluir maiúsculas e minúsculas.
- Um caractere está em maiúsculas se não mudar depois de ser mapeado para maiúsculas. Um caractere está em minúsculas se não mudar depois de ser mapeado para minúsculas. Esta é uma definição bastante geral, mas também pode se comportar de forma não intuitiva.
Se você estiver trabalhando com um subconjunto limitado de símbolos (especificamente, com letras), então 1 definição pode ser suficiente para você. Se o seu repertório for mais amplo - inclui símbolos semelhantes a letras que não são letras, a 2ª definição pode ser adequada para você. É recomendado pelo padrão Unicode, §4.2:
Os programadores que manipulam strings Unicode devem trabalhar com funções de string como isLowerCase (e seu primo funcional toLowerCase) se elas não funcionarem diretamente com propriedades de caracteres.
A função mencionada aqui é definida em §3.13 do padrão Unicode. Formalmente, a definição 3 usa as funções isLowerCase e isUpperCase de §3.13, definidas em termos das posições fixas em toLowerCase e toUpperCase, respectivamente.
Se sua linguagem de programação tem funções para verificar ou converter o caso de strings ou caracteres individuais, vale a pena investigar quais das definições acima são usadas na implementação. Se você estiver interessado, os métodos isupper () e islower () em Python usam a 2ª definição.
É impossível entender o caso de um personagem por sua aparência ou nome
Pelo aparecimento de muitos personagens, você pode dizer em que caso eles são. Por exemplo, "A" está em maiúsculas. Isso também fica claro pelo nome do símbolo: "LATIN MAIÚSCULA LETRA A". No entanto, às vezes, esse método não funciona. Pegue o ponto de código U + 1D34. É assim: ᴴ. No Unicode, é atribuído o nome: MODIFIER LETTER CAPITAL H. Então é maiúsculo, certo?
Na verdade, ele herda a propriedade Minúsculas, então pela definição # 2 está em minúsculas, apesar do fato de que se parece visualmente com um H maiúsculo, e o nome contém a palavra "MAIÚSCULA".
Alguns personagens não têm nenhum caso
A definição 135 em §3.13 dos estados padrão Unicode:
C faz distinção entre maiúsculas e minúsculas se e somente se C tiver uma propriedade Minúsculas ou Maiúsculas, ou General_Category for Titlecase_Letter.
Isso significa que muitos caracteres Unicode - na verdade, a maioria deles - não têm caixa. Perguntas sobre o caso deles não fazem sentido e as mudanças de caso não os afetam. No entanto, podemos obter a resposta a esta pergunta pela definição # 3.
Alguns personagens se comportam como se tivessem vários registros
A implicação é que se você usar a definição # 3 e perguntar se um caractere sem maiúsculas ou minúsculas está em maiúsculas ou minúsculas, você receberá a resposta "sim".
O padrão Unicode dá um exemplo (Tabela 4-1, linha 7) do caractere U + 02BD MODIFIER LETTER REVERSED COMMA (que se parece com isto: ʽ). Ele não tem as propriedades herdadas em minúsculas ou maiúsculas, não pertence à categoria Lt e, portanto, não tem maiúsculas e minúsculas. Ao mesmo tempo, a conversão para maiúsculas não o altera, e a conversão para minúsculas não o altera, portanto, pela 3ª definição, ele responde "sim" a ambas as perguntas: "você está maiúsculo?" e "você está em minúsculas?"
Parece que isso pode causar confusão desnecessária, mas o ponto é que a definição # 3 funciona com qualquer sequência de caracteres Unicode e permite que você simplifique os algoritmos de conversão de maiúsculas e minúsculas (caracteres sem caixa apenas se transformam em si).
O caso é sensível ao contexto
Você pode pensar que se as tabelas de conversão de maiúsculas e minúsculas do Unicode cobrem todos os caracteres, então essa conversão é simplesmente sobre como encontrar o lugar certo na tabela. Por exemplo, o banco de dados Unicode diz que U + 0041 LATIN CAPITAL LETTER A é minúsculo U + 0061 LATIN SMALL LETTER A. Simples, não é?
Um exemplo em que essa abordagem não funciona é o grego. O caractere Σ - ou seja, U + 03A3 SIGMA DA LETRA MAIÚSCULA GREGA - é mapeado para dois caracteres diferentes quando convertido para minúsculas, dependendo de onde ele está na palavra. Se estiver no final de uma palavra, será ς minúsculo (U + 03C2 GREGO SMALL LETTER FINAL SIGMA). Em outro lugar, será σ (U + 03C3 GREGO SMALL LETTER SIGMA).
Isso significa que o registrador não é um-para-um ou transitivo. Outro exemplo é ß (U + 00DF LATIN SMALL LETTER SHARP S, ou escet ). Será "SS" em maiúsculas, embora agora haja outra forma em maiúsculas (ẞ, U + 1E9E LATIN MAIÚSCULA LETRA SHARP S). E converter "SS" em minúsculas resulta em "ss", então (usando a terminologia Unicode para conversão de maiúsculas): toLowerCase (toUpperCase (ß))! = Ss.
O caso depende da localidade
Idiomas diferentes têm regras de conversão de caso diferentes. O exemplo mais popular: i (U + 0069 LATIN SMALL LETTER I) e I (U + 0049 LATIN CAPITAL LETTER I) são convertidos um para o outro na maioria dos locais - na maioria, mas não em todos. Nos locais az e tr (idiomas turcos), a maiúscula i será İ (U + 0130 LATIN CAPITAL LETTER I COM DOT ACIMA), e a minúscula I será ı (U + 0131 LATIN SMALL LETTER DOTLESS I). Às vezes, acertar realmente significa a diferença entre a vida e a morte.
O próprio Unicode não lida com todas as regras de conversão de caso possíveis para todos os locais. O banco de dados Unicode possui apenas regras gerais para converter todos os caracteres, não específicas para o local. Além disso, existem regras especiais para alguns idiomas e formas compostas - lituano, línguas turcas, algumas características do grego. Todo o resto não está lá. §3.13 da norma menciona isso e recomenda a introdução de regras de tradução específicas do local, se necessário.
Um exemplo seria um sinal de língua inglesa - é o caso do título de certos nomes. "O'brian" deve ser convertido para "O'Brian" (não "O'brian"). No entanto, ao fazer isso, "é" deve ser convertido em "É" e não em "É". Outro exemplo que não é tratado em Unicode é a combinação de letras holandês "ij", que, quando convertida para maiúsculas, deve ser convertida para maiúsculas se aparecer no início de uma palavra. Assim, a maior baía nos Países Baixos no registo de título será "IJsselmeer" e não "Ijsselmeer". Unicode tem os caracteres IJ U + 0132 LATIN CAPITAL LIGATURE IJ e ij U + 0133 LATIN SMALL LIGATURE IJ se você precisar deles. Por padrão, a conversão de maiúsculas e minúsculas os converte um no outro (embora os formulários de normalização Unicode usando equivalência de compatibilidade os dividam em dois caracteres separados).
Voltando ao material apresentado no relatório. A complexidade do gerenciamento de maiúsculas e minúsculas do Unicode significa que as comparações que não diferenciam maiúsculas de minúsculas não podem ser realizadas usando as funções de conversão de maiúsculas ou minúsculas padrão encontradas em muitas linguagens de programação. Para tais comparações, Unicode tem o conceito de dobradura de maiúsculas e §3.13 do padrão define as funções toCaseFold e isCaseFolded.
Você pode pensar que fundir para uma caixa dobrada é semelhante a fundir para uma minúscula - mas não é. O padrão Unicode avisa que uma string com caixa dobrada não precisa ser minúscula. Por exemplo, a linguagem Cherokee é dada - lá, em uma string que está em caixa dobrada, também haverá caracteres maiúsculos.
Em um dos slides de minha palestra, o Relatório Técnico Unicode nº 36 é implementado da forma mais completa possível em Python. A normalização NFKC é realizada e, em seguida, o método casefold () (disponível apenas no Python 3+) é chamado para a string resultante. E, mesmo assim, alguns casos extremos falham e isso não é realmente o que é recomendado para comparação de ID. A má notícia primeiro: Python não expõe propriedades Unicode suficientes para filtrar caracteres que não estão em XID_Start ou XID_Continue, ou caracteres que possuem uma propriedade Default_Ignorable_Code_Point. Pelo que eu sei, ele não oferece suporte ao mapeamento NFKC_Casefold. Também não há uma maneira fácil de usar o NFKC UAX # 31§5.1 modificado.
A boa notícia é que a maioria desses casos extremos não envolve nenhum risco de segurança real causado pelos símbolos em questão. E a dobragem de caixa não é, em princípio, definida como uma operação de preservação de normalização (daí o mapeamento NFKC_Casefold, que é normalizado novamente para NFC após a dobragem de caixa). Geralmente, ao comparar, você não se importa se ambas as strings são normalizadas após o pré-processamento. Você se preocupa se o pré-processamento não é inconsistente e se garante que apenas as linhas que "deveriam" diferir posteriormente serão diferentes depois. Se você estiver preocupado com isso, você pode normalizar manualmente novamente após a adição do registro.
Chega por agora
Este artigo, assim como o relatório anterior, não é exaustivo e dificilmente é possível encaixar todo esse material em uma única postagem. Espero que esta tenha sido uma visão geral útil das complexidades deste tópico e forneça pontos de partida suficientes para procurar mais informações. Portanto, em princípio, você pode parar por aqui.
Não seria ingênuo esperar que outras pessoas parassem de escrever revelações da série de "equívocos sobre X em que os programadores acreditam" e começassem a escrever artigos como "a verdade que os programadores deveriam saber"?