Rechazar entradas que no representan una compra

CBeginner
Practicar Ahora

Introducción

Un recibo debe contener compras aprobadas, no cualquier prefijo que una función de conversión numérica reconozca al principio de una línea. Sunny Snacks necesita ahora una validación completa y una comprobación de capacidad que no sobrescriba compras anteriores.

Esta unidad independiente prepara su propio catálogo y programa de recibos. Necesitas los arrays, las funciones, las estructuras y el trabajo en centavos enteros de Conservar cada línea del recibo. Aprenderás solo los detalles de cadenas y direcciones necesarios para un analizador decimal acotado; después rechazarás entradas inválidas sin cambiar la cesta. El terminal Workspace y el compilador están preparados; es un programa de recibos de texto con datos sintéticos, no un sistema de pagos. El tiempo es una estimación del autor.

Convertir todo el texto decimal

En este paso distinguirás un número completo válido de un prefijo numérico e implementarás el límite entre éxito y fallo del analizador.

Empieza en la pestaña Workspace. Los archivos preparados son independientes del laboratorio anterior. cd entra en el directorio de trabajo, ls permite comprobar los nombres y cat muestra texto:

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

parse-demo.c es un pequeño programa que llama a parse_number; lee una cantidad, imprime el entero aceptado o un rechazo y termina. till.c usará el mismo parse.h en el siguiente paso. El analizador actual siempre devuelve 0, así que rechaza incluso entradas válidas. Conserva catalog.h, driver.h, read-line.h y parse-demo.c sin cambios; edita parse.h y, después, till.c.

En C, el texto es un array de caracteres que termina con el terminador NUL de valor cero, escrito \0. Es distinto del dígito visible '0'. Por ejemplo:

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

El read_line proporcionado reserva 64 bytes: hasta 63 bytes de entrada y el terminador. getchar() lee un carácter; su resultado de tipo int también puede representar EOF. El bucle guarda hasta 63 caracteres, pero sigue leyendo una línea demasiado larga hasta su salto de línea, de modo que los restos nunca pasan a ser la siguiente respuesta. Devuelve 1 para una línea completa, -1 para una línea demasiado larga ya consumida y 0 para EOF. EOF también descarta una línea sin terminar. La condición read_line(text) != 1 del ejemplo rechaza los dos últimos casos antes de convertir.

La interfaz pública del analizador es:

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

Conserva este nombre y el orden de parámetros: el ejemplo y el controlador preparados los usan. Devuelve 1 si tiene éxito y 0 si falla. Al tener éxito guarda el entero convertido en la variable de resultado del llamador. Al fallar deja esa variable intacta. minimum y maximum son inclusivos: 0 es un número válido si el mínimo es 0, aunque la cantidad de una compra tenga mínimo 1.

Esta llamada necesita una pequeña idea sobre direcciones. Un argumento entero normal transmite un valor. &quantity significa la dirección de la casilla quantity del llamador. El parámetro int *result recibe esa dirección; *result = value escribe un valor en la casilla del llamador. La estrella en una declaración indica una variable que guarda una dirección; en una expresión accede a la casilla de esa dirección. Usamos estos parámetros de salida concretos, sin asignación dinámica ni un ejercicio general de punteros.

strtol, declarada en stdlib.h, convierte texto en un entero long. Pasamos explícitamente base 10, por lo que un cero inicial sigue siendo decimal. Su primer argumento apunta al primer carácter que debe leer. El segundo es otra salida: char *end guardará la dirección del primer carácter que no formó parte del número. &end pasa la dirección de esa variable que guarda una dirección, para que strtol pueda actualizarla. No copia una segunda cadena.

"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 prefijo no basta. Nuestra sintaxis permite espacios ASCII o tabuladores opcionales al principio y al final, un signo opcional y dígitos decimales, incluidos ceros iniciales. Los espacios internos, los puntos decimales, los prefijos hexadecimales, otros caracteres de espacio y las letras finales son inválidos. Un signo solo y un texto en blanco también son inválidos. El comando exacto done, en minúsculas, pertenece únicamente a la indicación Product ID del programa; no es texto numérico.

