Como comparar caracteres Unicode que “se parecem”?

94

Eu caio em um problema surpreendente.

Carreguei um arquivo de texto no meu aplicativo e tenho uma lógica que compara o valor com µ.

E percebi que mesmo que os textos sejam os mesmos, o valor de comparação é falso.

 Console.WriteLine("μ".Equals("µ")); // returns false
 Console.WriteLine("µ".Equals("µ")); // return true

Na linha posterior, o caractere µ é copiado e colado.

No entanto, esses podem não ser os únicos personagens assim.

Existe alguma maneira em C # para comparar os caracteres que parecem iguais, mas são realmente diferentes?

DJ
fonte
158
Parece que você encontrou o mu de Schrödinger.
BoltClock
19
Eles são personagens diferentes - embora tenham a mesma aparência, eles têm códigos de caracteres diferentes.
user2864740
93
Bem-vindo ao Unicode.
ta.speot.is
11
O que você quer alcançar? que aqueles dois devem ser iguais, então até mesmo seu código de personagem é diferente, mas o mesmo rosto?
Jade
28
“Parecem iguais” e “parecem iguais” são conceitos vagos. Eles significam identidade de glifos ou apenas similaridade? Quão perto? Observe que dois caracteres podem ter glifos idênticos em alguma fonte, muito semelhantes em outra e bastante diferentes em outra fonte. O que importa é por que você faria essa comparação e em que contexto (e a aceitabilidade de falsos positivos e falsos negativos).
Jukka K. Korpela

Respostas:

125

Em muitos casos, você pode normalizar ambos os caracteres Unicode para uma determinada forma de normalização antes de compará-los, e eles devem ser compatíveis. Claro, qual forma de normalização você precisa usar depende dos próprios personagens; só porque eles são parecidos não significa necessariamente que representem o mesmo personagem. Você também precisa considerar se é apropriado para seu caso de uso - veja o comentário de Jukka K. Korpela.

Para esta situação particular, se você consultar os links na resposta de Tony , verá que a tabela para U + 00B5 diz:

Decomposição <compat> GREGA PEQUENA LETRA MU (U + 03BC)

Isso significa que U + 00B5, o segundo caractere em sua comparação original, pode ser decomposto em U + 03BC, o primeiro caractere.

Portanto, você normalizará os caracteres usando a decomposição de compatibilidade total, com os formulários de normalização KC ou KD. Aqui está um exemplo rápido que escrevi para demonstrar:

using System;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        char first = 'μ';
        char second = 'µ';

        // Technically you only need to normalize U+00B5 to obtain U+03BC, but
        // if you're unsure which character is which, you can safely normalize both
        string firstNormalized = first.ToString().Normalize(NormalizationForm.FormKD);
        string secondNormalized = second.ToString().Normalize(NormalizationForm.FormKD);

        Console.WriteLine(first.Equals(second));                     // False
        Console.WriteLine(firstNormalized.Equals(secondNormalized)); // True
    }
}

Para mais detalhes sobre Unicode normalização e as diferentes formas de normalização referem-se System.Text.NormalizationForme a especificação Unicode .

BoltClock
fonte
26
Obrigado pelo link de especificações Unicode. Primeira vez que li sobre isso. Pequena nota dele: "Os formulários de normalização KC e KD não devem ser aplicados cegamente a texto arbitrário .. É melhor pensar nesses formulários de normalização como sendo mapeamentos em maiúsculas ou minúsculas: úteis em certos contextos para identificar significados centrais, mas também executar modificações no texto que podem nem sempre ser apropriadas. "
user2864740
149

Porque são símbolos realmente diferentes, mesmo que pareçam iguais, o primeiro é a letra real e tem char code = 956 (0x3BC)e o segundo é o micro-sinal e tem 181 (0xB5).

Referências:

Então, se você quiser compará-los e precisar que eles sejam iguais, você precisa lidar com isso manualmente ou substituir um caractere por outro antes da comparação. Ou use o seguinte código:

public void Main()
{
    var s1 = "μ";
    var s2 = "µ";

    Console.WriteLine(s1.Equals(s2));  // false
    Console.WriteLine(RemoveDiacritics(s1).Equals(RemoveDiacritics(s2))); // true 
}

