Transaksi yang dicatat harus bertahan antar sesi. Pada bagian ini, kita bangun modul storage.c yang menyimpan dan memuat transaksi ke file CSV. Formatnya adalah perluasan dari proyek 7: setiap baris punya enam field (id, type, amount, description, category, date) yang dipisahkan koma.
Konsep #
Format CSV untuk transaksi:
# id,type,amount,description,category,date
1,1,5000000.00,Gaji bulanan,Gaji,2024-03-01
2,-1,50000.00,Bensin,Transport,2024-03-02Kolom type bernilai 1 (income) atau -1 (expense) sesuai nilai enum TxnType. Field amount selalu positif karena tanda sudah ditentukan oleh type. Field description dan category diasumsikan tidak mengandung koma.
Mekanisme kerja: storage_save iterasi transaksi dan tulis dengan fprintf. storage_load baca baris per baris dengan fgets, parse dengan sscanf format 6 field, lalu validasi dengan transaction_create. Bila validasi gagal, baris dilewati (skip tanpa crash). Setelah load, next_id di-set ke max_id_seen + 1 agar id baru tidak bertabrakan.
Implementasi #
Fungsi parse_line dengan 6 field:
static int parse_line(const char *line, int *id, int *type,
double *amount, char *desc, size_t desc_cap,
char *cat, size_t cat_cap, char *date, size_t date_cap) {
char tmp_desc[TXN_DESC_MAX];
char tmp_cat[TXN_CATEGORY_MAX];
char tmp_date[16];
int consumed = 0;
int items = sscanf(line, "%d,%d,%lf,%63[^,],%31[^,],%10s%n",
id, type, amount, tmp_desc, tmp_cat, tmp_date,
&consumed);
if (items != 6) {
return 0;
}
char trailing = line[consumed];
if (trailing != '\0' && trailing != '\n' && trailing != '\r') {
return 0;
}
if (desc_cap < sizeof(tmp_desc) || cat_cap < sizeof(tmp_cat) ||
date_cap < sizeof(tmp_date)) {
return 0;
}
strcpy(desc, tmp_desc);
strcpy(cat, tmp_cat);
strcpy(date, tmp_date);
return 1;
}Fungsi storage_save:
int storage_save(const Finance *f, const char *path) {
if (f == NULL || path == NULL) {
return -1;
}
FILE *fp = fopen(path, "w");
if (fp == NULL) {
return -1;
}
fprintf(fp, "# id,type,amount,description,category,date\n");
int written = 0;
for (int i = 0; i < f->count; i++) {
const Transaction *t = &f->items[i];
int n = fprintf(fp, "%d,%d,%.2f,%s,%s,%s\n",
t->id, (int)t->type, t->amount,
t->description, t->category, t->date);
if (n < 0) {
fclose(fp);
return -1;
}
written++;
}
fclose(fp);
return written;
}Fungsi storage_load:
int storage_load(Finance *f, const char *path) {
if (f == NULL || path == NULL) {
return -1;
}
FILE *fp = fopen(path, "r");
if (fp == NULL) {
return -1;
}
finance_init(f);
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, type;
double amount;
char desc[TXN_DESC_MAX];
char cat[TXN_CATEGORY_MAX];
char date[16];
if (!parse_line(line, &id, &type, &amount, desc, sizeof(desc),
cat, sizeof(cat), date, sizeof(date))) {
continue;
}
if (f->count >= FINANCE_CAPACITY) {
break;
}
if (type != TXN_INCOME && type != TXN_EXPENSE) {
continue;
}
Transaction *slot = &f->items[f->count];
if (transaction_create(slot, id, (TxnType)type, amount,
desc, cat, date) != 0) {
continue;
}
if (id > max_id_seen) {
max_id_seen = id;
}
f->count++;
read++;
}
f->next_id = max_id_seen + 1;
fclose(fp);
return read;
}Verifikasi roundtrip:
/* demo_storage_finance.c - Test save/load finance */
#include <stdio.h>
#include "finance.h"
#include "storage.h"
int main(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_INCOME, 5000000.0, "Gaji bulanan",
"Gaji", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin",
"Transport", "2024-03-02");
finance_add(&f, TXN_EXPENSE, 30000.0, "Makan siang",
"Makan", "2024-03-02");
const char *path = "/tmp/finance_demo.csv";
int n = storage_save(&f, path);
printf("Menyimpan %d transaksi\n", n);
/* 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 Finance baru. */
Finance loaded;
int m = storage_load(&loaded, path);
printf("\nMemuat %d transaksi kembali\n", m);
printf("next_id setelah load: %d\n", loaded.next_id);
finance_print_all(&loaded);
return 0;
}Output yang Dihasilkan:
Menyimpan 3 transaksi
--- Isi file ---
# id,type,amount,description,category,date
1,1,5000000.00,Gaji bulanan,Gaji,2024-03-01
2,-1,50000.00,Bensin,Transport,2024-03-02
3,-1,30000.00,Makan siang,Makan,2024-03-02
Memuat 3 transaksi kembali
next_id setelah load: 4
Total transaksi: 3/512
[0001] 2024-03-01 MASUK Gaji Rp 5000000.00 Gaji bulanan
[0002] 2024-03-02 KELUAR Transport Rp -50000.00 Bensin
[0003] 2024-03-02 KELUAR Makan Rp -30000.00 Makan siangMari kita telaah baris-baris kode di atas:
- Baris 9 (
sscanfformat string):"%d,%d,%lf,%63[^,],%31[^,],%10s%n"membaca 6 field berturut-turut.%63[^,]membaca string hingga 63 karakter atau koma berikutnya.%10smembaca tanggal dengan maksimal 10 karakter.%nmenyimpan jumlah karakter yang sudah diproses untuk trailing check. - Baris 13-15 (trailing check): Setelah parsing, karakter pada posisi
consumedharus newline, NULL, atau CR. Bila ada karakter lain (mis. koma ekstra), baris dianggap rusak. - Baris 17-21 (size guard): Pastikan buffer pemanggil cukup besar. Bila tidak, return 0 (parse gagal). Pattern defensif mencegah buffer overflow.
- Baris 47 (
fprintfdengan(int)t->type): Cast enum ke int eksplisit. Standard C tidak menjamin ukuran enum, jadi cast ke int memastikan format specifier%dbenar. - Baris 75-77 (skip baris rusak): Bila
parse_linegagal,continueke iterasi berikutnya. Program tetap jalan meski ada barik rusak di file. - Baris 84-86 (
max_id_seentracking): Lacak id terbesar yang dilihat, lalu setnext_id = max + 1. Bila file berisi id 1,2,5,8 (ada gap),next_id= 9. Id baru tidak akan bertabrakan dengan id yang sudah ada. - Baris 80-82 (skip type invalid): Bila
typebukan 1 atau -1, lewati baris. Pola defensive mencegah enum invalid masuk ke struktur data.
Kode Lengkap #
Kode lengkap untuk storage.h dan storage.c ada di direktori proyek. Fungsi strip_newline dan parse_line dibuat static.
Pengujian #
Edge case yang harus dipastikan:
- File dengan baris komentar
#di tengah → dilewati. - File dengan baris kosong → dilewati.
- File dengan baris rusak (kurang kolom) → dilewati, transaksi lain tetap dimuat.
- File dengan id tidak berurutan (1, 5, 8) →
next_id= 9. - File dengan type invalid (mis.
2) → baris tersebut dilewati. - File tidak ada →
storage_loadmengembalikan -1 (program tetap jalan dengan Finance kosong).
gcc -std=c11 -Wall -Wextra -o /tmp/demo_storage_finance \
demo_storage_finance.c storage.c finance.c transaction.c -lm
/tmp/demo_storage_finance