Rejeitar entradas que não podem ser uma compra

CBeginner
Pratique Agora

Introdução

Um recibo deve conter compras aprovadas, não qualquer prefixo que uma função de conversão numérica reconheça no começo de uma linha. Agora a Sunny Snacks precisa de validação completa e de uma verificação de capacidade que não sobrescreva compras anteriores.

Esta unidade independente prepara seu próprio catálogo e programa de recibos. Você precisa dos arrays, funções, estruturas e cálculos em centavos inteiros de Preservar cada linha do recibo. Aprenderá apenas os detalhes de strings e endereços necessários para um analisador decimal com limites; depois rejeitará entradas inválidas sem alterar a cesta. O terminal Workspace e o compilador estão prontos; este é um programa de recibos de texto com dados sintéticos, não um sistema de pagamento. O tempo é uma estimativa do autor.

Converter o texto decimal completo

Nesta etapa, você distinguirá um número completo válido de um prefixo numérico e implementará o limite entre sucesso e falha do analisador.

Comece na aba Workspace. Os arquivos preparados são independentes do laboratório anterior. cd entra no diretório de trabalho, ls permite conferir os nomes e cat mostra texto:

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

parse-demo.c é um pequeno programa que chama parse_number; lê uma quantidade, imprime o inteiro aceito ou uma rejeição e termina. till.c usará o mesmo parse.h na próxima etapa. O analisador atual sempre retorna 0, então rejeita até entradas válidas. Preserve catalog.h, driver.h, read-line.h e parse-demo.c sem alterações; edite parse.h e, depois, till.c.

Em C, texto é um array de caracteres que termina com o terminador NUL de valor zero, escrito \0. Ele é diferente do dígito visível '0'. Por exemplo:

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

O read_line fornecido reserva 64 bytes: até 63 bytes de entrada e o terminador. getchar() lê um caractere; seu resultado do tipo int também pode representar EOF. O laço guarda até 63 caracteres, mas continua lendo uma linha longa demais até sua quebra de linha, para que os restos nunca se tornem a próxima resposta. Retorna 1 para uma linha completa, -1 para uma linha longa demais já consumida e 0 para EOF. EOF também descarta uma linha inacabada. A condição read_line(text) != 1 do exemplo rejeita os dois últimos casos antes da conversão.

A interface pública do analisador é:

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

Preserve esse nome e a ordem dos parâmetros: o exemplo e o controlador preparados os usam. Retorne 1 em caso de sucesso e 0 em caso de falha. No sucesso, guarde o inteiro convertido na variável de resultado do chamador. Na falha, deixe essa variável intacta. minimum e maximum são inclusivos: 0 é um número válido se o mínimo for 0, embora a quantidade de uma compra tenha mínimo 1.

Essa chamada precisa de uma pequena ideia sobre endereços. Um argumento inteiro comum transmite um valor. &quantity significa o endereço da posição quantity do chamador. O parâmetro int *result recebe esse endereço; *result = value escreve um valor na posição do chamador. O asterisco em uma declaração indica uma variável que guarda um endereço; em uma expressão, acessa a posição nesse endereço. Usamos esses parâmetros de saída específicos, sem alocação dinâmica nem um exercício geral de ponteiros.

strtol, declarada em stdlib.h, converte texto em um inteiro long. Passamos explicitamente a base 10, então um zero inicial continua decimal. Seu primeiro argumento aponta para o primeiro caractere a ler. O segundo é outra saída: char *end guardará o endereço do primeiro caractere que não fez parte do número. &end passa o endereço dessa variável que guarda um endereço, para que strtol possa atualizá-la. Não copia uma segunda string.

"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

Converter um prefixo não basta. Nossa sintaxe permite espaços ASCII ou tabulações opcionais no início e no fim, um sinal opcional e dígitos decimais, incluindo zeros à esquerda. Espaços internos, pontos decimais, prefixos hexadecimais, outros caracteres de espaço e letras finais são inválidos. Um sinal sozinho e texto vazio ou só com espaços também são inválidos. O comando exato done, em minúsculas, pertence apenas ao prompt Product ID do programa; não é texto numérico.

