Résolution étape par étape et présentation des Collections

Un corrigé PHP progressif pour apprendre à regrouper, trier et exploiter des données, avec une ouverture sur l’usage des Collections.

→ Challenge Correction: +3 Days ! Introduction

Introduction

Dans ce challenge, on doit aider Naya à analyser une liste de frictions rencontrées pendant des trajets. L’objectif est de repérer, pour chaque type de friction, les 3 durées les plus importantes. Ensuite, on estime le gain possible si ces 3 frictions étaient réduites de 50 %. Enfin, on affiche les 5 gains obtenus dans l’ordre décroissant, séparés par des points-virgules.

Le problème peut sembler être un simple calcul, mais la vraie difficulté est ailleurs : il faut d’abord organiser les données.

Au départ, on reçoit une liste de chaînes comme parking;46 ou red_light;88. Pour calculer proprement les gains, on va transformer cette liste brute en données regroupées par type.

On va commencer avec une solution PHP classique, basée sur des tableaux et des boucles. Ensuite, on fera une ouverture vers les Collections, une approche plus déclarative qu’on retrouve notamment dans certains frameworks PHP, et qui se rapproche de méthodes très utilisées en JavaScript.

Au programme :

Etape 1 : organiser les données

Les données sont fournies sous forme de chaînes de caractères. Chaque élément contient deux informations séparées par un ; : le type de friction et la durée perdue. Par exemple, la chaîne parking;46 signifie que la friction est de type parking et qu’elle représente une perte de 46 secondes.

Avant de chercher les plus grandes valeurs, on va donc commencer par parcourir le tableau $frictions, découper chaque chaîne avec explode(), puis ranger chaque durée dans un tableau associatif. La clé du tableau sera le type de friction, et la valeur sera la liste des durées associées à ce type.

$durationsByType = [];

foreach ($frictions as $friction) {
    [$type, $seconds] = explode(';', $friction);

    $durationsByType[$type][] = (int) $seconds;
}

// Après cette étape on obtient une structure sous cette forme : (exemple)

[
    'parking' => [46, 74, 16, 204, 234, 78, 262, 156, 122, 218],
    'red_light' => [88, 44, 154, 134, 108, 234, 226, 108, 54],
    'detour' => [270, 122, 272, 56, 162, 110, 292, 124],
    'transfer' => [26, 162, 86, 38, 110, 210, 288],
    'crowd' => [50, 20, 282, 220, 108, 96],
]

La ligne importante est celle-ci :

$durationsByType[$type][] = (int) $seconds;

Elle ajoute la durée dans le bon groupe. Si le type est parking, la durée est ajoutée dans $durationsByType['parking']. Si le type est detour, elle est ajoutée dans $durationsByType['detour'].

On convertit aussi la durée en entier avec (int). C’est une bonne habitude : après un explode(), les données récupérées sont des chaînes de caractères. Comme on veut faire des calculs, on transforme explicitement la durée en nombre.

Etape 2 : calculer le gain pour chaque type

Maintenant que les durées sont regroupées par type, on peut calculer le gain potentiel pour chaque groupe.

Pour chaque type de friction, il faut :

  1. trier les durées de la plus grande à la plus petite ;
  2. garder les 3 premières valeurs ;
  3. additionner ces 3 valeurs ;
  4. diviser le résultat par 2 pour simuler une réduction de 50 %.

On stocke ensuite chaque gain dans un tableau $gains.

$gains = [];

foreach ($durationsByType as $durations) {
    // On trie les valeurs par ordre décroissant, les plus élevées en premières donc
    rsort($durations);

    // On prend une "part" du tableau, les 3 éléments à partir de l'index 0, donc les 3 premiers
    $topDurations = array_slice($durations, 0, 3);

    // On fait la somme de ces 3 éléments qu'on divise par 2
    $gains[] = array_sum($topDurations) / 2;
}

Etape 3 : trier les gains et produire la sortie

Le challenge ne demande pas d’afficher les types de frictions. Il demande seulement les gains, dans l’ordre décroissant, séparés par des ;.

On trie donc le tableau $gains du plus grand au plus petit, puis on utilise implode() pour produire une chaîne au bon format.

rsort($gains, SORT_NUMERIC);

echo implode(';', $gains);

Solution complète :