static string RemoveDiacritics(string text) 
{
    var normalizedString = text.Normalize(NormalizationForm.FormKC);
    var stringBuilder = new StringBuilder();

    foreach (var c in normalizedString)
    {
        var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
        if (unicodeCategory != UnicodeCategory.NonSpacingMark)
        {
            stringBuilder.Append(c);
        }
    }

    return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}

E a demonstração

Tony
fonte
11
Por curiosidade, qual é a razão para ter dois símbolos µ? Você não vê um K dedicado com o nome "Sinal de Kilo" (ou vê?).
MartinHaTh
12
@MartinHaTh: De acordo com a Wikipedia, é "por razões históricas" .
BoltClock
12
O Unicode tem muitos caracteres de compatibilidade trazidos de conjuntos de caracteres mais antigos (como ISO 8859-1 ), para tornar a conversão desses conjuntos de caracteres mais fácil. Na época em que os conjuntos de caracteres eram restritos a 8 bits, eles incluíam alguns glifos (como algumas letras gregas) para os usos matemáticos e científicos mais comuns. A reutilização de glifos com base na aparência era comum, então nenhum 'K' especializado foi adicionado. Mas sempre foi uma solução alternativa; o símbolo correto para "micro" é o mu minúsculo grego real, o símbolo correto para Ohm é o ômega capital real e assim por diante.
VGR
8
Nada melhor do que quando algo é feito por passas histéricas
paulm
11
Existe um K especial para cereais?
86

Ambos têm códigos de caracteres diferentes: consulte isto para obter mais detalhes

Console.WriteLine((int)'μ');  //956
Console.WriteLine((int)'µ');  //181

Onde, o primeiro é:

Display     Friendly Code   Decimal Code    Hex Code    Description
====================================================================
μ           &mu;            &#956;          &#x3BC;     Lowercase Mu
µ           &micro;         &#181;          &#xB5;      micro sign Mu

Imagem

Vishal Suthar
fonte
39

Para o exemplo específico de μ(mu) e µ(micro sinal), o último tem uma decomposição de compatibilidade com o primeiro, então você pode normalizar a string para FormKCou FormKDpara converter os microssinais em mus.

No entanto, existem muitos conjuntos de caracteres que se parecem, mas não são equivalentes em nenhuma forma de normalização Unicode. Por exemplo, A(latim), Α(grego) e А(cirílico). O site Unicode tem um arquivo confusables.txt com uma lista deles, com o objetivo de ajudar os desenvolvedores a se protegerem contra ataques homográficos . Se necessário, você pode analisar esse arquivo e construir uma tabela para “normalização visual” de strings.

dan04
fonte
Definitivamente, é bom saber ao usar o Normalize. Parece surpreendente que eles permaneçam distintos.
user2864740
4
@ user2864740: Se um tau grego maiúsculo não permanecesse distinto de uma letra T romana, seria muito difícil classificar o texto grego e romano em ordem alfabética. Além disso, se uma fonte usasse um estilo visual diferente para as letras gregas e romanas, seria muito perturbador se as letras gregas cujas formas se assemelhavam às letras romanas fossem reproduzidas de forma diferente daquelas que não o eram.
supercat
7
Mais importante, unificar os alfabetos europeus tornaria ToUpper/ ToLowerdificultaria a implementação. Você precisa "B".ToLower()estar bem inglês, mas βem grego e вem russo. Como está, apenas o turco (sem ponto i) e alguns outros idiomas precisam de regras de capitalização diferentes do padrão.
dan04
@ dan04: Será que alguém já pensou em atribuir pontos de código exclusivos a todas as quatro variações do "i" e "I" turco? Isso teria eliminado qualquer ambiguidade no comportamento de toUpper / toLower.
supercat de
34

Pesquise os dois caracteres em um banco de dados Unicode e veja a diferença .

Uma é a minúscula letra grega µ e a outra é o micro sinal µ .

Name            : MICRO SIGN
Block           : Latin-1 Supplement
Category        : Letter, Lowercase [Ll]
Combine         : 0
BIDI            : Left-to-Right [L]
Decomposition   : <compat> GREEK SMALL LETTER MU (U+03BC)
Mirror          : N
Index entries   : MICRO SIGN
Upper case      : U+039C
Title case      : U+039C
Version         : Unicode 1.1.0 (June, 1993)

