Du bon usage des ruptures… en programmation (part. 1)

La « rupture » est une technique algorithmique qui peut rendre de grands services, mais elle est peu connue et souvent mal maîtrisée. Je vais tenter de démystifier le sujet au travers d’un tutoriel composé de :

  • une première partie introduisant le sujet, au travers d’un cas relativement simple (voire simpliste)
  • une seconde partie, dans laquelle je proposerai un exemple plus approfondi, et surtout plus proche de la vie réelle

Vous pourrez tester les exemples de code de ce tutoriel, soit dans votre environnement PHP préféré (WAMP, XAMPP, etc..), soit via le site PHP Sandbox.

La « rupture » se révèle particulièrement utile dans les traitements de type « reporting », ainsi que dans les traitements manipulant de gros volumes de données. Ce sont souvent les développeurs travaillant sur gros systèmes, avec des langages de la même génération que le Cobol, qui maîtrisent le mieux cette technique. Nous allons utiliser la même technique en PHP pour générer un petit tableau HTML de type « statistique de vente mensuelle ».

Pour illustrer mon propos, je vous propose de partir d’un tableau de statistiques de ventes :

Mois Catégorie Ventes
Janvier chaussures 10000
Janvier tee-shirts 1000
Janvier pulls 50000
Janvier vestes 20000
Février chaussures 5000
Février tee-shirts 4000
Février pulls 3500

 

Les données sont triées par mois et par catégorie de produit. Le libellé correspondant au mois revenant de manière récurrente sur plusieurs lignes, il serait intéressant de pouvoir générer un tableau de ce type :

Mois Catégorie Ventes Total
Janvier chaussures 10000 81000
tee-shirts 1000
pulls 50000
vestes 20000
Février chaussures 5000 12500
tee-shirts 4000
pulls 3500

 

Dans l’exemple de tableau ci-dessus, vous noterez deux éléments importants :

  • nous effectuons une totalisation des ventes par mois, il nous faut donc être en mesure d’identifier tout changement de mois, donc toute « rupture » sur le mois pour adapter l’affichage du tableau et pour effectuer le cumul des ventes par mois
  • d’un point de vue HTML, pour « caler » une seule cellule « mois » en face de plusieurs cellules « catégorie », nous devons jouer sur l’attribut HTML « rowspan ». Par exemple, le mois de « février » couvre 3 lignes du tableau, car son attribut « rowspan » a la valeur « 3 ». Le positionnement de l’attribut « rowspan » a un impact direct sur la manière dont notre algorithme va générer le tableau HTML dans son ensemble.

Pour bien comprendre les contraintes et les solutions qui s’offrent à nous, voici le jeu d’essai simplifié avec lequel nous allons travailler dans la première partie de ce tutoriel :

$stats = array();
$stats[] = array(
 'mois' => 'Janvier',
 'categorie' => 'chaussures',
 'ventes' => 10000
);
$stats[] = array(
 'mois' => 'Janvier',
 'categorie' => 'tee-shirts',
 'ventes' => 1000
);
$stats[] = array(
 'mois' => 'Janvier',
 'categorie' => 'pulls',
 'ventes' => 50000
);
$stats[] = array(
 'mois' => 'Janvier',
 'categorie' => 'vestes',
 'ventes' => 20000
);
$stats[] = array(
 'mois' => 'Février',
 'categorie' => 'chaussures',
 'ventes' => 5000
);
$stats[] = array(
 'mois' => 'Février',
 'categorie' => 'tee-shirts',
 'ventes' => 4000
);
$stats[] = array(
 'mois' => 'Février',
 'categorie' => 'pulls',
 'ventes' => 3500,
);

Je vous propose d’étudier un premier exemple de script PHP, qui génère parfaitement le tableau statistique que nous voulons obtenir (avec les totalisations par mois), mais sans utiliser de notion de rupture.

Dans la version ci-dessous (n’utilisant pas de notion de rupture), j’ai d’abord écrit une boucle « foreach » pour analyser l’ensemble du jeu de données, et préparer deux tableaux consolidant les données :

  • le premier tableau permet de stocker le total des ventes par mois,
  • le second permet de stocker, pour chaque mois, le détail des catégories de produits avec leurs chiffres de vente respectifs.