Usa un índice de array begin para saltar únicamente los espacios y tabuladores iniciales permitidos. &text[begin] es la dirección del primer carácter restante. Antes de llamar a strtol, comprueba que sea un dígito o un signo; así su tratamiento más amplio de los espacios no amplía silenciosamente nuestra sintaxis. Tras convertir, end == &text[begin] significa que no se consumió ningún dígito. *end lee el carácter en la dirección de parada; end++ avanza esa dirección un carácter. Salta solo espacios o tabuladores finales y exige después el terminador.

Hay otro límite: el texto puede representar un valor demasiado grande incluso para un long. errno, de errno.h, es el indicador de error de la biblioteca. Ponlo a cero antes de la llamada; ERANGE después indica desbordamiento de rango. Comprueba tanto ese error como el mínimo y el máximo del negocio antes de convertir el long a int. Un cero numérico devuelto no es, por sí solo, una señal de error.

Sustituye parse.h por el analizador completo. La función de la cabecera es static, así que pertenece a cada programa que la incluye. cat > reemplaza el archivo y el here-document entre comillas copia literalmente el C siguiente hasta la última línea 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

La instrucción final *result = (int)value escribe el entero del llamador solo después de superar todas las comprobaciones. Por eso una entrada inválida no cambia un resultado existente. La conversión explícita (int) realiza la conversión que ahora es segura.

Compila el pequeño ejemplo. -std=c11 selecciona C11, -Wall -Wextra activa advertencias y -o da nombre al ejecutable:

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

En Quantity (1..20):, escribe +0002 y pulsa Enter. El resultado es Accepted quantity: 2 y vuelve la indicación del shell. La entrada tiene signo y ceros iniciales, pero representa el entero normal 2.

Ejecuta de nuevo el ejemplo:

./parse-demo

En la indicación de cantidad, escribe 2bars. El resultado esperado es Rejected quantity.. Es un fallo intencionado: convertir el prefijo 2 no validó toda la línea. El estado de salida de un programa es un pequeño entero para quien lo llama: aquí 0 indica éxito normal y 2 indica que el ejemplo rechazó la entrada. Ejecuta inmediatamente echo $? para mostrar ese último estado. Cualquier comando intermedio lo reemplazaría.

echo $?

La salida esperada es 2. Este rechazo no significa que hayan fallado el terminal o el compilador. Ejecuta el ejemplo por tercera vez:

./parse-demo

Introduce 9999999999999999999999999999999999999999 y pulsa Enter. También se rechaza: la línea tiene menos de 63 bytes, pero su número decimal desborda el rango de conversión. En cambio, 21 cabe en un long, pero supera la cantidad máxima 20. Son razones distintas para el mismo rechazo seguro.

Si la compilación indica que falta un archivo, recupérate con cd y ls. Si hay errores de sintaxis, guarda la cabecera corregida y recompila. El programa parse-demo incluye tu cabecera al compilar; ejecutar un binario antiguo no prueba una edición nueva.

Aprobar una compra antes de cambiar el estado

En este paso combinarás el analizador con el array de compras, rechazarás una cesta llena antes de escribir y guardarás un recibo tras varias entradas rechazadas.

Empieza en Workspace, en /home/labex/project/receipts. El till.c preparado ya contiene los tipos de registro, la búsqueda en el catálogo y la impresión completa del laboratorio anterior. Su approve_purchase todavía tiene un hueco.

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

Lee el controlador desde sus indicaciones hacia el interior. Lee una línea completa de ID, reconoce el done exacto, analiza el ID decimal y comprueba que exista el producto. Después lee una cantidad y llama a tu analizador con límites 1..20. !parse_number(...) significa que el analizador devolvió falso. Una lectura o conversión fallida vuelve a Product ID sin llamar a la aprobación. Una cantidad inválida descarta solo el candidato pendiente, no las compras anteriores.

La aprobación también protege el límite de su array. Busca el índice del producto, rechaza un ID ausente o una cantidad fuera de 1..20 y rechaza cuando line_count >= MAX_LINES. || significa que basta cualquiera de esas condiciones para rechazar. Solo después escribe ambos campos, incrementa line_count y devuelve 1. Escribir en basket[MAX_LINES] una sola vez ya sale del array de seis entradas; rechazar después no deshace esa escritura inválida. Las posiciones utilizables son de 0 a 5.

