Proyek pengelola keuangan dimulai dari pertanyaan: bagaimana satu transaksi keuangan direpresentasikan dalam program? Setiap transaksi punya jenis (pemasukan atau pengeluaran), jumlah uang, deskripsi, kategori, dan tanggal. Pada bagian ini, kita rancang struct Transaction dengan field-field tersebut dan enum TxnType untuk menandai arah aliran uang.
Konsep #
Transaksi keuangan memiliki dua arah: uang masuk (income) dan uang keluar (expense). Kita pakai enum TxnType dengan nilai +1 untuk income dan -1 untuk expense agar mudah menghitung signed value:
signed_value = type * amount
= (+1) * amount (untuk income, menghasilkan positif)
= (-1) * amount (untuk expense, menghasilkan negatif)Dengan pola ini, saldo bisa dihitung dengan menjumlahkan signed value semua transaksi tanpa perlu if-else di setiap perhitungan.
Mekanisme kerja validator tanggal: kita periksa panjang string (10 karakter), format posisi tanda hubung di indeks 4 dan 7, dan pastikan semua karakter lain adalah digit. Setelah format valid, kita parse dengan sscanf ke (y, m, d) dan periksa rentang: tahun 1900-2999, bulan 1-12, hari 1-31. Validator ini sederhana tetapi cukup untuk demo pembelajaran.
Implementasi #
Header transaction.h:
/* transaction.h - Model data transaksi keuangan */
#ifndef TRANSACTION_H
#define TRANSACTION_H
#define TXN_DESC_MAX 64
#define TXN_CATEGORY_MAX 32
typedef enum {
TXN_INCOME = 1,
TXN_EXPENSE = -1
} TxnType;
typedef struct {
int id;
TxnType type;
double amount;
char description[TXN_DESC_MAX];
char category[TXN_CATEGORY_MAX];
char date[11]; /* YYYY-MM-DD + null */
} Transaction;
int transaction_create(Transaction *t, int id, TxnType type,
double amount, const char *description,
const char *category, const char *date);
double transaction_signed_value(const Transaction *t);
void transaction_print(const Transaction *t);
int transaction_date_is_valid(const char *date);
#endif /* TRANSACTION_H */Implementasi validator tanggal dan create:
/* transaction.c - Implementasi model Transaction */
#include "transaction.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
int transaction_date_is_valid(const char *date) {
if (date == NULL) {
return 0;
}
if (strlen(date) != 10) {
return 0;
}
for (int i = 0; i < 10; i++) {
char c = date[i];
if (i == 4 || i == 7) {
if (c != '-') {
return 0;
}
} else {
if (!isdigit((unsigned char)c)) {
return 0;
}
}
}
int y, m, d;
if (sscanf(date, "%d-%d-%d", &y, &m, &d) != 3) {
return 0;
}
if (y < 1900 || y > 2999) {
return 0;
}
if (m < 1 || m > 12) {
return 0;
}
if (d < 1 || d > 31) {
return 0;
}
return 1;
}
int transaction_create(Transaction *t, int id, TxnType type,
double amount, const char *description,
const char *category, const char *date) {
if (t == NULL || description == NULL ||
category == NULL || date == NULL) {
return -1;
}
if (id <= 0) {
return -2;
}
if (type != TXN_INCOME && type != TXN_EXPENSE) {
return -3;
}
if (amount <= 0.0 || isnan(amount) || isinf(amount)) {
return -4;
}
if (!transaction_date_is_valid(date)) {
return -5;
}
t->id = id;
t->type = type;
t->amount = amount;
strncpy(t->description, description, TXN_DESC_MAX - 1);
t->description[TXN_DESC_MAX - 1] = '\0';
strncpy(t->category, category, TXN_CATEGORY_MAX - 1);
t->category[TXN_CATEGORY_MAX - 1] = '\0';
strncpy(t->date, date, sizeof(t->date) - 1);
t->date[sizeof(t->date) - 1] = '\0';
return 0;
}
double transaction_signed_value(const Transaction *t) {
if (t == NULL) {
return 0.0;
}
return t->type * t->amount;
}
void transaction_print(const Transaction *t) {
if (t == NULL) {
printf("(null transaction)\n");
return;
}
const char *label = (t->type == TXN_INCOME) ? "MASUK " : "KELUAR";
printf("[%04d] %s %s %s Rp%12.2f %s\n",
t->id, t->date, label, t->category,
t->type * t->amount, t->description);
}Verifikasi model:
/* demo_txn.c - Uji coba model Transaction */
#include <stdio.h>
#include "transaction.h"
int main(void) {
Transaction t1;
int rc = transaction_create(&t1, 1, TXN_INCOME, 5000000.0,
"Gaji bulanan", "Gaji", "2024-03-01");
printf("rc=%d signed=%.2f\n", rc, transaction_signed_value(&t1));
transaction_print(&t1);
Transaction t2;
rc = transaction_create(&t2, 2, TXN_EXPENSE, 50000.0,
"Bensin", "Transport", "2024-03-02");
printf("rc=%d signed=%.2f\n", rc, transaction_signed_value(&t2));
transaction_print(&t2);
/* Cek tanggal invalid */
printf("cek '2024/03/01': %d\n", transaction_date_is_valid("2024/03/01"));
printf("cek '2024-13-01': %d\n", transaction_date_is_valid("2024-13-01"));
printf("cek '2024-03-32': %d\n", transaction_date_is_valid("2024-03-32"));
printf("cek '2024-03-15': %d\n", transaction_date_is_valid("2024-03-15"));
return 0;
}Output yang Dihasilkan:
rc=0 signed=5000000.00
[0001] 2024-03-01 MASUK Gaji Rp 5000000.00 Gaji bulanan
rc=0 signed=-50000.00
[0002] 2024-03-02 KELUAR Transport Rp -50000.00 Bensin
cek '2024/03/01': 0
cek '2024-13-01': 0
cek '2024-03-32': 0
cek '2024-03-15': 1Mari kita telaah baris-baris kode di atas:
- Baris 7-10 (
TxnTypeenum): Nilai enum+1/-1bukan kebetulan.signed_value = type * amountmenghasilkan positif untuk income dan negatif untuk expense tanpa if-else. Pola ini mempermudah akumulasi saldo. - Baris 13 (
char date[11]): Fielddateberukuran 11 untuk menampungYYYY-MM-DD(10 karakter) + null terminator. Ukuran eksplisit menghindari overflow. - Baris 24-30 (
strlen(date) != 10): Validasi panjang paling awal. Bila tanggal salah panjang, kita bisa langsung return tanpa iterasi karakter. - Baris 31-41 (loop karakter): Iterasi 10 karakter. Posisi 4 dan 7 harus
-, posisi lain harus digit. Pakaiisdigitdari<ctype.h>dengan cast(unsigned char)untuk menghindari undefined behavior pada karakter negatif. - Baris 43-46 (
sscanf): Setelah format karakter valid, parse ke(y, m, d)untuk validasi rentang.sscanfdipakai karena format sudah dipastikan benar. - Baris 80-82 (
transaction_print): Format cetak menampilkan tanda signed value (positif untuk income, negatif untuk expense).Rp%12.2fmenghasilkan lebar 12 karakter dengan 2 desimal, sehingga kolom nominal rapi sejajar.
Kode Lengkap #
Kode lengkap untuk transaction.h dan transaction.c ada di direktori proyek. Kode ini menjadi fondasi untuk semua bagian berikutnya.
Pengujian #
Verifikasi model dengan test edge case:
- Tanggal dengan bulan 13 → ditolak.
- Tanggal dengan hari 32 → ditolak.
- Tanggal dengan format slash
2024/03/15→ ditolak. - Tanggal dengan tahun 1899 → ditolak (di bawah batas minimum).
- Amount negatif → ditolak.
- Amount NaN → ditolak.
gcc -std=c11 -Wall -Wextra -o /tmp/demo_txn demo_txn.c \
transaction.c -lm
/tmp/demo_txn