IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

Bktero

Comment tester de manière élégante l'appartenance à des plages de valeurs en C ?

Noter ce billet
par , 27/10/2014 à 21h41 (1651 Affichages)
Je cherchais une idée d'article pour tester cette nouvelle fonctionnalité de Developpez.com (au passage, saluons Anomaly et les personnes l'ayant aidé !). J'ai répondu à cette discussion sur les forums aujourd'hui et cela m'a donné une idée : comment gérer des manière élégante des tests d'appartenances à des plages de valeurs, en C ?

Le problème exposé par le PO (posteur initial ^^) est le suivant :
Ecrire un programme qui demande l'âge de l'utilisateur et affiche sa catégorie :
  • Poussin : 6-7 ans
  • Pupille : 8-9 ans
  • Minime : 10-11 ans
  • Cadet : 12+
  • Pas de catégorie : 5 ans et -
De manière plus large, on résonnera sur l'appartenance d'une valeur donnée à des plages possibles. Le titre parle de C mais les principes sont génériques et applicables à d'autres langages.

La technique qui ressort de cette discussion est de passer par de longues listes de cases à l'intérieur d'un switch sur l'âge. Comme je le dis dans mon message en #18, je pense que cette technique est mauvaise. La maintenance est quasiment impossible quand on rajoute des plages et qu'on souhaite changer les bornes, il est beaucoup trop facile d'avoir des cas en double, la gestion de plages non continues est très compliquée.

GCC propose une extension intéressante, comme montrée au message #12 : les Case Ranges. Elle peut rendre élégante et compacte la solution switch / case et donc acceptable. Elle a l'inconvénient de perdre la portabilité...

La meilleure solution est sans doute de passer par une structure if / else if / else et de multiplier les else if au fur et à mesure que des plages se rajoutent. A l'indentation près, on obtient quelque chose de semblable avec des if / else imbriqués mais la différence est que le code devient illisible et donc dur à maintenir si des plages s'ajoutent ; il faut donc bien utiliser des else if.

Pour être encore plus élégant, on peut déléguer les tests à des fonctions. La structure devient alors très claire à lire, proche d'un énoncé humain : si c'est un poussin, si c'est un minime, etc. Si un jour les tests deviennent un peu plus complexes que deux inégalités (par exemple, si une catégorie est est l'union de deux plages non continues ou si les catégories diffèrent selon si on est une fille ou un garçon), il est aisé de changer les fonctions sans toucher à la structure conditionnelle. De plus, ces fonctions resserviront sans doute ailleurs dans le code. On retrouve deux principes récurrents de la programmation : encapusuler les détails pouvant changer dans le futur et factoriser ce qui est redondant.

Voici ce que ça peut donner en C :
Code C : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdbool.h>
#include <stdio.h>
 
#define ENTREE_POUSSIN  6
#define ENTREE_PUPILLE  8
#define ENTREE_MINIME   10 
#define ENTREE_CADET    12
 
typedef unsigned int age_t;
 
bool isPoussin(age_t age)
{
    return age >= ENTREE_POUSSIN && age < ENTREE_PUPILLE;
}
 
bool isPupille(age_t age)
{
    return age >= ENTREE_PUPILLE && age < ENTREE_MINIME;
}  
 
bool isMinime(age_t age)
{
    return age >= ENTREE_MINIME && age < ENTREE_CADET;
}
 
bool isCadet(age_t age)
{
    return age >= ENTREE_CADET;
}
 
int main(void)
{
    age_t age = 0;
    puts("Age ?");
    scanf("%u", &age); // pas terrible mais simple pour cet exemple
 
    printf("Il a %u ans ?\n", age);
 
    if(isPoussin(age))
    {
       puts("C'est un poussin !");
    }
    else if(isPupille(age))
    {
        puts("C'est un pupille !");
    }
    else if(isMinime(age))
    {
        puts("C'est un minime !");
    }
     else if(isCadet(age))
    {
        puts("C'est un cadet !");
    }
    else
    {
        puts("Too young to die, to old to rock n' roll...");
        // https://www.youtube.com/watch?v=Rwn0R1PFUwU
    }
}

