Come posso eseguire un confronto con caratteri Unicode per confronto di caratteri?

La mia applicazione ha un target internazionale, le persone di molti paesi lo useranno e inseriranno del testo (testo che devo elaborare) usando la loro lingua.

Se, per esempio, devo elencare le differenze di due stringhe usando un confronto carattere per carattere, questo semplice codice C # è sufficiente o mi manca qualcosa?

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])); } 

Il codice dato è efficace per eseguire questa attività in diverse lingue (i miei utenti non sono limitati ai caratteri USA impostati)?

Codifica

Unicode definisce un elenco di caratteri (lettere, numeri, simboli analfabetici, codici di controllo e altri) ma la loro rappresentazione (in byte) è definita come codifica . Le codifiche Unicode più comuni al giorno d’oggi sono UTF-8, UTF-16 e UTF-32. UTF-16 è ciò che di solito è associato a Unicode perché è ciò che è stato scelto per il supporto Unicode in ambiente Windows, Java, NET, C e C ++ (su Windows). Siate consapevoli che non è l’unico e durante la vostra vita incontrerete sicuramente anche il testo UTF-8 (specialmente dal web e sul file system Linux) e UTF-32 (al di fuori del mondo Windows). Un articolo preliminare deve leggere l’articolo: Il minimo assoluto Ogni sviluppatore di software deve assolutamente conoscere positivamente Unicode e Set di caratteri (nessuna scusa!) E UTF-8 ovunque – Manifesto . Il secondo link IMO (indipendentemente dalla tua opinione UTF-8 vs UTF-16) è piuttosto illuminante.

Lasciatemi citare Wikipedia:

Poiché i personaggi più comunemente usati sono tutti nel piano multilingue multilingue, la gestione delle coppie surrogate spesso non è stata accuratamente testata. Questo porta a bug persistenti e potenziali falle nella sicurezza, anche in applicazioni software popolari e ben recensite (ad esempio CVE-2008-2938, CVE-2012-2135)

Per vedere dove il problema è appena iniziato con alcuni semplici calcoli matematici: Unicode definisce circa 110K punti di codice (si noti che non tutti sono grafemi ). “Tipo di carattere Unicode” in C, C ++, C #, VB.NET, Java e molti altri linguaggi in ambiente Windows (con notevole eccezione di VBScript sulle vecchie pagine ASP classiche) è codificato in UTF-16, quindi è di due byte (il nome del tipo qui è intuitivo ma completamente fuorviante perché è un’unità di codice , non un personaggio né un punto di codice).

Si prega di verificare questa distinzione perché è fondamentale: un’unità di codice è logicamente diversa da un personaggio e, anche se a volte coincidono, non sono la stessa cosa. Come influisce sulla tua vita di programmazione? Immagina di avere questo codice C # e le tue specifiche (scritto da qualcuno che pensa alla vera definizione di Carattere) dice “la lunghezza della password deve essere di 4 caratteri “:

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

Quel codice è brutto, sbagliato e rotto . Length proprietà Length restituisce il numero di unità di codice nella variabile stringa di text e ora sai che sono diverse. Il tuo codice convaliderà n̊o̅ come password valida (ma è composta da due caratteri, quattro punti di codice, che quasi sempre coincidono con le unità di codice). Ora prova a immaginare questo applicato a tutti i livelli della tua applicazione: un campo di database codificato UTF-8 ingenuamente convalidato con codice precedente (dove l’input è UTF-16), gli errori si sumno e il tuo amico polacco Świętosław Koźmicki non sarà felice di questo . Ora pensa di convalidare il nome dell’utente con la stessa tecnica e i tuoi utenti sono cinesi (ma non preoccuparti, se non ti interessa, saranno i tuoi utenti per un tempo molto breve). Un altro esempio: questo ingenuo algoritmo C # per contare caratteri distinti in una stringa fallirà per lo stesso motivo:

 myString.Distinct().Count() 

Se l’utente immette questo carattere Han 𠀑, il codice verrà erroneamente restituito … 2 perché la sua rappresentazione UTF-16 è 0xD840 0xDC11 (BTW ognuno di essi, da solo, non è un carattere Unicode valido perché è un surrogato alto e basso, rispettivamente ). Le ragioni sono spiegate in maggior dettaglio in questo post , viene fornita anche una soluzione funzionante quindi ripeto qui il codice essenziale:

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