Une fois ces deux tableaux précalculés, la seconde partie du script consiste à utiliser deux boucles « foreach » imbriquées pour produire le tableau HTML final :

$stats_mois_total = array();
$stats_mois_detail = array();
// boucle de précalcul
foreach ($stats as $stats_datas) {
  if (!array_key_exists($stats_datas['mois'],
     $stats_mois_total)) {
      $stats_mois_total [$stats_datas['mois']] = 0;
      $stats_mois_detail [$stats_datas['mois']] =
          array();
  }
  $stats_mois_total [$stats_datas['mois']] +=
      $stats_datas['ventes'];
  $stats_mois_detail [$stats_datas['mois']][] = 
    array(
     'categorie' => $stats_datas['categorie'],
     'ventes' => $stats_datas['ventes'],
    );
}
// boucle d'affichage du tableau de stats
echo '<table border="1px">' . PHP_EOL;
echo '<tr><td>Mois</td><td>Catégorie</td><td>Ventes</td><td>Total</td></tr>' . PHP_EOL;
foreach ($stats_mois_total as $stat_key => $stat_value) {
   $rowspan = count($stats_mois_detail[$stat_key]);
   $first_line = true;
   foreach ($stats_mois_detail[$stat_key] as
       $stat_value2) {
     echo '<tr>' . PHP_EOL;
     if ($first_line) {
        echo "<td rowspan='$rowspan'>" . $stat_key .
           '</td>' . PHP_EOL;
     }
     echo '<td>' . $stat_value2['categorie'] . 
        '</td>' . PHP_EOL;
     echo '<td align="right">' .
         $stat_value2['ventes'] . '</td>' . PHP_EOL;
     if ($first_line) {
        echo "<td rowspan='$rowspan'>" . $stat_value .
          '</td>' . PHP_EOL;
        $first_line = false;
     }
     echo '</tr>' . PHP_EOL;
   }
}
echo '</table>' . PHP_EOL;

La technique ci-dessus fonctionne bien, je vous invite à l’essayer. Mais elle présente l’inconvénient majeur de nous obliger à analyser la totalité du jeu de données d’origine, avant d’être en mesure de produire la moindre édition. Si notre jeu de données d’origine est volumineux, le traitement d’analyse risque de prendre un certain temps… mais surtout l’empreinte mémoire du traitement risque d’être conséquente, du fait que l’on duplique un grand nombre de données, en particulier dans le second tableau ($stats_mois_detail).

Et avec les ruptures, cela donne quoi ?

Dans l’exemple qui va suivre, nous produisons le même tableau, mais cette fois avec une boucle  « foreach » principale gérant une notion de rupture (sur le mois), et confiant à une fonction PHP distincte (la fonction « rupture_mois_edit ») le soin d’éditer les données relatives au seul mois considéré :

function rupture_mois_edit($stat_key, $stat_values, $stat_total) {
   $output = '';
   $rowspan = count($stat_values);
   $first_line = true;
   foreach ($stat_values as $stat_value) {
     $output .= '<tr>' . PHP_EOL;
     if ($first_line) {
        $output .= "<td rowspan='$rowspan'>" . 
          $stat_key . '</td>' . PHP_EOL;
     }
     $output .= '<td>' . $stat_value['categorie'] .
        '</td>' . PHP_EOL;
     $output .= '<td align="right">' . 
        $stat_value['ventes'] . '</td>' . PHP_EOL;
     if ($first_line) {
       $output .= "<td rowspan='$rowspan'>" . 
           $stat_total . '</td>' . PHP_EOL;
       $first_line = false;
     }
     $output .= '</tr>' . PHP_EOL;
   }
   return $output;
}

echo '<table border="1px">' . PHP_EOL;
echo '<tr><td>Mois</td><td>Catégorie</td><td>Ventes</td><td>Total</td></tr>' . PHP_EOL;