$ c99 conditional.c 
$ ./a.out 
Age ?
10
Il a 10 ?
C'est un minime !
Le mérite d'une telle solution ne m'est pas totalement imputable : je reprend les principes énoncés par Steve McConnel au chapitre 15 ("Using Conditionals") de son livre Code Complete (livre dont je conseille la lecture à tout personne trempant dans l'informatique au sens large).

Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Viadeo Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Twitter Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Google Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Facebook Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Digg Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Delicious Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog MySpace Envoyer le billet « Comment tester de manière élégante l'appartenance à des plages de valeurs en C ? » dans le blog Yahoo

Mis à jour 23/11/2014 à 19h36 par Bktero

Catégories
C

Commentaires

  1. Avatar de Sve@r
    • |
    • permalink
    Bonjour

    Mon premier commentaire aux nouveaux blogs du fofo. Dommage, j'aurais aimé avoir possibilité de citer la partie que je commente mais j'ai pas trouvé de bouton approprié (mais on peut le créer à la mano)

    Citation Envoyé par Bktero
    Le titre parle de C mais les principes sont génériques et applicables à d'autres langages.
    Donc peut-on envisager d'adapter cet article aux autres langages bien souvent plus étendus que le C et donc plus puissants ? Par exemple Python n'a pas de switch/case (accessoirement il a tant d'autres possibilités que cela ne me gêne même plus) mais il connait l'opérateur in. Associé au générateur xrange() cela devient trivial

    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    def getCat(age):
    	for (gen, libelle) in (
    		(xrange(0, 5), "sans catégorie"),
    		(xrange(5, 8), "Poussin"),
    		(xrange(8, 10), "Benjamin"),
    		(xrange(10, 13), "Minime"),
    		(xrange(13, 19), "Cadet"),
    	): if age in gen: return libelle
    # getCat()

    Citation Envoyé par Bktero
    Pour être encore plus élégant, on peut déléguer les tests à des fonctions. La structure devient alors très claire à lire, proche d'un énoncé humain : si c'est un poussin, si c'est un minime, etc.
    Ca ça me gêne un petit peu. Certes je vois bien l'analogie avec les isalpha(), isalnum(), isdigit() etc mais ce qui me gêne c'est d'adapter ce genre de méthodes pour des éléments aussi liés les uns aux autres. Autant avec les isalpha, isalnum, isdigit, isspace etc on a couvert toutes les possibilités et si éventuellement on veut rajouter un "isvoyelle()" on peut le faire sans toucher aux autres fonctions, ici si on veut rajouter la catégorie "Espoir" entre "Minime" et "Cadet" on est obligé de reprendre les #define ainsi que les fonctions isMinime() et isCadet()...

    Citation Envoyé par Bktero
    De manière plus large, on résonnera sur l'appartenance d'une valeur donnée à des plages possibles
    En fait, la façon de résoudre ce problème dépendra surtout si les plages sont liées les unes avec les autres (les pupilles sont adjacentes aux poussins mais que faire si on veut intercaler demain une autre catégorie) ou si elles sont idépendantes (rajouter un isvoyelle() n'impactera en rien les autres istruc())

    PS: j'espère que je ne fais pas erreur en écrivant ce commentaire qui s'apparente à une début de débat lequel aurait alors sa place sur le vrai forum...
    Mis à jour 01/11/2014 à 13h23 par Sve@r
  2. Avatar de Bktero
    • |
    • permalink
    Citation Envoyé par Sve@r
    Mon premier commentaire aux nouveaux blogs du fofo. Dommage, j'aurais aimé avoir possibilité de citer la partie que je commente mais j'ai pas trouvé de bouton approprié (mais on peut le créer à la mano)
    Avec les commentaires, on peut faire répondre pour avoir une citation du commentaire souhaité

    Citation Envoyé par Sve@r
    Par exemple Python n'a pas de switch/case (accessoirement il a tant d'autres possibilités que cela ne me gêne même plus) mais il connait l'opérateur in. Associé au générateur xrange() cela devient trivial

    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    def getCat(age):
    	for (gen, libelle) in (
    		(xrange(0, 5), "sans catégorie"),
    		(xrange(5, 8), "Poussin"),
    		(xrange(8, 10), "Benjamin"),
    		(xrange(10, 13), "Minime"),
    		(xrange(13, 19), "Cadet"),
    	): if age in gen: return libelle
    # getCat()
    Ah c'est pas mal comme solution ! J'avais un truc bien beau et maintenable avec une dictionnaire mais il y avait une entrée par valeur souhaitée. Pour des plages longues, ce n'était pas du tout maintenable ou même faisable.

    Citation Envoyé par Sve@r
    Ca ça me gêne un petit peu. Certes je vois bien l'analogie avec les isalpha(), isalnum(), isdigit() etc mais ce qui me gêne c'est d'adapter ce genre de méthodes pour des éléments aussi liés les uns aux autres. Autant avec les isalpha, isalnum, isdigit, isspace etc on a couvert toutes les possibilités et si éventuellement on veut rajouter un "isvoyelle()" on peut le faire sans toucher aux autres fonctions, ici si on veut rajouter la catégorie "Espoir" entre "Minime" et "Cadet" on est obligé de reprendre les #define ainsi que les fonctions isMinime() et isCadet()...
    L'intérêt est surtout si les tests deviennent compliqués. Ici, on pourrait s'en passer. Quelque soit la technique, tu seras forcément embêté quand tu voudras rajouter des catégories intermédiaires ; des catégories au-dessus et en-dessous, c'est bons. Ici, avec les #define, il est simple de changer les bornes des catégories.

    Citation Envoyé par Sve@r
    PS: j'espère que je ne fais pas erreur en écrivant ce commentaire qui s'apparente à une début de débat lequel aurait alors sa place sur le vrai forum...
    Ben non c'est sympa de répondre ici En plus je crois qu'on a largement discuté sur le forum.
  3. Avatar de kolodz
    • |
    • permalink
    Il y a moyen d'édité le billet pour ajouter un =C sur la balise code pour avoir la coloration syntaxique ?
  4. Avatar de Bktero
    • |
    • permalink
    Citation Envoyé par kolodz
    Il y a moyen d'édité le billet pour ajouter un =C sur la balise code pour avoir la coloration syntaxique ?
    C'est une bonne remarque, je viens de le faire
  5. Avatar de stendhal666
    • |
    • permalink
    Un peu plus d'un an après le post...

    En s'inspirant de la solution Python on peut avoir aussi bien en C:

    Code C : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     
    typedef struct category {
      int low, high;
      const char* name;
    };
     
    const category categories [] = 
    {
      { .low = 0, .high = 5, .name = "sans categorie" },
      // etc.
    };
    const int NB_CAT = sizeof(categories) / sizeof(category);
     
    const char* get_category(int age) {
      for (int i = 0; i < NB_CAT; ++i) {
        if (age >= categories[i].low && age < categories[i].high)
          return categories[i];
      }
      return "too old :-(";
    }