Questo è approssimativamente equivalente a codePointCount() in Java per contare i punti di codice in una stringa. Abbiamo bisogno di AsEnumerable() perché GetTextElementEnumerator() restituisce IEnumerator invece di IEnumerable , una semplice implementazione è descritta in Dividere una stringa in blocchi della stessa lunghezza .

Questo qualcosa è legato solo alla lunghezza della corda? Certo che no, se gestisci l’ input da tastiera Char by Char potresti aver bisogno di correggere il tuo codice. Vedi ad esempio questa domanda sui personaggi coreani gestiti nell’evento KeyUp .

Non correlato ma IMO utile per capire, questo codice C (preso da questo post ) funziona su char (ASCII / ANSI o UTF-8) ma fallirà se convertito direttamente per usare wchar_t :

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

Nota che in C ++ 11 c’è un nuovo grande set di classi per gestire alias di tipo encoding e clearer: char8_t , char16_t e char32_t per, rispettivamente, caratteri codificati UTF-8, UTF-16 e UTF-32. Tieni presente che hai anche std::u8string , std::u16string e std::u32string . Notare che anche se length() (e il suo alias di size() restituirà il conteggio delle unità di codice che è ansible eseguire facilmente conversioni di codifica con la funzione di modello codecvt() e utilizzando questi tipi di IMO, il codice verrà reso più chiaro ed esplicito ( non è sorprendente la size() di u16string restituirà il numero di elementi char16_t ). Per maggiori dettagli sui caratteri che contano in C ++, controlla questo bel post . In C le cose sono molto più semplici con la codifica char e UTF-8: questo post IMO è una lettura obbligata.

Differenze culturali

Non tutte le lingue sono simili, non condividono nemmeno alcuni concetti di base. Ad esempio la nostra attuale definizione di grafema può essere piuttosto lontana dal nostro concetto di carattere . Lasciatemi spiegare un esempio: in alfabeto Hangul coreano le lettere sono combinate in un’unica sillaba (e sia le lettere che le sillabe sono caratteri, appena rappresentati in modo diverso quando sono soli e in una parola con altre lettere). Word ( Guk ) è una sillaba composta da tre lettere , e (la prima e l’ultima lettera sono uguali ma sono pronunciate con suoni diversi quando sono all’inizio o alla fine di una parola, ecco perché sono traslitterato g e k ).

Le sillabe ci permettono di introdurre un altro concetto: sequenze precomposte e decomposte . Hangyl sillaba han può essere rappresentata come un singolo carattere ( U+0D55C ) o una sequenza decomposta di lettere , e . Ad esempio, se stai leggendo un file di testo puoi avere entrambi (e gli utenti possono inserire entrambe le sequenze nelle caselle di immissione) ma devono essere confrontati allo stesso modo. Si noti che se si digitano le lettere in sequenza, queste verranno visualizzate sempre come singola sillaba (copia e incolla singoli caratteri – senza spazi e prova), ma la forma finale (precomposta o scomposta) dipende dal proprio IME.

In ceco “ch” è un digramma e viene trattato come una singola lettera. Ha la sua regola per le regole di confronto (è tra H e I ), con l’ordinamento ceco fyzika viene prima di chemie ! Se contate i Personaggi e dite ai vostri utenti che la parola Chechtal è composta da 8 Personaggi, penseranno che il vostro software sia infestato e il vostro supporto per la loro lingua è semplicemente limitato a un mucchio di risorse tradotte. Aggiungiamo eccezioni: in puchoblík (e in poche altre parole) C e H non sono un digrafo e sono separati. Nota che ci sono anche altri casi come “dž” in slovacco e altri dove è contato come singolo carattere anche se usa due / tre punti di codice UTF-16! Lo stesso accade anche in molte altre lingue (per esempio ll in catalano). I veri linguaggi hanno più eccezioni e casi speciali di PHP!

Si noti che l’aspetto da solo non è sempre sufficiente per l’equivalenza, ad esempio: A ( U+0041 LETTER CAPITAL LETTER A) non è equivalente a À ( U+0410 CYRILLIC CAPITAL LETTER A). Viceversa, il carattere 2 ( U+0662 ARABO-INDIC DIGIT TWO) e 2 ( U+06F2 ESTENDIBILE-INDICATO DIGIT DUE) sono visivamente e concettualmente equivalenti ma sono diversi punti di codice Unicode (vedere anche il prossimo paragrafo su numeri e sinonimi ).

Simboli come ? e ! a volte sono usati come caratteri, ad esempio la prima lingua Haida ). In alcune lingue (come la prima forma scritta delle lingue dei nativi americani) anche i numeri e gli altri simboli sono stati presi in prestito dall’alfabeto latino e usati come lettere (attenzione se devi gestire quelle lingue e devi togliere i caratteri alfanumerici dai simboli, Unicode puo ‘ t distinguere questo), un esempio ! Kung in lingua africana Khoisan. In catalano quando ll non è un digrafo usano un diacritico (o un middot ( +U00B7 ) …) per separare i caratteri, come in cel (in questo caso il conteggio dei caratteri è 6 e le unità di codice / i punti di codice sono 7 dove un’ipotetica cella di parole non esistenti risulterebbe in 5 caratteri).