Todos los cálculos siguen en centavos enteros. Las cuatro filas proporcionadas usan IDs 1..999 y precios 0..9999 centavos, incluido un artículo gratuito válido; seis cantidades de como máximo 20 limitan el total a 1,199,880 centavos. Conserva cada compra aprobada por separado y en orden, aunque se repita el ID. La misma lógica debe servir al recompilar con otro catálogo proporcionado de cuatro filas en esos límites. Los nombres de estructuras y variables locales pueden variar; las interfaces de función preparadas deben seguir coincidiendo con el controlador.

Sustituye till.c por el programa completo. Se repite la base de registros e impresión de esta unidad independiente para no necesitar archivos de una VM anterior. parse.h sigue siendo el analizador que completaste en el paso anterior de esta 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

Compila el código nuevo:

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

El controlador envía las indicaciones y los mensajes al error estándar. Solo un recibo completo va a la salida estándar. Redirígelo a un archivo nuevo y sigue escribiendo en las indicaciones visibles:

./till > receipt.txt

Introduce la transacción siguiente en orden. Pulsa Enter después de cada ID y cantidad. Un ID desconocido no pide cantidad. Tras un rechazo vuelves a Product ID; no intentes seguir contestando a la indicación de cantidad anterior.

Entrada de Product ID Entrada de Quantity Observación esperada
101 2 Added line 1
102 2bars Cantidad rechazada; se conserva la línea 1
999 No se pide cantidad ID desconocido; se conserva la línea 1
103 20 Added line 2, límite superior válido de cantidad
104 1 Added line 3, producto gratuito válido
102 1 Added line 4
101 1 Added line 5, separada de la línea 1
104 2 Added line 6
103 1 Rechazo por cesta llena; se conservan las seis líneas
done No se pide cantidad Completar el recibo; volver al shell

Lee el resultado guardado solo cuando vuelva la indicación del shell:

cat receipt.txt

Contenido 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

La entrada rechazada 2bars, el ID 999 y la séptima compra no crearon filas ni cambiaron ninguna anterior. Los IDs 103 y 104 muestran Water, pero mantienen precios distintos. Un precio cero no es una cantidad cero. La suma entera es 250 + 1500 + 0 + 230 + 125 + 0 = 2105 centavos.

El recibo completo lleva el título del puesto, las cinco columnas indicadas separadas por barras, todas las compras en orden y exactamente un TOTAL al final. Pueden variar las líneas en blanco y los espacios alrededor de los campos, pero una cabecera ausente, registros adicionales no reconocidos o filas después del total son una salida incompleta. Conserva receipt.txt, till.c, parse.h, el ejecutable till y todos los archivos proporcionados. La comprobación del curso también ejecuta copias intactas del código con detección de límites en tiempo de ejecución en un directorio desechable. Puede detectar errores ejercitados por esas entradas; no demuestra seguridad completa de memoria ni repara tus archivos.

Para observar la diferencia entre cancelar y finalizar, inicia otra transacción con un nombre de salida distinto:

./till > cancelled.txt

Introduce el ID 101, pulsa Enter y después Ctrl+D en la indicación de cantidad vacía. EOF cancela con Cancelled: no receipt. y no publica un recibo completo. También descarta una línea de entrada parcial sin terminar. Lee inmediatamente su estado de salida nativo:

echo $?

La salida esperada es 2. El receipt.txt anterior permanece porque este comando usa otro nombre de archivo. > habría truncado un archivo anterior del mismo nombre antes de ejecutar el programa. En cambio, done en Product ID finaliza con estado 0, incluso con una cesta vacía cuyo total es 0.00; done nunca es un comando de cantidad.

Si una compra válida se rechaza, comprueba la validación de texto completo y los límites inclusivos del analizador, y después la condición de aprobación. Si desaparecen filas anteriores, comprueba que el camino de error vuelva antes de cambiar la cesta o el contador. Guarda, recompila y repite la transacción requerida en una ejecución nueva con la cesta vacía, sin editar el texto del recibo. Un recibo correcto guardado y un programa reutilizable deben coincidir.

Resumen

Usaste una línea de caracteres acotada, un puntero al final de la conversión, una comprobación de desbordamiento y límites de negocio inclusivos antes de aprobar una compra. Tu programa conservó todas las líneas anteriores tras entradas inválidas y un rechazo por cesta llena, sin impedir los artículos gratuitos ni las cantidades límite. Finalizar la compra y cancelar por EOF tienen ahora resultados claros y distintos.