¿Cómo comparar caracteres Unicode que “se parecen”?

Caigo en un problema sorprendente.

Cargué un archivo de texto en mi aplicación y tengo una lógica que compara el valor que tiene μ.

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

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

En la línea posterior, el carácter μ se copia pegado.

Sin embargo, estos pueden no ser los únicos personajes que son así.

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

En muchos casos, puede normalizar ambos caracteres Unicode a una cierta forma de normalización antes de compararlos, y ellos deberían poder hacer coincidir. Por supuesto, qué forma de normalización necesitas usar depende de los personajes mismos; solo porque 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 se refiere a los enlaces en la respuesta de Tony , verá que la tabla para U + 00B5 dice:

Descomposición GRIETA GRIEGA MU (U + 03BC)

Esto significa que U + 00B5, el segundo personaje en su comparación original, puede descomponerse en U + 03BC, el primer personaje.

Por lo tanto, normalizará los caracteres utilizando la descomposición de compatibilidad completa, 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 más información sobre la normalización Unicode y las diferentes formas de normalización, consulte System.Text.NormalizationForm y la especificación Unicode .

Como en realidad son símbolos diferentes, aunque parezcan iguales, primero es la letra real y tiene el code = 956 (0x3BC) y el segundo es el signo de micro y tiene 181 (0xB5) .

Referencias

  • Carácter Unicode ‘GRUESA PEQUEÑA LETRA MU’ (U + 03BC)
  • Carácter Unicode ‘MICRO SIGN’ (U + 00B5)

Por lo tanto, si desea compararlos y necesita que sean iguales, debe manejarlos manualmente o reemplazar uno por otro antes de realizar una comparación. O usa 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

Ambos tienen diferentes códigos de personaje: refiéranlo para más detalles

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

Donde, el primero es:

 Display Friendly Code Decimal Code Hex Code Description ==================================================================== μ μ μ μ Lowercase Mu µ µ µ µ micro sign Mu 

Imagen

Para el ejemplo específico de μ (mu) y µ (micro-señal), este último tiene una descomposición de compatibilidad con el primero, por lo que puede normalizar la cadena a FormKC o FormKD para convertir los micro signos a mus.

Sin embargo, hay muchos conjuntos de caracteres que se parecen pero que no son equivalentes en ninguna forma de normalización Unicode. Por ejemplo, A (latín), Α (griego) y А (cirílico). El sitio web de 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 las cadenas.

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

Una es la Letra pequeña griega µ y la otra es la Micro Señal µ .

 Name : MICRO SIGN Block : Latin-1 Supplement Category : Letter, Lowercase [Ll] Combine : 0 BIDI : Left-to-Right [L] Decomposition :  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) 

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 otro tipo de entradas, para esto debemos usar normalizar usando 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 que encontré

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

Por lo tanto, para comparar la equivalencia, normalmente deberíamos utilizar FormKC es decir, la normalización de FormKD o FormKD es decir, la normalización de NFKD.
Estaba un poco curioso de saber más sobre todos los caracteres Unicode, así que hice una muestra que iteraría sobre todo el carácter Unicode en UTF-16 y obtuve algunos resultados de los que quiero hablar

  • Información sobre los caracteres cuyos valores normalizados FormC y FormD no son equivalentes
    Total: 12,118
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-253, ..... 44032-55203
  • Información sobre los caracteres cuyos valores normalizados de FormKC y FormKD no 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 FormC y FormD no eran equivalentes, los valores normalizados de FormKC y FormKD tampoco eran equivalentes, excepto estos caracteres
    Caracteres: 901 '΅', 8129 '῁', 8141 '῍', 8142 '῎', 8143 '῏', 8157 '῝', 8158 '῞'
    , 8159 '῟', 8173 '῭', 8174 '΅'
  • Carácter adicional cuyo valor normalizado de FormKC y FormKD no era equivalente, pero los valores normalizados de FormC y FormD eran equivalentes
    Total: 119
    Personajes: 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' 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 pueden ser normalizados , lanzan ArgumentException si lo intentan
    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. Unicode_compatibility_characters

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

Usted pregunta “cómo compararlos” pero no nos dice lo que 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 necesita para una comparación que los encuentre coincidentes.

Sin embargo, podría haber un problema porque la normalización de compatibilidad Unicode hará que muchos otros personajes se igualen. Si desea que solo se trate a estos dos personajes como iguales, debe desplegar sus propias funciones de normalización o comparación.

Para una solución más específica, necesitamos conocer su problema específico. ¿Cuál es el contexto en el que se encontró con este problema?

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

En primer lugar, las 2 entidades que intentas comparar son glyph , un glifo es parte de un conjunto de glifos proporcionados por lo que normalmente se conoce como “fuente”, lo que normalmente viene en un ttf , otf o cualquier archivo formato que está 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 se puede esperar tener 2 símbolos idénticos o incluso “mejores” idénticos, es una frase que no tiene sentido si considera el contexto, al menos debe especificar qué fuente o conjunto de glifos está considerando al formular una pregunta como esta.

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

Posiblemente pueda terminar interpretando un libro de física como un antiguo libro griego sin mencionar el hecho de que los OCR son generalmente caros en términos de recursos.

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

Es posible dibujar ambos caracteres con el mismo estilo y tamaño de fuente con DrawString método DrawString . Después de que se hayan generado 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 absoluto charcters iguales, sino también similares (con tolerancia definida).