Les tests binaires – une fonctionnalité sous-estimée

Imaginons qu’en tant que développeur (dans le cas contraire, passe à un autre article), on doit réaliser plusieurs actions suivant plusieurs cas de figures possibles (disons, une quinzaine), qui peuvent se combiner.
Et suivant certains cas de figures, certains actions à éxécuter se répètent, et peuvent même se combiner suivant certains cas de figure. Pour ne pas dubliquer le code, on identifie donc 5 blocs d’actions différentes. La situation la plus simple serait donc d’utiliser plusieurs variables : (je vais mettre mes exemples en PHP, mais les tests binaires existent dans tous les langages de programmation dignes de ce nom – déjà si ça existe en PHP…)

$caseA = false;
$caseB = true;
$caseC = true;
$caseD = false;
$caseE = true;

$action1 =
$action2 =
$action3 =
$action4 =
$action5 = false;

if ($caseA === true) {
    $action1 =
    $action3 = true;
}

if ($caseB === true) {
    $action2 =
    $action3 =
    $action5 = true;
}
if ($caseB === true) {
    $action5 = true;
    $action 3 = false;
}
// etc


// idem pour chaque autre cas

Maintenant imaginons que le nombre d’actions possibles soit dynamique, et qu’on est dans un contexte ou le nombre de variable pour les tester est limité ou non extensible.
Et pire : qu’on puisse avoir à exécuter actions combinés suivant chaque variable de contexte
A peu de choses près, tu auras quelque chose qui ressemble à ceci :

$cases = ['A' => false, 'B' => true, 'C' => true, 'D' => false, 'E' => true];

execute($cases);

/**
 * @param array $cases
*/
function execute(array $cases)
{
    $action1 =
    $action2 =
    $action3 =
    $action4 =
    $action 5 = false;

    if (isset($cases['A']) && $cases['A'] === true) {
        $action1 =
        $action3 = true;
    }

    if (isset($cases['B']) && $cases['B'] === true) {
        $action2 =
        $action3 =
        $action5 = true;
    }
    if (isset($cases['C']) && $cases['C'] === true) {
        $action5 = true;
        $action3 = false;
    }
    // et ainsi de suite pour chaque cas

    if ($action1) {
        //traitement action 1
    }
    if ($action2) {
        //traitement action 2
    }
    // et ainsi de suite pour chaque action
}

Pourtant il existe une manière beaucoup plus simple :

$case = 7;

/**
 * @param int $case on calculera le valeur de cette variable en fonction du nombres d'actions différentes qu'on veut éxécute
 * Dans cette exemple vaudra de 0 à 16 : pour 5 actions à gérer qui peuvent ou non se combiner
 */
function execute($case)
{
    if ($case & 1) {
        // traitement action 1 si $case est un chiffre pair
    }

    if ($case & 2) {
        // traitement action 2 si $case est un chiffre impair sauf 0
    }

    if ($case & 4) {
        // traitement action 3 si $case vaut de 4 à 7 ou de 12 à 15
    }

    if ($case & 8) {
        // traitement action 4 si $case vaut de 8 à 15
    }

    if ($case & 16) {
        // traitement action 5 seulement si case vaut 16
    }

    // Et c'est tout. Ici on traite l'action directement dans le test de cas de figure !
}

Ce « & » simple est un test binaire AND, à ne pas confondre avec le « && », qui est un test logique : Ainsi par exemple, si case vaut 7, il passera seulement les 3 premiers tests : si vaut 8, par contre il passera seulement dans le 4ème test.
Si il faut 10, il passera seulement les tests 2 et 4 (8+2)
Si il vaut 15, il passera les 4 premiers test ; si il vaut 16, seulement le 5ème.

Tu commences à piger ? Ca peut paraitre assez prise de tête pour pas grand chose, mais imaginez les gains et la lisibilité si les figures ou et les actions sont plus nombreuses. Les tests binaires AND sont donc pratiques et efficaces dans le cas d’une variables de mode de fonctionnement.

Voici une classe permettant de factoriser un peu tout ça.


/**
 * Class for binary operations
 *
 * @link https://thomas.bondois.info/les-tests-binaires-une-fonctionnalite-sous-estimee/
 * @link http://php.net/manual/fr/language.operators.bitwise.php
 *
 * @licence CC-BY-SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/
 * @author Thomas Bondois
*/
abstract class Bitwiz
{
    /**
     * 2^0 in php : pow(2, 0) = 2**0 = 1 = 0x1 = 0b1
     * Notation en entier, binaire ou hexadécimale, incrementée à chaque fois par multiplicateur binaire (base 2) : 2^(x+1)
     * PHP convertira les notations hexa (base 16) ou binaire (base 2) en (int). pow() est interdit dans le cas d'une constante
     * On pourrait remplacer directement par (int)1 mais la notation binaire ou hexadécimale met en évidence que
     * la valeur de constante choisie n'est pas arbitraire, mais sera traduit en binaire dans les méthodes de la classe
     * A noter que l'expression 2^0 sera traduit par une opération binaire 2 OR 0, et non pas une puissance.
     * @since PHP 5.4, on peut utiliser la forme binaire : 0b1
     * @since PHP 5.6, on peut utiliser ** pour la forme exponentielle, ex : 2**0 au lieu de pow(2, 0), autorisant ainsi son usage dans les constantes
     *
     * @var int
     */
    const CASE_A = 0b1;