<?php
//== NE PAS TOUCHER
$frictions = ['parking;46', 'parking;74', 'red_light;88', 'detour;270', 'detour;122', 'parking;16', 'transfer;26', 'red_light;44', 'detour;272', 'parking;204', 'red_light;154', 'transfer;162', 'crowd;50', 'crowd;20', 'parking;234', 'parking;78', 'crowd;282', 'parking;262', 'parking;156', 'transfer;86', 'parking;122', 'crowd;220', 'red_light;134', 'detour;56', 'detour;162', 'red_light;108', 'red_light;234', 'detour;110', 'crowd;108', 'transfer;38', 'detour;292', 'crowd;96', 'transfer;110', 'red_light;226', 'detour;124', 'transfer;210', 'red_light;108', 'red_light;54', 'transfer;288', 'parking;218'];
//== NE PAS TOUCHER

$durationsByType = [];

foreach ($frictions as $friction) {
    [$type, $seconds] = explode(';', $friction);

    $durationsByType[$type][] = (int) $seconds;
}

$gains = [];

foreach ($durationsByType as $durations) {
    rsort($durations);

    $topDurations = array_slice($durations, 0, 3);

    $gains[] = array_sum($topDurations) / 2;
}

rsort($gains);

echo implode(';', $gains);

Cette solution est volontairement progressive. On prépare d’abord les données, puis on calcule les gains, puis on formate le résultat. C’est souvent une bonne méthode en algorithmie : quand les données de départ ne sont pas dans une forme pratique, on commence par les transformer.

Présentation des Collections en PHP

La solution précédente est celle à utiliser dans l’environnement du challenge, car elle fonctionne en PHP pur. Mais il existe une autre manière d’écrire ce type de traitement : les Collections.

Les Collections permettent de manipuler des tableaux avec des méthodes chaînées. C’est une approche plus déclarative : au lieu d’écrire nous-mêmes toutes les boucles, on décrit les transformations successives à appliquer.

Dans ce challenge, cette approche correspond très bien au raisonnement. On part d’une liste brute de frictions, on transforme chaque chaîne en donnée structurée, on groupe par type, on calcule un gain pour chaque groupe, puis on trie les gains.

Voici ce que donnerait une version avec Collections :

<?php

echo collect($frictions)
->map(function (string $friction): array {
[$type, $seconds] = explode(';', $friction);

return [
'type' => $type,
'seconds' => (int) $seconds,
];
})
->groupBy('type')
->map(function ($frictionsOfType) {
return $frictionsOfType
->sortByDesc('seconds')
->take(3)
->sum('seconds') / 2;
})
->sortDesc()
->values()
->implode(';');

Cette version fait exactement le même travail que la solution précédente. Un peu d’explications :

  • La première méthode, map(), transforme chaque chaîne comme parking;46 en tableau structuré avec un type et une durée en seconds.
  • Ensuite, groupBy('type') regroupe les frictions par type. C’est l’équivalent de notre tableau associatif $durationsByType.
  • Le second map() calcule le gain pour chaque groupe : on trie les frictions par durée décroissante, on garde les 3 premières, on additionne les secondes, puis on divise par 2.
  • Enfin, sortDesc() trie les gains du plus grand au plus petit, values() réindexe proprement la liste, et implode(';') produit la sortie finale.

Cette version est plus compacte et plus expressive, mais elle nécessite que les Collections soient disponibles. Dans l’environnement Tainix de ce challenge, ce n’est pas le cas. Mais cela reste une approche intéressante quand on se trouvera dans un projet en dehors de Tainix !

Comment charger et utiliser les Collections ?

Les Collections ne font pas partie de PHP par défaut. Dans l’exemple précédent, on utilise la syntaxe des Collections Laravel, qui vient du package illuminate/collections.

Dans un projet PHP classique, il faut donc d’abord installer ce package avec Composer :

composer require illuminate/collections

Puis importer la classe Collection avec use :

use Illuminate\Support\Collection;

// Utilisation :
$collection = Collection::make($frictions);

Dans Laravel, c’est encore plus simple : les Collections sont déjà disponibles, et on peut généralement utiliser la fonction helper collect() directement :

$collection = collect($frictions);

Conclusion

Ce challenge montre qu’avant de faire un calcul, il faut parfois commencer par organiser les données. La liste de départ contient des chaînes comme parking;46, qui ne sont pas très pratiques à exploiter directement. En regroupant les durées par type de friction, on transforme le problème en une suite d’étapes simples.

La solution PHP classique utilise des notions essentielles : explode(), les tableaux associatifs, foreach, rsort(), array_slice(), array_sum() et implode(). C’est la solution à retenir pour l’environnement Tainix.

L’ouverture sur les Collections permet de découvrir une autre manière de penser le même problème. On ne change pas l’algorithme, mais on l’écrit sous forme de pipeline de transformations. C’est une approche plus proche de ce qu’on peut rencontrer dans certains frameworks PHP ou en JavaScript.

La leçon importante à retenir : quand les données sont bien organisées, les calculs deviennent beaucoup plus faciles.


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover