web-dev-qa-db-fra.com

Comment éviter si / sinon si chaîne lors du classement d'un en-tête dans 8 directions?

J'ai le code suivant:

if (this->_car.getAbsoluteAngle() <= 30 || this->_car.getAbsoluteAngle() >= 330)
  this->_car.edir = Car::EDirection::RIGHT;
else if (this->_car.getAbsoluteAngle() > 30 && this->_car.getAbsoluteAngle() <= 60)
  this->_car.edir = Car::EDirection::UP_RIGHT;
else if (this->_car.getAbsoluteAngle() > 60 && this->_car.getAbsoluteAngle() <= 120)
  this->_car.edir = Car::EDirection::UP;
else if (this->_car.getAbsoluteAngle() > 120 && this->_car.getAbsoluteAngle() <= 150)
  this->_car.edir = Car::EDirection::UP_LEFT;
else if (this->_car.getAbsoluteAngle() > 150 && this->_car.getAbsoluteAngle() <= 210)
  this->_car.edir = Car::EDirection::LEFT;
else if (this->_car.getAbsoluteAngle() > 210 && this->_car.getAbsoluteAngle() <= 240)
  this->_car.edir = Car::EDirection::DOWN_LEFT;
else if (this->_car.getAbsoluteAngle() > 240 && this->_car.getAbsoluteAngle() <= 300)
  this->_car.edir = Car::EDirection::DOWN;
else if (this->_car.getAbsoluteAngle() > 300 && this->_car.getAbsoluteAngle() <= 330)
  this->_car.edir = Car::EDirection::DOWN_RIGHT;

Je veux éviter la chaîne ifs; c'est vraiment moche. Existe-t-il une autre manière, peut-être plus propre, d’écrire cela?

111
Oraekia
#include <iostream>

enum Direction { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT };

Direction GetDirectionForAngle(int angle)
{
    const Direction slices[] = { RIGHT, UP_RIGHT, UP, UP, UP_LEFT, LEFT, LEFT, DOWN_LEFT, DOWN, DOWN, DOWN_RIGHT, RIGHT };
    return slices[(((angle % 360) + 360) % 360) / 30];
}

int main()
{
    // This is just a test case that covers all the possible directions
    for (int i = 15; i < 360; i += 30)
        std::cout << GetDirectionForAngle(i) << ' ';

    return 0;
}

Voici comment je le ferais. (Selon mon commentaire précédent).

177
Borgleader

Vous pouvez utiliser map::lower_bound et stocker la limite supérieure de chaque angle dans une carte.

Exemple de travail ci-dessous:

_#include <cassert>
#include <map>

enum Direction
{
    RIGHT,
    UP_RIGHT,
    UP,
    UP_LEFT,
    LEFT,
    DOWN_LEFT,
    DOWN,
    DOWN_RIGHT
};

using AngleDirMap = std::map<int, Direction>;

AngleDirMap map = {
    { 30, RIGHT },
    { 60, UP_RIGHT },
    { 120, UP },
    { 150, UP_LEFT },
    { 210, LEFT },
    { 240, DOWN_LEFT },
    { 300, DOWN },
    { 330, DOWN_RIGHT },
    { 360, RIGHT }
};

Direction direction(int angle)
{
    assert(angle >= 0 && angle <= 360);

    auto it = map.lower_bound(angle);
    return it->second;
}

int main()
{
    Direction d;

    d = direction(45);
    assert(d == UP_RIGHT);

    d = direction(30);
    assert(d == RIGHT);

    d = direction(360);
    assert(d == RIGHT);

    return 0;
}
_
71
Steve Lorimer

Créez un tableau dont chaque élément est associé à un bloc de 30 degrés:

Car::EDirection dirlist[] = { 
    Car::EDirection::RIGHT, 
    Car::EDirection::UP_RIGHT, 
    Car::EDirection::UP, 
    Car::EDirection::UP, 
    Car::EDirection::UP_LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::DOWN_LEFT,
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN_RIGHT, 
    Car::EDirection::RIGHT
};

Ensuite, vous pouvez indexer le tableau avec l’angle/30:

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];

Aucune comparaison ou branchement requis.

Cependant, le résultat est légèrement légèrement différent de l'original. Les valeurs sur les bordures, c'est-à-dire 30, 60, 120, etc. sont placées dans la catégorie suivante. Par exemple, dans le code d'origine, les valeurs valides pour UP_RIGHT vont de 31 à 60. Le code ci-dessus attribue les valeurs 30 à 59 à UP_RIGHT.

On peut contourner ce problème en soustrayant 1 de l’angle:

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];

Cela nous donne maintenant RIGHT pour 30, UP_RIGHT pour 60, etc.

Dans le cas de 0, l'expression devient (-1 % 360) / 30. Ceci est valide parce que -1 % 360 == -1 et -1 / 30 == 0, nous obtenons donc toujours un index de 0.

La section 5.6 du norme C++ confirme ce comportement:

4 L'opérateur binaire / renvoie le quotient, et l'opérateur binaire % renvoie le reste de la division de la première expression. à la seconde. Si le deuxième opérande de / ou % est égal à zéro, le comportement est indéfini. Pour les opérandes intégrés, l'opérateur / renvoie le quotient algébrique avec toute partie décimale supprimée. si le quotient a/b est représentable dans le type du résultat, (a/b)*b + a%b est égal à a.

MODIFIER:

De nombreuses questions ont été soulevées concernant la lisibilité et la maintenabilité d'une construction de ce type. La réponse donnée par motoDrizzt est un bon exemple de simplification de la construction d'origine qui est plus facile à gérer et qui n'est pas aussi "laide".

En développant sa réponse, voici un autre exemple utilisant l’opérateur ternaire. Étant donné que chaque cas de la publication d'origine attribue la même variable, l'utilisation de cet opérateur peut aider à améliorer la lisibilité.

int angle = ((this->_car.getAbsoluteAngle() % 360) + 360) % 360;

this->_car.edir = (angle <= 30)  ?  Car::EDirection::RIGHT :
                  (angle <= 60)  ?  Car::EDirection::UP_RIGHT :
                  (angle <= 120) ?  Car::EDirection::UP :
                  (angle <= 150) ?  Car::EDirection::UP_LEFT :
                  (angle <= 210) ?  Car::EDirection::LEFT : 
                  (angle <= 240) ?  Car::EDirection::DOWN_LEFT :
                  (angle <= 300) ?  Car::EDirection::DOWN:  
                  (angle <= 330) ?  Car::EDirection::DOWN_RIGHT :
                                    Car::EDirection::RIGHT;
57
dbush

Ce code n'est pas laid, il est simple, pratique, lisible et facile à comprendre. Il sera isolé selon sa propre méthode, afin que personne ne soit obligé de le gérer au quotidien. Et juste au cas où quelqu'un aurait à le vérifier - peut-être parce qu'il est en train de déboguer votre application pour un problème ailleurs - c'est tellement facile qu'il lui faudra deux secondes pour comprendre le code et ce qu'il fait.

Si je faisais un tel débogage, je serais heureux de ne pas avoir à passer cinq minutes à essayer de comprendre ce que fait votre fonction. À cet égard, toutes les autres fonctions échouent complètement, car elles modifient une routine simple, sans bugs, dans un désordre compliqué que les utilisateurs, lors du débogage, seront forcés d'analyser et de tester en profondeur. En tant que chef de projet moi-même, je serais fortement contrarié par un développeur qui prend une tâche simple. Au lieu de la mettre en œuvre de manière simple et sans danger, vous perdez du temps à l'implémenter de manière trop compliquée. Pensez simplement à tout le temps que vous avez perdu à y penser, puis à venir à SO demander, et tout cela simplement pour ne pas aggraver la maintenance et la lisibilité de la chose.

Cela dit, votre code contient une erreur courante qui le rend moins lisible, et quelques améliorations peuvent être apportées assez facilement:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;
else if (angle <= 60)
  return Car::EDirection::UP_RIGHT;
else if (angle <= 120)
  return Car::EDirection::UP;
else if (angle <= 150)
  return Car::EDirection::UP_LEFT;
else if (angle <= 210)
  return Car::EDirection::LEFT;
else if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;
else if (angle <= 300)
  return Car::EDirection::DOWN;
else if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Mettez ceci dans une méthode, affectez la valeur renvoyée à l'objet, réduisez la méthode et oubliez-la pour le reste de l'éternité.

P.S. il y a un autre bug sur le seuil de 330, mais je ne sais pas comment vous voulez le traiter, donc je ne l'ai pas corrigé du tout.


mise à jour ultérieure

Selon les commentaires, vous pouvez même vous débarrasser du reste, le cas échéant:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;

if (angle <= 60)
  return Car::EDirection::UP_RIGHT;

if (angle <= 120)
  return Car::EDirection::UP;

if (angle <= 150)
  return Car::EDirection::UP_LEFT;

if (angle <= 210)
  return Car::EDirection::LEFT;

if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;

if (angle <= 300)
  return Car::EDirection::DOWN;

if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Je ne l'ai pas fait car j'estime qu'à un moment donné, cela devient simplement une question de préférences personnelles et le but de ma réponse était (et reste) de donner une perspective différente à votre préoccupation à propos de "la laideur du code". Quoi qu'il en soit, comme je l'ai dit, quelqu'un l'a souligné dans les commentaires et je pense qu'il est logique de le montrer.

49
motoDrizzt

En pseudocode:

angle = (angle + 30) %360; // Offset by 30. 

Donc, nous avons 0-60, 60-90, 90-150, ... comme catégories. Dans chaque quadrant à 90 degrés, une partie en a 60, une en a 30. Donc, maintenant:

i = angle / 90; // Figure out the quadrant. Could be 0, 1, 2, 3 

j = (angle - 90 * i) >= 60? 1: 0; // In the quardrant is it perfect (eg: RIGHT) or imperfect (eg: UP_RIGHT)?

index = i * 2 + j;

Utilisez l'index dans un tableau contenant les énumérations dans l'ordre approprié.

39
Bijay Gurung
switch (this->_car.getAbsoluteAngle() / 30) // integer division
{
    case 0:
    case 11: this->_car.edir = Car::EDirection::RIGHT; break;
    case 1: this->_car.edir = Car::EDirection::UP_RIGHT; break;
    ...
    case 10: this->_car.edir = Car::EDirection::DOWN_RIGHT; break;
}
18
Caleth

En ignorant votre première if qui est un cas un peu spécial, les autres suivent exactement le même schéma: min, max et direction; pseudo-code:

if (angle > min && angle <= max)
  _car.edir = direction;

Rendre ce vrai C++ pourrait ressembler à:

enum class EDirection {  NONE,
   RIGHT, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT, DOWN, DOWN_RIGHT };

struct AngleRange
{
    int min, max;
    EDirection direction;
};

Maintenant, plutôt que d’écrire un tas de ifs, passez simplement en revue vos différentes possibilités:

EDirection direction_from_angle(int angle, const std::vector<AngleRange>& angleRanges)
{
    for (auto&& angleRange : angleRanges)
    {
        if ((angle > angleRange.min) && (angle <= angleRange.max))
            return angleRange.direction;
    }

    return EDirection::NONE;
}

(throwing une exception plutôt que returning NONE est une autre option).

Ce que vous appelleriez alors:

_car.edir = direction_from_angle(_car.getAbsoluteAngle(), {
    {30, 60, EDirection::UP_RIGHT},
    {60, 120, EDirection::UP},
    // ... etc.
});

Cette technique s'appelle programmation pilotée par les données . En plus de vous débarrasser d'un tas de ifs, cela vous permettrait d'ajouter facilement plus d'instructions (par exemple, NNW) ou de réduire le nombre (gauche, droite, haut, bas) sans retravailler un autre code.


(Le traitement de votre premier cas particulier est laissé comme "un exercice pour le lecteur" :-))

16
Ðаn

Bien que les variantes proposées basées sur une table de consultation pour angle / 30 soient probablement préférables, voici une alternative qui utilise une recherche binaire codée en dur pour minimiser le nombre de comparaisons.

static Car::EDirection directionFromAngle( int angle )
{
    if( angle <= 210 )
    {
        if( angle > 120 )
            return angle > 150 ? Car::EDirection::LEFT
                               : Car::EDirection::UP_LEFT;
        if( angle > 30 )
            return angle > 60 ? Car::EDirection::UP
                              : Car::EDirection::UP_RIGHT;
    }
    else // > 210
    {
        if( angle <= 300 )
            return angle > 240 ? Car::EDirection::DOWN
                               : Car::EDirection::DOWN_LEFT;
        if( angle <= 330 )
            return Car::EDirection::DOWN_RIGHT;
    }
    return Car::EDirection::RIGHT; // <= 30 || > 330
}
12
x4u