Name            : GREEK SMALL LETTER MU
Block           : Greek and Coptic
Category        : Letter, Lowercase [Ll]
Combine         : 0
BIDI            : Left-to-Right [L]
Mirror          : N
Upper case      : U+039C
Title case      : U+039C
See Also        : micro sign U+00B5
Version         : Unicode 1.1.0 (June, 1993)
Subin Jacob
fonte
4
Como isso conseguiu 37 votos positivos? Ele não responde à pergunta ("Como comparar caracteres Unicode"), apenas comenta por que esse exemplo específico não é igual. Na melhor das hipóteses, deve ser um comentário sobre a questão. Eu entendo que as opções de formatação de comentários não permitem postá-lo tão bem quanto as opções de formatação de resposta, mas isso não deve ser um motivo válido para postar como uma resposta.
Konerak
5
Na verdade, a questão era diferente, perguntando por que a verificação de igualdade μ e µ retorna falso. Esta resposta responde. Posteriormente, OP fez outra pergunta (esta pergunta) como comparar dois personagens que se parecem. Ambas as perguntas tiveram as melhores respostas e posteriormente um dos moderadores uniu as duas perguntas selecionando a melhor resposta da segunda como a melhor. Alguém editou esta pergunta, para que ela resuma
Subin Jacob
Na verdade, não adicionei nenhum conteúdo após a mesclagem
Subin Jacob
24

EDITAR Após a fusão desta questão com Como comparar 'μ' e 'µ' em C #
Resposta original postada:

 "μ".ToUpper().Equals("µ".ToUpper()); //This always return true.

EDITAR Depois de ler os comentários, sim, não é bom usar o método acima porque pode fornecer resultados errados para algum outro tipo de entrada, para isso devemos usar normalizar usando decomposição de compatibilidade total conforme mencionado no wiki . (Graças à resposta postada por BoltClock )

    static string GREEK_SMALL_LETTER_MU = new String(new char[] { '\u03BC' });
    static string MICRO_SIGN = new String(new char[] { '\u00B5' });

    public static void Main()
    {
        string Mus = "µμ";
        string NormalizedString = null;
        int i = 0;
        do
        {
            string OriginalUnicodeString = Mus[i].ToString();
            if (OriginalUnicodeString.Equals(GREEK_SMALL_LETTER_MU))
                Console.WriteLine(" INFORMATIO ABOUT GREEK_SMALL_LETTER_MU");
            else if (OriginalUnicodeString.Equals(MICRO_SIGN))
                Console.WriteLine(" INFORMATIO ABOUT MICRO_SIGN");

            Console.WriteLine();
            ShowHexaDecimal(OriginalUnicodeString);                
            Console.WriteLine("Unicode character category " + CharUnicodeInfo.GetUnicodeCategory(Mus[i]));

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormC);
            Console.Write("Form C Normalized: ");
            ShowHexaDecimal(NormalizedString);               

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormD);
            Console.Write("Form D Normalized: ");
            ShowHexaDecimal(NormalizedString);               

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormKC);
            Console.Write("Form KC Normalized: ");
            ShowHexaDecimal(NormalizedString);                

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormKD);
            Console.Write("Form KD Normalized: ");
            ShowHexaDecimal(NormalizedString);                
            Console.WriteLine("_______________________________________________________________");
            i++;
        } while (i < 2);
        Console.ReadLine();
    }

    private static void ShowHexaDecimal(string UnicodeString)
    {
        Console.Write("Hexa-Decimal Characters of " + UnicodeString + "  are ");
        foreach (short x in UnicodeString.ToCharArray())
        {
            Console.Write("{0:X4} ", x);
        }
        Console.WriteLine();
    }

Resultado

INFORMATIO ABOUT MICRO_SIGN    
Hexa-Decimal Characters of µ  are 00B5
Unicode character category LowercaseLetter
Form C Normalized: Hexa-Decimal Characters of µ  are 00B5
Form D Normalized: Hexa-Decimal Characters of µ  are 00B5
Form KC Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KD Normalized: Hexa-Decimal Characters of µ  are 03BC
 ________________________________________________________________
 INFORMATIO ABOUT GREEK_SMALL_LETTER_MU    
Hexa-Decimal Characters of µ  are 03BC
Unicode character category LowercaseLetter
Form C Normalized: Hexa-Decimal Characters of µ  are 03BC
Form D Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KC Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KD Normalized: Hexa-Decimal Characters of µ  are 03BC
 ________________________________________________________________

