¿Cómo puedo realizar un personaje consciente de Unicode por comparación de caracteres?

Mi aplicación tiene un objective internacional, personas de muchos países lo usarán e ingresarán texto (texto que debo procesar) utilizando su propio idioma.

Si, por ejemplo, tengo que enumerar las diferencias de dos cadenas usando una comparación carácter por personaje, ¿es suficiente este simple código C o me falta algo?

var differences = new List<Tuple>(); for (int i=0; i < myString1.Length; ++i) { if (myString1[i] != myString2[i]) differences.Add(new Tuple(i, myString1[i], myString2[i])); } 

¿Se aplica un código efectivo para realizar esta tarea en diferentes idiomas (mis usuarios no están limitados al conjunto de caracteres de EE. UU.)?

Codificación

Unicode define una lista de caracteres (letras, números, símbolos analfabeticos, códigos de control y otros) pero su representación (en bytes) se define como encoding . Las codificaciones Unicode más comunes en la actualidad son UTF-8, UTF-16 y UTF-32. UTF-16 es lo que generalmente se asocia con Unicode porque es lo que se ha elegido para el soporte Unicode en Windows, Java, entorno NET, lenguaje C y C ++ (en Windows). Tenga en cuenta que no es el único y durante su vida seguramente también encontrará texto UTF-8 (especialmente desde la web y en el sistema de archivos Linux) y UTF-32 (fuera del mundo de Windows). Un artículo de introducción obligatoria: El mínimo absoluto. Todo desarrollador de software. Absolutamente, definitivamente, debe saber sobre Unicode y juegos de caracteres (¡Sin excusas!) Y UTF-8 Everywhere – Manifesto . IMO especialmente el segundo enlace (independientemente de su opinión UTF-8 vs UTF-16) es bastante esclarecedor.

Permítanme citar Wikipedia:

Debido a que los personajes más comúnmente utilizados se encuentran todos en el plano multilingüe básico, el manejo de los pares sustituidos a menudo no se prueba minuciosamente. Esto conduce a errores persistentes y potenciales agujeros de seguridad, incluso en software de aplicación popular y bien revisado (por ejemplo, CVE-2008-2938, CVE-2012-2135)

Para ver dónde está el problema simplemente comience con algunos cálculos simples: Unicode define alrededor de 110K puntos de código (tenga en cuenta que no todos son grafemas ). El “tipo de carácter Unicode” en C, C ++, C #, VB.NET, Java y muchos otros lenguajes en el entorno Windows (con la notable excepción de VBScript en páginas ASP antiguas clásicas) está codificado en UTF-16 y tiene dos bytes (aquí intuitivo pero completamente engañoso porque es una unidad de código , no un personaje ni un punto de código).

Compruebe esta distinción porque es fundamental: una unidad de código es lógicamente diferente de un personaje y, aunque a veces coincidan, no son lo mismo. ¿Cómo afecta esto tu vida de progtwigción? Imagina que tienes este código C # y tus especificaciones (escritas por alguien que piensa en la verdadera definición de personaje) dice que “la longitud de la contraseña debe ser de 4 caracteres “:

 bool IsValidPassword(string text ) { return text.Length >= 4; } 

Ese código es feo, incorrecto y roto . Length propiedad de Length devuelve el número de unidades de código en la variable de cadena de text y ahora sabe que son diferentes. Su código validará n̊o̅ como contraseña válida (pero está compuesto por dos caracteres, cuatro puntos de código, que casi siempre coinciden con las unidades de código). Ahora intenta imaginar esto aplicado a todas las capas de tu aplicación: un campo de base de datos codificada UTF-8 validado con código previo (donde la entrada es UTF-16), los errores se sumrán y tu amigo polaco Świętosław Koźmicki no estará contento con esto. . Ahora piense que debe validar el nombre del usuario con la misma técnica y que sus usuarios sean chinos (pero no se preocupe, si no le importa, serán sus usuarios por muy poco tiempo). Otro ejemplo: este ingenuo algoritmo de C # para contar distintos caracteres en una cadena fallará por la misma razón:

 myString.Distinct().Count() 

Si el usuario ingresa este carácter Han 𠀑, entonces su código retornará erróneamente … 2 porque su representación UTF-16 es 0xD840 0xDC11 (por cierto, cada uno de ellos, solo, no es un carácter Unicode válido porque son alto y bajo sustituto, respectivamente ) Las razones se explican con más detalle en esta publicación , también se proporciona una solución de trabajo, así que simplemente repito aquí el código esencial:

 StringInfo.GetTextElementEnumerator(text) .AsEnumerable() .Distinct() .Count(); 