Use um índice de array begin para pular somente os espaços e tabulações iniciais permitidos. &text[begin] é o endereço do primeiro caractere restante. Antes de chamar strtol, confira se ele é um dígito ou um sinal; assim, o tratamento mais amplo de espaços dessa função não expande silenciosamente nossa sintaxe. Após a conversão, end == &text[begin] significa que nenhum dígito foi consumido. *end lê o caractere no endereço de parada; end++ avança esse endereço em um caractere. Pule apenas espaços ou tabulações finais e depois exija o terminador.

Existe outro limite: o texto pode representar um valor grande demais até para um long. errno, de errno.h, é o indicador de erro da biblioteca. Zere-o antes da chamada; ERANGE depois indica estouro de intervalo. Confira esse erro e o mínimo e máximo de negócio antes de converter o long para int. Um zero numérico retornado não é, por si só, um sinal de erro.

Substitua parse.h pelo analisador completo. A função do cabeçalho é static, então pertence a cada programa que a inclui. cat > substitui o arquivo e o here-document entre aspas copia literalmente o C a seguir até a última linha 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

A instrução final *result = (int)value escreve o inteiro do chamador somente após passar por todas as verificações. Por isso, uma entrada inválida não muda um resultado existente. A conversão explícita (int) realiza a conversão que agora é segura.

Compile o pequeno exemplo. -std=c11 seleciona C11, -Wall -Wextra ativa avisos e -o dá nome ao executável:

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

Em Quantity (1..20):, digite +0002 e pressione Enter. O resultado é Accepted quantity: 2 e o prompt do shell retorna. A entrada tem sinal e zeros à esquerda, mas representa o inteiro comum 2.

Execute o exemplo novamente:

./parse-demo

No prompt de quantidade, digite 2bars. O resultado esperado é Rejected quantity.. Essa é uma falha intencional: converter o prefixo 2 não validou a linha inteira. O status de saída de um programa é um pequeno inteiro para quem o chama: aqui 0 indica sucesso normal e 2 indica que o exemplo rejeitou a entrada. Execute imediatamente echo $? para mostrar esse último status. Qualquer comando intermediário o substituiria.

echo $?

A saída esperada é 2. Essa rejeição não significa que o terminal ou o compilador falhou. Execute o exemplo pela terceira vez:

./parse-demo

Digite 9999999999999999999999999999999999999999 e pressione Enter. Também é rejeitado: a linha tem menos de 63 bytes, mas seu número decimal excede o intervalo de conversão. Já 21 cabe em um long, mas ultrapassa a quantidade máxima 20. São razões diferentes para a mesma rejeição segura.

Se a compilação informar um arquivo ausente, recupere-se com cd e ls. Se houver erros de sintaxe, salve o cabeçalho corrigido e recompile. O programa parse-demo inclui seu cabeçalho na compilação; executar um binário antigo não testa uma edição nova.

Aprovar uma compra antes de alterar o estado

Nesta etapa, você combinará o analisador com o array de compras, rejeitará uma cesta cheia antes de escrever e salvará um recibo após várias entradas rejeitadas.

Comece em Workspace, em /home/labex/project/receipts. O till.c preparado já contém os tipos de registro, a busca no catálogo e a impressão completa do laboratório anterior. Seu approve_purchase ainda tem uma lacuna.

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

Leia o controlador a partir dos prompts para dentro. Ele lê uma linha completa de ID, reconhece o done exato, analisa o ID decimal e verifica se o produto existe. Depois lê uma quantidade e chama seu analisador com limites 1..20. !parse_number(...) significa que o analisador retornou falso. Uma leitura ou conversão malsucedida volta a Product ID sem chamar a aprovação. Uma quantidade inválida descarta apenas o candidato pendente, não as compras anteriores.

A aprovação também protege o limite do array. Busque o índice do produto, rejeite um ID ausente ou uma quantidade fora de 1..20 e rejeite quando line_count >= MAX_LINES. || significa que qualquer uma dessas condições basta para rejeitar. Só depois escreva os dois campos, incremente line_count e retorne 1. Escrever em basket[MAX_LINES] uma única vez já sai do array de seis entradas; rejeitar depois não desfaz essa escrita inválida. As posições utilizáveis vão de 0 a 5.