Ao ler informações em Unicode_equivalence , encontrei

A escolha dos critérios de equivalência pode afetar os resultados da pesquisa. Por exemplo, algumas ligaduras tipográficas como U + FB03 (ffi), ..... então uma busca por U + 0066 (f) como substring teria sucesso em uma normalização NFKC de U + FB03, mas não na normalização NFC de U + FB03.

Portanto, para comparar a equivalência, devemos normalmente usar FormKCnormalização NFKC ou FormKDnormalização NFKD.
Eu estava um pouco curioso para saber mais sobre todos os caracteres Unicode, então fiz uma amostra que iteraria sobre todos os caracteres Unicode UTF-16e obtive alguns resultados que gostaria de discutir

  • Informações sobre personagens cujas FormCe FormDvalores normalizados não eram equivalentes
    Total: 12,118
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-253, ..... 44032-55203
  • Informações sobre personagens cujas FormKCe FormKDvalores normalizados não eram equivalentes
    Total: 12,245
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-228, ..... 44032-55203, 64420-64421, 64432-64433, 64490-64507, 64512-64516, 64612-64617, 64663-64667, 64735-64736, 65153-65164, 65269-65274
  • Todo o personagem cujo FormCe FormDnormalizada valor não eram equivalentes, não FormKCe FormKDvalores normalizados também não foram equivalentes, exceto esses personagens
    Personagens:901 '΅', 8129 '῁', 8141 '῍', 8142 '῎', 8143 '῏', 8157 '῝', 8158 '῞'
    , 8159 '῟', 8173 '῭', 8174 '΅'
  • Caráter extra cujo FormKCe FormKDvalor normalizado não foram equivalentes, mas lá FormCe FormDnormalizados valores foram equivalentes
    Total: 119
    Personagens:452 'DŽ' 453 'Dž' 454 'dž' 12814 '㈎' 12815 '㈏' 12816 '㈐' 12817 '㈑' 12818 '㈒' 12819 '㈓' 12820 '㈔' 12821 '㈕', 12822 '㈖' 12823 '㈗' 12824 '㈘' 12825 '㈙' 12826 '㈚' 12827 '㈛' 12828 '㈜' 12829 '㈝' 12830 '㈞' 12910 '㉮' 12911 '㉯' 12912 '㉰' 12913 '㉱' 12914 '㉲' 12915 '㉳' 12916 '㉴' 12917 '㉵' 12918 '㉶' 12919 '㉷' 12920 '㉸' 12921 '㉹' 12922 '㉺' 12923 '㉻' 12924 '㉼' 12925 '㉽' 12926 '㉾' 13056 '㌀' 13058 '㌂' 13060 '㌄' 13063 '㌇' 13070 '㌎' 13071 '㌏' 13072 '㌐' 13073 '㌑' 13075 '㌓' 13077 '㌕' 13080 '㌘' 13081 '㌙' 13082 '㌚' 13086 '㌞' 13089 '㌡' 13092 '㌤' 13093 '㌥' 13094 '㌦' 13099 '㌫' 13100 '㌬' 13101 '㌭' 13102 '㌮' 13103 '㌯' 13104 '㌰' 13105 '㌱' 13106 '㌲' 13108 '㌴' 13111 '㌷' 13112 '㌸' 13114 '㌺' 13115 '㌻' 13116 '㌼' 13117 '㌽' 13118 '㌾' 13120 '㍀' 13130 '㍊' 13131 '㍋' 13132 '㍌' 13134 '㍎' 13139 '㍓' 13140 '㍔' 13142 '㍖' .......... ﺋ' 65164 'ﺌ' 65269 'ﻵ' 65270 'ﻶ' 65271 'ﻷ' 65272 'ﻸ' 65273 'ﻹ' 65274'
  • Existem alguns personagens que não podem ser normalizados , eles jogam ArgumentExceptionse tentados
    Total:2081 Characters(int value): 55296-57343, 64976-65007, 65534

Esses links podem ser realmente úteis para entender quais regras regem a equivalência Unicode

  1. Unicode_equivalence
  2. Unicode_compatibility_characters