La stessa parola può essere scritta usando in più di un modulo. Questo potrebbe essere qualcosa di cui ti devi preoccupare se, ad esempio, fornisci una ricerca a tutto testo. Per esempio la parola cinese 家 (casa) può essere traslitterata come Jiā in pinyin e in giapponese la stessa parola può anche essere scritta con lo stesso Kanji 家 o come い え in Hiragana (e anche altri) o traslitterata in romaji come es . È limitato alle parole? No, anche i caratteri, per i numeri è piuttosto comune: 2 (numero arabo in alfabeto latino), 2 (in arabo e persiano) e (cinese e giapponese) sono esattamente lo stesso numero cardinale. Aggiungiamo un po ‘di complessità: in cinese è anche molto comune scrivere lo stesso numero di (semplificato: ). Non ho nemmeno menzionato i prefissi (micro, nano, kilo e così via). Vedi questo post per un esempio reale di questo problema. Non è limitato solo alle lingue dell’estremo orientale: l’apostrofo ( U+0027 APOSTROPHE o migliore ( U+2019 PREMIO SINGOLO MARCIA DESTRA) viene usato spesso in ceco e slovacco invece della sua controparte sovrapposta ( U+02BC MODIFIER LETTER APOSTROPHE): e d ‘ sono quindi equivalenti (simile a quello che ho detto su middot in catalano).

Maybeyou dovrebbe gestire correttamente le minuscole “ss” in tedesco per essere paragonate a ß (e i problemi sorgeranno per il confronto tra maiuscole e minuscole). Il problema simile è in turco se è necessario fornire una corrispondenza stringa non esatta per i e i relativi moduli (vedere la sezione su Case ).

Se lavori con testo professionale potresti anche incontrare legature; anche in inglese, ad esempio, l’ estetica è 9 punti in codice ma 10 caratteri! Lo stesso vale, ad esempio, per il carattere ethel œ ( U+0153 LATIN SMALL LIGATURE OE, assolutamente necessario se si sta lavorando con testo francese); l’antipasto equivale a horse d’œvre (ma anche ethel e œthel ). Entrambe sono legature lessicali (insieme a German ß ) ma potresti anche incontrare legature tipografiche (come ff U+FB00 LATIN SMALL LIGATURE FF) e hanno parte del set di caratteri Unicode ( forms di presentazione ). Al giorno d’oggi i segni diacritici sono molto più comuni anche in inglese (vedi il post di tchrist sulle persone liberate dalla tirannia della macchina da scrivere , per favore leggi attentamente la citazione di Bringhurst). Pensi che tu (e i tuoi utenti) non scriverai mai facciate , ingenui e prêt-à-porter o “classy” noöne o coöperation ?

Qui non cito nemmeno il conteggio delle parole perché aprirà ancora più problemi: in coreano ogni parola è composta da sillabe ma, ad esempio, in cinese e giapponese, i caratteri sono contati come parole (a meno che non si voglia implementare il conteggio delle parole usando un dizionario). Ora prendiamo questa frase cinese: 这 是 一个 本 文本 rougly equivalente alla frase giapponese こ れ は, サ ン プ ル の テ キ ス ト で す. Come li contate? Inoltre se sono traslitterati a Shì yīgè shìlì wénběn e Kore wa, sanpuru no tekisutodesu allora dovrebbero essere abbinati in una ricerca testuale?

Parlando di giapponese: caratteri latini a tutta larghezza sono diversi dai caratteri a metà larghezza e se il tuo input è testo romaji giapponese devi gestirlo altrimenti i tuoi utenti resteranno stupiti quando T non sarà paragonabile a T (in questo caso cosa dovrebbe essere solo glifi sono diventati punti di codice).

OK, è abbastanza per evidenziare la superficie del problema?

Personaggi duplicati

Unicode (primario per compatibilità ASCII e altri motivi storici) ha caratteri duplicati, prima di eseguire un confronto è necessario eseguire la normalizzazione altrimenti à (punto di codice singolo) non sarà uguale a ( un plus U+0300 COMBINING GRAVE ACCENT). È un caso raro in un angolo? Non proprio, dai un’occhiata anche a questo esempio del mondo reale di Jon Skeet. Inoltre (vedere la sezione Differenza colturale) le sequenze precomposte e decomposte introducono duplicati .

Si noti che i segni diacritici non sono solo fonte di confusione. Quando l’utente sta digitando con la tastiera probabilmente inserirà ' ( U+0027 APOSTROPHE) ma dovrebbe corrispondere anche ' ( U+2019 RIGHT SINGLE MARK DI QUOTAZIONE) normalmente utilizzato in tipografia (lo stesso è vero per molti molti simboli Unicode quasi equivalenti dal punto di vista dell’utente ma distinti in tipografia, immagina di scrivere una ricerca testuale all’interno di libri digitali).

In breve, due stringhe devono essere considerate uguali (questo è un concetto molto importante!) Se sono canonicamente equivalenti e sono canonicamente equivalenti se hanno lo stesso significato e aspetto linguistico, anche se sono composte da diversi punti di codice Unicode.

Astuccio

Se devi eseguire un confronto senza distinzione tra maiuscole e minuscole, avrai ancora più problemi . Presumo che tu non esegua confronti tra maiuscole e minuscole senza l’uso di toupper() o equivalenti a meno che, uno per tutti, vuoi spiegare ai tuoi utenti perché 'i'.ToUpper() != 'I' per la lingua turca ( I non è superiore caso di i quale è © . BTW lettera minuscola per I è ı ).

Un altro problema è eszett ß in tedesco (una legatura per lunghi s + brevi s usati – in tempi antichi – anche in inglese elevato alla dignità di un personaggio). Ha una versione maiuscola ma (in questo momento) .NET Framework restituisce erroneamente "ẞ" != "ß".ToUpper() (ma il suo uso è obbligatorio in alcuni scenari, vedi anche questo post ). Sfortunatamente non sempre ss diventa (maiuscolo), non sempre ss è uguale a ß (minuscolo) e anche sz a volte è in maiuscolo. Confuso, giusto?

Ancora di più

La globalizzazione non riguarda solo il testo: le date e i calendari, la formattazione e l’analisi dei numeri, i colors e il layout. Un libro non basterà a descrivere tutte le cose a cui dovresti preoccuparti, ma quello che vorrei sottolineare qui è che poche stringhe localizzate non renderanno la tua applicazione pronta per un mercato internazionale.

Anche solo per il testo sorgono altre domande : come si applica alla regex? Come dovrebbero essere gestiti gli spazi? Uno spazio em equivale a uno spazio en ? In un’applicazione professionale come “USA” deve essere confrontato con “USA” (in una ricerca a testo libero)? Sulla stessa linea di pensiero: come gestire i segni diacritici in confronto?

Come gestire l’archiviazione del testo? Dimentica che puoi tranquillamente rilevare la codifica, per aprire un file devi conoscerne la codifica. Ovviamente, a meno che tu non abbia intenzione di fare come i parser HTML con o XML / XHTML encoding="UTF-8" in ).