Esto es más o menos equivalente a codePointCount() en Java para contar puntos de código en una cadena. Necesitamos AsEnumerable() porque GetTextElementEnumerator() devuelve IEnumerator lugar de IEnumerable , se describe una implementación simple en Dividir una cadena en fragmentos de la misma longitud .

¿Esto solo está relacionado con la longitud de la cuerda? Por supuesto que no, si manejas la entrada de teclado Char by Char es posible que necesites reparar tu código. Vea, por ejemplo, esta pregunta sobre los caracteres coreanos manejados en el evento KeyUp .

Sin relación pero IMO útil para entender, este código C (tomado de esta publicación ) funciona en char (ASCII / ANSI o UTF-8) pero no funcionará si se convierte directamente para usar wchar_t :

 wchar_t* pValue = wcsrchr(wcschr(pExpression, L'|'), L':') + 1; 

Tenga en cuenta que en C ++ 11 hay un gran conjunto de clases para manejar alias de encoding y tipo más claro: char8_t , char16_t y char32_t para, respectivamente, caracteres codificados UTF-8, UTF-16 y UTF-32. Tenga en cuenta que también tiene std::u8string , std::u16string y std::u32string . Tenga en cuenta que incluso si length() (y su alias de size() aún devuelven el recuento de unidades de código, puede realizar fácilmente conversiones de encoding con la función de plantilla codecvt() y usar estos tipos de IMO hará que su código sea más claro y explícito ( no es el size() asombroso size() de u16string devolverá el número de elementos char16_t ). Para obtener más detalles sobre el recuento de caracteres en C ++, consulte esta buena publicación . En C las cosas son bastante más fáciles con la encoding char y UTF-8: esta IMO es una lectura obligada.

Diferencias culturales

No todos los idiomas son similares, ni siquiera comparten algunos conceptos básicos. Por ejemplo, nuestra definición actual de grafema puede estar muy lejos de nuestro concepto de carácter . Permítanme explicar con un ejemplo: en coreano las letras del alfabeto hangul se combinan en una sola sílaba (y tanto las letras como las sílabas son caracteres, simplemente representados de una manera diferente cuando están solos y en una palabra con otras letras). La palabra ( Guk ) es una sílaba compuesta por tres letras , y (la primera y la última letra son las mismas pero se pronuncian con diferentes sonidos cuando están al principio o al final de una palabra, es por eso que están transliterado g y k ).

Las sílabas nos permiten introducir otro concepto: secuencias precompuestas y descompuestas . Hangul sílaba Han se puede representar como un solo carácter ( U+0D55C ) o una secuencia descompuesta de letras , y . Si estás, por ejemplo, leyendo un archivo de texto, puedes tener ambos (y los usuarios pueden ingresar ambas secuencias en tus cuadros de entrada), pero deben ser iguales. Tenga en cuenta que si escribe esas letras secuencialmente, se mostrarán siempre como una sola sílaba (copie y pegue caracteres individuales, sin espacios, e intente), pero la forma final (precompuesta o descompuesta) depende de su IME.

En checo “ch” es un dígrafo y se trata como una sola letra. Tiene su propia regla de intercalación (está entre H e I ), ¡con la clasificación checa fyzika viene antes que chemie ! Si cuentas Caracteres y les dices a tus usuarios que la palabra Chechtal está compuesta por 8 Personajes, pensarán que tu software tiene errores y tu apoyo para su idioma se limita a un conjunto de recursos traducidos. Agreguemos excepciones: en puchoblík (y algunas otras palabras) C y H no son un dígrafo y están separados. Tenga en cuenta que también hay otros casos como “dž” en eslovaco y otros donde se cuenta como un solo carácter, incluso si utiliza dos / tres puntos de código UTF-16. Lo mismo sucede también en muchos otros idiomas (por ejemplo, ll en catalán). ¡Los idiomas verdaderos tienen más excepciones y casos especiales que PHP!

Tenga en cuenta que la apariencia por sí sola no siempre es suficiente para la equivalencia, por ejemplo: A ( U+0041 LETRA MAYÚSCULA LATINA A) no es equivalente a А ( U+0410 LETRA MAYÚSCULA CYRÍLICA A). Por el contrario, el carácter 2 ( U+0662 DÍGITO ÁRABE-INDÍGENO DOS) y 2 ( U+06F2 ÁRABE- U+06F2 EXTENDIDO DOS) son visualmente y conceptualmente equivalentes, pero son puntos de código Unicode diferentes (consulte también el siguiente párrafo sobre números y sinónimos ).

Símbolos como ? y ! a veces se usan como caracteres, por ejemplo, el lenguaje Haida más antiguo). En algunos idiomas (como las primeras formas escritas de los idiomas de los nativos americanos), también se han tomado prestados números y otros símbolos del alfabeto latino y se usan como letras (tenga en cuenta que si tiene que manejar esos idiomas y tiene que quitar los símbolos alfanuméricos, Unicode puede ‘ t distinguir esto), un ejemplo ! Kung en lengua africana khoisan. En catalán, cuando ll no es un dígrafo, usan un signo diacrítico (o un +U00B7 ( +U00B7 ) …) para separar los caracteres, como en cel·les (en este caso, el recuento de caracteres es 6 y las unidades de código / puntos de código son 7 donde una hipotética palabra no existente celles daría como resultado 5 caracteres).

