Reject Input That Cannot Be a Purchase

CBeginner
Practice Now

Introduction

A receipt should contain approved purchases, not whatever a number-conversion function happens to recognize at the beginning of a line. Sunny Snacks now needs complete input validation and a capacity check that cannot overwrite earlier purchases.

This independent unit prepares its own catalog and receipt program. You need the arrays, functions, structs and integer-cent receipt work from Keep Every Line of the Receipt. You will learn only the string/address details needed for a bounded decimal parser, then reject bad input without changing the basket. The Workspace terminal and compiler are ready; this is a synthetic text-receipt program, not a payment system. Time is an author estimate.

Convert Complete Decimal Text

In this step, you will distinguish a complete valid number from a numeric prefix, then implement the parser's success and failure boundary.

Start in the Workspace Tab. The prepared files are independent of the previous lab. cd enters the working directory, ls checks file names, and cat displays text:

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

parse-demo.c is a small caller for parse_number; it reads one quantity, prints an accepted integer or a rejection, and exits. till.c will use the same parse.h in the next step. The current parser always returns 0, so it rejects even good input. Keep catalog.h, driver.h, read-line.h and parse-demo.c unchanged; edit parse.h and, later, till.c.

Text in C is a character array ending with the zero-valued NUL terminator, written \0. It is different from the visible digit '0'. For example:

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

The supplied read_line reserves 64 bytes: at most 63 input bytes plus the terminator. getchar() reads one input character; its int result can also represent EOF. The loop counts up to 63 characters but continues reading an overlong line until its newline, so leftover characters never become the next answer. It returns 1 for a complete line, -1 for a drained overlong line, or 0 for EOF. EOF also discards an unfinished line. The demo's read_line(text) != 1 rejects either incomplete case before conversion.

The parser's public interface is:

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

Keep this function name and parameter order: the prepared demo and driver call it. It returns 1 for success, or 0 for failure. On success it stores the converted integer in the caller's result variable. On failure it leaves that variable unchanged. minimum and maximum are inclusive: 0 is a valid numeric value when minimum is 0, although a purchase quantity has minimum 1.

Here is the small address idea that this call needs. A normal integer argument passes a value. &quantity means the address of the caller's quantity box. The parameter int *result receives that address; *result = value writes a value into the caller's box. The star in a declaration names an address-holding variable; the star in an expression accesses the box at that address. We use these specific output parameters, without dynamic allocation or a general pointer exercise.

strtol, declared in stdlib.h, converts text to a long integer. We explicitly pass base 10, so a leading zero remains decimal. Its first argument points to the first character to read. Its second argument is another output: char *end will hold the address of the first character that was not part of the number. &end passes the address of that address-holding variable so strtol can update it. It does not copy a second 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

A converted prefix is not enough. Our numeric syntax allows optional leading/trailing ASCII space or Tab, an optional sign and decimal digits, including leading zeros. Internal spaces, decimal points, hexadecimal prefixes, other whitespace characters and trailing letters are invalid. A sign alone and blank text are invalid. The exact lowercase command done belongs only at the till's Product ID prompt; it is not numeric text.

Use an array index begin to skip only the allowed leading space and Tab characters. &text[begin] is the address of the first remaining character. Check that this character is a digit or sign before calling strtol; this prevents its broader whitespace handling from silently expanding our input syntax. After conversion, end == &text[begin] means no digits were consumed. *end reads the character at the stopping address; end++ advances that address one character. Skip only trailing space/Tab, then require the terminator.

There is one more boundary: text can represent a value too large even for a long. errno, from errno.h, is the library's error indicator. Clear it before the call; ERANGE afterward means range overflow. Check both that error and the business minimum/maximum before converting the long to int. A returned numeric zero alone is not an error signal.

Replace parse.h with the complete parser. The header's function is static, so it belongs to each program that includes it. cat > replaces the file and the quoted here-document copies the following C literally through the final EOF line.

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

The final *result = (int)value writes the caller's integer only after all checks succeeded. Thus a bad input cannot change an existing result value. The cast (int) performs the now-safe conversion.

Compile the small demo. -std=c11 selects C11, -Wall -Wextra enable warnings, and -o names its executable:

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

At Quantity (1..20):, type +0002 and press Enter. The result is Accepted quantity: 2, and the shell prompt returns. The input has a sign and leading zeros but still represents the ordinary integer 2.

Now run the demo again:

./parse-demo

At the quantity prompt, enter 2bars. The expected result is Rejected quantity.. This is an intentional failure: converting the prefix 2 did not validate the whole line. A program's exit status is a small integer for its caller: 0 means normal success here; 2 means the demo rejected its input. Immediately run echo $? to print that last status. Any intervening command would replace it.

echo $?

Expected output is 2. This rejection does not mean the terminal or compiler failed. Run the demo a third time:

