Rejeter les entrées qui ne peuvent pas être un achat

CBeginner
Pratiquer maintenant

Introduction

Un reçu doit contenir des achats approuvés, pas n'importe quel préfixe reconnu par une fonction de conversion numérique au début d'une ligne. Sunny Snacks a maintenant besoin d'une validation complète et d'un contrôle de capacité qui n'écrase pas les achats précédents.

Cette unité indépendante prépare son propre catalogue et son programme de reçus. Vous devez maîtriser les tableaux, fonctions, structures et calculs en centimes entiers de Conserver chaque ligne du reçu. Vous apprendrez uniquement les détails des chaînes et des adresses nécessaires à un analyseur décimal borné, puis rejetterez les entrées invalides sans modifier le panier. Le terminal Workspace et le compilateur sont prêts ; il s'agit d'un programme de reçus textuels utilisant des données synthétiques, pas d'un système de paiement. La durée est une estimation de l'auteur.

Convertir un texte décimal complet

Dans cette étape, vous distinguerez un nombre complet valide d'un préfixe numérique et implémenterez la séparation entre réussite et échec de l'analyseur.

Commencez dans l'onglet Workspace. Les fichiers préparés sont indépendants du laboratoire précédent. cd entre dans le répertoire de travail, ls permet de vérifier les noms et cat affiche le texte :

cd /home/labex/project/receipts
ls
cat parse-demo.c
cat parse.h
cat read-line.h

parse-demo.c est un petit programme qui appelle parse_number ; il lit une quantité, affiche l'entier accepté ou un rejet, puis se termine. till.c utilisera le même parse.h à l'étape suivante. L'analyseur actuel renvoie toujours 0 : il rejette donc même les entrées valides. Gardez catalog.h, driver.h, read-line.h et parse-demo.c inchangés ; modifiez parse.h, puis till.c.

En C, le texte est un tableau de caractères qui se termine par le terminateur NUL de valeur zéro, écrit \0. Il est distinct du chiffre visible '0'. Par exemple :

array index:    0     1     2
text:         '2'   '0'  '\0'

Le read_line fourni réserve 64 octets : jusqu'à 63 octets de saisie et le terminateur. getchar() lit un caractère ; son résultat de type int peut aussi représenter EOF. La boucle stocke jusqu'à 63 caractères, mais continue à lire une ligne trop longue jusqu'à son saut de ligne : les restes ne deviennent donc jamais la réponse suivante. Elle renvoie 1 pour une ligne complète, -1 pour une ligne trop longue entièrement consommée et 0 pour EOF. EOF abandonne aussi une ligne inachevée. La condition read_line(text) != 1 de la démonstration rejette les deux derniers cas avant la conversion.

L'interface publique de l'analyseur est :

int parse_number(const char text[], int minimum, int maximum, int *result)

Conservez ce nom et cet ordre des paramètres : la démonstration et le pilote préparés les utilisent. Renvoyez 1 en cas de réussite et 0 en cas d'échec. En cas de réussite, stockez l'entier converti dans la variable résultat de l'appelant. En cas d'échec, laissez cette variable intacte. minimum et maximum sont inclusifs : 0 est un nombre valide si le minimum vaut 0, même si la quantité d'un achat a un minimum de 1.

Cet appel nécessite une petite notion d'adresse. Un argument entier ordinaire transmet une valeur. &quantity signifie l'adresse de la case quantity de l'appelant. Le paramètre int *result reçoit cette adresse ; *result = value écrit une valeur dans la case de l'appelant. L'étoile dans une déclaration indique une variable qui contient une adresse ; dans une expression, elle accède à la case située à cette adresse. Nous utilisons ces paramètres de sortie précis, sans allocation dynamique ni exercice général sur les pointeurs.

strtol, déclarée dans stdlib.h, convertit du texte en un entier long. Nous passons explicitement la base 10 : un zéro initial reste donc décimal. Son premier argument pointe vers le premier caractère à lire. Le second est une autre sortie : char *end contiendra l'adresse du premier caractère qui n'a pas fait partie du nombre. &end passe l'adresse de cette variable contenant une adresse, afin que strtol puisse la mettre à jour. Cela ne copie pas une seconde chaîne.

"2bars" -> converted long value 2; end points at 'b'
"20"    -> converted long value 20; end points at '\0'
"+"     -> no conversion; end equals the starting address