Se puede escribir la misma palabra usando en más de una forma. Esto puede ser algo que debe preocuparse si, por ejemplo, proporciona una búsqueda de texto completo. Por ejemplo, la palabra china 家 (casa) puede transcribirse como Jiā en pinyin y en japonés la misma palabra también puede escribirse con el mismo Kanji 家 o como い え en Hiragana (y otros también) o transliterarse en romaji como es decir . ¿Esto está limitado a las palabras? No, también los personajes, para los números es bastante común: 2 (número árabe en el alfabeto romano), 2 (en árabe y persa) y (chino y japonés) son exactamente el mismo número cardinal. Agreguemos algo de complejidad: en chino también es muy común escribir el mismo número que (simplificado: ). Ni siquiera menciono los prefijos (micro, nano, kilo, etc.). Vea esta publicación para ver un ejemplo real de este problema. No se limita solo a los idiomas del lejano oriente: el apóstrofo ( U+0027 APOSTROPHE o mejor ( U+2019 RIGHT SINGLE QUOTATION MARK) se utiliza a menudo en checo y eslovaco en lugar de en su contraparte superpuesta ( U+02BC LETTER APOSTROPHE): d’ and d ‘ son entonces equivalentes (similar a lo que dije sobre middot en catalán).

Quizás debería manejar adecuadamente minúsculas “ss” en alemán para compararlas con ß (y surgirán problemas para la comparación insensible a mayúsculas y minúsculas). Un problema similar está en turco si tiene que proporcionar una coincidencia de cadena no exacta para i y sus formularios (consulte la sección sobre Caso ).

Si trabajas con texto profesional , también puedes encontrar ligaduras; incluso en inglés, por ejemplo æsthetics tiene 9 puntos de código pero 10 caracteres. Lo mismo se aplica, por ejemplo, para el carácter ethel œ ( U+0153 LETRATO PEQUEÑO LATINO OE, absolutamente necesario si está trabajando con texto en francés); horse d’ouvre es equivalente a horse d’œvre (pero también ethel y œthel ). Ambas son ligaduras léxicas (junto con alemán ß ) pero también puede encontrar ligaduras tipográficas (como ff U+FB00 LATIN SMALL LIGATURE FF) y tienen su propia parte en el juego de caracteres Unicode ( formularios de presentación ). Hoy diacríticos son mucho más comunes incluso en inglés (ver la publicación de Tchrist sobre personas liberadas de la tiranía de la máquina de escribir , por favor, lea cuidadosamente la cita de Bringhurst). ¿Crees que tú (y tus usuarios) nunca escribirán façade , ingenuo y prêt-à-porter o noöne o coöperation “con clase”?

Aquí ni siquiera menciono el conteo de palabras porque abrirá aún más problemas: en coreano, cada palabra está compuesta por sílabas pero, por ejemplo, en chino y japonés, los caracteres se cuentan como palabras (a menos que desee implementar conteo de palabras usando un diccionario). Ahora tomemos esta oración china: 文本 是 示例 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本 文本. ¿Cómo los cuentas? Además, si se transcriben a Shì yīgè shìlì wénběn y Kore wa, sanpuru no tekisutodesu, ¿ entonces deberían combinarse en una búsqueda de texto?

Hablando de japonés: los caracteres latinos de ancho completo son diferentes de los caracteres de medio ancho y si su entrada es de texto romaji japonés debe manejar esto; de lo contrario, los usuarios se sorprenderán cuando T no se compare con T (en este caso, lo que debería ser) los glifos se convirtieron en puntos de código).

