¿Cómo comparar caracteres Unicode que "se parecen"?

94

Caigo en un tema sorprendente.

Cargué un archivo de texto en mi aplicación y tengo algo de lógica que compara el valor con µ.

Y me di cuenta de que incluso si los textos son los mismos, el valor de comparación es falso.

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

En la línea posterior se copia el carácter µ.

Sin embargo, es posible que estos no sean los únicos personajes que sean así.

¿Hay alguna forma en C # de comparar los caracteres que se ven iguales pero en realidad son diferentes?

DJ
fuente
158
Parece que has encontrado el mu de Schrödinger.
BoltClock
19
Son personajes diferentes, aunque tienen el mismo aspecto, tienen códigos de caracteres diferentes.
user2864740
93
Bienvenido a Unicode.
ta.speot.is el
11
¿Qué quieres lograr? que esos dos deben ser iguales, entonces incluso su código de carácter es diferente pero la misma cara?
Jade
28
"Se parecen" y "se ven iguales" son conceptos vagos. ¿Se refieren a la identidad de los glifos o simplemente a una similitud cercana? ¿Qué cerca? Tenga en cuenta que dos caracteres pueden tener glifos idénticos en alguna fuente, muy similares en otra y bastante diferentes en otra fuente. Lo que importa es por qué haría tal comparación y en qué contexto (y la aceptabilidad de falsos positivos y falsos negativos).
Jukka K. Korpela

Respuestas:

125

En muchos casos, puede normalizar ambos caracteres Unicode a una determinada forma de normalización antes de compararlos, y deberían poder coincidir. Por supuesto, la forma de normalización que debe utilizar depende de los propios personajes; el hecho de que se parezcan no significa necesariamente que representen el mismo personaje. También debe considerar si es apropiado para su caso de uso; consulte el comentario de Jukka K. Korpela.

Para esta situación particular, si consulta los enlaces en la respuesta de Tony , verá que la tabla para U + 00B5 dice:

Descomposición <compat> GRIEGA MINÚSCULA MU (U + 03BC)

Esto significa que U + 00B5, el segundo carácter en su comparación original, se puede descomponer en U + 03BC, el primer carácter.

Por lo tanto, normalizará los caracteres mediante la descomposición de compatibilidad total, con las formas de normalización KC o KD. Aquí hay un ejemplo rápido que escribí para demostrar:

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 obtener detalles sobre la normalización Unicode y las diferentes formas de normalización, consulte System.Text.NormalizationFormy la especificación Unicode .

BoltClock
fuente
26
Gracias por el enlace de especificaciones Unicode. Primera vez que leí sobre eso. Una pequeña nota de él: "Las formas de normalización KC y KD no deben aplicarse ciegamente a texto arbitrario. Es mejor pensar en estas formas de normalización como asignaciones en mayúsculas o minúsculas: útiles en ciertos contextos para identificar significados centrales, pero también modificaciones al texto que pueden no siempre ser apropiadas ".
user2864740
149

Debido a que son símbolos realmente diferentes, incluso si se ven iguales, el primero es la letra real y tiene char code = 956 (0x3BC)y el segundo es el micro signo y tiene181 (0xB5) .

Referencias:

Entonces, si desea compararlos y necesita que sean iguales, debe manejarlo manualmente o reemplazar un carácter por otro antes de la comparación. O use el siguiente 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);
}

Y la demo

Tony
fuente
11
Por curiosidad, ¿cuál es el motivo para tener dos símbolos µ? No ve una K dedicada con el nombre "Signo Kilo" (¿o no?).
MartinHaTh
12
@MartinHaTh: Según Wikipedia, es "por razones históricas" .
BoltClock
12
Unicode tiene muchos caracteres de compatibilidad traídos de conjuntos de caracteres más antiguos (como ISO 8859-1 ), para facilitar la conversión de esos conjuntos de caracteres. Cuando los conjuntos de caracteres se limitaban a 8 bits, incluían algunos glifos (como algunas letras griegas) para los usos matemáticos y científicos más comunes. La reutilización de glifos basada en la apariencia era común, por lo que no se agregó una 'K' especializada. Pero siempre fue una solución alternativa; el símbolo correcto para "micro" es la minúscula griega mu, el símbolo correcto para Ohm es la mayúscula omega real, y así sucesivamente.
VGR
8
Nada mejor que cuando se hace algo para pasas histéricas
Paulm
11
¿Existe una K especial para los cereales?
86

Ambos tienen códigos de caracteres diferentes: consulte esto para obtener más detalles

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

Donde, el primero es:

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

Imagen

Vishal Suthar
fuente
39

Para el ejemplo específico de μ(mu) y µ(micro signo), el último tiene una descomposición de compatibilidad con el primero, por lo que puede normalizar la cadena FormKCaoFormKD convertir los micro signos en mus.