Convertir un préfixe ne suffit pas. Notre syntaxe autorise des espaces ASCII ou tabulations facultatifs au début et à la fin, un signe facultatif et des chiffres décimaux, y compris des zéros initiaux. Les espaces internes, points décimaux, préfixes hexadécimaux, autres caractères d'espacement et lettres finales sont invalides. Un signe seul et un texte vide ou ne contenant que des espaces sont également invalides. La commande exacte done, en minuscules, appartient uniquement à l'invite Product ID du programme ; ce n'est pas un texte numérique.

Utilisez un indice de tableau begin pour sauter uniquement les espaces et tabulations initiaux autorisés. &text[begin] est l'adresse du premier caractère restant. Avant d'appeler strtol, vérifiez qu'il s'agit d'un chiffre ou d'un signe ; ainsi, sa gestion plus large des espaces n'élargit pas silencieusement notre syntaxe. Après la conversion, end == &text[begin] signifie qu'aucun chiffre n'a été consommé. *end lit le caractère à l'adresse d'arrêt ; end++ avance cette adresse d'un caractère. Sautez seulement les espaces ou tabulations finaux, puis exigez le terminateur.

Une autre limite existe : le texte peut représenter une valeur trop grande même pour un long. errno, fourni par errno.h, est l'indicateur d'erreur de la bibliothèque. Remettez-le à zéro avant l'appel ; ERANGE après l'appel indique un dépassement de plage. Vérifiez cette erreur ainsi que le minimum et le maximum métier avant de convertir le long en int. Une valeur numérique zéro renvoyée n'est pas, à elle seule, un signe d'erreur.

Remplacez parse.h par l'analyseur complet. La fonction de l'en-tête est static : elle appartient donc à chaque programme qui l'inclut. cat > remplace le fichier et le here-document entre apostrophes copie littéralement le C suivant jusqu'à la dernière ligne EOF.

cat > parse.h <<'EOF'
#include <errno.h>
#include <stdlib.h>

static int parse_number(const char text[], int minimum, int maximum, int *result) {
    int begin = 0;
    while (text[begin] == ' ' || text[begin] == '\t') begin++;
    char first = text[begin];
    if (first != '+' && first != '-' && (first < '0' || first > '9')) return 0;
    char *end;
    errno = 0;
    long value = strtol(&text[begin], &end, 10);
    if (end == &text[begin] || errno == ERANGE) return 0;
    while (*end == ' ' || *end == '\t') end++;
    if (*end != '\0' || value < minimum || value > maximum) return 0;
    *result = (int)value;
    return 1;
}
EOF

L'instruction finale *result = (int)value écrit l'entier de l'appelant seulement après la réussite de tous les contrôles. Une entrée invalide ne modifie donc pas un résultat existant. La conversion explicite (int) effectue cette conversion désormais sûre.

Compilez la petite démonstration. -std=c11 sélectionne C11, -Wall -Wextra active des avertissements et -o nomme l'exécutable :

gcc -std=c11 -Wall -Wextra parse-demo.c -o parse-demo
./parse-demo

À Quantity (1..20):, tapez +0002, puis appuyez sur Entrée. Le résultat est Accepted quantity: 2 et l'invite du shell revient. L'entrée contient un signe et des zéros initiaux, mais représente l'entier ordinaire 2.

Exécutez à nouveau la démonstration :

./parse-demo

À l'invite de quantité, tapez 2bars. Le résultat attendu est Rejected quantity.. Cet échec est intentionnel : convertir le préfixe 2 n'a pas validé toute la ligne. Le statut de sortie d'un programme est un petit entier destiné à son appelant : ici, 0 indique une réussite normale et 2 indique que la démonstration a rejeté l'entrée. Exécutez immédiatement echo $? pour afficher ce dernier statut. Toute commande intermédiaire le remplacerait.

echo $?

La sortie attendue est 2. Ce rejet ne signifie pas que le terminal ou le compilateur a échoué. Exécutez la démonstration une troisième fois :

./parse-demo

Saisissez 9999999999999999999999999999999999999999, puis appuyez sur Entrée. Cette entrée est aussi rejetée : la ligne comporte moins de 63 octets, mais son nombre décimal dépasse la plage de conversion. En revanche, 21 tient dans un long, mais dépasse la quantité maximale de 20. Ce sont deux raisons différentes pour le même rejet sûr.

