Les chaînes de caractères peuvent parfois être complexes, particulièrement lorsqu'elles utilisent l'encodage Unicode. La bibliothèque Hoa\Ustring propose plusieurs opérations sur des chaînes de caractères UTF-8.

Table des matières

Introduction

Lorsque nous manipulons des chaînes de caractères, le format Unicode s'impose par sa compatibilité avec les formats de base historiques (comme ASCII) et par sa grande capacité à comprendre une très large plage de caractères et de symboles, pour toutes les cultures et toutes les régions de notre monde. PHP propose plusieurs outils pour manipuler de telles chaînes, comme les extensions mbstring, iconv ou encore l'excellente intl qui se base sur ICU, l'implémentation de référence d'Unicode. Malheureusement, il faut parfois mélanger ces extensions pour arriver à nos fins et au prix d'une certaine complexité et d'une verbosité regrettable.

La bibliothèque Hoa\Ustring répond à ces problématiques en proposant une façon simple de manipuler des chaînes de caractères, de manière performante et efficace. Elle propose également des algorithmes évolués pour des opérations de recherche sur des chaînes de caractères.

Chaîne de caractères Unicode

La classe Hoa\Ustring\Ustring représente une chaîne de caractères Unicode UTF-8 et permet de la manipuler facilement. Elle implémente les interfaces ArrayAccess, Countable et IteratorAggregate. Nous allons utiliser trois exemples dans trois langues différentes : français, arabe et japonais. Ainsi :

$french   = new Hoa\Ustring\Ustring('Je t\'aime');
$arabic   = new Hoa\Ustring\Ustring('أحبك');
$japanese = new Hoa\Ustring\Ustring('私はあなたを愛して');

Maintenant, voyons les opérations possibles sur ces trois chaînes.

Manipulation de la chaîne

Commençons par les opérations élémentaires. Si nous voulons compter le nombre de caractères (et non pas d'octets), nous allons utiliser la fonction count de PHP. Ainsi :

var_dump(
    count($french),
    count($arabic),
    count($japanese)
);

/**
 * Will output:
 *     int(9)
 *     int(4)
 *     int(9)
 */

Quand nous parlons de position sur un texte, il n'est pas adéquat de parler de droite ou de gauche, mais plutôt de début ou de fin, et cela à partir de la direction (sens d'écriture) du texte. Nous pouvons connaître cette direction grâce à la méthode Hoa\Ustring\Ustring::getDirection. Elle retourne la valeur d'une des constantes suivantes :

  • Hoa\Ustring\Ustring::LTR, pour left-to-right, si le texte s'écrit de gauche à droite ;
  • Hoa\Ustring\Ustring::RTL, pour right-to-left, si le texte s'écrit de droite à gauche.

Observons le résultat sur nos exemples :

var_dump(
    $french->getDirection()   === Hoa\Ustring\Ustring::LTR, // is left-to-right?
    $arabic->getDirection()   === Hoa\Ustring\Ustring::RTL, // is right-to-left?
    $japanese->getDirection() === Hoa\Ustring\Ustring::LTR  // is left-to-right?
);

/**
 * Will output:
 *     bool(true)
 *     bool(true)
 *     bool(true)
 */

Le résultat de cette méthode est calculé grâce à la méthode statique Hoa\Ustring\Ustring::getCharDirection qui calcule la direction d'un seul caractère.

Si nous voulons concaténer une autre chaîne à la fin ou au début, nous utiliserons respectivement les méthodes Hoa\Ustring\Ustring::append et Hoa\Ustring\Ustring::prepend. Ces méthodes, comme la plupart de celles qui modifient la chaîne, retournent l'objet lui-même, ce afin de chaîner les appels. Par exemple :

echo $french->append('… et toi, m\'aimes-tu ?')->prepend('Mam\'zelle ! ');

/**
 * Will output:
 *     Mam'zelle ! Je t'aime… et toi, m'aimes-tu ?
 */