Sin embargo, hay muchos conjuntos de caracteres que se parecen pero no son equivalentes en ninguna forma de normalización Unicode. Por ejemplo, A(latín), Α(griego) y А(cirílico). El sitio web Unicode tiene un archivo confusables.txt con una lista de estos, destinado a ayudar a los desarrolladores a protegerse contra los ataques homógrafos . Si es necesario, puede analizar este archivo y crear una tabla para la "normalización visual" de cadenas.

dan04
fuente
Definitivamente es bueno saber cuándo se usa Normalizar. Parece sorprendente que sigan siendo distintos.
user2864740
4
@ user2864740: si una tau griega mayúscula no se diferenciara de una letra T romana, sería muy difícil que el texto griego y romano se clasificaran de forma sensata en orden alfabético. Además, si un tipo de letra usara un estilo visual diferente para las letras griegas y romanas, sería una gran distracción si las letras griegas cuyas formas se parecían a las letras romanas se representaran de manera diferente a las que no lo hacen.
supercat
7
Más importante aún, la unificación de los alfabetos europeos haría ToUpper/ ToLowerdifícil de implementar. Debería "B".ToLower()estar ben inglés, pero βen griego y вen ruso. Tal como está, solo el turco (sin puntos i) y un par de otros idiomas necesitan reglas de mayúsculas y minúsculas diferentes de las predeterminadas.
dan04
@ dan04: Me pregunto si alguien alguna vez consideró asignar puntos de código únicos a las cuatro variaciones de la "i" e "I" turcas. Eso habría eliminado cualquier ambigüedad en el comportamiento de toUpper / toLower.
supercat
34

Busque ambos caracteres en una base de datos Unicode y vea la diferencia .

Una es la letra minúscula griega µ y la otra es el micro signo µ .

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
fuente
4
¿Cómo consiguió esto 37 votos a favor? No responde la pregunta ("Cómo comparar caracteres Unicode"), solo comenta por qué este ejemplo en particular no es igual. En el mejor de los casos, debería ser un comentario sobre la pregunta. Entiendo que las opciones de formato de comentarios no permiten publicarlo tan bien como lo hacen las opciones de formato de respuesta, pero esa no debería ser una razón válida para publicar como respuesta.
Konerak
5
En realidad, la pregunta era diferente, preguntando por qué el control de igualdad μ y μ devuelve falso. Esta respuesta responde. Más tarde, OP hizo otra pregunta (esta pregunta) cómo comparar dos personajes que se parecen. Ambas preguntas tuvieron mejores respuestas y luego uno de los moderadores fusionó ambas preguntas seleccionando la mejor respuesta de la segunda como mejor. Alguien editó esta pregunta, para que resuma
Subin Jacob
En realidad, no
agregué
24

EDITAR Después de la fusión de esta pregunta con Cómo comparar 'μ' y 'µ' en C #
Respuesta original publicada:

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

EDITAR Después de leer los comentarios, sí, no es bueno usar el método anterior porque puede proporcionar resultados incorrectos para algún otro tipo de entradas, para esto debemos usar normalizar usando la descomposición de compatibilidad completa como se menciona en wiki . (Gracias a la respuesta publicada 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();
    }

Salida

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
 ________________________________________________________________

Mientras leía información en Unicode_equivalence encontré

La elección de los criterios de equivalencia puede afectar los resultados de la búsqueda. Por ejemplo, algunas ligaduras tipográficas como U + FB03 (ffi), ..... de modo que una búsqueda de U + 0066 (f) como subcadena tendría éxito en una normalización NFKC de U + FB03 pero no en la normalización NFC de U + FB03.

Por tanto, para comparar la equivalencia deberíamos utilizar normalmente, por FormKCejemplo, la normalización NFKC o, por FormKDejemplo, la normalización NFKD.
Tenía un poco de curiosidad por saber más sobre todos los caracteres Unicode, así que hice una muestra que iteraría sobre todo el carácter Unicode UTF-16y obtuve algunos resultados que quiero discutir

  • Información sobre caracteres cuyos valores normalizados FormCy FormDno eran equivalentes
    Total: 12,118
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-253, ..... 44032-55203
  • Información sobre caracteres cuyos valores normalizados FormKCy FormKDno eran 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
  • Todos los caracteres cuyo valor normalizado FormCy FormDno eran equivalentes, allí FormKCy los FormKDvalores normalizados tampoco eran equivalentes excepto estos caracteres
    Caracteres:901 '΅', 8129 '῁', 8141 '῍', 8142 '῎', 8143 '῏', 8157 '῝', 8158 '῞'
    , 8159 '῟', 8173 '῭', 8174 '΅'
  • Cuyo carácter adicional FormKCy FormKDvalor normalizado no eran equivalentes, pero FormCy FormDvalores normalizados fueron equivalentes
    Total: 119
    Caracteres: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'
  • Hay algunos personajes que no se pueden normalizar , lanzan ArgumentExceptionsi se intenta
    Total:2081 Characters(int value): 55296-57343, 64976-65007, 65534