Si la compilation signale un fichier manquant, retrouvez votre emplacement avec cd et ls. En cas d'erreurs de syntaxe, enregistrez l'en-tête corrigé et recompilez. Le programme parse-demo inclut votre en-tête lors de la compilation ; exécuter un ancien binaire ne teste pas une nouvelle modification.

Approuver un achat avant de modifier l'état

Dans cette étape, vous combinerez l'analyseur avec le tableau des achats, rejetterez un panier plein avant toute écriture et enregistrerez un reçu après plusieurs entrées rejetées.

Commencez dans Workspace, dans /home/labex/project/receipts. Le till.c préparé contient déjà les types d'enregistrements, la recherche dans le catalogue et l'impression complète du laboratoire précédent. Son approve_purchase contient encore une partie manquante.

cd /home/labex/project/receipts
cat catalog.h
cat till.c
cat driver.h

Lisez le pilote en partant de ses invites. Il lit une ligne complète d'identifiant, reconnaît le done exact, analyse l'identifiant décimal et vérifie que le produit existe. Puis il lit une quantité et appelle votre analyseur avec les limites 1..20. !parse_number(...) signifie que l'analyseur a renvoyé faux. Un échec de lecture ou de conversion revient à Product ID sans appeler l'approbation. Une quantité invalide abandonne uniquement le candidat en attente, pas les achats précédents.

L'approbation protège aussi les limites de son tableau. Recherchez l'indice du produit, rejetez un identifiant absent ou une quantité hors de 1..20, puis rejetez lorsque line_count >= MAX_LINES. || signifie que n'importe laquelle de ces conditions suffit à rejeter. Ensuite seulement, écrivez les deux champs, incrémentez line_count et renvoyez 1. Écrire une seule fois dans basket[MAX_LINES] sort déjà du tableau de six entrées ; rejeter ensuite n'annule pas cette écriture invalide. Les positions utilisables vont de 0 à 5.

Tous les calculs restent en centimes entiers. Les quatre lignes fournies utilisent des identifiants 1..999 et des prix 0..9999 centimes, dont un article gratuit valide ; six quantités d'au plus 20 limitent le total à 1,199,880 centimes. Conservez chaque achat approuvé séparément et dans l'ordre, même si l'identifiant se répète. La même logique doit fonctionner lors d'une recompilation avec un autre catalogue fourni de quatre lignes dans ces limites. Les noms de structures et de variables locales peuvent varier ; les interfaces de fonction préparées doivent toujours correspondre au pilote.

Remplacez till.c par le programme complet. La base des enregistrements et de l'impression est répétée dans cette unité indépendante pour ne pas nécessiter les fichiers d'une VM précédente. parse.h reste l'analyseur que vous avez complété à l'étape précédente de cette VM.

cat > till.c <<'EOF'
#include <stdio.h>
#include "catalog.h"
#include "parse.h"

typedef struct {
    int id;
    char name[32];
    int cents;
} Product;

typedef struct {
    int product_index;
    int quantity;
} Purchase;

static const Product products[] = CATALOG_ROWS;
static const int product_count = sizeof products / sizeof products[0];
static Purchase basket[MAX_LINES];
static int line_count;

static int find_product(int id) {
    for (int i = 0; i < product_count; i++)
        if (products[i].id == id) return i;
    return -1;
}

static int approve_purchase(int id, int quantity) {
    int index = find_product(id);
    if (index < 0 || quantity < 1 || quantity > MAX_QUANTITY || line_count >= MAX_LINES)
        return 0;
    basket[line_count].product_index = index;
    basket[line_count].quantity = quantity;
    line_count++;
    return 1;
}

static void print_receipt(void) {
    int total = 0;
    puts(SHOP_TITLE);
    puts("ID | ITEM | QTY | UNIT | SUBTOTAL");
    for (int i = 0; i < line_count; i++) {
        Product product = products[basket[i].product_index];
        int quantity = basket[i].quantity;
        int subtotal = product.cents * quantity;
        total += subtotal;
        printf("%d | %s | %d | %d.%02d | %d.%02d\n",
               product.id, product.name, quantity,
               product.cents / 100, product.cents % 100,
               subtotal / 100, subtotal % 100);
    }
    printf("TOTAL | %d.%02d\n", total / 100, total % 100);
}

#include "driver.h"
EOF

Compilez le nouveau code :

gcc -std=c11 -Wall -Wextra till.c -o till