Nous avons également les méthodes Hoa\Ustring\Ustring::toLowerCase et Hoa\Ustring\Ustring::toUpperCase pour, respectivement, mettre la chaîne en minuscules ou en majuscules. Par exemple :

echo $french->toUpperCase();

/**
 * Will output:
 *     MAM'ZELLE ! JE T'AIME… ET TOI, M'AIMES-TU ?
 */

Nous pouvons aussi ajouter des caractères en début ou en fin de chaîne pour atteindre une taille minimum. Cette opération est plus couramment appelée le padding (pour des raisons historiques remontant aux machines à écrire). C'est pourquoi nous trouvons la méthode Hoa\Ustring\Ustring::pad qui prend trois arguments : la taille minimum, les caractères à ajouter et une constante indiquant si nous devons ajouter en fin ou en début de chaîne (respectivement Hoa\Ustring\Ustring::END, par défaut, et Hoa\Ustring\Ustring::BEGINNING).

echo $arabic->pad(20, ' ');

/**
 * Will output:
 *                     أحبك
 */

Une opération similairement inverse permet de supprimer, par défaut, les espaces en début et en fin de chaîne grâce à la méthode Hoa\Ustring\Ustring::trim. Par exemple, pour revenir à notre chaîne arabe originale :

echo $arabic->trim();

/**
 * Will output:
 *     أحبك
 */

Si nous voulons supprimer d'autres caractères, nous pouvons utiliser son premier argument qui doit être une expression régulière. Enfin, son second argument permet de préciser de quel côté nous voulons supprimer les caractères : en début, en fin ou les deux, toujours en utilisant les constantes Hoa\Ustring\Ustring::BEGINNING et Hoa\Ustring\Ustring::END. Nous pouvons combiner ces constantes pour exprimer « les deux côtés », ce qui est la valeur par défaut : Hoa\Ustring\Ustring::BEGINNING | Hoa\Ustring\Ustring::END. Par exemple, pour supprimer tous les nombres et les espaces uniquement à la fin, nous écrirons :

$arabic->trim('\s|\d', Hoa\Ustring\Ustring::END);

Nous pouvons également réduire la chaîne à une sous-chaîne en précisant la position du premier caractère puis la taille de la sous-chaîne à la méthode Hoa\Ustring\Ustring::reduce :

echo $french->reduce(3, 6)->reduce(2, 4);

/**
 * Will output:
 *     aime
 */

Si nous voulons obtenir un caractère en particulier, nous pouvons exploiter l'interface ArrayAccess. Par exemple, pour obtenir le premier caractère de chacun de nos exemples (en les reprenant depuis le début) :

var_dump(
    $french[0],
    $arabic[0],
    $japanese[0]
);

/**
 * Will output:
 *     string(1) "J"
 *     string(2) "أ"
 *     string(3) "私"
 */

Si nous voulons le dernier caractère, nous utiliserons l'index -1. L'index n'est pas borné à la taille de la chaîne. Si jamais l'index dépasse cette taille, alors un modulo sera appliqué.

Nous pouvons aussi modifier ou supprimer un caractère précis avec cette méthode. Par exemple :

$french->append(' ?');
$french[-1] = '!';
echo $french;

/**
 * Will output:
 *     Je t'aime !
 */

Une autre méthode fort utile est la transformation en ASCII. Attention, ce n'est pas toujours possible, selon votre installation. Par exemple :

$title = new Hoa\Ustring\Ustring('Un été brûlant sur la côte');
echo $title->toAscii();

/**
 * Will output:
 *     Un ete brulant sur la cote
 */

Nous pouvons aussi transformer de l'arabe ou du japonais vers de l'ASCII. Les symboles, comme les symboles Mathématiques ou les emojis, sont aussi transformés :

$emoji = new Hoa\Ustring\Ustring('I ❤ Unicode');
$maths = new Hoa\Ustring\Ustring('∀ i ∈ ℕ');

echo
    $arabic->toAscii(), "\n",
    $japanese->toAscii(), "\n",
    $emoji->toAscii(), "\n",
    $maths->toAscii(), "\n";

