En route vers PHP 5.5 : Nouvelles classes et itérateurs pour l'extension intl

31 octobre 2012php, php-5.5
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Comme à peu près à chaque nouvelle version de PHP, l’extension intl, facilitant la gestion de l’internationalisation et de la localisation, s’enrichit encore ; cette fois-ci, il s’agit de quelques nouvelles classes permettant de manipuler des dates et des timezones, ainsi que d’itérateurs facilitant le parcours de textes par mots / lignes / phrases ou par caractères.

IntlBreakIterator, IntlRuleBasedBreakIterator, IntlCodePointBreakIterator

La version actuelle de la branche master de PHP (et donc, assez probablement, PHP 5.5) ajoute quelques nouveaux itérateurs à l’extension intl : IntlBreakIterator, IntlRuleBasedBreakIterator, et IntlCodePointBreakIterator.

La première de ces trois classes est une classe de base pour les deux suivantes ; celles-ci permettent d’itérer sur un texte, en le découpant en fonction de limites – qui peuvent correspondre à des mots / phrases / lignes avec IntlRuleBasedBreakIterator, ou à des caractères avec IntlCodePointBreakIterator.

Itération par mots

Comme premier exemple, voici une portion de code que nous pourrions utiliser pour itérer sur les mots d’un texte :

/* @var IntlRuleBasedBreakIterator $wordIterator */
$wordIterator = IntlBreakIterator::createWordInstance('fr_FR');
$wordIterator->setText("Ceci est une phrase, composée de plusieurs mots.");

foreach ($wordIterator as $word) {
    var_dump($word);
}

En exécutant cette portion de code, nous obtiendrions comme sortie quelque chose de ce type :

int(0)
int(4)
int(5)
int(8)
int(9)
int(12)
int(13)
int(19)
int(20)
int(21)
int(30)
int(31)
int(33)
int(34)
int(43)
int(44)
int(48)
int(49)

Les nombres retournés ici correspondent aux positions des limites de mots – vous noterez qu’il y a une limite avant et après chaque mot (autrement dit, les espaces et signes de ponctuation sont considérés comme des « mots » en eux-même, et ne font pas parti des mots qu’ils séparent).

Pour faciliter le parcours, à partir de ce premier itérateur, il est possible d’en obtenir un second qui, lui, itére sur le texte, et plus sur les positions :

/* @var IntlPartsIterator $partsIterator */
$partsIterator = $wordIterator->getPartsIterator();

foreach ($partsIterator as $part) {
    var_dump($part);
}

Ici, nous obtiendrons réellement la liste des mots (et de leurs séparateurs) :

string(4) "Ceci"
string(1) " "
string(3) "est"
string(1) " "
string(3) "une"
string(1) " "
string(6) "phrase"
string(1) ","
string(1) " "
string(9) "composée"
string(1) " "
string(2) "de"
string(1) " "
string(9) "plusieurs"
string(1) " "
string(4) "mots"
string(1) "."

D’aucun diraient que ceci n’est pas forcément très utile et qu’il pourrait suffire de découper la phrase sur les espaces ou signes de ponctuation… Mais cet itérateur, comme les autres classes de l’extension intl, est sensible à la locale. Et la définition de mot n’est pas nécessairement la même dans toutes les langues.

Itération par phrases

Un peu dans la même logique, il est possible d’itérer sur un texte par phrases :

/* @var IntlRuleBasedBreakIterator $sentenceIterator */
$sentenceIterator = IntlRuleBasedBreakIterator::createSentenceInstance('fr_FR');
$sentenceIterator->setText("Ceci est une phrase, composée de plusieurs mots. Et voici une seconde phrase ? Hello !");

foreach ($sentenceIterator as $sentence) {
    var_dump($sentence);
}

A titre d’illustration, plutôt que de travailler avec la classe IntlBreakIterator comme je l’avais fait précédemment, j’ai ici choisi d’utiliser directement IntlRuleBasedBreakIterator.

Ici encore, l’itérateur renvoit les positions des séparations :

int(0)
int(50)
int(80)
int(87)

Et, comme précédemment, nous pouvons itérer sur les phrases textuelles en elles-mêmes à l’aide d’un second itérateur, accessible à partir du premier :

/* @var IntlPartsIterator $partsIterator */
$partsIterator = $sentenceIterator->getPartsIterator();

foreach ($partsIterator as $part) {
    var_dump($part);
}

Ce qui nous donnerait la sortie que nous attendrions :