OK, ¿esto es suficiente para resaltar la superficie del problema?

Personajes duplicados

Unicode (primario para compatibilidad ASCII y otras razones históricas) tiene caracteres duplicados, antes de hacer una comparación debe realizar la normalización; de lo contrario, un (punto de código único) no será igual a ( a más U+0300 COMBINACIÓN DE ACENTO GRAVE). ¿Es este un caso raro en la esquina? No realmente, también eche un vistazo a este ejemplo del mundo real de Jon Skeet. También (vea la sección Diferencia de Cultivo ) las secuencias precompuestas y descompuestas introducen duplicados .

Tenga en cuenta que los diacríticos no son solo fuente de confusión. Cuando el usuario está escribiendo con su teclado, probablemente ingrese ' ( U+0027 APOSTROPHE) pero se supone que también debe coincidir ' ( U+2019 RIGHT SINGLE QUOTATION MARK) normalmente utilizado en tipografía (lo mismo es cierto para muchos símbolos Unicode casi equivalentes desde el punto de vista del usuario, pero distinto en tipografía, imagina escribir una búsqueda de texto dentro de libros digitales).

En resumen, dos cadenas deben considerarse iguales (¡este es un concepto muy importante!) Si son canónicamente equivalentes y son canónicamente equivalentes si tienen el mismo significado y apariencia lingüística, incluso si están compuestas de diferentes puntos de código Unicode.

Caso

Si tiene que realizar una comparación insensible a mayúsculas y minúsculas, tendrá aún más problemas . Supongo que no realizas comparaciones insensibles a mayúsculas y minúsculas utilizando toupper() o equivalente a menos que, de todos 'i'.ToUpper() != 'I' , quieras explicar a tus usuarios por qué 'i'.ToUpper() != 'I' para el idioma turco ( I no es superior caso de i que es İ . Por cierto letra minúscula para I es ı ).

Otro problema es el eszett ß en alemán (una ligadura para s largos y cortos utilizados en la antigüedad, también en inglés elevado a la dignidad de un personaje). Tiene una versión en mayúscula pero (en este momento) .NET Framework devuelve erróneamente "ẞ" != "ß".ToUpper() (pero su uso es obligatorio en algunos escenarios, ver también esta publicación ). Desafortunadamente, no siempre ss se convierte en (mayúsculas), no siempre ss es igual a ß (minúsculas) y también sz a veces es en mayúsculas. Confuso, ¿verdad?

Aún más

La globalización no se trata solo de texto: qué ocurre con las fechas y los calendarios, el formato y el análisis de los números, los colores y el diseño. Un libro no será suficiente para describir todo lo que debería interesarle, pero lo que destacaría aquí es que pocas cadenas localizadas no prepararán su aplicación para un mercado internacional.

Incluso en el texto surgen más preguntas : ¿cómo se aplica a regex? ¿Cómo se deben manejar los espacios? ¿Es un espacio em igual a un espacio en ? En una aplicación profesional , ¿cómo se debe comparar “EE. UU.” Con “EE. UU.” (En una búsqueda de texto libre)? En la misma línea de pensamiento: ¿cómo gestionar diacríticos en comparación?

¿Cómo manejar el almacenamiento de texto? Olvídese de que puede detectar de manera segura la encoding, para abrir un archivo necesita conocer su encoding. Por supuesto, a menos que planee hacer como los analizadores HTML con o XML / XHTML encoding="UTF-8" en ).

“Introducción” histórica

Lo que vemos como texto en nuestros monitores es solo un pedazo de bytes en la memoria de la computadora. Por convención, cada valor (o grupo de valores, como un int32_t representa un número) representa un carácter . Cómo se dibuja ese personaje en la pantalla se delega a otra cosa (para simplificar, piensa un poco en una fuente ).

Si arbitrariamente decidimos que cada carácter está representado con un byte, tenemos disponibles 256 símbolos (como cuando usamos int8_t , System.SByte o java.lang.Byte para un número, tenemos un rango numérico de 256 valores). Lo que necesitamos ahora para decidir cada valor que personaje representa, un ejemplo de esto es ASCII (limitado a 7 bits, 128 valores) con extensiones personalizadas para usar también 128 valores superiores.

