This chapter is not yet translated. Contributions are welcomed!
Les chaînes de caractères peuvent parfois être complexes,
particulièrement lorsqu'elles utilisent l'encodage Unicode.
La bibliothèque Hoa\String
propose plusieurs opérations de
chaînes de caractères UTF-8.
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\String
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.
La classe Hoa\String\String
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\String\String('Je t\'aime');
$arabic = new Hoa\String\String('أحبك');
$japanese = new Hoa\String\String('私はあなたを愛して');
Maintenant, voyons les opérations possibles sur ces trois chaînes.
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\String\String::getDirection
. Elle retourne la valeur
d'une des constantes suivantes :
Hoa\String\String::LTR
, pour
left-to-right, si le texte s'écrit de gauche à
droite ;Hoa\String\String::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\String\String::LTR, // is left-to-right?
$arabic->getDirection() === Hoa\String\String::RTL, // is right-to-left?
$japanese->getDirection() === Hoa\String\String::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\String\String::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\String\String::append
et
Hoa\String\String::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 méthodes. 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\String\String::toLowerCase
et
Hoa\String\String::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\String\String::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\String\String::END
, par défaut, et
Hoa\String\String::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\String\String::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\String\String::BEGINNING
et
Hoa\String\String::END
. Nous pouvons combiner ces constantes
pour exprimer « les deux côtés », ce qui est la valeur par défaut :
Hoa\String\String::BEGINNING |
Hoa\String\String::END
. Par exemple, pour supprimer tous les nombres et
les espaces uniquement à la fin, nous écrirons :
$arabic->trim('\s|\d', Hoa\String\String::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\String\String::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 notre exemple (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 naturellement pas toujours possible. Par exemple, nous pouvons transformer du français en ASCII mais pas de l'arabe en ASCII. Ainsi :
$title = new Hoa\String\String('Un été brûlant sur la côte');
echo $title->toAscii();
/**
* Will output:
* Un ete brulant sur la cote
*/
Pour que cette méthode fonctionne correctement, il faut que l'extension
intl
soit présente, pour que la
classe Normalizer
existe.
Si cette classe n'existe pas, la méthode
Hoa\String\String::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.
Plus généralement, pour des changements d'encodage brut,
nous pouvons utiliser la méthode statique
Hoa\String\String::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\String\String::isUtf8
; par exemple :
var_dump(
Hoa\String\String::isUtf8('a'),
Hoa\String\String::isUtf8(Hoa\String\String::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\String\String::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\String\String::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] => ك
* )
*/
Si cette fois nous ne nous intéressons pas aux caractères Unicode mais aux
caractères machines char
(soit 1 octet), nous
avons quelques opérations supplémentaires. La première est
Hoa\String\String::getBytesLength
qui 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\String\String::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\String\String::getByteAt
. Encore une fois,
l'index n'est pas borné.
Enfin, une autre méthode bien pratique est
Hoa\String\String::getWidth
qui calcule la
largeur d'une chaîne, c'est à dire le nombre de colonnes
utilisées par cette chaîne lors de son affichage. 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 efficacement.
Les chaînes peuvent également être comparées entre elles
grâce à la méthode Hoa\String\String::compare
:
$string = new Hoa\String\String('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\String\String::getCollator
(si la classe
Collator
existe, auquel
cas, Hoa\String\String::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\String\String::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\String
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\String\String::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 suivant : 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\String\String::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\String\String
propose des constantes qui sont des
aliases de constantes PHP et qui permettent une meilleure lecture du code:
Hoa\String\String::WITHOUT_EMPTY
, alias de
PREG_SPLIT_NO_EMPTY
;Hoa\String\String::WITH_DELIMITERS
, alias de
PREG_SPLIT_DELIM_CAPTURE
;Hoa\String\String::WITH_OFFSET
, alias de
PREG_OFFSET_CAPTURE
et
PREG_SPLIT_OFFSET_CAPTURE
;Hoa\String\String::GROUP_BY_PATTERN
, alias de
PREG_PATTERN_ORDER
;Hoa\String\String::GROUP_BY_TUPLE
, alias de
PREG_SET_ORDER
.Comme ce sont des aliases stricts, nous pouvons écrire :
$string = new Hoa\String\String('abc1 defg2 hikl3 xyz4');
$string->match(
'#(\w+)(\d)#',
$matches,
Hoa\String\String::WITH_OFFSET
| Hoa\String\String::GROUP_BY_TUPLE,
0,
true
);
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\String\String::toCode
,
et pour obtenir un caractère à partir d'un code, nous pouvons utiliser la
méthode statique Hoa\String\String::fromCode
. Nous avons aussi la
méthode statique Hoa\String\String::toBinaryCode
qui retourne la
représentation sous forme binaire d'un caractère. Prenons un exemple :
var_dump(
Hoa\String\String::toCode('Σ'),
Hoa\String\String::toBinaryCode('Σ'),
Hoa\String\String::fromCode(0x1a9)
);
/**
* Will output:
* int(931)
* string(32) "00000000000000000000001110100011"
* string(2) "Σ"
*/
Nous pouvons préciser la taille de la représentation binaire à l'aide du
second argument de la méthode Hoa\String\String::toBinaryCode
(32
par défaut).
La bibliothèque Hoa\String
propose des algorithmes de
recherches sophistiquées sur les chaînes de caractères à
travers la classe Hoa\String\Search
.
Nous allons étudier l'algorithme
Hoa\String\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\String\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\String\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.
La classe Hoa\String
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.