Si vous voulez vraiment éviter la duplication, vous pouvez l'exprimer sous forme de formule mathématique.

Tout d'abord, supposons que nous utilisons @ Geek's Enum

Enum EDirection { RIGHT =0, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT,DOWN, DOWN_RIGHT}

Nous pouvons maintenant calculer l'énum en utilisant des mathématiques entières (sans avoir besoin de tableaux).

EDirection angle2dir(int angle) {
    int d = ( ((angle%360)+360)%360-1)/30;
    d-=d/3; //some directions cover a 60 degree arc
    d%=8;
    //printf ("assert(angle2dir(%3d)==%-10s);\n",angle,dir2str[d]);
    return (EDirection) d;
}

Comme @motoDrizzt le souligne, un code concis n'est pas nécessairement du code lisible. Il a le petit avantage de l'exprimer en mathématique, il est donc évident que certaines directions couvrent un arc plus large. Si vous voulez aller dans cette direction, vous pouvez ajouter quelques assertions pour vous aider à comprendre le code.

assert(angle2dir(  0)==RIGHT     ); assert(angle2dir( 30)==RIGHT     );
assert(angle2dir( 31)==UP_RIGHT  ); assert(angle2dir( 60)==UP_RIGHT  );
assert(angle2dir( 61)==UP        ); assert(angle2dir(120)==UP        );
assert(angle2dir(121)==UP_LEFT   ); assert(angle2dir(150)==UP_LEFT   );
assert(angle2dir(151)==LEFT      ); assert(angle2dir(210)==LEFT      );
assert(angle2dir(211)==DOWN_LEFT ); assert(angle2dir(240)==DOWN_LEFT );
assert(angle2dir(241)==DOWN      ); assert(angle2dir(300)==DOWN      );
assert(angle2dir(301)==DOWN_RIGHT); assert(angle2dir(330)==DOWN_RIGHT);
assert(angle2dir(331)==RIGHT     ); assert(angle2dir(360)==RIGHT     );

Après avoir ajouté les assertions, vous avez ajouté la duplication, mais la duplication dans les assertions n'est pas si grave. Si vous avez une affirmation incohérente, vous le saurez assez tôt. Les assertions peuvent être compilées en dehors de la version afin de ne pas alourdir l'exécutable que vous distribuez. Néanmoins, cette approche est probablement la plus applicable si vous souhaitez optimiser le code plutôt que de le rendre moins laid.

2
gmatht

Je suis en retard pour la fête, mais nous pourrions utiliser des drapeaux enum et des contrôles de plage pour faire quelque chose de bien.

enum EDirection {
    RIGHT =  0x01,
    LEFT  =  0x02,
    UP    =  0x04,
    DOWN  =  0x08,
    DOWN_RIGHT = DOWN | RIGHT,
    DOWN_LEFT = DOWN | LEFT,
    UP_RIGHT = UP | RIGHT,
    UP_LEFT = UP | LEFT,

    // just so we be clear, these won't have much use though
    IMPOSSIBLE_H = RIGHT | LEFT, 
    IMPOSSIBLE_V = UP | DOWN
};

la vérification (pseudo-code), en supposant que l'angle soit absolu (entre 0 et 360):

int up    = (angle >   30 && angle <  150) * EDirection.UP;
int down  = (angle >  210 && angle <  330) * EDirection.DOWN;
int right = (angle <=  60 || angle >= 330) * EDirection.Right;
int left  = (angle >= 120 && angle <= 240) * EDirection.LEFT;

EDirection direction = (Direction)(up | down | right | left);

switch(direction){
    case RIGHT:
         // do right
         break;
    case UP_RIGHT:
         // be honest
         break;
    case UP:
         // whats up
         break;
    case UP_LEFT:
         // do you even left
         break;
    case LEFT:
         // 5 done 3 to go
         break;
    case DOWN_LEFT:
         // your're driving me to a corner here
         break;
    case DOWN:
         // :(
         break;
    case DOWN_RIGHT:
         // completion
         break;

    // hey, we mustn't let these slide
    case IMPOSSIBLE_H:
    case IMPOSSIBLE_V:
        // treat your impossible case here!
        break;
}
1
Leonardo Pina