string(50) "Ceci est une phrase, composée de plusieurs mots. "
string(30) "Et voici une seconde phrase ? "
string(7) "Hello !"

Notez que sur ce texte parcouru avec une locale française, les espaces placés après les symboles de ponctuation ont été remontés comme faisant parti de la phrase.

Itération par caractères

Dans une chaine de caractères en Unicode, chaque caractère occupe un nombre variable d’octets ; on ne peut pas considérer que le Xème caractère d’une chaine correspond au Xème octet de celle-ci – c’est d’ailleurs pour cela que les fonctions de manipulation de chaines standard de PHP ne sont pas adaptées à l’Unicode, puisqu’elles travaillent en mode octet.

Un nouvel itérateur a été ajouté à la branche master de PHP (et donc, probablement, à PHP 5.5), pour parcourir une chaine de caractères caractère par caractère : IntlCodePointBreakIterator.
Il est utilisable exactement comme IntlRuleBasedBreakIterator que nous avons vu juste au-dessus : voici un exemple d’instanciation de cet itérateur :

/* @var IntlCodePointBreakIterator $iterator */
$codePointIterator = IntlBreakIterator::createCodePointInstance();
$codePointIterator->setText("✔ caractères 😺");

foreach ($codePointIterator as $char) {
    var_dump($char);
}

Cette portion de code nous donnerait la sortie suivante :

int(0)
int(3)
int(4)
int(5)
int(6)
int(7)
int(8)
int(9)
int(10)
int(12)
int(13)
int(14)
int(15)
int(16)
int(20)

Notez que certains caractères font plus d’un octet ; en particulier :

  • fait 3 octets : les octets 0 à 2 de la chaine,
  • è fait 2 octets : les octets 10 et 11 de la chaine,
  • et 😺1 fait 4 octets : les octets 16 à 19 de la chaine.

Et pour obtenir les caractères en eux-mêmes, nous utiliserions encore une fois un second itérateur :

/* @var IntlPartsIterator $partsIterator */
$partsIterator = $codePointIterator->getPartsIterator();

foreach ($partsIterator as $part) {
    var_dump($part);
}

Qui nous donnerait la sortie suivante :

string(3) "✔"
string(1) " "
string(1) "c"
string(1) "a"
string(1) "r"
string(1) "a"
string(1) "c"
string(1) "t"
string(2) "è"
string(1) "r"
string(1) "e"
string(1) "s"
string(1) " "
string(4) "😺"

D’ailleurs, on voit bien ici que chaque caractère obtenu fait un caractère (c’était un peu le but, n’est-ce pas), mais que PHP voit des chaines de plus d’un octet (le chiffre indiqué entre parenthèses par var_dump()) pour les trois caractères spéciaux cités plus haut.

IntlCalendar et IntlGregorianCalendar

La version actuelle de la branche master de PHP (et donc, probablement, PHP 5.5) apporte deux nouvelles classes en rapport avec la gestion des dates : la classe générique IntlCalendar, et la sous-classe IntlGregorianCalendar qui correspond au calendrier le plus communément utilisé.

Un objet de type IntlCalendar permet d’obtenir toutes les informations de temps nécessaires au formatage d’un couple date/heure en fonction d’une locale donnée.

La façon la plus simple de voir quels types d’informations sont stockées par ce type d’objet est d’en instancier un :

$obj = IntlCalendar::createInstance('Europe/Paris', 'fr_FR');
var_dump( $obj );

Ici, en fonction du jour et de l’heure à laquelle cette portion de code serait exécutée, la sortie obtenue ressemblerait à quelque chose de ce type :

object(IntlGregorianCalendar)#1 (5) {
  ["valid"]=>
  bool(true)
  ["type"]=>
  string(9) "gregorian"
  ["timeZone"]=>
  array(4) {
    ["valid"]=>
    bool(true)
    ["id"]=>
    string(12) "Europe/Paris"
    ["rawOffset"]=>
    int(3600000)
    ["currentOffset"]=>
    int(7200000)
  }
  ["locale"]=>
  string(5) "fr_FR"
  ["fields"]=>
  array(23) {
    ["era"]=>
    int(1)
    ["year"]=>
    int(2012)
    ["month"]=>
    int(8)
    ["week of year"]=>
    int(39)
    ["week of month"]=>
    int(4)
    ["day of year"]=>
    int(274)
    ["day of month"]=>
    int(30)
    ["day of week"]=>
    int(1)
    ["day of week in month"]=>
    int(5)
    ["AM/PM"]=>
    int(1)
    ["hour"]=>
    int(3)
    ["hour of day"]=>
    int(15)
    ["minute"]=>
    int(55)
    ["second"]=>
    int(33)
    ["millisecond"]=>
    int(481)
    ["zone offset"]=>
    int(3600000)
    ["DST offset"]=>
    int(3600000)
    ["year for week of year"]=>
    int(2012)
    ["localized day of week"]=>
    int(7)
    ["extended year"]=>
    int(2012)
    ["julian day"]=>
    int(2456201)
    ["milliseconds in day"]=>
    int(57333481)
    ["is leap month"]=>
    int(0)
  }
}

