はじめに
Sunny Snacks には、お客様が同じスナックをもう一度買った場合も、購入を一件ずつ残すレシートが必要です。まず商品一個の完成済みサンプルを実行し、その後、購入を個別に保存して各行の単価、小計、全体の合計を出力する C プログラムを完成させます。
C の変数、条件分岐、ループ、配列、関数に加え、基本的なターミナル入力とパスの知識が必要です。この実験では、必要な構造体とテキスト出力の詳細を説明します。ブラウザー環境にはコンパイラーと Workspace ターミナルが準備されています。金額は架空の整数セント値であり、決済・会計システムではありません。読解と実習の所要時間は著者の見積もりで、初心者による実測値ではありません。
商品一個のレシートを読む
このステップでは、小さな完成済みサンプルを実行し、商品一個の購入を二個の購入に変更します。
Workspace タブを開いてください。ターミナルは /home/labex/project/receipts から始まります。cd はディレクトリを移動し、pwd は現在地を表示し、ls はファイルを一覧表示します。パスや名前を打ち間違えたときにも、このコマンドで復帰できます。
cd /home/labex/project/receipts
pwd
ls
sample.c は商品一個の完成済みサンプルです。till.c は次のステップで使う、複数商品の未完成プログラムです。カタログ、パーサー、入力ドライバーは提供済みファイルなので変更しないでください。cat はテキストファイルを編集せずに表示します。
cat sample.c
サンプルの quantity 変数は 1 から始まります。unit_cents は 125、つまり通貨単位一つと 25 セントです。乗算でセント単位の小計を計算します。計算中はこの単位を維持してください。100 による整数除算で整数部分を求め、% 100 で余りを求めます。printf では、%d が整数、%s が終端のある C 文字列を出力し、%02d が小数部分を二桁に埋め、\n が改行します。これにより、浮動小数点計算を使わずに 250 セントを 2.50 と表示できます。
プログラムをコンパイルします。gcc は C コンパイラー、-std=c11 は C11 の選択、-Wall -Wextra は警告の有効化、-o sample は実行ファイル名の指定です。./sample は現在のディレクトリにあるそのファイルを実行します。
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample
通常の完全な結果は次のとおりです。
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 1.25
ID は商品識別子を意味し、101 はこのカタログ項目を指します。UNIT は商品一個の価格、SUBTOTAL は今回の購入数量に単価を掛けた値です。縦棒で読みやすく列を区切っています。最後の TOTAL は全購入の小計の合計で、ここでは購入は一件だけです。
数量を 2 に変更します。次の sed コマンドは、指定した古いテキストを新しいテキストに正確に置換します。-i は変更を保存します。単一引用符によって、置換指定を一つのシェル引数として渡します。
sed -i 's/int quantity = 1;/int quantity = 2;/' sample.c
編集後は再コンパイルしてください。ソースコードを変更しても、古い実行ファイルは自動更新されません。シェル演算子 > は標準出力をファイルへリダイレクトします。C プログラムが始まる前にファイルを開くか、既存内容を切り詰めるため、指定された新しい出力名を使います。
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample > sample-receipt.txt
cat sample-receipt.txt
数量は 2、小計と合計はいずれも 2.50 になり、単価は 1.25 のままです。125 × 2 = 250 セントを手計算でも確認してください。sample.c、実行ファイル sample、sample-receipt.txt を保持します。
ファイルが見つからない場合は、学んだ cd で戻り、ls で名前を確認してください。コンパイラーがエラーを報告したら、指摘された行を修正し、実行前に再コンパイルします。コンパイルに失敗しても、古い実行ファイルが残っていることがあります。
購入を一件ずつ保存して出力する
このステップでは、購入を個別のレコードとして保持し、購入三行を持つ完全なレシートを作成します。
Workspace の /home/labex/project/receipts から始めます。カタログと未完成プログラムを読みましょう。
cd /home/labex/project/receipts
cat catalog.h
cat till.c
ヘッダーファイル catalog.h には、売り場名、最大購入件数六件、最大数量 20、四つの商品項目が定義されています。#include "catalog.h" はコンパイル時にこれらの定義を取り込みます。#define は定数や初期化子に名前を付けます。CATALOG_ROWS は商品配列を初期化する行を提供します。カタログ定義の行末にあるバックスラッシュ(\)は、同じ #define を次の物理行に続けます。この実験では catalog.h、parse.h、driver.h、read-line.h を変更しないでください。
構造体は関連するフィールドを一つのレコードにまとめます。Product は整数の id、名前用の 32 文字配列、整数価格 cents をまとめます。typedef によって、このレコード型を短い名前 Product で使えます。C 文字列は値がゼロの文字 \0 で終わるため、名前の配列には表示文字に加えて終端文字の領域も必要です。提供された名前は収まります。任意の名前を解析する課題ではありません。
Purchase は二つ目のレコード型です。配列内の商品位置と、その購入の数量を保持します。basket[line_count].quantity では、角括弧が購入を選び、ドットが数量フィールドを選びます。位置はゼロから始まります。グローバルな static 配列と line_count はゼロで初期化されます。sizeof products / sizeof products[0] は、配列に商品レコードが何個含まれるかを計算します。
提供された find_product ループは商品 IDで検索し、配列内の位置を返します。存在しなければ -1 を返します。Water という二つの商品は ID と価格が異なり、103 は 75 セント、104 は無料です。名前だけでは商品を特定できません。カタログには四商品があり、ID は 1~999、価格は 0~9999 セントです。この範囲の別の四行カタログを提供して再コンパイルした場合も、同じプログラムが動作する必要があります。
配列に書き込む前に ID、数量、残り容量を検査して approve_purchase を完成させます。その後で初めて、商品位置と数量を basket[line_count] に保存し、カウンターを増やします。数量は 1~20 で、価格ゼロは有効です。同じ ID の購入を結合しないでください。後の購入は別の行です。最大価格と最大数量で六件購入しても、6 × 20 × 9999 = 1,199,880 セントで、符号付き 32 ビット整数に収まります。
puts は終端のある文字列を出力し、改行を追加します。printf は NUL 終端文字列用の %s や整数用の %d といった書式指定を使います。print_receipt は承認済み購入をすべてたどります。Product product = ... はそのカタログレコードをローカル変数にコピーします。各小計を整数で計算して合計に加え、タイトルと列見出しの後から、唯一の最後の TOTAL の前まで、完全な購入行を順序どおりに出力します。五つのフィールドを縦棒で区切ってください。フィールド周辺の空白や空行は変わってもかまいませんが、余分なレコードや欠けたフィールドがあれば完全なレシートではありません。
次の完成コードで till.c を置き換えます。cat > till.c はファイルを置き換え、<<'EOF' はヒアドキュメントを開始します。引用符付きの区切り語により、EOF だけの行まで C テキストをそのままコピーします。最後の行も含め、ブロック全体をコピーしてください。
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
parse.h と driver.h は、すでに動作する入力ループを提供します。ドライバーは ID と数量を尋ね、作成した承認関数と出力関数を呼び出します。既存の関数名とシグネチャは接続点なので、表示どおりに保ってください。内部のレコード型名やローカル変数名そのものは評価対象ではありません。入力解析の境界は、次の実験で学びます。
完成したプログラムをコンパイルします。
gcc -std=c11 -Wall -Wextra till.c -o till
ドライバーはプロンプトとメッセージを標準エラー出力に送り、ターミナル上に表示し続けます。完成したレシートだけが標準出力に送られます。新しい出力ファイルを指定して実行します。
./till > receipt.txt
プログラムは入力待ちです。表の各値を対応するプロンプトに入力し、その都度 Enter を押してください。シェルのコマンドとして入力してはいけません。done は小文字で完全一致する確定コマンドで、Product ID プロンプトでのみ機能します。
| Product ID への入力 | Quantity への入力 | 結果 |
|---|---|---|
101 |
2 |
Added line 1 |
102 |
1 |
Added line 2 |
101 |
1 |
Added line 3、以前の 101 の購入も保持 |
done |
数量の入力なし | レシートを確定してシェルに戻る |
プロンプトと Added line メッセージが引き続き見えるのは、ドライバーが別のストリームである標準エラー出力に書き込むためです。完全なレシートだけが標準出力に送られ、> によって receipt.txt に保存されます。シェルプロンプトが戻ったら、実際に保存されたレシートを読みます。
cat receipt.txt
期待される内容は次のとおりです。
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 2 | 1.25 | 2.50
102 | Fruit cup | 1 | 2.30 | 2.30
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 6.05
Oat bar の二回の購入は別々に残っています。Fruit cup も含めて小計を足すと、250 + 230 + 125 = 605 セントです。合計だけが正しく見えても、これらの行がなければ、お客様の取引明細が失われます。
数値には十進数字を使い、任意の符号、先頭・末尾の ASCII スペースまたはタブを許可します。先頭のゼロも使えます。ID はカタログ内に存在し、数量は 1..20、各行は Enter の前まで最大 63 バイトでなければなりません。ID または数量の拒否は、処理中の候補だけを破棄し、承認済み購入を変えずに Product ID に戻ります。提供されたリーダーは長すぎる行を消費し、残りを後続の回答に分けません。承認できる購入は最大六件です。空のかごで done を入力すると、合計 0.00 の有効な空レシートになります。
確定せずにキャンセルするには、空の入力プロンプトで Ctrl+D を押します。この EOF(ファイル終端)信号、または未完了の行の途中での EOF は、ステータス 2 でキャンセルし、完全なレシートを出力しません。未完了の取引はエクスポートも保持もしません。リダイレクトはすでに出力ファイルを開いているため、意図的にキャンセルを試す場合は別の名前を使ってください。数量としての done は無効な数量テキストであり、確定ではありません。
レシートに最後の購入しかない場合は、basket[line_count] に保存し、承認のたびにカウンターを増やしているか確認してください。保存と再コンパイルを行い、同じ必要購入を入力する新しい取引を開始します。実行ごとに空のかごから始まり、古いレシートのテキストは再読込しません。till.c、実行ファイル till、提供ファイル、完全な receipt.txt を保持します。コースのチェックでは、一時ディレクトリでも未変更のコードのコピーを、実行時の境界検出付きで実行します。与えた入力で起きる誤りは検出できますが、完全なメモリー安全性の証明ではなく、ファイルの修復も行いません。
まとめ
商品一個のサンプルを、複数行の完全なレシートへと発展させました。構造体、購入を個別に持つ配列、安定した商品 ID、整数セントでの計算を使い、実際の結果を保存して読み取りました。次の実験では、購入をその配列に追加する前の入力検証に取り組みます。