/**
 * Will output:
 *     ahbk
 *     sihaanatawo aishite
 *     I (heavy black heart)️ Unicode
 *     (for all) i (element of) N
 */

Pour que cette méthode fonctionne correctement, il faut que l'extension intl soit présente, pour que la classe Transliterator existe. Si elle n'existe pas, la classe Normalizer doit exister. Si cette classe n'existe pas non plus, la méthode Hoa\Ustring\Ustring::toAscii peut quand même essayer une transformation mais moins efficace. Pour cela, il faut passer true en seul argument. Ce tour de force est déconseillé dans la plupart des cas.

Nous trouvons également la méthode getTransliterator qui retourne un objet Transliterator, ou null si cette classe n'existe pas. Cette méthode prend en argument un identifiant de translitération. Nous conseillons de lire la documentation sur le translitérateur d'ICU pour comprendre cet identifiant. La méthode transliterate permet de translitérer la chaîne courante à partir d'un identifiant et d'un index de début et de fin. Elle fonctionne de la même façon que la méthode Transliterator::transliterate.

Plus généralement, pour des changements d'encodage brut, nous pouvons utiliser la méthode statique Hoa\Ustring\Ustring::transcode, avec en premier argument une chaîne de caractères, en deuxième argument l'encodage d'origine et en dernier argument l'encodage final souhaité (par défaut UTF-8). Pour la liste des encodages, il faut se reporter à l'extension iconv ou entrer la commande suivante dans un terminal :

$ iconv --list

Pour savoir si une chaîne est encodée en UTF-8, nous pouvons utiliser la méthode statique Hoa\Ustring\Ustring::isUtf8 ; par exemple :

var_dump(
    Hoa\Ustring\Ustring::isUtf8('a'),
    Hoa\Ustring\Ustring::isUtf8(Hoa\Ustring\Ustring::transcode('a', 'UTF-8', 'UTF-16'))
);

/**
 * Will output:
 *     bool(true)
 *     bool(false)
 */

Nous pouvons éclater la chaîne en plusieurs sous-chaînes en utilisant la méthode Hoa\Ustring\Ustring::split. En premier argument, nous avons une expression régulière (type PCRE), puis un entier représentant le nombre maximum d'éléments à retourner et enfin une combinaison de constantes. Ces constantes sont les mêmes que celles de preg_split.

Par défaut, le deuxième argument vaut -1, qui symbolise l'infini, et le dernier argument vaut PREG_SPLIT_NO_EMPTY. Ainsi, si nous voulons obtenir tous les mots d'une chaîne, nous écrirons :

print_r($title->split('#\b|\s#'));

/**
 * Will output:
 *     Array
 *     (
 *         [0] => Un
 *         [1] => ete
 *         [2] => brulant
 *         [3] => sur
 *         [4] => la
 *         [5] => cote
 *     )
 */

Si nous voulons itérer sur tous les caractères, il est préférable d'exploiter l'interface IteratorAggregate, soit la méthode Hoa\Ustring\Ustring::getIterator. Voyons plutôt sur l'exemple en arabe :

foreach ($arabic as $letter) {
    echo $letter, "\n";
}

/**
 * Will output:
 *     أ
 *     ح
 *     ب
 *     ك
 */

Nous remarquons que l'itération se fait suivant la direction du texte, c'est à dire que le premier élément de l'itération est la première lettre de la chaîne en partant du début.

Bien sûr, si nous voulons obtenir un tableau des caractères, nous pouvons utiliser la fonction iterator_to_array de PHP :

print_r(iterator_to_array($arabic));

/**
 * Will output:
 *     Array
 *     (
 *         [0] => أ
 *         [1] => ح
 *         [2] => ب
 *         [3] => ك
 *     )
 */

Les chaînes peuvent également être comparées entre elles grâce à la méthode Hoa\Ustring\Ustring::compare :