    /**
     * 2^1 in php : pow(2, 1) = 2**1 = 2 = 0x2 = 0b10
     * @var int
     */
    const CASE_B = 0b10;

    /**
     * 2^2 in php : 4 = 0x4 = 0b100
     * @var int
     */
    const CASE_C = 0b100;

    /**
     * 2^3 in php : pow(2, 3) = 2**3 = 8 = 0x8 = 0b1000
     * @var int
     */
    const CASE_D = 0b1000;

    /**
     * 2^4 in php : pow(2, 4) = 2**4 = 16 = 0x10 = 0b10000
     * @var int
     */
    const CASE_E = 0b10000;

    /**
     * 2^5 in phph = 32 : 0x20 = 0b100000
     * @var int
     */
    const CASE_F = 0b100000;

    /**
     * 2^6 in php = 64 : 0x40 = 0b1000000
     * var int
     */
    const CASE_G = 0b1000000;

    /**
     * 2^7 in php : pow(2, 7) = 2**7 = 128 = 0x80 = 0b10000000
     * var int
     */
    const CASE_H = 0b1000000;
    const CASE_I = 0b10000000;
    const CASE_J = 0b100000000;
    const CASE_K = 0b1000000000;
    const CASE_L = 0b10000000000;
    const CASE_M = 0b100000000000;
    const CASE_N = 0b1000000000000;
    const CASE_O = 0b10000000000000;
    const CASE_P = 0b100000000000000;
    const CASE_Q = 0b1000000000000000;
    const CASE_R = 0b10000000000000000;
    const CASE_S = 0b100000000000000000;
    const CASE_T = 0b1000000000000000000;
    const CASE_U = 0b10000000000000000000;
    const CASE_V = 0b100000000000000000000;
    const CASE_W = 0b1000000000000000000000;
    const CASE_X = 0b10000000000000000000000;
    const CASE_Y = 0b100000000000000000000000;
    const CASE_Z = 0b1000000000000000000000000;

    /**
     * Test binary-AND
     * @param int $var
     * @param int $bit
     * @return bool
     */
    static public function testAnd($var, $bit)
    {
        return (bool)static::getAnd($var, $bit);
    }

    /**
     * @param int $var
     * @param int $bit
     * @return bool
     */
    static public function testOr($var, $bit)
    {
        return (bool)static::getOr($var, $bit);
    }

    /**
     * Test binary-XOR
     * @param int $var
     * @param int $bit
     * @return bool
     */
    static public function testXor($var, $bit)
    {
        return (bool)static::getXor($var, $bit);
    }

    /**
     * Test binary Left shift
     * @param int $var
     * @param int $bit
     * @return bool
     */
    static public function testLeft($var, $bit)
    {
        return (bool)static::getLeft($var, $bit);
    }

    /**
     * Test binary Right shift
     * @param int $var
     * @param int $bit
     * @return bool
     */
    static public function testRight($var, $bit)
    {
        return (bool)static::getRight($var, $bit);
    }

    /**
     * @param int $var
     * @return bool
     */
    static public function testNot($var)
    {
        return (bool)static::getNot($var);
    }

    /**
     * @param int $var
     * @param int $bit
     * @return int
     */
    public static function getAnd($var, $bit)
    {
        return $var & $bit;
    }

    /**
     * @param int $var
     * @param int $bit
     * @return int
     */
    public static function getOr($var, $bit)
    {
        return $var | $bit;
    }

    /**
     * @param int $var
     * @param int $bit
     * @return int
     */
    public static function getXor($var, $bit)
    {
        return $var ^ $bit;
    }

    /**
     * @param int $var
     * @param int $bit
     * @return int
     */
    public static function getLeft($var, $bit)
    {
        return $var > $bit;
    }

    /**
     * @param int $var
     * @return int
     */
    public static function getNot($var)
    {
        return ~ $var;
    }

    /**
     * sample use case
     * @param int $maxValue [optional] range max display value
     * @return string
     */
    static function getHtmlHelper($maxValue = self::CASE_E)
    {
        $result = '

Binary tests results

'; foreach (['test', 'get'] as $methodType) { foreach(['and', 'or', 'xor', 'left', 'right', 'not'] as $methodPart) { $method = $methodType . ucfirst($methodPart); $result .= "

$method

"; $result .= '
'; for ($value = 0; $value ".PHP_EOL."For $value : case "; foreach (['A', 'B', 'C', 'D', 'E', 'F', 'G'] as $constant) { $result .= "$constant (" . constant('static::CASE_' . $constant) . ") = "; if ($methodPart != 'not') { $result .= static::$method($value, constant('static::CASE_' . $constant)) ?: '°'; //affichage d'un petit zero visuellement plus clair si false, au lieu de 0 ou '' } else { $result .= static::$method($value) ?: '°'; } $result .= " | "; } } } $result.= '
'; } return $result; } } // class

Exemple d’utilisation :

$case = Bitwiz::CASE_A + Bitwiz::CASE_B + Bitwiz::CASE_C; / /or : $case = 7; 

function execute($case)
{
    if (Bitwiz::testAnd($case, Bitwiz::CASE_A)) {
        echo "action 1 OK";
    }
    // etc
}

Pour avoir afficher une liste de résultats des tests suivant tous les cas possibles (dont les cumuls) :

echo Bitwiz::getHtmlHelper();

Un commentaire ?