Введение
Чек должен содержать одобренные покупки, а не любой префикс, который функция преобразования чисел распознала в начале строки. Теперь Sunny Snacks нужны полная проверка и контроль вместимости, не перезаписывающий предыдущие покупки.
Этот независимый модуль готовит собственный каталог и программу чеков. Нужны знания массивов, функций, структур и целочисленных расчётов в центах из работы «Сохранение каждой строки чека». Вы изучите только те детали строк и адресов, которые нужны десятичному анализатору с ограничениями, а затем будете отклонять неверный ввод без изменения корзины. Терминал Workspace и компилятор готовы; это программа текстовых чеков с синтетическими данными, а не платёжная система. Время оценено автором.
Преобразование полного десятичного текста
На этом шаге вы отличите допустимое полное число от числового префикса и реализуете границу успеха и неудачи анализатора.
Начните во вкладке Workspace. Подготовленные файлы независимы от предыдущей работы. cd переходит в рабочий каталог, ls позволяет проверить имена, а cat показывает текст:
cd /home/labex/project/receipts
ls
cat parse-demo.c
cat parse.h
cat read-line.h
parse-demo.c — небольшая программа, вызывающая parse_number: она читает количество, печатает принятое целое число или отказ и завершается. На следующем шаге till.c использует тот же parse.h. Текущий анализатор всегда возвращает 0, поэтому отклоняет даже допустимый ввод. Не меняйте catalog.h, driver.h, read-line.h и parse-demo.c; редактируйте parse.h, а позже — till.c.
В C текст — массив символов, завершающийся терминатором NUL с нулевым значением, записываемым как \0. Он отличается от видимой цифры '0'. Например:
array index: 0 1 2
text: '2' '0' '\0'
Выданная read_line выделяет 64 байта: до 63 байт ввода и терминатор. getchar() читает один символ; результат типа int также может представлять EOF. Цикл сохраняет до 63 символов, но продолжает читать слишком длинную строку до перевода строки, чтобы остатки никогда не стали следующим ответом. Он возвращает 1 для полной строки, -1 для полностью потреблённой слишком длинной строки и 0 для EOF. EOF также отбрасывает незаконченную строку. Условие read_line(text) != 1 в примере отклоняет два последних случая до преобразования.
Открытый интерфейс анализатора:
int parse_number(const char text[], int minimum, int maximum, int *result)
Сохраните это имя и порядок параметров: их используют подготовленные пример и драйвер. Возвращайте 1 при успехе и 0 при неудаче. При успехе сохраняйте преобразованное целое число в переменной результата вызывающего кода. При неудаче оставляйте эту переменную неизменной. minimum и maximum включают границы: 0 — допустимое число при минимуме 0, хотя минимум количества покупки равен 1.
Для этого вызова нужна небольшая идея об адресах. Обычный целочисленный аргумент передаёт значение. &quantity означает адрес ячейки quantity вызывающего кода. Параметр int *result получает этот адрес; *result = value записывает значение в ячейку вызывающего кода. Звёздочка в объявлении обозначает переменную, хранящую адрес, а в выражении обращается к ячейке по этому адресу. Здесь используются только эти конкретные выходные параметры, без динамического выделения памяти и общего упражнения по указателям.
strtol, объявленная в stdlib.h, преобразует текст в целое число long. Мы явно передаём основание 10, поэтому ведущий ноль остаётся десятичным. Первый аргумент указывает на первый читаемый символ. Второй — ещё один выход: char *end сохранит адрес первого символа, который не вошёл в число. &end передаёт адрес этой переменной, хранящей адрес, чтобы strtol могла её обновить. Вторая строка при этом не копируется.
"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
Преобразовать префикс недостаточно. Наш синтаксис допускает необязательные ASCII-пробелы или табуляции в начале и конце, необязательный знак и десятичные цифры, включая ведущие нули. Внутренние пробелы, десятичные точки, шестнадцатеричные префиксы, другие пробельные символы и конечные буквы недопустимы. Один знак и пустой или состоящий лишь из пробелов текст также недопустимы. Точная команда done в нижнем регистре относится только к приглашению Product ID программы; это не числовой текст.
Используйте индекс массива begin, чтобы пропустить только разрешённые начальные пробелы и табуляции. &text[begin] — адрес первого оставшегося символа. До вызова strtol проверьте, что это цифра или знак: так более широкая обработка пробельных символов этой функцией не расширит наш синтаксис незаметно. После преобразования end == &text[begin] означает, что не было потреблено ни одной цифры. *end читает символ по адресу остановки; end++ сдвигает этот адрес на один символ. Пропустите только конечные пробелы или табуляции, затем требуйте терминатор.
Есть и другая граница: текст может представлять значение, слишком большое даже для long. errno из errno.h — индикатор ошибки библиотеки. Обнулите его перед вызовом; ERANGE после вызова указывает на выход за диапазон. Проверьте эту ошибку и минимум с максимумом предметной области до преобразования long в int. Само по себе возвращённое числовое значение ноль не является признаком ошибки.
Замените parse.h полным анализатором. Функция в заголовке имеет спецификатор static, поэтому принадлежит каждой программе, включающей её. cat > заменяет файл, а here-document с разделителем в кавычках буквально копирует следующий код C до последней строки 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
Последняя инструкция *result = (int)value записывает целое число вызывающего кода только после прохождения всех проверок. Поэтому неверный ввод не меняет существующий результат. Явное приведение (int) выполняет уже безопасное преобразование.
Скомпилируйте небольшой пример. -std=c11 выбирает C11, -Wall -Wextra включает предупреждения, а -o задаёт имя исполняемого файла:
gcc -std=c11 -Wall -Wextra parse-demo.c -o parse-demo
./parse-demo
В Quantity (1..20): введите +0002 и нажмите Enter. Результат — Accepted quantity: 2, затем возвращается приглашение оболочки. Ввод имеет знак и ведущие нули, но представляет обычное целое число 2.
Запустите пример ещё раз:
./parse-demo
В приглашении количества введите 2bars. Ожидаемый результат — Rejected quantity.. Это намеренный отказ: преобразование префикса 2 не проверило всю строку. Статус завершения программы — небольшое целое число для вызывающего кода: здесь 0 означает обычный успех, а 2 — отклонение ввода примером. Немедленно выполните echo $?, чтобы показать последний статус. Любая промежуточная команда заменит его.
echo $?
Ожидаемый вывод — 2. Этот отказ не означает сбой терминала или компилятора. Запустите пример в третий раз:
./parse-demo
Введите 9999999999999999999999999999999999999999 и нажмите Enter. Значение тоже отклоняется: строка короче 63 байт, но десятичное число выходит за диапазон преобразования. Напротив, 21 помещается в long, но превышает максимальное количество 20. Это разные причины одного безопасного отказа.
Если компиляция сообщает об отсутствующем файле, восстановите расположение с помощью cd и ls. При синтаксических ошибках сохраните исправленный заголовок и перекомпилируйте. Программа parse-demo включает ваш заголовок при компиляции; запуск старого бинарного файла не проверяет новую правку.
Одобрение покупки до изменения состояния
На этом шаге вы объедините анализатор с массивом покупок, отклоните полную корзину до записи и сохраните чек после нескольких отказов ввода.
Начните в Workspace, в /home/labex/project/receipts. Подготовленный till.c уже содержит типы записей, поиск в каталоге и полную печать из предыдущей работы. В approve_purchase пока есть пропуск.
cd /home/labex/project/receipts
cat catalog.h
cat till.c
cat driver.h
Читайте драйвер от его приглашений внутрь. Он читает полную строку ID, распознаёт точный done, разбирает десятичный ID и проверяет существование товара. Затем читает количество и вызывает ваш анализатор с границами 1..20. !parse_number(...) означает, что анализатор вернул ложь. Неудачное чтение или преобразование возвращает к Product ID без вызова одобрения. Неверное количество отбрасывает лишь ожидающего кандидата, а не предыдущие покупки.
Одобрение также защищает границу массива. Найдите индекс товара, отклоните отсутствующий ID или количество вне 1..20 и отклоните покупку при line_count >= MAX_LINES. || означает, что любого из этих условий достаточно для отказа. Только затем записывайте оба поля, увеличивайте line_count и возвращайте 1. Даже одна запись в basket[MAX_LINES] уже выходит за массив из шести элементов; последующий отказ не отменяет эту неверную запись. Доступные позиции — от 0 до 5.
Все расчёты остаются в целых центах. Четыре выданные строки используют ID 1..999 и цены 0..9999 центов, включая допустимый бесплатный товар; шесть количеств не более 20 ограничивают итог суммой 1,199,880 центов. Сохраняйте каждую одобренную покупку отдельно и по порядку, даже если ID повторяется. Та же логика должна работать после перекомпиляции с другим выданным каталогом из четырёх строк в этих пределах. Имена структур и локальных переменных могут различаться; подготовленные интерфейсы функций должны по-прежнему соответствовать драйверу.
Замените till.c полной программой. Основа записей и печати повторена для этого независимого модуля, чтобы не требовались файлы предыдущей VM. parse.h остаётся анализатором, завершённым на предыдущем шаге этой 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
Скомпилируйте новый код:
gcc -std=c11 -Wall -Wextra till.c -o till
Драйвер отправляет приглашения и сообщения в стандартный поток ошибок. Только полный чек идёт в стандартный вывод. Перенаправьте его в новый файл и продолжайте отвечать на видимые приглашения:
./till > receipt.txt
Введите следующую транзакцию по порядку. Нажимайте Enter после каждого ID и количества. Неизвестный ID не запрашивает количество. После отказа вы возвращаетесь к Product ID; не пытайтесь продолжать ответ на предыдущее приглашение количества.
| Ввод Product ID | Ввод Quantity | Ожидаемое наблюдение |
|---|---|---|
101 |
2 |
Added line 1 |
102 |
2bars |
Количество отклонено; строка 1 сохранена |
999 |
Количество не запрашивается | Неизвестный ID; строка 1 сохранена |
103 |
20 |
Added line 2, допустимая верхняя граница количества |
104 |
1 |
Added line 3, допустимый бесплатный товар |
102 |
1 |
Added line 4 |
101 |
1 |
Added line 5, отдельно от строки 1 |
104 |
2 |
Added line 6 |
103 |
1 |
Отказ из-за полной корзины; все шесть строк сохранены |
done |
Количество не запрашивается | Завершить чек; вернуться в оболочку |
Читайте сохранённый результат только после возвращения приглашения оболочки:
cat receipt.txt
Ожидаемое содержимое:
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
Отклонённый ввод 2bars, ID 999 и седьмая покупка не создали строк и не изменили предыдущие. ID 103 и 104 отображают Water, но сохраняют разные цены. Нулевая цена — не нулевое количество. Целочисленная сумма равна 250 + 1500 + 0 + 230 + 125 + 0 = 2105 центов.
Полный чек содержит название прилавка, пять указанных столбцов с разделителями, все покупки по порядку и ровно один последний TOTAL. Пустые строки и пробелы вокруг полей могут различаться, но отсутствующий заголовок, лишние нераспознанные записи или строки после итога означают неполный вывод. Сохраните receipt.txt, till.c, parse.h, исполняемый файл till и все выданные файлы. Проверка курса также запускает неизменённые копии кода с динамическим обнаружением выхода за границы в одноразовом каталоге. Она может обнаружить ошибки, выполненные на этих входных данных; это не доказательство полной безопасности памяти и не исправление ваших файлов.
Чтобы увидеть разницу между отменой и завершением, начните другую транзакцию с другим именем выходного файла:
./till > cancelled.txt
Введите ID 101, нажмите Enter, затем Ctrl+D в пустом приглашении количества. EOF отменяет операцию с Cancelled: no receipt. и не публикует полный чек. Он также отбрасывает частично введённую незаконченную строку. Немедленно прочитайте собственный статус завершения программы:
echo $?
Ожидаемый вывод — 2. Предыдущий receipt.txt остаётся, потому что эта команда использует другое имя файла. > обнулил бы старый одноимённый файл до запуска программы. Напротив, done в Product ID завершает со статусом 0, даже для пустой корзины с итогом 0.00; done никогда не является командой количества.
Если допустимая покупка отклоняется, проверьте в анализаторе обработку всего текста и включительные границы, затем условие одобрения. Если исчезают предыдущие строки, проверьте возврат из ветки ошибки до изменения корзины или счётчика. Сохраните, перекомпилируйте и повторите нужную транзакцию в новом запуске с пустой корзиной, не редактируя текст чека. Правильно сохранённый чек и повторно используемая программа должны согласовываться.
Резюме
Перед одобрением покупки вы использовали строку символов ограниченной длины, указатель конца преобразования, проверку переполнения и включительные границы предметной области. Программа сохранила все предыдущие строки после неверного ввода и отказа из-за полной корзины, продолжая принимать бесплатные товары и граничные количества. Завершение покупки и отмена по EOF теперь имеют понятные разные результаты.