Estos enlaces pueden ser realmente útiles para comprender qué reglas rigen para la equivalencia Unicode

  1. Unicode_equivalence
  2. Caracteres_compatibilidad_unicode
dbw
fuente
4
Extraño pero funciona ... quiero decir que son dos caracteres diferentes con diferentes significados y convertirlos a superiores los hace iguales. No veo la lógica, pero una buena solución +1
BudBrot
45
Esta solución enmascara el problema y podría causar problemas en un caso general. Este tipo de prueba encontraría eso "m".ToUpper().Equals("µ".ToUpper());y "M".ToUpper().Equals("µ".ToUpper());también es cierto. Esto puede no ser deseable.
Andrew Leach
6
-1 - esta es una idea terrible. No trabaje con Unicode de esta manera.
Konrad Rudolph
1
En lugar de trucos basados ​​en ToUpper (), ¿por qué no utilizar String.Equals ("μ", "μ", StringComparison.CurrentCultureIgnoreCase)?
svenv
6
Hay una buena razón para distinguir entre "MICRO SIGN" y "GRIEGO MINÚSCULA LETRA MU" - para decir que "mayúscula" de micro sign sigue siendo micro sign. Pero la capitalización cambia de micro a mega, feliz ingeniería.
Greg
9

Lo más probable es que haya dos códigos de caracteres diferentes que hacen (visiblemente) el mismo carácter. Aunque técnicamente no son iguales, se ven iguales. Eche un vistazo a la tabla de personajes y vea si hay varias instancias de ese personaje. O imprima el código de carácter de los dos caracteres en su código.

PMF
fuente
6

Pregunta "cómo compararlos" pero no nos dice qué quiere hacer.

Hay al menos dos formas principales de compararlos:

O los comparas directamente como eres y son diferentes

O usa la Normalización de compatibilidad Unicode si lo que necesita es una comparación que los encuentre para coincidir.

Sin embargo, podría haber un problema porque la normalización de la compatibilidad Unicode hará que muchos otros caracteres se comparen igual. Si desea que solo estos dos caracteres sean tratados como iguales, debe lanzar sus propias funciones de normalización o comparación.

Para una solución más específica, necesitamos conocer su problema específico. ¿En qué contexto se encontró con este problema?

hippie
fuente
1
¿Son canónicamente equivalentes el "micro signo" y el carácter mu en minúscula? El uso de la normalización canónica le proporcionaría una comparación más estricta.
Tanner Swett
@ TannerL.Swett: En realidad, ni siquiera estoy seguro de cómo comprobar eso en la parte superior de mi cabeza ...
hippietrail
1
De hecho, estaba importando un archivo con fórmula física. Tienes razón sobre la normalización. Tengo que analizarlo más profundamente ..
DJ
¿Qué tipo de archivo? ¿Algo hecho a mano en texto Unicode sin formato por una persona? ¿O algo generado por una aplicación en un formato específico?
hippietrail
5

Si quisiera ser pedante, diría que su pregunta no tiene sentido, pero como nos acercamos a la Navidad y los pájaros cantan, continuaré con esto.

En primer lugar, las 2 entidades que intentas comparar son glyphs, un glifo es parte de un conjunto de glifos proporcionado por lo que generalmente se conoce como "fuente", lo que generalmente viene en un ttf,otf o cualquier formato de archivo que tenga utilizando.

Los glifos son una representación de un símbolo dado y, dado que son una representación que depende de un conjunto específico, no puede esperar tener 2 símbolos idénticos similares o incluso "mejores", es una frase que no tiene sentido si considera el contexto, al menos debe especificar qué fuente o conjunto de glifos está considerando cuando formula una pregunta como esta.

Lo que generalmente se usa para resolver un problema similar al que está encontrando, es un OCR, esencialmente un software que reconoce y compara glifos. Si C # proporciona un OCR de forma predeterminada, no lo sé, pero generalmente es muy malo. idea si realmente no necesita un OCR y sabe qué hacer con él.

Es posible que termine interpretando un libro de física como un libro griego antiguo sin mencionar el hecho de que los OCR son generalmente costosos en términos de recursos.

Hay una razón por la cual esos caracteres están localizados de la forma en que están localizados, simplemente no hagas eso.

usuario2485710
fuente
1

Es posible dibujar ambos caracteres con el mismo estilo y tamaño de fuente con DrawString método. Una vez generados dos mapas de bits con símbolos, es posible compararlos píxel por píxel.

La ventaja de este método es que puede comparar no solo caracteres absolutamente iguales, sino también similares (con tolerancia definida).

Ivan Kochurkin
fuente