Tri alphabétique en JavaScript (sous InDesign)
October 26, 2010 | Tips | fr | en
Étonnamment, JavaScript n'offre aucun moyen de ranger proprement des mots selon l'ordre alphabétique réel. Bien que la méthode Array.sort() soit connue pour produire, par défaut, un tri lexicographique, vous vous apercevrez bien vite que le résultat est aberrant dans la plupart des contextes de la vie courante. En fait, le mécanisme intime du tri JS se borne à comparer des rangs Unicode, tant et si bien que 'Z' (U+005A) précède 'e' (U+0065), qui lui-même précède 'ç' (U+00E7), etc. Par ailleurs, vous avez tous constaté avec chagrin qu'InDesign n'offre aucune fonction de tri alphabétique. Voici donc un outil expérimental pour vous aider à remettre en ordre vos alphabets latins.
Le fameux « Algorithme de Collation Unicode (UCA) » expose cruellement les enjeux du tri lexicographique :
“Collation implementations must deal with the often-complex linguistic conventions that communities of people have developed over the centuries for ordering text in their language, and provide for common customizations based on user preferences. [...] Many people see the Unicode code charts, and expect the characters in their language to be in the ‘correct’ order in the code charts. Because collation varies by language —not just by script—, it is not possible to arrange code points for characters so that simple binary string comparison produces the desired collation order for all languages. Because multi-level sorting is a requirement, it is not even possible to arrange code points for characters so that simple binary string comparison produces the desired collation order for any particular language. Separate data tables are required for correct sorting order.”
Cela étant, et considérant les problèmes de performance liés au scripting, le principal défi auquel nous devons faire face est celui de synthétiser un algorithme acceptable en termes d'efficacité, sans pour autant noyer notre code dans l'océan infini des tables Unicode. Ainsi, je ne pense pas qu'il serait sage d'adresser l'intégralité de la base UCA dans un script InDesign ! Et même dans ce cas, il nous faudrait encore implémenter le « tailoring », outil d'appoint permettant de réajuster l'algorithme principal afin de se conformer aux réquisits de chaque langue — cf. Unicode Common Locale Data Repository.
À la recherche d'un compromis
À ma connaissance, Peter Kahrel est le seul auteur qui ait trouvé une solution pratique et robuste au problème du classement alphabétique dans InDesign. Son « Language-Aware Paragraph Sorter » est un script ciselé qui supporte plusieurs langues et un jeu d'options impressionnant : tri « lettre par lettre » (vs « mot par mot »), sensibilité à un style de caractère, tri rétrograde, suppression des doublons, etc. Mieux, Peter a inventé une syntaxe aussi simple qu'élégante permettant de spécifier et de personnaliser les règles de collation propre à chaque langue :
Note. — Les utilisateurs non anglophones du trieur de Peter Kahrel rencontreront probablement un petit problème d'adressage des langues depuis le fichier sortorders.txt qui est fourni avec le script d'origine. Cela tient probablement au fait que la collection Application.languagesWithVendors
sollicitée par le script retourne des informations localisées linguistiquement, alors que la table sortorders.txt a été ajustée à l'interface US d'InDesign. Pour permettre au script de reconnaître les langues telles qu'elles sont nommées en français, vous devez éditer sortorders.txt et remplacer les premiers champs de la table par leur correspondant dans l'interface française. Par exemple, [No Language]
devient [Aucune langue]
, Polish
devient Polonais
, etc. Les langues disponibles sous InDesign sont répertoriées dans la palette Caractères, rubrique Langue. Une fois cette opération effectuée, le script affichera correctement les correspondances entre la langue sélectionnée et son alphabet :
M'inspirant du travail de Peter Kahrel, j'ai alors cherché à réaliser un composant JavaScript que je puisse réutiliser dans mes propres projets, chaque fois que j'aurais à appliquer un tri alphabétique « sensible à la langue ». L'idée était d'implémenter une sorte d'algorithme de collation simplifié, basé sur des règles générales de classement lexicographique en Europe — European Ordering Rules (EOR) — et s'intéressant spécifiquement aux alphabets latins ou dérivés du latin. Je souhaitais traiter l'ensemble des caractères situés dans les blocs latins d'Unicode, y compris les blocs étendus, les formes spéciales et certaines lettres de l'IPA (alphabet phonétique international) susceptibles d'être translittérées dans l'alphabet de base. Je souhaitais également gérer un algorithme de classement multi-niveaux (premier ordre, diacritiques, casse). Et bien sûr avec prise en charge spécifique de différentes langues.
L'interface TCollator
Ma quête a débouché sur un objet expérimental, TCollator
, qui intègre le maximum de fonctionnalités sans utiliser l'UCA. Pour ce faire, je me suis principalement abreuvé à trois sources : l'utilitaire de recherche avancée de BabelMap (excellent navigateur Unicode d'Andrew West), les Mimer SQL Unicode Collation Charts et un large réservoir d'articles Wikipedia relatifs à ces sujets, en particulier « Collating sequence » qui en offre un bon panorama.
TCollator
constitue une bibliothèque minimale. Il vous suffit d'inclure le fichier au début de votre propre script, de cette façon :
#include 'collator.jsxinc' // Votre code ici
C'est tout ! La bibliothèque crée uniquement une variable nommée TCollator
, qui se comporte comme une interface. Toutes les méthodes sont statiques (c'est-à-dire que vous ne créez pas d'instances de l'objet). Voyons comment cela fonctionne sur un simple exemple :
// Mon 1er script avec TCollator #include 'collator.jsxinc' var arr = ["HLM","ÉDF","muze","où","œuf", "oval","muß","à-ha","a-ha"]; // Tri JS par defaut arr.sort(); alert( "Tri JS par défaut:\r\r" + arr.join('\r') ); // Tri avec TCollator arr.sort(TCollator.compare); alert( "Tri avec TCollator:\r\r" + arr.join('\r') );
Résultat :
Dans le fragment de code ci-dessus, l'instruction importante est bien sûr :
arr.sort(TCollator.compare)
.
TCollator.compare
est une propriété statique pointant sur une fonction, laquelle fonction nous transmettons à arr.sort()
. Il s'ensuit que la méthode Array.sort
n'invoque plus la routine de comparaison par défaut de JavaScript, mais notre routine TCollator.compare
, qui compare deux chaînes de caractères selon nos propres critètes de classement.
Ainsi, alors que JS estime à tort que le mot "muze" se classe avant "muß" ( "muze" < "muß" ! ), la fonction TCollator.compare
apporte une réponse opposée... et correcte :
var cmp = TCollator.compare("muß","muze") ; if( cmp < 0 ) alert( "muß < muze"); if( cmp > 0 ) alert( "muß > muze"); if( cmp == 0 ) alert( "muß == muze"); // => muß < muze
Par quel miracle cela fonctionne-t-il ? En profondeur, TCollator
embarque une sorte de base de données stockant les traits caractéristiques d'un grand nombre de caractères latins. Le composant sait, par exemple, que 'ß' (U+00DF) est une lettre minuscule qu'on peut tenir pour équivalente au digramme 'ss' sous certaines conditions. Vous avez d'ailleurs la possibilité d'interroger directement cette base de connaissance grâce à l'utilitaire TCollator.charInfos()
:
#include 'collator.jsxinc'; alert( TCollator.charInfos("ß") );
Réponse :
TCollator.charInfos(myCharacter)
renvoie un object rassemblant diverses informations sur le caractère soumis en argument :
PROPRIÉTÉ | TYPE | DESCRIPTION |
---|---|---|
case | string | "LOWERCASE", "TITLECASE", "UPPERCASE", ou "SMALLCAP". |
character | string | Le caractère lui-même. |
collationInfo | string | Détails additionnels. |
diacritics | array | Tableau de chaînes parmi : "ACUTE", "GRAVE", "BREVE", "CIRCUMFLEX", "CARON", "RING", "DIAERESIS", "DOUBLE ACCENT", "TILDE", "DOT", "CEDILLA", "OGONEK", "MACRON", "STROKE", "HOOK", "SWITCH", "MODIFIER", "SPECIAL". |
expanded | string | Forme élémentaire décomposée d'un multigramme. Par ex. : U+00E6 -> ae, U+01C4 -> DZ, U+00DF -> ss, U+FB03 -> ffi |
group | string | "LATIN", "SPECIAL", "NUMERAL", ou "PUNCTUATION" |
isMultigram | bool | Indique si le caractère est un digramme, un trigramme, une ligature. |
isSupported | bool | Indique si le caractère est pris en charge par TCollation. |
kind | string | Classe élémentaire à laquelle appartient la lettre. (Certaines translittérations sont possibles.) |
unicode | string | Rang Unicode sous la forme "UUUU" (ex. : "012A"). |
Note. — L'objet retourné possède également une méthode toString()
personnalisée, ce qui simplifie l'affichage des informations via alert( )
. Si vous envisagez d'utiliser TCollator.charInfos()
dans votre script, n'oubliez pas de consulter la propriété isSupported
avant de vous engager dans d'autres traitements.
Affiner les critères de classement
Chaque fois que c'est possible, un caractère est converti et reclassé parmi ceux de l'alphabet fondamental, ce qui permet de lui attribuer un poids par défaut lors du mécanisme de comparaison. Cela produit généralement des résultats très satisfaisants, même lorsque vous triez des mots étrangers contenant des caractères ou diacritiques inhabituels. TCollator
agit par défaut au niveau 3, c'est-à-dire qu'il discrimine les diacritiques et la casse des lettres selon les principes de la spécification EOR. Par exemple, trier le tableau ["boc", "BÔB", "bõb", "bob","BOB","bôb", "BOA"]
via TCollator.compare
débouchera sur le classement suivant :
BOA bob BOB bòb bôb BÔB bõb boc
Si vous n'avez pas besoin de tels raffinements, vous pouvez améliorer les performances en réduisant le niveau de collation grâce à la méthode TCollation.reset()
:
TCollator.reset( langCode , level )
.
• langCode (String; Déf. : "DEF"). L'un des codes fournis par TCollator.getLocales()
(voir infra).
• level (Number; Déf. : 3): Niveau ou « profondeur » de collation, un entier compris entre 0 et 3 :
LEVEL | DESCRIPTION |
---|---|
0 | Niveau élémentaire ne distinguant pas les chiffres, la casse ni les diacritiques (sauf ceux spécialement isolés dans l'alphabet de la langue sélectionnée). On a donc par défaut : õ = ò = O (quelle que soit la casse) et: 0 = 1 = 2 ... |
1 | Similaire au niveau 0, sauf que les chiffres sont différenciés : 0 < 1 < 2 ... |
2 | Applique le niveau 1, puis les règles EOR (European Ordering Rules) pour discriminer les diacritiques et certaines formes spéciales qui ne seraient pas gérés dans la langue sélectionnée : bob < bòb < bôb < bõb ... |
3 | Applique le niveau 2, puis discrimine la casse si besoin : bob < BOB < bôb < BÔB ... |
Remarquez, à titre d'exemple, que le caractère Dz (U+01F2 – CAPITAL LETTER D WITH SMALL LETTER Z) sera analysé par défaut comme un digramme possédant une casse de titre (titlecase), basé sur D et étendu à "Dz". (Consultez TCollator.charInfos("\u01F2")
pour visualiser ces détails.) En clair, à moins qu'une langue adresse expressément ce caractère comme une lettre distincte de son alphabet (le slovaque, par ex.), TCollator
verra U+01F2 comme strictement équivalent à "Dz". Au niveau 3, cela conduit donc à :
dz < U+01F2 = Dz < DZ
Le premier argument de la méthode TCollator.reset(langCode,level)
prend par défaut la valeur "DEF"
, qui implémente un mécanisme générique convenant à la plupart des systèmes d'écriture dérivés de l'alphabet latin, ou dans le cas d'un lexique multilingue. Cependant, vous pourriez avoir besoin d'appliquer les règles de classement d'une langue en particulier. Ce point est crucial dans les alphabets qui introduisent des lettres supplémentaires ou traitent certains digrammes comme des lettres séparées. Par exemple, en danois et en norvégien, Æ, Ø et Å se rangent après Z, et Aa s'analyse comme un équivalent de Å. Jusqu'en 1997, l'espagnol classait les digrammes CH et LL comme des lettres à part entières (niveau 1), tandis que Ñ demeure une lettre distincte rangée après N.
Pour faire face à cette diversité, TCollator
propose une quarantaine d'alphabets différents. Chacun est associé à un code de deux ou trois lettres qu'il suffira de passer en argument à TCollator.reset()
pour initialiser l'algorithme dans cette langue particulière.
Note. — Pour restaurer la configuration générique, utilisez :
TCollator.reset('DEF')
, ou simplement TCollator.reset()
.
La liste complète des alphabets / langues supportés est accessible sous forme de tableau (Array
) par TCollator.getLocales()
. Chaque élément de ce tableau est à son tour un tableau de deux chaînes de caractères : d'abord un nom descriptif, ensuite le code à utiliser.
NOM | CODE | NOM | CODE |
---|---|---|---|
[Default] | DEF | Indonesian | DEF |
Albanian | SQ | Irish Gaelic | DEF |
Basque | ES2 | Italian | DEF |
Bosnian | BCS | Kurdish (Hawar) | KU |
Breton | BR | Latvian | LV |
Croatian | BCS | Lithuanian | LT |
Czech | CS | Luxembourgish | DEF |
Danish | DNA | Malay | DEF |
Dutch (IJ=Y) | NL1 | Maltese | MT |
Dutch (Modern) | NL2 | Moldavian | RO |
English | DEF | Norwegian | DNA |
Esperanto | EO | Polish | PL |
Estonian | ET | Portuguese | DEF |
Faroese | FO | Romanian | RO |
Filipino (Modern) | TL | Serbian | BCS |
Finnish (Multilingual) | FI2 | Slovak | SK |
Finnish (Official) | FI1 | Slovene | SL |
French | DEF | Spanish (Modern) | ES2 |
Frisian | FY | Spanish (Traditional) | ES1 |
German (Decompose umlauts) | DE2 | Swedish (Modern) | SV |
German (Dictionary) | DEF | Swedish (V=W) | FI1 |
Greenlandic | KL | Turkish | TR |
Hungarian | HU | Turkmen | TK |
Icelandic | IS | Welsh | CY |
À titre d'illustration, configurons TCollator
pour ordonner une liste de mots norvégiens :
#include 'collator.jsxinc'; // Config : norvegien - niveau 3 TCollator.reset('DNA',3); var myStrings = ["åbner","brænder","jeg","altså","ordet", "vild","bryst","ære","øje","brød","fjord"]; myStrings.sort(TCollator.compare); alert( myStrings.join('\r') ); /* RESULTAT : ======== altså bryst brænder brød fjord jeg ordet vild ære øje åbner ======== */
Exemple de script InDesign
Voici un autre script simple qui exploite TCollator
pour extraire et ordonner rapidement une liste de mots à partir du texte sélectionné dans le document actif :
#include 'collator.jsxinc' function alphaSort() { var s = app.selection, i, t, str; if( !s.length ) return false; if( !('contents' in s=s[0]) ) { if( !('parentStory' in s) ) return false; s = s.parentStory; } s = s.contents. replace(/[\s\.,;:!?]+/g,'|'). split('|'); if( s.length > 1000 ) s.length = 1000; s.sort(TCollator.compare); i = s.length; t = false; while( i-- ) { str = s[i]; if( str && str!=t) { t = str; continue; } s.splice(i,1); } return (s.length && s)||false; } var res = alphaSort(); if( false===res ) alert( "Please select some text!" ); else alert( "Alphabetical Sort (default settings):\r\r" + res.join(' | ') );
Rappelons que TCollator
est conçu comme un outil primitif pour élaborer des scripts plus complexes. Il n'interagit pas en lui-même avec l'UI d'InDesign (de telle sorte que vous pourriez probablement réutiliser cette bibliothèque dans d'autres environnement ExtendScript de la suite CS). Si vous souhaitez trier des paragraphes au sein d'un document, utilisez le script de Peter Kahrel mentionné plus haut, ou créez votre propre script en sollicitant le composant TCollator
comme dans l'exemple ci-dessus.
Avertissement
TCollator
est actuellement un pur outil expérimental, en version beta. Merci d'avance de vos retours, surtout si vous testez cet utilitaire dans plusieurs des langues / alphabets proposés. Je ne saurais garantir que l'algorithme de comparaison respecte en tous points les conventions lexicographiques et usages culturels pratiqués ici et là.
TCollator
gère un cache interne pour maintenir des performances acceptables avec des listes plafonnant autour de quelques milliers de mots. Si vous observez des ralentissements après plusieurs tris basés sur Array.sort(TCollator.compare)
au sein d'un même script, invoquez TCollator.reset
pour vider le cache.
Comments
Excellent work (as usual)!
Thanks for your support.
@+
Marc
This is great, Marc, it works very well, even in this beta version. And thanks for that comment on localised language names, I added a note about that.
Peter
Marc, bien que comprenant un concept sur dix, j'ai plaisir à te lire. Je comprends que certains cherchent à utiliser tes multiples talents car c'est drôle et superbement bien écrit.
Bref, même en expliquant un machin ardu de chez ARDU, tu arrives à me faire sourire et me donner envie de tout lire (alors que, voir le point 1…!).
Marc,
Have you considered a mode that ignores all diacritics? For instance, suppose you have a book written in English and that you need an author index. The index would be sorted as if there were no diacritics at all, ignoring the rules of any language. So Łukasiewicz would be sorted among all other names starting with an L, not separately.
Peter
Hi Peter,
TCollator should do exactly what you need, unless you set up a specific language. The default mechanism treats Ł as L (at the first level).
So if you sort ["Lu", "Łu", "ło", "La", "Łe", "Ło", "lo"] through TCollator.compare, you get:
La < Łe < lo < ło < Ło < Lu < Łu
Diacritics are only distinguished at level 2 (Lu < Łu), but L and Ł are regarded as the same letter at level 1.
If you don't want to apply any sublevel order, use TCollator.reset('DEF',0). In this case, the previous list may be sorted as below:
La < Łe < ło = Ło = lo < Łu = Lu
Here TCollator does not see any difference between ło, Ło and lo, so these words can appear in any order.
Anyway, TCollator regards Ł and L as two distinct first-level letters *ONLY* if you load the POLISH alphabet ('PL'):
TCollator.reset('PL');
Then my previous example will result in:
La < lo < Lu < Łe < ło < Ło < Łu
To conclude, the default configuration of TCollator is usually the good one to sort foreign words in English. As you expect, “Łukasiewicz would be sorted among all other names starting with an L, not separately.”
@+
Marc
Ah, excellent, thanks very much.
Peter
That sounds great...! Testing it now. Quesiton is: how do I implement it for, let's say, index generations or TOCs? Will I need to re-order my lists after they are generated or things can be done in the progress.
Michael
Hi Michael,
Can you rephrase, I don't understand your problem.
Note. — TCollator.compare simply provides a comparison function which you can typically use in JS Array.sort(). This is not a DOM-level utility.
@+
Marc
I tried this in Indesign extendscript and did not get what I expected, a < b
var a = "4MX692";
var b = "9ME100";
TCollator.reset();
var cmp = TCollator.compare(a,b) ;
if( cmp < 0 ) $.writeln( "a < b");
if( cmp > 0 ) $.writeln( "a > b");
if( cmp == 0 ) $.writeln( "a == b");
Hi Marc,
I implemented the collator with my index-sorter script and it works smoothly. So thank you. But... is there a way to extend it to non-Latin languages? Also, is there a way to call back a sorting character group - e.g. I'm sorting by A but then letters A acute, A grave, A tilde get sorted in the same group. Is there a way to see which letters comprise this group. Michael
@jamie
[Sorry for the delay.]
The fact that:
TCollator.compare('4MX692','9ME100') > 0
sounds like a bug at first sight but this is because I've misdocumented the LEVEL 1 comparison. (Sorry about that.)
Actually, TCollator is *in no case* intended to compare numbers (or strings that contain digits). It can only *discriminate* digits, which is not the same thing.
To understand why you did not get what you expected, keep in mind that each level has its own ‘weight’ and that comparison levels are applied successively. TCollator compares A and B at the (n+1)th level if, and only if, A equals B at the previous level.
So, at the very first level "4MX692" and "9ME100" are just seen as ".MX..." and ".ME..." (where each dot means something like: “there is a numeral character here.”) The comparison routine then returns a positive flag—as "MX" > "ME"—and the next level is not performed because the comparison is successful.
Anyway, if you need to sort numbers, use the basic comparison routine:
function(a,b){return a-b;}
If you need to sort ASCII alphanumeric strings—such as product references—use the default Array.sort() routine with no argument, or implement a custom comparator in the case where specific sequences of digits should be parsed as actual numbers. Depending on your requirements, parseInt() or parseFloat() could be helpful.
@+
Marc
@mikez
Thanks for your feedback.
> is there a way to extend [TCollator] to non-Latin languages?
Probably yes, but not from the outside of the ‘library.’ What I did with almost 1200 Latin characters could be done with other Unicode sets. Behind the scene, TCollator relies on a kind of database whose fields are stored through binary flags. But this is a very customized syntax and I'm not sure we can transpose that system to any language.
> Also, is there a way to call back a sorting character
> group - e.g. I'm sorting by A but then letters A acute,
> A grave, A tilde get sorted in the same group. Is there
> a way to see which letters comprise this group.
The `TCollator.charInfos()` helper returns an object that may help you. In particular, the `kind` property gives you the group name of the supplied character. Not sure this is what you're looking after.
E.g.:
alert( TCollator.charInfos('Ã').kind );
displays 'A'.
@+
Marc
Hi Marc,
Thanks for previous responses. I have been using collator for some time and here is what I ran into.
I have an index that lists:
Presession37->75
Presession37->214
Presession37->215
Presession37->366
Presession37->771
Presession37->900
Presession37->1237
Presession38->1070
Presession38->1241
Presession38->1242
Presession38->1243
Presession37<2>alter-isand->1238
Presession38<2>alter-isand->1244
Presession37<2>backtrackand->509
Presession37<2>clearingacaseand->1239
Presession37<2>Clearingrundownand->772
Presession37<2>description->216
Presession37<2>E-Meterand->684
Presession37<2>FailedHelpand->217
Presession37<2>Formula13and->218
Presession37<2>Formulasand->510
Presession37<2>gettingin->1065
Presession37<2>invertedcommunicationand->511
Presession37<2>IsthereanyquestionIshouldn’taskyou?and->773
Presession37<2>justificationsand->219
Presession37<2>objectof->512
Presession37<2>presenttimeproblemsand->220
Presession37<2>Presession1and->774
Presession37<2>PTproblemsand->1066
Presession38<2>runningaDianeticAssist->1245
Presession37<2>sensitivity16and->76
Presession37<2>sensitivity16and->775
Presession37<2>startingof,ModelSessionand->1067
Presession37<2>tryingtorunallcasewith->1068
Presession37<2>withholdsand->221
Presession37<2>withholdsand<3>gettingoff->776
Presession37<2>withholdsand<3>pulling->1240
Presession37<2>withholdsand<3>stoppingacase->1069
Presession1<2>description->211
Presession1<2>Help,Control,Communicate,Interestand->212
Presession1<2>interestinScientologyand->770
Presession1<2>useofinauditing->213
somehow i can't make collator sort it by
Pressession1, Presession37, Presession38.
What could be the problem.
Michael
Hi mikez,
As previously said, TCollator is really not intended to sort numeral and/or markup-based data. Its main purpose is to properly alphabetize latin words. Specific syntax requires specific sort routine.
A possibility is to create your own compare() function to precompute or filter special fields (such as numbers or tags). Then you can use TCollator.compare *within* that function to refine the comparison on lexical entries.
@+
Marc
Trop de la balle !
Merci.