Todos os cálculos continuam em centavos inteiros. As quatro linhas fornecidas usam IDs 1..999 e preços 0..9999 centavos, incluindo um item gratuito válido; seis quantidades de no máximo 20 limitam o total a 1,199,880 centavos. Preserve cada compra aprovada separadamente e em ordem, mesmo se o ID se repetir. A mesma lógica deve servir ao recompilar com outro catálogo fornecido de quatro linhas nesses limites. Os nomes de estruturas e variáveis locais podem variar; as interfaces de função preparadas devem continuar compatíveis com o controlador.

Substitua till.c pelo programa completo. A base de registros e impressão é repetida nesta unidade independente para não precisar de arquivos de uma VM anterior. parse.h continua sendo o analisador que você completou na etapa anterior desta 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

Compile o código novo:

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

O controlador envia prompts e mensagens para a saída de erro padrão. Só um recibo completo vai para a saída padrão. Redirecione-o para um arquivo novo e continue respondendo aos prompts visíveis:

./till > receipt.txt

Digite a transação a seguir em ordem. Pressione Enter após cada ID e quantidade. Um ID desconhecido não pede quantidade. Após uma rejeição, você volta a Product ID; não tente continuar respondendo ao prompt de quantidade anterior.

Entrada de Product ID Entrada de Quantity Observação esperada
101 2 Added line 1
102 2bars Quantidade rejeitada; linha 1 preservada
999 Quantidade não solicitada ID desconhecido; linha 1 preservada
103 20 Added line 2, limite superior válido de quantidade
104 1 Added line 3, produto gratuito válido
102 1 Added line 4
101 1 Added line 5, separada da linha 1
104 2 Added line 6
103 1 Rejeição por cesta cheia; as seis linhas são preservadas
done Quantidade não solicitada Concluir o recibo; voltar ao shell

Leia o resultado salvo somente quando o prompt do shell voltar:

cat receipt.txt

Conteúdo esperado:

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

A entrada rejeitada 2bars, o ID 999 e a sétima compra não criaram linhas nem mudaram as anteriores. Os IDs 103 e 104 exibem Water, mas mantêm preços diferentes. Preço zero não é quantidade zero. A soma inteira é 250 + 1500 + 0 + 230 + 125 + 0 = 2105 centavos.

O recibo completo tem o título do balcão, as cinco colunas indicadas separadas por barras, todas as compras em ordem e exatamente um TOTAL no fim. Linhas em branco e espaços ao redor dos campos podem variar, mas um cabeçalho ausente, registros extras não reconhecidos ou linhas após o total são saída incompleta. Preserve receipt.txt, till.c, parse.h, o executável till e todos os arquivos fornecidos. A verificação do curso também executa cópias intactas do código com detecção de limites em tempo de execução em um diretório descartável. Pode detectar erros exercitados por essas entradas; não demonstra segurança completa de memória nem repara seus arquivos.

Para observar a diferença entre cancelar e finalizar, inicie outra transação com um nome de saída diferente:

./till > cancelled.txt

Digite o ID 101, pressione Enter e depois Ctrl+D no prompt de quantidade vazio. EOF cancela com Cancelled: no receipt. e não publica um recibo completo. Também descarta uma linha de entrada parcial inacabada. Leia imediatamente seu status de saída nativo:

echo $?

A saída esperada é 2. O receipt.txt anterior permanece porque esse comando usa outro nome de arquivo. > teria truncado um arquivo anterior do mesmo nome antes de executar o programa. Já done em Product ID finaliza com status 0, mesmo com uma cesta vazia cujo total é 0.00; done nunca é um comando de quantidade.

Se uma compra válida for rejeitada, confira a validação do texto completo e os limites inclusivos do analisador, depois a condição de aprovação. Se linhas anteriores desaparecerem, confira se o caminho de erro retorna antes de alterar a cesta ou o contador. Salve, recompile e repita a transação exigida em uma nova execução com a cesta vazia, sem editar o texto do recibo. Um recibo correto salvo e um programa reutilizável devem corresponder.

Resumo

Você usou uma linha de caracteres com tamanho limitado, um ponteiro para o fim da conversão, uma verificação de estouro e limites de negócio inclusivos antes de aprovar uma compra. Seu programa preservou todas as linhas anteriores após entradas inválidas e uma rejeição por cesta cheia, sem impedir itens gratuitos nem quantidades nos limites. Finalizar a compra e cancelar por EOF agora têm resultados claros e distintos.