dbw
fonte
4
Estranho, mas funciona ... Quer dizer, eles são dois caracteres diferentes com significados diferentes e convertê-los em superior torna-os iguais? Eu não vejo a lógica, mas boa solução +1
BudBrot
45
Esta solução mascara o problema e pode causar problemas em um caso geral. Esse tipo de teste descobriria isso "m".ToUpper().Equals("µ".ToUpper());e "M".ToUpper().Equals("µ".ToUpper());também é verdadeiro. Isso pode não ser desejável.
Andrew Leach
6
-1 - esta é uma ideia terrível. Não trabalhe com Unicode desta forma.
Konrad Rudolph
1
Em vez de truques baseados em ToUpper (), por que não usar String.Equals ("μ", "μ", StringComparison.CurrentCultureIgnoreCase)?
svenv
6
Há uma boa razão para distinguir entre "MICRO SINAL" e "GREGO PEQUENO LETRA MU" - para dizer que "maiúsculas" do microssinal ainda é microssintro. Mas a capitalização muda de micro para mega, engenharia feliz.
Greg
9

Provavelmente, existem dois códigos de caracteres diferentes que fazem (visivelmente) o mesmo caractere. Embora tecnicamente não sejam iguais, eles parecem iguais. Dê uma olhada na tabela de personagens e veja se existem várias instâncias desse personagem. Ou imprima o código de caractere dos dois caracteres em seu código.

PMF
fonte
6

Você pergunta "como compará-los", mas não nos diz o que deseja fazer.

Existem pelo menos duas maneiras principais de compará-los:

Ou você os compara diretamente como você é e eles são diferentes

Ou você usa a normalização de compatibilidade Unicode se precisar de uma comparação que os encontre correspondentes.

Pode haver um problema porque a normalização da compatibilidade Unicode fará com que muitos outros caracteres sejam comparados da mesma forma. Se você deseja que apenas esses dois caracteres sejam tratados como iguais, você deve rolar suas próprias funções de normalização ou comparação.

Para uma solução mais específica, precisamos saber seu problema específico. Qual é o contexto em que você se deparou com esse problema?

hippietrail
fonte
1
O "micro sinal" e o caractere mu minúsculo canonicamente são equivalentes? Usar a normalização canônica forneceria uma comparação mais estrita.
Tanner Swett
@ TannerL.Swett: Na verdade, nem tenho certeza de como verificar isso de cabeça ...
hippietrail
1
Na verdade, estava importando um arquivo com a fórmula da física. Você está certo sobre a normalização. Eu tenho que passar por isso mais profundamente ..
DJ
Que tipo de arquivo? Algo feito à mão em texto Unicode simples por uma pessoa? Ou algo gerado por um aplicativo em um formato específico?
hippietrail
5

Se eu quisesse ser pedante, diria que sua pergunta não faz sentido, mas como estamos nos aproximando do Natal e os pássaros cantando, vou prosseguir.

Primeiro, as 2 entidades que você está tentando comparar são glyphs, um glifo é parte de um conjunto de glifos fornecidos por aquilo que é normalmente conhecido como uma "fonte", a coisa que geralmente vem em um ttf, otfou qualquer formato de arquivo que você está usando.

Os glifos são uma representação de um determinado símbolo e, uma vez que são uma representação que depende de um conjunto específico, você não pode esperar ter 2 símbolos semelhantes ou mesmo "melhores" idênticos, é uma frase que não faz sentido se você considerar o contexto, deve pelo menos especificar que fonte ou conjunto de glifos está considerando ao formular uma pergunta como esta.

O que geralmente é usado para resolver um problema semelhante ao que você está encontrando, é um OCR, essencialmente um software que reconhece e compara glifos. Se C # fornece um OCR por padrão, não sei disso, mas geralmente é muito ruim ideia se você realmente não precisa de um OCR e sabe o que fazer com ele.

Você pode acabar interpretando um livro de física como um livro grego antigo, sem mencionar o fato de que OCR geralmente são caros em termos de recursos.

Há uma razão pela qual esses caracteres são localizados da maneira como são localizados, apenas não faça isso.

user2485710
fonte
1

É possível desenhar ambos os caracteres com o mesmo estilo de fonte e tamanho com o DrawStringmétodo. Após a geração de dois bitmaps com símbolos, é possível compará-los pixel a pixel.

A vantagem desse método é que você pode comparar não apenas caracteres absolutamente iguais, mas também semelhantes (com tolerância definida).

Ivan Kochurkin
fonte