“Introduzione” storica

Quello che vediamo come testo sui nostri monitor è solo una porzione di byte nella memoria del computer. Per convenzione ogni valore (o gruppo di valori, come un int32_t rappresenta un numero) rappresenta un carattere . Il modo in cui quel personaggio viene disegnato sullo schermo viene delegato a qualcos’altro (per semplificare un po ‘la riflessione su un font ).

Se decidiamo arbitrariamente che ogni carattere è rappresentato con un byte, allora abbiamo a disposizione 256 simboli (come quando usiamo int8_t , System.SByte o java.lang.Byte per un numero abbiamo un intervallo numerico di 256 valori). Ciò di cui abbiamo bisogno ora per decidere ogni valore che carattere rappresenta, un esempio di questo è ASCII (limitato a 7 bit, 128 valori) con estensioni personalizzate per utilizzare anche i 128 valori superiori.

Fatto , codifica dei caratteri habemus per 256 simboli (incluse lettere, numeri, caratteri analfabetici e codici di controllo). Sì, ogni estensione ASCII è proprietaria ma le cose sono chiare e facili da gestire. L’elaborazione del testo è così comune che abbiamo solo bisogno di aggiungere un tipo di dati appropriato nelle nostre lingue preferite ( char in C, notare che formalmente non è un alias per unsigned char signed char o signed char ma un tipo distinto ; char in Pascal; character in FORTRAN e così via) e poche funzioni di libreria per gestirlo.

