T: / Corrigés des challenges / PHP
On prend les éléments de l’énoncé, pas à pas, pour construire des tests unitaires et verrouiller notre code.
Septième corrigé des challenges de l’histoire originale Greenoïd, créée à l’occasion de la Battle Dev Thales 2024.
Il était question dans ce challenge d’appliquer plusieurs opérations sur des chaines de caractères : les passer en binaire, les composer, les repasser en base 10, mettre en place des correspondances avec des caractères, etc. Il y avait une complexité algorithmique sur le fait de devoir « composer » les clés une à une sans doublon, sinon il s’agissait d’opérations assez abordables mais qu’il convenait de bien structurer pour les réaliser pas à pas correctement.
L’énoncé présentait donc les étapes à réaliser, avec à chaque fois des exemples de données. Je propose dans ce corrigé de s’appuyer sur ces exemples de données pour résoudre ce challenge en TDD, Test Driven Development, c’est à dire d’écrire d’abord les tests puis d’écrire le code qui permet de répondre aux tests. Pour cela, on travaillera en PHP avec le framework Pest. Mais la logique est la même peu importe le langage ou le framework de tests unitaires utilisés.
Au programme :
La première étape consistait en transformer une clé de type « 383jh4bn5s525 » en une valeur binaire sur 8 bits, c’est à dire composée de 8 caractères (0 ou 1). Pour cela, 4 étapes :
La première étape « Transformer chaque caractère en une valeur numérique » pouvait être découpée en 2 tests unitaires :
test('Chaque caractère a une valeur (chiffre)', function() {
// un “0” vaut 0, un “1” vaut 1, un “2” vaut 2, un “3”, vaut 3
expect(CommunicationNetwork::charToNumber('0'))->toBe(0);
expect(CommunicationNetwork::charToNumber('1'))->toBe(1);
expect(CommunicationNetwork::charToNumber('2'))->toBe(2);
expect(CommunicationNetwork::charToNumber('3'))->toBe(3);
// Jusque 9
expect(CommunicationNetwork::charToNumber('9'))->toBe(9);
});
test('Chaque caractère a une valeur (lettre)', function() {
// un “a” vaut 10, un “b” vaut 11, un “c” vaut 12, etc.
expect(CommunicationNetwork::charToNumber('a'))->toBe(10);
expect(CommunicationNetwork::charToNumber('b'))->toBe(11);
expect(CommunicationNetwork::charToNumber('c'))->toBe(12);
});
Je reprends dans les commentaires des tests unitaires les éléments de l’énoncé. Je me suis permis ici de grouper plusieurs assertions par test, ce qui n’est pas forcément une bonne pratique.
Si on exécute ces tests, forcément, ils renvoient des erreurs, puisque la classe CommunicationNetwork n’existe pas encore. Mais c’est le principe du TDD : les tests d’abord, le code ensuite.
On crée donc la classe CommunicationNetwork avec la première méthode « nue » :
class CommunicationNetwork
{
public static function charToNumber(string $char): int
{
return 0;
}
}
Si je relance les tests, il n’y a plus d’erreur technique, mais les tests sont toujours échoués.
Voici une version possible de la méthode charToNumber :
public static function charToNumber(string $char): int
{
if (is_numeric($char)) {
return (int) $char;
}
$alphabet = range('a', 'z');
return array_search($char, $alphabet) + 10;
}
Un peu d’explications :
Il y a bien sûr d’autres façons de faire, mais avec ce code, mes tests passent au vert, je peux continuer !
Maintenant j’ai dans l’énoncé l’exemple d’une clé transformée, je peux donc créer un test associé :
test('Passage des clés binaires en 8 bits', function() {
expect(CommunicationNetwork::keyTo8Bits('ab34'))->toBe('00011100');
});
Je n’ai qu’un exemple, mais on va s’en contenter. Comme précédemment, je crée une méthode qui permet de valider ce test :
public static function keyTo8Bits(string $key): string
{
$value = 0;
foreach (str_split($key) as $char) {
$value += self::charToNumber($char);
}
$value = decbin($value);
return str_pad($value, 8, '0', STR_PAD_LEFT);
}
Un peu d’explications :
Et hop, test validé !
L’énoncé donnait un exemple de composition :
On pouvait donc écrire le test unitaire associé :
test('Composition de deux clés', function() {
expect(CommunicationNetwork::composition('1001', '1100'))->toBe('1010');
});
Encore une fois, un seul exemple, mais qu’on peut « inverser » et qui doit toujours donner la même valeur. Pour avoir alors 2 assertions :
test('Composition de deux clés', function() {
expect(CommunicationNetwork::composition('1001', '1100'))->toBe('1010');
expect(CommunicationNetwork::composition('1100', '1001'))->toBe('1010');
});
Et la méthode « composition » :
public static function composition(string $key1, string $key2): string
{
$composition = '';
$length = strlen($key1);
for ($i = 0; $i < $length; $i++) {
$composition .= $key1[$i] === $key2[$i] ? '1' : '0';
}
return $composition;
}
Un peu d’explications :
La composition de toutes les clés
L’énoncé indiquait : « Il faut donc déterminer toutes les compositions possibles, sans doublon. Si tu as analysé la clé 3 avec la clé 5, plus besoin d’analyser la clé 5 avec la clé 3.«
Pour réussir cela, on pouvait imbriquer 2 boucles for l’une dans l’autre, en utilisant l’index de la première boucle comme point de départ de la seconde :
$compositions = [];
for ($i = 0; $i < 8; $i++) {
for ($j = $i + 1; $j < 8; $j++) {
// Ajout de la combinaison unique (sans doublon)
$compositions[] = CommunicationNetwork::composition($binaryKeys[$i], $binaryKeys[$j]);
}
}
La subtilité se trouve dans l’initialisation de $j avec $j = $i + 1; on utilise $i. La première boucle a 8 itérations, lorsque la seconde a un nombre d’itérations qui diminue à chaque fois, d’abord 7, puis 6, puis 5, etc. Permettant d’éviter le traitement de doublons.
Pour le passage en base 10 et le tri, PHP propose des fonctions natives :
On va se concentrer alors sur le passage d’un nombre vers le caractère de la clé finale attendue. Avec les données de l’énoncé, on pouvait créer ce test avec 4 assertions :
test('Base 10 vers caractère', function() {
expect(CommunicationNetwork::toChar(196))->toBe('g');
expect(CommunicationNetwork::toChar(144))->toBe('0');
expect(CommunicationNetwork::toChar(178))->toBe('y');
expect(CommunicationNetwork::toChar(243))->toBe('r');
});
Et la méthode « toChar » :
public static function toChar(int $number): string
{
$position = $number % 36;
$table = array_merge(range(0, 9), range('a', 'z'));
return (string) $table[$position];
}
Un peu d’explications :
Utilisation de array_map en PHP
On peut utiliser array_map de 2 façons différentes pour réaliser nos différentes opérations. Considérons que nos compositions sont dans $compositions :
// Passage en base 10
$compositions = array_map('bindec', $compositions);
// Tri
sort($compositions);
// Passage en caractère
$compositions = array_map([CommunicationNetwork::class, 'toChar'], $compositions);
Un peu d’explications :
Dans le contexte de la Battle Dev, l’heure n’était pas écrire des tests unitaires, il fallait aller vite ! Mais dans un autre contexte, pour des challenges qui présentent à la fois des étapes bien définies, et des exemples de données (entrées / sorties), l’écriture de tests unitaires est un bon moyen de bien structurer son code et d’apprendre à le découper de la bonne façon. Bien souvent, un code testable facilement est un code qui est bien structuré.
N’hésite pas à repartir d’un autre challenge de code et d’appliquer à nouveau cette méthodologie !
Et voici la solution complète, en 3 parties :
Les tests unitaires
<?php
declare(strict_types=1);
use Challenges\GREENOID_7\CommunicationNetwork;
test('Chaque caractère a une valeur (chiffre)', function() {
// un “0” vaut 0, un “1” vaut 1, un “2” vaut 2, un “3”, vaut 3
expect(CommunicationNetwork::charToNumber('0'))->toBe(0);
expect(CommunicationNetwork::charToNumber('1'))->toBe(1);
expect(CommunicationNetwork::charToNumber('2'))->toBe(2);
expect(CommunicationNetwork::charToNumber('3'))->toBe(3);
// Jusque 9
expect(CommunicationNetwork::charToNumber('9'))->toBe(9);
});
test('Chaque caractère a une valeur (lettre)', function() {
// un “a” vaut 10, un “b” vaut 11, un “c” vaut 12, etc.
expect(CommunicationNetwork::charToNumber('a'))->toBe(10);
expect(CommunicationNetwork::charToNumber('b'))->toBe(11);
expect(CommunicationNetwork::charToNumber('c'))->toBe(12);
});
test('Passage des clés binaires en 8 bits', function() {
expect(CommunicationNetwork::keyTo8Bits('ab34'))->toBe('00011100');
});
test('Composition de deux clés', function() {
expect(CommunicationNetwork::composition('1001', '1100'))->toBe('1010');
expect(CommunicationNetwork::composition('1100', '1001'))->toBe('1010');
});
test('Base 10 vers caractère', function() {
expect(CommunicationNetwork::toChar(196))->toBe('g');
expect(CommunicationNetwork::toChar(144))->toBe('0');
expect(CommunicationNetwork::toChar(178))->toBe('y');
expect(CommunicationNetwork::toChar(243))->toBe('r');
});
La preuve en image :
La classe CommunicationNetwork
<?php
declare(strict_types=1);
namespace Challenges\GREENOID_7;
class CommunicationNetwork
{
public static function charToNumber(string $char): int
{
if (is_numeric($char)) {
return (int) $char;
}
$alphabet = range('a', 'z');
return array_search($char, $alphabet) + 10;
}
public static function keyTo8Bits(string $key): string
{
$value = 0;
foreach (str_split($key) as $char) {
$value += self::charToNumber($char);
}
$value = decbin($value);
return str_pad($value, 8, '0', STR_PAD_LEFT);
}
public static function composition(string $key1, string $key2): string
{
$composition = '';
$length = strlen($key1);
for ($i = 0; $i < $length; $i++) {
$composition .= $key1[$i] === $key2[$i] ? '1' : '0';
}
return $composition;
}
public static function toChar(int $number): string
{
$position = $number % 36;
$table = array_merge(range(0, 9), range('a', 'z'));
return (string) $table[$position];
}
}
Le programme principal
<?php
declare(strict_types=1);
namespace Challenges\GREENOID_7;
use Tainix\Html;
// Données
$keys = ['383jh4bn5s525', '91o27x84if8', '14vo45o28l', '8w6nkr2g899g5', '48lcj889m1', 'ac3x2f21i3c7', 'h1n4749bw254', 'ttp994j27267'];
// Résolution
// Passage des clés en binaire sur 8 bits
$binaryKeys = [];
foreach ($keys as $key) {
$binaryKeys[] = CommunicationNetwork::keyTo8Bits($key);
}
// Compositions
$compositions = [];
for ($i = 0; $i < 8; $i++) {
for ($j = $i + 1; $j < 8; $j++) {
// Ajout de la combinaison unique (sans doublon)
$compositions[] = CommunicationNetwork::composition($binaryKeys[$i], $binaryKeys[$j]);
}
}
// Passage en base 10
$compositions = array_map('bindec', $compositions);
// Tri
sort($compositions);
// Passage en caractère
$compositions = array_map([CommunicationNetwork::class, 'toChar'], $compositions);
// Solution
echo implode($compositions);
Other content to discover