Le pilote envoie les invites et messages sur l'erreur standard. Seul un reçu complet va sur la sortie standard. Redirigez-le vers un nouveau fichier et continuez à saisir vos réponses aux invites visibles :

./till > receipt.txt

Saisissez la transaction suivante dans l'ordre. Appuyez sur Entrée après chaque identifiant et quantité. Un identifiant inconnu ne demande pas de quantité. Après un rejet, vous revenez à Product ID ; ne continuez pas à répondre à l'ancienne invite de quantité.

Saisie de Product ID Saisie de Quantity Observation attendue
101 2 Added line 1
102 2bars Quantité rejetée ; la ligne 1 est conservée
999 Aucune quantité demandée Identifiant inconnu ; la ligne 1 est conservée
103 20 Added line 2, limite supérieure valide de quantité
104 1 Added line 3, produit gratuit valide
102 1 Added line 4
101 1 Added line 5, séparée de la ligne 1
104 2 Added line 6
103 1 Panier plein : rejet ; les six lignes sont conservées
done Aucune quantité demandée Terminer le reçu ; revenir au shell

Lisez le résultat enregistré seulement lorsque l'invite du shell revient :

cat receipt.txt

Contenu attendu :

SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 2 | 1.25 | 2.50
103 | Water | 20 | 0.75 | 15.00
104 | Water | 1 | 0.00 | 0.00
102 | Fruit cup | 1 | 2.30 | 2.30
101 | Oat bar | 1 | 1.25 | 1.25
104 | Water | 2 | 0.00 | 0.00
TOTAL | 21.05

L'entrée rejetée 2bars, l'identifiant 999 et le septième achat n'ont créé aucune ligne et n'ont modifié aucune ligne précédente. Les identifiants 103 et 104 affichent Water, mais conservent des prix distincts. Un prix nul n'est pas une quantité nulle. La somme entière est 250 + 1500 + 0 + 230 + 125 + 0 = 2105 centimes.

Le reçu complet comporte le titre du comptoir, les cinq colonnes indiquées séparées par des barres verticales, tous les achats dans l'ordre et exactement un TOTAL à la fin. Les lignes vides et les espaces autour des champs peuvent varier, mais un en-tête absent, des enregistrements supplémentaires non reconnus ou des lignes après le total constituent une sortie incomplète. Conservez receipt.txt, till.c, parse.h, l'exécutable till et tous les fichiers fournis. Le contrôle du cours exécute aussi des copies inchangées du code avec une détection des dépassements de limites à l'exécution dans un répertoire jetable. Il peut détecter les erreurs exercées par ces entrées ; il ne prouve pas une sûreté mémoire complète et ne répare pas vos fichiers.

Pour observer la différence entre annuler et terminer, démarrez une autre transaction avec un nom de sortie différent :

./till > cancelled.txt

Saisissez l'identifiant 101, appuyez sur Entrée, puis sur Ctrl+D à l'invite de quantité vide. EOF annule avec Cancelled: no receipt. sans publier de reçu complet. Il abandonne aussi une ligne de saisie partielle inachevée. Lisez immédiatement son statut de sortie natif :

echo $?

La sortie attendue est 2. Le receipt.txt précédent subsiste parce que cette commande utilise un autre nom de fichier. > aurait tronqué un ancien fichier du même nom avant l'exécution du programme. En revanche, done à Product ID termine avec le statut 0, même pour un panier vide dont le total vaut 0.00 ; done n'est jamais une commande de quantité.

Si un achat valide est rejeté, vérifiez la validation du texte entier et les limites inclusives de l'analyseur, puis la condition d'approbation. Si des lignes précédentes disparaissent, vérifiez que le chemin d'erreur revient avant de modifier le panier ou le compteur. Enregistrez, recompilez et répétez la transaction requise dans une nouvelle exécution avec un panier vide, sans modifier le texte du reçu. Un reçu correct enregistré et un programme réutilisable doivent concorder.

Résumé

Vous avez utilisé une ligne de caractères de taille bornée, un pointeur de fin de conversion, un contrôle de dépassement et des limites métier inclusives avant d'approuver un achat. Votre programme a conservé toutes les lignes précédentes après des entrées invalides et le rejet d'un panier plein, tout en acceptant les articles gratuits et les quantités aux limites. La finalisation de l'achat et l'annulation par EOF ont désormais des résultats clairs et distincts.