Comme nous pouvons le constater, nous avons obtenu un objet de type IntlGregorianCalendar, qui regroupe 23 informations différentes, toutes en rapport avec la date et l’heure.

Accessoirement, il est possible d’instancier directement IntlGregorianCalendar :

$cal = IntlGregorianCalendar::createInstance('Europe/Paris', 'fr_FR');       // timezone, locale

Ou en utilisant le constructeur :

$cal = new IntlGregorianCalendar('Europe/Paris', 'fr_FR');


Et voici quelques exemples d’appels de méthodes de notre objet :

var_dump(
    $cal->get(IntlCalendar::FIELD_YEAR),
    $cal->get(IntlCalendar::FIELD_MONTH),
    $cal->get(IntlCalendar::FIELD_DAY_OF_YEAR)
);

var_dump( $cal->isLeapYear(2011) );     // bool(false)
var_dump( $cal->isLeapYear(2012) );     // bool(true)

var_dump( $cal->getMinimalDaysInFirstWeek() );      // int(4)

Ce qui nous donnerait les sorties correspondantes reproduites ci-dessous :

int(2012)
int(8)
int(274)
bool(false)
bool(true)
int(4)


A noter au besoin : on peut obtenir la liste des locales disponibles avec une portion de code de ce type :

$locales = IntlCalendar::getAvailableLocales();
var_dump($locales);

Qui donnerait :

array(491) {
  [0]=>
  string(2) "af"
  [1]=>
  string(5) "af_NA"
  [2]=>
  string(5) "af_ZA"
  [...]
  string(5) "fr_DJ"
  [182]=>
  string(5) "fr_FR"
  [183]=>
  string(5) "fr_GA"
  [...]

IntlTimeZone

La classe IntlTimeZone représente une zone de temps, et permet d’en obtenir une représentation textuelle localisée, ainsi que des informations liées à l’heure d’été / hiver.

Pour obtenir une instance d’objet IntlTimeZone, vous pouvez utiliser une portion de code de ce type :

$tz = IntlTimeZone::createTimeZone('Europe/Paris');
var_dump($tz);

La sortie retournée ici par var_dump() ressemblerait à celle-ci :

object(IntlTimeZone)#1 (4) {
  ["valid"]=>
  bool(true)
  ["id"]=>
  string(12) "Europe/Paris"
  ["rawOffset"]=>
  int(3600000)
  ["currentOffset"]=>
  int(7200000)
}

On voit ici clairement que la classe IntlTimeZone représente avant tout, en interne, une différence de temps entre la timezone spécifiée et le temps UTC.

En l’occurence, la timezone française correspond de base à GMT+1 (soit 3,600,000 millisecondes) ; et puisque nous étions à l’heure d’été au moment où j’ai écrit ceci (fin septembre 2012 – oui, entre le moment où j’ai rédigé cet article et le moment où je l’ai publié, quelques semaines se sont écoulées, et, entre temps, nous sommes passés à l’heure d’hiver ; mais cet exemple était de toute façon plus intéressant en heure d’été), la timezone française correspond actuellement à GMT+2 (soit 7,200,000 millisecondes).


Cette classe IntlTimeZone permet d’obtenir des informations sur la TimeZone ; par exemple, pour déterminer si une timezone utilise le mécanisme heure d’été / heure d’hiver, nous pourrions utiliser :

var_dump( $tz->useDaylightTime() );

Qui nous renverrait, pour notre TZ française :

bool(true)

Mais IntlTimeZone permet aussi d’obtenir une représentation textuelle, sous forme de chaine de caractères localisée, d’une timezone.
Par exemple, les quelques lignes suivantes :