// initialisation de la variable $rupture_mois 
// il est recommandé de l'initialiser avec une valeur
// que vous êtes sûr de ne pas retrouver dans le jeu
// de données à "parcourir"
$rupture_init = 'XXXXX';
$rupture_mois = $rupture_init;
$total_mois = 0;

foreach ($stats as $stat_key => $stat_value) {
  // détection du cas où on a déjà les données d'un
  // mois en mémoire, données que l'on souhaite
  // éditer avant de passer à un nouveau mois
  if ($stat_value['mois'] != $rupture_mois && 
       $rupture_mois != $rupture_init) {
    echo rupture_mois_edit($rupture_mois, 
           $stats_mois, $total_mois);
  }
  // détection du cas où on change de mois et 
  // où on démarre un nouveau cycle
  if ($stat_value['mois'] != $rupture_mois) {
     $rupture_mois = $stat_value['mois'];
     $stats_mois = array();
     $total_mois = 0;
  }
  $total_mois += $stat_value['ventes'];
  $stats_mois [] = array(
    'categorie' => $stat_value['categorie'],
    'ventes' => $stat_value['ventes'],
  );
}

// Attention, il ne faut pas oublier d'appeler 
// une dernière fois, après le foreach, la 
// fonction d'édition de la rupture (si on l'oublie 
// on n'éditera pas le dernier mois, qui est stocké
// dans le tableau $stats_mois)
if ($rupture_mois != $rupture_init) {
  echo rupture_mois_edit($rupture_mois, 
          $stats_mois, $total_mois);
}
echo '</table>' . PHP_EOL;

Le code est sans aucun doute plus volumineux que dans l’exemple précédent, mais c’est aussi dû au fait que j’ai été généreux en termes de commentaires.

L’algorithme relatif à la boucle principale pourrait s’apparenter au pseudo-code suivant, dans lequel j’ai utilisé volontairement des noms de fonction différents de ceux utilisés dans le script PHP (dans un souci de clarification de l’algorithme) :

Lecture_séquentielle_tableau ($stats)
Tant_que Non fin de tableau ($stats) {
   Appel_fonction Détecter_changement_mois
   Appel_fonction Cumul_mois
   Lecture_séquentielle_tableau ($stats)
   Appel_fonction Editer_Total_mois
} FinTantQue

L’algorithme ci-dessus fonctionnerait bien avec une boucle de lecture écrite en PL/SQL… mais avec la boucle « foreach » du PHP, les actions de « lecture séquentielle » et de détection de « fin de fichier » sont confondues, c’est pourquoi notre algorithme final ressemble plutôt à ceci :

Lecture_séquentielle_tableau ($stats)
Tant_que Non fin de tableau ($stats) {
   Appel_fonction Editer_Total_mois_précédent
   Appel_fonction Détecter_changement_mois
   Appel_fonction Cumul_mois
} FinTantQue
Appel_fonction Editer_Total_mois_précédent

Vous noterez ici qu’il est impératif de rappeler une dernière fois, après le « foreach » principal, la fonction « Editer_Total_mois_précédent ».  Sans ce dernier appel, notre tableau statistique serait incomplet. Je vous invite à commettre l’erreur volontairement, de manière à bien en mesurer les effets.

Fort de ces quelques précisions, je vous recommande d’analyser le script PHP ci-dessus, afin de bien en saisir la logique. Vous constaterez qu’une rupture, c’est en définitive un moyen simple de détecter un changement d’état. En terme de résultat produit, cela s’apparente beaucoup à la clause GROUP BY d’une requête SQL de type SELECT. On pourra s’appuyer sur un algorithme de ce type chaque fois que nos données seront stockées dans un système de fichier n’offrant pas les facilités du langage SQL. Pour que cela fonctionne, cependant, il faudra s’assurer que les données traitées sont bien triées  conformément à la rupture définie.

Notre exemple de rupture était ici relativement simple, car nous n’avions qu’un seul critère de rupture : le mois.

Serait-il possible de gérer des ruptures sur plusieurs niveaux de données ? La réponse est bien évidemment « oui », et nous nous attellerons au développement d’un exemple plus proche de la « vraie vie », dans la seconde partie de ce tutoriel (dont la rédaction est en cours).

A suivre donc…