Inventaris yang hanya hidup di RAM hilang saat program keluar. Untuk aplikasi nyata, data harus bertahan antar sesi. Pada bagian ini, kita bangun modul storage.c yang menyimpan dan memuat inventaris ke file CSV. Format CSV dipilih karena mudah dibaca manusia, bisa diedit di teks editor biasa, dan dapat diimpor ke aplikasi spreadsheet.

Konsep #

Format file CSV (Comma-Separated Values) adalah teks yang setiap baris berisi satu record dengan field dipisahkan koma. Untuk model Product kita tetapkan format:

csv
# id,name,price,stock
1,Beras 5kg,65000.00,10
2,Gula Pasir,15000.00,5

Baris yang diawali # diperlakukan sebagai komentar dan diabaikan saat parsing. Hal ini memungkinkan kita menyimpan header dokumentasi yang tidak memengaruhi data.

Mekanisme kerja: storage_save membuka file dengan mode "w" (truncate + write), lalu iterasi setiap produk dan menulis dengan fprintf. storage_load membuka file dengan mode "r", membaca baris per baris dengan fgets, lalu memparse setiap baris dengan sscanf menggunakan format string khusus.

Implementasi #

Fungsi storage_save:

c
int storage_save(const Inventory *inv, const char *path) {
    if (inv == NULL || path == NULL) {
        return -1;
    }
    FILE *fp = fopen(path, "w");
    if (fp == NULL) {
        return -1;
    }
    fprintf(fp, "# id,name,price,stock\n");
    int written = 0;
    for (int i = 0; i < inv->count; i++) {
        const Product *p = &inv->items[i];
        int n = fprintf(fp, "%d,%s,%.2f,%d\n",
                        p->id, p->name, p->price, p->stock);
        if (n < 0) {
            fclose(fp);
            return -1;
        }
        written++;
    }
    fclose(fp);
    return written;
}

Fungsi parse_line dan storage_load:

c
static int parse_line(const char *line, int *id, char *name,
                      size_t name_cap, double *price, int *stock) {
    int consumed = 0;
    int items = sscanf(line, "%d,%63[^,],%lf,%d%n",
                       id, name, price, stock, &consumed);
    if (items != 4) {
        return 0;
    }
    char trailing = line[consumed];
    if (trailing != '\0' && trailing != '\n' && trailing != '\r') {
        return 0;
    }
    if (name_cap < PRODUCT_NAME_MAX) {
        return 0;
    }
    return 1;
}

int storage_load(Inventory *inv, const char *path) {
    if (inv == NULL || path == NULL) {
        return -1;
    }
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return -1;
    }
    inventory_init(inv);
    char line[256];
    int read = 0;
    int max_id_seen = 0;
    while (fgets(line, sizeof(line), fp) != NULL) {
        if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') {
            continue;
        }
        strip_newline(line);
        int id;
        char name[PRODUCT_NAME_MAX];
        double price;
        int stock;
        if (!parse_line(line, &id, name, sizeof(name), &price, &stock)) {
            continue;
        }
        if (inv->count >= INVENTORY_CAPACITY) {
            break;
        }
        Product *slot = &inv->items[inv->count];
        if (product_create(slot, id, name, price, stock) != 0) {
            continue;
        }
        if (id > max_id_seen) {
            max_id_seen = id;
        }
        inv->count++;
        read++;
    }
    inv->next_id = max_id_seen + 1;
    fclose(fp);
    return read;
}

Verifikasi persistensi dengan roundtrip save-load:

c
/* demo_storage.c - Uji coba save/load */
#include <stdio.h>
#include "inventory.h"
#include "storage.h"

int main(void) {
    Inventory inv;
    inventory_init(&inv);
    inventory_add(&inv, "Beras 5kg", 65000.0, 10);
    inventory_add(&inv, "Gula Pasir", 15000.0, 5);
    inventory_add(&inv, "Minyak Goreng", 18000.0, 7);

    const char *path = "/tmp/inventory_demo.csv";
    int n = storage_save(&inv, path);
    printf("Menyimpan %d produk ke %s\n", n, path);

    /* Tampilkan isi file. */
    FILE *fp = fopen(path, "r");
    char buf[256];
    printf("--- Isi file ---\n");
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("%s", buf);
    }
    fclose(fp);

    /* Load ke inventory baru. */
    Inventory loaded;
    int m = storage_load(&loaded, path);
    printf("\nMemuat %d produk kembali\n", m);
    printf("next_id setelah load: %d\n", loaded.next_id);
    inventory_print_all(&loaded);
    return 0;
}

Output yang Dihasilkan:

text
Menyimpan 3 produk ke /tmp/inventory_demo.csv
--- Isi file ---
# id,name,price,stock
1,Beras 5kg,65000.00,10
2,Gula Pasir,15000.00,5
3,Minyak Goreng,18000.00,7

Memuat 3 produk kembali
next_id setelah load: 4
Total produk: 3/256
[000001] Beras 5kg                        Rp  65000.00  stok=  10
[000002] Gula Pasir                       Rp  15000.00  stok=   5
[000003] Minyak Goreng                    Rp  18000.00  stok=   7

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (fopen(path, "w")): Mode "w" selalu membuat file baru. Bila file sudah ada, isinya dihapus dulu. Untuk append (mis. menulis log), gunakan mode "a".
  2. Baris 12 (fprintf dengan %.2f): Format harga selalu dua desimal. Ini menjaga konsistensi: 100.0 dan 100.00 keduanya ditulis sebagai 100.00.
  3. Baris 23-28 (parse_line): Format string "%d,%63[^,],%lf,%d%n" berarti: baca int, koma, lalu string hingga 63 karakter atau koma berikutnya ( menggunakan [方^,] ), koma, double, koma, int. %n menyimpan jumlah karakter yang sudah diproses.
  4. Baris 28-31 (trailing check): Setelah %n, kita pastikan karakter setelah field terakhir hanya whitespace. Bila ada karakter asing (mis. koma ekstra), parsing dianggap gagal.
  5. Baris 51 (strip_newline): Sebelum parse, hapus \n dan \r dari akhir baris. Tanpa langkah ini, sscanf bisa gagal karena karakter newline ikut tersisa.
  6. Baris 73-76 (max_id_seen tracking): Kita lacak id terbesar yang dilihat saat load, lalu set next_id = max + 1. Tanpa ini, id produk baru bisa bertabrakan dengan id yang sudah dimuat dari file.

Kode Lengkap #

Kode lengkap untuk storage.h dan storage.c ada di direktori proyek. Fungsi strip_newline dibuat static karena hanya dipakai internal.

Pengujian #

Test roundtrip save-load adalah test paling penting untuk persistensi. Pastikan:

  • File yang ditulis bisa dibaca kembali dengan jumlah produk yang sama.
  • Field next_id benar setelah load (mis. bila file berisi id 1,2,5,8, maka next_id = 9).
  • File dengan baris rusak dilewati tanpa crash.
  • File yang tidak ada menyebabkan storage_load mengembalikan -1 (program tetap jalan dengan inventory kosong).
bash
gcc -std=c11 -Wall -Wextra -o /tmp/demo_storage demo_storage.c \
    storage.c inventory.c product.c -lm
/tmp/demo_storage