./parse-demo

Enter 9999999999999999999999999999999999999999 and press Enter. It also rejects: this line is shorter than 63 bytes, but its decimal number overflows the conversion range. By contrast, 21 fits a long but fails the quantity maximum 20. Those are different reasons for the same safe refusal.

If compilation reports a missing file, recover with cd and ls. For syntax errors, save the corrected header and compile again. The parse-demo caller includes your header at compilation; running an old executable will not test a new edit.

Approve a Purchase Before Changing State

In this step, you will combine the parser with the purchase array, refuse a full basket before writing, and save a receipt after several rejected inputs.

Start in Workspace, in /home/labex/project/receipts. The prepared till.c already contains complete record types, catalog lookup and receipt printing from the previous lab. Its approve_purchase still has a gap.

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

Read the driver from its prompts inward. It reads a whole ID line, recognizes exact done, parses the decimal ID and checks that the product exists. Then it reads a quantity and calls your parser with bounds 1..20. !parse_number(...) means the parser returned false. A failed read or conversion returns to the Product ID prompt without calling approval. An invalid quantity discards just that pending candidate, not earlier purchases.

Approval also protects its own array boundary. Find the product index, reject an absent ID or quantity outside 1..20, and reject when line_count >= MAX_LINES. || means any one of those conditions is enough to refuse. Only afterward write both fields, increase line_count, and return 1. Writing basket[MAX_LINES] even once is outside a six-entry array; a later rejection does not undo that invalid write. The usable entries are 0 through 5.

All calculations stay in integer cents. Four supplied catalog rows use IDs 1..999 and prices 0..9999 cents, including a valid free product; six quantities of at most 20 bound the total at 1,199,880 cents. Keep every separately approved purchase in order, even when its ID repeats. Recompiling against another supplied four-row catalog within these limits should reuse the same logic. Struct and local variable names may vary; the prepared callable function interfaces must still match the driver.

Replace till.c with the complete program. This repeats the independent unit's record and printing foundation so you do not need a previous VM's files. parse.h remains your completed parser from this step's 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 the new source:

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

The driver sends prompts and messages to standard error. Only a completed receipt goes to standard output. Redirect it to a new file, then continue typing at the visible prompts:

./till > receipt.txt

Enter the following transaction in order. Press Enter after each ID and quantity. An unknown ID has no quantity prompt. After a rejection you return to Product ID; do not try to continue at the previous quantity prompt.

Product ID input Quantity input Expected observation
101 2 Added line 1
102 2bars Rejected quantity; line 1 remains
999 No quantity prompt Unknown product ID; line 1 remains
103 20 Added line 2, valid upper quantity boundary
104 1 Added line 3, valid zero-price product
102 1 Added line 4
101 1 Added line 5, separate from line 1
104 2 Added line 6
103 1 Full-basket rejection; all six lines remain
done No quantity prompt Complete receipt; return to shell

Read the saved result only after the shell prompt returns:

cat receipt.txt

Expected contents:

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

The rejected 2bars, ID 999 and seventh purchase produced no rows and changed no earlier row. IDs 103 and 104 both display Water, yet their prices stay distinct. Zero price is not zero quantity. The integer sum is 250 + 1500 + 0 + 230 + 125 + 0 = 2105 cents.

The completed receipt format is the shop title, the five named pipe-separated columns, all ordered purchase rows, and exactly one final TOTAL. Blank lines and spaces around fields may vary, but missing headers, unrecognized extra rows or records after the total are incomplete output. Keep receipt.txt, till.c, parse.h, executable till and all supplied files. The course check also runs unchanged source copies with runtime bounds detection in a disposable directory. It can detect errors exercised by those inputs; it does not prove complete memory safety or repair your files.

To see how cancellation differs from checkout, start another transaction with a different output name:

./till > cancelled.txt

Enter ID 101, press Enter, then press Ctrl+D at the empty quantity prompt. EOF cancels with Cancelled: no receipt. and publishes no completed receipt. It also discards an unterminated partial input line. Immediately read its native status:

echo $?

Expected output is 2. The earlier receipt.txt remains because this command used another file name. > would have truncated an old same-named file before the program ran. done at Product ID instead checks out with status 0, including an empty basket with total 0.00; done is never a quantity command.

If a legitimate purchase is rejected, check the parser's full-text and inclusive-range tests, then the approval guard. If old rows disappear, check that an error path returns before changing the basket or count. Save, recompile and replay the required transaction from an empty new execution rather than editing receipt text. A good saved receipt and a reusable program must agree.

Summary

You used a bounded character line, conversion end pointer, overflow check and inclusive business bounds before approving a purchase. Your program preserved all earlier lines after bad input and a full-basket rejection, while zero-price products and boundary quantities remained usable. Checkout and EOF now have clear, different results.