Eso está hecho , encoding de caracteres habemus para 256 símbolos (incluyendo letras, números, caracteres analfabeticos y códigos de control). Sí, cada extensión ASCII es propietaria pero las cosas son claras y fáciles de administrar. El procesamiento de texto es tan común que solo necesitamos agregar un tipo de datos adecuado en nuestros idiomas favoritos ( char en C, tenga en cuenta que formalmente no es un alias para unsigned char o signed char pero un tipo distinto ; char en Pascal; character en FORTRAN y etc.) y pocas funciones de biblioteca para gestionar eso.

Desafortunadamente no es tan fácil. ASCII se limita a un conjunto de caracteres muy básico e incluye solo caracteres latinos utilizados en EE. UU. (Por eso su nombre preferido debe ser usASCII). Es tan limitado que incluso las palabras en inglés con signos diacríticos no son compatibles (si esto hizo el cambio en el lenguaje moderno o viceversa es otra historia ). Verá que también tiene otros problemas (por ejemplo, su orden de clasificación incorrecto con los problemas de comparación ordinal y alfabética ).

¿Cómo lidiar con eso? Introduzca un nuevo concepto: páginas de códigos . Mantenga un conjunto fijo de caracteres básicos (ASCII) y agregue otros 128 caracteres específicos para cada idioma. El valor 0x81 representará el carácter cirílico Б (en la página de códigos DOS 866) y el carácter griego Ϊ (en la página de códigos DOS 869).

Ahora surgen problemas serios: 1) no se puede mezclar en el mismo archivo de texto diferentes alfabetos. 2) Para entender correctamente un texto, también debe saber con qué página de códigos se expresa. ¿Dónde? No hay un método estándar para eso y tendrás que manejar este usuario o con una conjetura razonable (?!). Incluso hoy en día el “formato” de archivo ZIP está limitado a ASCII para los nombres de los archivos (puede usar UTF-8 – vea más adelante – pero no es estándar) porque no hay un formato ZIP estándar). En este post, una solución de trabajo Java . 3) Incluso las páginas de códigos no son estándar y cada entorno tiene conjuntos diferentes (incluso las páginas de códigos de DOS y las páginas de códigos de Windows son diferentes) y también varían los nombres. 4) 255 caracteres son todavía muy pocos para, por ejemplo, el idioma chino o japonés, entonces se han introducido codificaciones más complicadas ( Shift JIS , por ejemplo).

La situación era terrible en ese momento (~ 1985) y un estándar era absolutamente necesario. Llegó ISO / IEC 8859 y, al menos, resolvió el punto 3 en la lista de problemas anterior. Los puntos 1, 2 y 4 aún no se resolvieron y se necesitaba una solución (especialmente si su objective no es solo texto sin formato, sino también caracteres de tipografía especiales ). Este estándar (después de muchas revisiones) todavía está con nosotros hoy en día (y de alguna manera coincide con la página de códigos de Windows-1252) pero probablemente nunca lo usará a menos que esté trabajando con algún sistema heredado.

El estándar que surgió para salvarnos de este caos es mundialmente conocido: Unicode . De la Wikipedia :

Unicode es un estándar de la industria informática para la encoding, representación y manejo consistentes del texto expresado en la mayoría de los sistemas de escritura del mundo. […] la última versión de Unicode contiene un repertorio de más de 110,000 caracteres que cubren 100 scripts y conjuntos de símbolos múltiples.

Los idiomas, las bibliotecas y los sistemas operativos se han actualizado para ser compatibles con Unicode. Ahora tenemos todos los personajes que necesitamos, un código común compartido para cada uno, y el pasado es solo una pesadilla. Reemplace char con wchar_t (y acepte vivir con wcout , wstring y amigos), solo use System.Char o java.lang.Character y viva feliz. ¿Derecha?

NO. Nunca es tan fácil . La misión Unicode se trata de “… encoding, representación y manejo de texto …” , no traduce y adapta diferentes culturas en un código abstracto (y es imposible hacerlo a menos que mates la belleza en la variedad de todos nuestros idiomas). Además, la encoding en sí misma introduce algunas cosas (¡no tan obvias!) Que debemos tener en cuenta.