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] => ك
     *     )
     */

### Comparaison et recherche

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('#(?:(?&lt;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])(?&lt;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.