$string = new Hoa\Ustring\Ustring('abc');
var_dump(
    $string->compare('wxyz')
);

/**
 * Will output:
 *     string(-1)
 */

Cette méthode retourne -1 si la chaîne initiale vient avant (par ordre alphabétique), 0 si elle est identique et 1 si elle vient après. Si nous voulons utiliser la pleine puissance du mécanisme sous-jacent, nous pouvons appeler la méthode statique Hoa\Ustring\Ustring::getCollator (si la classe Collator existe, sinon Hoa\Ustring\Ustring::compare utilisera une comparaison simple octet par octets sans tenir compte d'autres paramètres). Ainsi, si nous voulons trier un tableau de chaînes, nous écrirons plutôt :

$strings = array('c', 'Σ', 'd', 'x', 'α', 'a');
Hoa\Ustring\Ustring::getCollator()->sort($strings);
print_r($strings);

/**
 * Could output:
 *     Array
 *     (
 *         [0] => a
 *         [1] => c
 *         [2] => d
 *         [3] => x
 *         [4] => α
 *         [5] => Σ
 *     )
 */

La comparaison entre deux chaînes dépend de la locale, c'est à dire de la régionalisation du système, comme la langue, le pays, la région etc. Nous pouvons utiliser la bibliothèque Hoa\Locale pour modifier ces données, mais ce n'est pas une dépendance de Hoa\Ustring pour autant.

Nous pouvons également savoir si une chaîne correspond à un certain motif, toujours exprimé avec une expression régulière. Pour cela, nous allons utiliser la méthode Hoa\Ustring\Ustring::match. Cette méthode repose sur les fonctions preg_match et preg_match_all de PHP, mais en modifiant les options du motif afin qu'il supporte Unicode. Nous avons les paramètres suivants : le motif, une variable par référence pour récupérer les captures, les flags, la position de début de recherche (offset) et enfin un booléen indiquant si la recherche est globale ou non (respectivement si nous devons utiliser preg_match_all ou preg_match). Par défaut, la recherche n'est pas globale.

Ainsi, nous allons vérifier que notre exemple en français contient bien aime avec son complément d'objet direct :

$french->match('#(?:(?<direct_object>\w)[\'\b])aime#', $matches);
var_dump($matches['direct_object']);

/**
 * Will output:
 *     string(1) "t"
 */

Cette méthode retourne false si une erreur est survenue (par exemple si le motif n'est pas correct), 0 si aucune correspondance n'a été trouvée, le nombre de correspondances trouvées sinon.

Similairement, nous pouvons chercher et remplacer des sous-chaînes par d'autres sous-chaînes suivant un motif, toujours exprimé avec une expression régulière. Pour cela, nous allons utiliser la méthode Hoa\Ustring\Ustring::replace. Cette méthode repose sur les fonctions preg_replace et preg_replace_callback de PHP, mais toujours en modifiant les options du motif afin qu'il supporte Unicode. En premier argument, nous trouvons le ou les motifs, en deuxième argument, le ou les remplacements et en dernier argument la limite de remplacements à faire. Si le remplacement est un callable, alors la fonction preg_replace_callback sera utilisée.

Ainsi, nous allons modifier notre exemple français pour qu'il soit plus poli :

$french->replace('#(?:\w[\'\b])(?<verb>aime)#', function ($matches) {
    return 'vous ' . $matches['verb'];
});

echo $french;

/**
 * Will output:
 *     Je vous aime
 */

La classe Hoa\Ustring\Ustring propose des constantes qui sont des aliases de constantes PHP et qui permettent une meilleure lecture du code:

  • Hoa\Ustring\Ustring::WITHOUT_EMPTY, alias de PREG_SPLIT_NO_EMPTY ;
  • Hoa\Ustring\Ustring::WITH_DELIMITERS, alias de PREG_SPLIT_DELIM_CAPTURE ;
  • Hoa\Ustring\Ustring::WITH_OFFSET, alias de PREG_OFFSET_CAPTURE et PREG_SPLIT_OFFSET_CAPTURE ;
  • Hoa\Ustring\Ustring::GROUP_BY_PATTERN, alias de PREG_PATTERN_ORDER ;
  • Hoa\Ustring\Ustring::GROUP_BY_TUPLE, alias de PREG_SET_ORDER.

Comme ce sont des aliases stricts, nous pouvons écrire :

$string = new Hoa\Ustring\Ustring('abc1 defg2 hikl3 xyz4');
$string->match(
    '#(\w+)(\d)#',
    $matches,
    Hoa\Ustring\Ustring::WITH_OFFSET
  | Hoa\Ustring\Ustring::GROUP_BY_TUPLE,
    0,
    true
);

Caractères

La classe Hoa\Ustring\Ustring offre des méthodes statiques travaillant sur un seul caractère Unicode. Nous avons déjà évoqué la méthode getCharDirection qui permet de connaître la direction d'un caractère. Nous trouvons aussi getCharWidth qui calcule le nombre de colonnes nécessaires pour l'affichage d'un seul caractère. Ainsi :

var_dump(
    Hoa\Ustring\Ustring::getCharWidth(Hoa\Ustring\Ustring::fromCode(0x7f)),
    Hoa\Ustring\Ustring::getCharWidth('a'),
    Hoa\Ustring\Ustring::getCharWidth('㽠')
);

/**
 * Will output:
 *     int(-1)
 *     int(1)
 *     int(2)
 */

Cette méthode retourne -1 ou 0 si le caractère n'est pas imprimable (par exemple si c'est un caractère de contrôle, comme 0x7f qui correspond à DELETE), 1 ou plus si c'est un caractère qui peut être imprimé. Dans notre exemple, s'imprime sur 2 colonnes.

Pour plus de sémantique, nous avons accès à la méthode Hoa\Ustring\Ustring::isCharPrintable qui permet de savoir si un caractère est imprimable ou pas.

Si nous voulons calculer le nombre de colonnes pour tout une chaîne, il faut utiliser la méthode Hoa\Ustring\Ustring::getWidth. Ainsi :

var_dump(
    $french->getWidth(),
    $arabic->getWidth(),
    $japanese->getWidth()
);

/**
 * Will output:
 *     int(9)
 *     int(4)
 *     int(18)
 */

Essayez dans un terminal avec une police mono-espacée. Vous verrez que le japonais demande 18 colonnes pour s'afficher. Cette mesure est très utile si nous voulons connaître la largeur d'une chaîne pour la positionner correctement.

La méthode getCharWidth est différente de getWidth car elle prend en compte des caractères de contrôles. Elle est destinée à être utilisée, par exemple, avec des terminaux (voir la bibliothèque Hoa\Console).

Enfin, si cette fois nous ne nous intéressons pas aux caractères Unicode mais aux caractères machines char (soit 1 octet), nous avons une opération supplémentaire. La méthode Hoa\Ustring\Ustring::getBytesLength va compter la taille de la chaîne en octets :

var_dump(
    $arabic->getBytesLength(),
    $japanese->getBytesLength()
);

/**
 * Will output:
 *     int(8)
 *     int(27)
 */

Si nous comparons ces résultats avec ceux de la méthode Hoa\Ustring\Ustring::count, nous comprenons que les caractères arabes sont encodés sur 2 octets alors que les caractères japonais sont encodés sur 3 octets. Nous pouvons également obtenir un octet précis à l'aide de la méthode Hoa\Ustring\Ustring::getByteAt. Encore une fois, l'index n'est pas borné.

Code-point

Chaque caractère est représenté en machine par un entier, appelé code-point. Pour obtenir le code-point d'un caractère, nous pouvons utiliser la méthode statique Hoa\Ustring\Ustring::toCode, et pour obtenir un caractère à partir d'un code, nous pouvons utiliser la méthode statique Hoa\Ustring\Ustring::fromCode. Nous avons aussi la méthode statique Hoa\Ustring\Ustring::toBinaryCode qui retourne la représentation sous forme binaire d'un caractère. Prenons un exemple :

var_dump(
    Hoa\Ustring\Ustring::toCode('Σ'),
    Hoa\Ustring\Ustring::toBinaryCode('Σ'),
    Hoa\Ustring\Ustring::fromCode(0x1a9)
);

/**
 * Will output:
 *     int(931)
 *     string(32) "1100111010100011"
 *     string(2) "Σ"
 */

Algorithmes de recherche

La bibliothèque Hoa\Ustring propose des algorithmes de recherches sophistiquées sur les chaînes de caractères à travers la classe Hoa\Ustring\Search.

Nous allons étudier l'algorithme Hoa\Ustring\Search::approximated qui fait une recherche d'une sous-chaîne dans une chaîne avec au maximum k différences (une différence étant une insertion, une délétion ou une modification). Prenons un exemple classique avec une représentation ADN : nous allons chercher toutes les sous-chaînes s'approchant de GATAA à 1 différence près (au maximum) dans CAGATAAGAGAA. Pour cela, nous allons donc écrire :

$x      = 'GATAA';
$y      = 'CAGATAAGAGAA';
$k      = 1;
$search = Hoa\Ustring\Search::approximated($y, $x, $k);
$n      = count($search);

echo 'Try to match ', $x, ' in ', $y, ' with at most ', $k, ' difference(s):', "\n";
echo $n, ' match(es) found:', "\n";

foreach ($search as $position) {
    echo '    • ', substr($y, $position['i'], $position['l'), "\n";
}

/**
 * Will output:
 *     Try to match GATAA in CAGATAAGAGAA with at most 1 difference(s):
 *     4 match(es) found:
 *         • AGATA
 *         • GATAA
 *         • ATAAG
 *         • GAGAA
 */

Cette méthode retourne un tableau de tableaux. Chaque sous-tableau représente un résultat et contient trois indexes : i pour la position du premier caractère (octet) du résultat, j pour la position du dernier caractère et l pour la taille du résultat (tout simplement j - i). Ainsi, nous pouvons calculer les résultats en utilisant notre chaîne initiale (ici $y) et ces indexes.

Avec notre exemple, nous avons quatre résultats. Le premier est AGATA, soit GATAA avec un caractère déplacé, et AGATA existe bien dans CAGATAAGAGAA. Le deuxième résultat est GATAA, notre sous-chaîne, qui existe bel et bien dans CAGATAAGAGAA. Le troisième résultat est ATAAG, soit GATAA avec un caractère déplacé, et ATAAG existe bien dans CAGATAAGAGAA. Enfin, le dernier résultat est GAGAA, soit GATAA avec un caractère modifié, et GAGAA existe bien dans CAGATAAGAGAA.

Prenons un autre exemple, plus concret cette fois-ci. Nous allons considérer la chaîne --testIt --foobar --testThat --testAt (qui représente les options possibles d'une ligne de commande), et nous allons chercher --testot, une option qu'aurait pu donner l'utilisateur. Cette option n'existe pas telle quelle. Nous allons donc utiliser notre algorithme de recherche avec 1 différence au maximum. Voyons plutôt :

$x      = 'testot';
$y      = '--testIt --foobar --testThat --testAt';
$k      = 1;
$search = Hoa\Ustring\Search::approximated($y, $x, $k);
$n      = count($search);

// …

/**
 * Will output:
 *     Try to match testot in --testIt --foobar --testThat --testAt with at most 1 difference(s)
 *     2 match(es) found:
 *         • testIt
 *         • testAt
 */

Les résultats testIt et testAt sont des vraies options, donc nous pouvons les proposer à l'utilisateur. C'est un mécanisme utilisé par Hoa\Console pour proposer des corrections à l'utilisateur s'il se trompe.

Conclusion

La bibliothèque Hoa\Ustring propose des facilités pour manipuler des chaînes encodées au format Unicode, mais aussi pour effectuer des recherches sophistiquées sur des chaînes.