Sfortunatamente non è così facile. ASCII è limitato a un set di caratteri molto semplice e include solo caratteri latini usati negli Stati Uniti (ecco perché il suo nome preferito dovrebbe essere usASCII). È così limitato che anche le parole inglesi con segni diacritici non sono supportate (se questo ha fatto il cambiamento nel linguaggio moderno o viceversa è un’altra storia ). Vedrai che ha anche altri problemi (ad esempio il suo ordinamento errato con i problemi di confronto ordinale e alfabetico ).

Come affrontarlo? Introduci un nuovo concetto: le code page . Mantieni un set fisso di caratteri di base (ASCII) e aggiungi altri 128 caratteri specifici per ogni lingua. Il valore 0x81 rappresenterà il carattere cirillico Б (nella codepage DOS 866) e il carattere greco Ϊ (nella codepage DOS 869).

Ora sorgono gravi problemi: 1) non è ansible mescolare nello stesso file di testo diversi alfabeti. 2) Per comprendere correttamente un testo devi anche sapere con quale tabella codici è espressa. Dove? Non esiste un metodo standard per questo e dovrai gestire questo utente richiedente o con un’ipotesi ragionevole (?!). Anche oggigiorno il “formato” di file ZIP è limitato ad ASCII per i nomi di file (è ansible utilizzare UTF-8 – vedere più avanti – ma non è standard – perché non esiste un formato ZIP standard). In questo post una soluzione di lavoro Java . 3) Anche le code page non sono standard e ogni ambiente ha set diversi (anche le code page DOS e le code page di Windows sono diverse) e anche i nomi variano. 4) 255 caratteri sono ancora troppo pochi per, ad esempio, la lingua cinese o giapponese, quindi sono state introdotte codifiche più complicate ( Shift JIS , ad esempio).

La situazione era terribile in quel momento (~ 1985) e uno standard era assolutamente necessario. È arrivata la ISO / IEC 8859 che, almeno, ha risolto il punto 3 nella precedente lista dei problemi. I punti 1, 2 e 4 erano ancora non risolti e una soluzione era necessaria (specialmente se il tuo objective non è solo testo grezzo ma anche caratteri tipografici speciali ). Questo standard (dopo molte revisioni) è ancora con noi al giorno d’oggi (e in qualche modo coincide con la code page Windows-1252) ma probabilmente non lo userai mai a meno che tu non stia lavorando con qualche sistema legacy.

Lo standard che è emerso per salvarci da questo caos è noto in tutto il mondo: Unicode . Da Wikipedia :

Unicode è uno standard del settore informatico per la codifica coerente, la rappresentazione e la gestione del testo express nella maggior parte dei sistemi di scrittura del mondo. […] l’ultima versione di Unicode contiene un repertorio di oltre 110.000 caratteri che copre 100 script e più set di simboli.

Lingue, librerie, sistemi operativi sono stati aggiornati per supportare Unicode. Ora abbiamo tutti i personaggi di cui abbiamo bisogno, un codice condiviso comune per ognuno, e il passato è solo un incubo. Sostituisci char con wchar_t (e accetta di vivere con wcout , wstring e amici), usa semplicemente System.Char o java.lang.Character e vivi felice. Destra?

NO. Non è mai così facile . La missione Unicode riguarda “… codifica, rappresentazione e gestione del testo …” , non traduce e adatta le diverse culture in un codice astratto (ed è imansible farlo a meno che non si uccida la bellezza nella varietà di tutti le nostre lingue). Inoltre la codifica stessa introduce alcune cose (non così ovvie ?!) a cui dobbiamo preoccuparci.