var_dump( $tz->getDisplayName() );
var_dump( $tz->getDisplayName(false, IntlTimeZone::DISPLAY_SHORT_COMMONLY_USED, 'fr_FR') );
var_dump( $tz->getDisplayName(true, IntlTimeZone::DISPLAY_SHORT_COMMONLY_USED, 'fr_FR') );
var_dump( $tz->getDisplayName($tz->useDaylightTime(), IntlTimeZone::DISPLAY_SHORT_COMMONLY_USED, 'fr_FR') );

Renverraient une sortie de ce type :

string(36) "heure normale de l’Europe centrale"
string(9) "UTC+01:00"
string(9) "UTC+02:00"
string(9) "UTC+02:00"

Et en spécifiant d’autres locales, comme ceci :

var_dump( $tz->getDisplayName(true, IntlTimeZone::DISPLAY_LONG, 'fr_FR') );
var_dump( $tz->getDisplayName(true, IntlTimeZone::DISPLAY_LONG, 'en_US') );
var_dump( $tz->getDisplayName(true, IntlTimeZone::DISPLAY_LONG, 'de') );

Nous obtiendrions :

string(34) "heure avancée d’Europe centrale"
string(28) "Central European Summer Time"
string(29) "Mitteleuropäische Sommerzeit"

Ou encore, en travaillant sur une autre timezone :

$tzTaht = IntlTimeZone::createTimeZone('Pacific/Tahiti');
var_dump( $tzTaht->useDaylightTime() );
var_dump( $tzTaht->getDisplayName() );
var_dump( $tzTaht->getDisplayName(true, IntlTimeZone::DISPLAY_SHORT_COMMONLY_USED, 'fr_FR') );
var_dump( $tzTaht->getDisplayName(true, IntlTimeZone::DISPLAY_GENERIC_LOCATION, 'fr_FR') );

Nous aurions une sortie ressemblant à celle-ci :

bool(false)
string(23) "heure normale de Tahiti"
string(9) "UTC-10:00"
string(39) "Heure : Polynésie française (Tahiti)"

Peut-être plus à venir ?

Tout juste hier, une nouvelle RFC en rapport avec l’extension intl a été postée : RFC: ext/intl::UConverter.

Pour l’instant, rien en rapport avec ceci n’a encore été mergé sur la branche master ; je considère donc que « ça n’existe pas dans PHP 5.5 »… mais les quelques retours qui ont jusqu’à présent été posté sur la mailing-list internals@ ont l’air plutôt bons ; à voir comment les choses évolueront dans les prochaines semaines, donc ;-)

Voir aussi / plus d’informations sur les classes Intl

Les classes Intl de PHP sont, de manière générale, un simple mapping vers les classes de même nom (sans le préfixe Intl) de la bibliothèque ICU. La documentation de celle-ci est donc fréquemment une bonne source d’informations :


De manière générale, indépendamment du fait que nous parlions ici d’intl, pour obtenir plus d’informations sur une classe et ses méthodes, vous pouvez utiliser l’API de Reflection de PHP.

En fait, ceci est même facilité en ligne de commande, avec les cinq options suivantes, acceptées par le binaire php :

--rf <name>      Show information about function <name>.
--rc <name>      Show information about class <name>.
--re <name>      Show information about extension <name>.
--rz <name>      Show information about Zend extension <name>.
--ri <name>      Show configuration for extension <name>.

Par exemple, pour obtenir la liste des méthodes (et constantes, propriétés, …) de la classe IntlGregorianCalendar, nous pourrions utiliser :

$HOME/bin/php-5.5/bin/php --rc IntlGregorianCalendar
Class [ <internal:intl> class IntlGregorianCalendar extends IntlCalendar ] {

  - Constants [0] {
  }

  - Static properties [0] {
  }

  - Static methods [5] {
    Method [ <internal:intl, inherits IntlCalendar> static public method createInstance ] {

      - Parameters [2] {
        Parameter #0 [ <optional> $timeZone ]
        Parameter #1 [ <optional> $locale ]
      }
    }
    
    [...]

    Method [ <internal:intl, inherits IntlCalendar> public method getErrorMessage ] {

      - Parameters [0] {
      }
    }
  }
}

Sans être la solution parfaite, lorsque la documentation d’une classe n’est pas encore écrite, ceci devrait déjà vous donner quelques pistes quant à son utilisation – et, dans le cas de l’extension intl, vous pouvez vous reporter à la documentation de la bibliothèque ICU pour connaitre le rôle de chaque méthode.

``` Oh, un caractère qui représente un chat \o/