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.


