Data transaksi sudah bisa disimpan dan dimuat. Sekarang saatnya mengubah data mentah menjadi insight: laporan per kategori yang menunjukkan kategori mana yang paling banyak pengeluarannya, dan laporan bulanan yang menunjukkan tren pemasukan/pengeluaran per bulan. Pada bagian ini, kita bangun modul report.c dengan dua fungsi utama.
Konsep #
Laporan per kategori mengelompokkan transaksi berdasarkan field category (case-insensitive). Untuk setiap kategori unik, kita hitung total pemasukan, total pengeluaran, dan saldo. Hasil disimpan ke array CategoryReport.
Laporan bulanan mengelompokkan transaksi berdasarkan 7 karakter pertama tanggal (YYYY-MM). Karena data sudah terurut kronologis (atau bisa diurutkan dulu), kita iterasi dan cetak subtotal tiap kali bulan berganti.
Mekanisme kerja: untuk laporan per kategori, kita iterasi transaksi dan untuk setiap transaksi cari apakah kategori-nya sudah ada di array out. Bila ada, tambah ke kategori yang sudah ada. Bila belum, buat entri baru. Pola ini mirip dengan dictionary/map di bahasa lain, diimplementasikan secara manual di C.
Implementasi #
Struktur CategoryReport dan prototype:
/* report.h - Laporan keuangan */
#ifndef REPORT_H
#define REPORT_H
#include "finance.h"
typedef struct {
char category[TXN_CATEGORY_MAX];
double income;
double expense;
double balance;
int count;
} CategoryReport;
int report_by_category(const Finance *f, CategoryReport *out, int out_cap);
void report_print_category(const CategoryReport *r, int count);
void report_print_monthly(const Finance *f);
#endif /* REPORT_H */Implementasi report_by_category:
static void cat_report_init(CategoryReport *r, const char *category) {
memset(r, 0, sizeof(*r));
strncpy(r->category, category, TXN_CATEGORY_MAX - 1);
r->category[TXN_CATEGORY_MAX - 1] = '\0';
}
int report_by_category(const Finance *f, CategoryReport *out, int out_cap) {
if (f == NULL || out == NULL || out_cap <= 0) {
return 0;
}
int unique = 0;
for (int i = 0; i < f->count; i++) {
const Transaction *t = &f->items[i];
int found = -1;
for (int k = 0; k < unique; k++) {
if (eq_ci(out[k].category, t->category)) {
found = k;
break;
}
}
if (found < 0) {
if (unique >= out_cap) {
continue;
}
cat_report_init(&out[unique], t->category);
found = unique;
unique++;
}
if (t->type == TXN_INCOME) {
out[found].income += t->amount;
} else {
out[found].expense += t->amount;
}
out[found].balance += transaction_signed_value(t);
out[found].count++;
}
return unique;
}Fungsi cetak laporan per kategori:
void report_print_category(const CategoryReport *r, int count) {
if (r == NULL || count <= 0) {
printf("Tidak ada data untuk laporan.\n");
return;
}
printf("\n=== Laporan per Kategori ===\n");
printf("%-20s %12s %12s %12s %6s\n",
"Kategori", "Pemasukan", "Pengeluaran", "Saldo", "#Txn");
double total_inc = 0, total_exp = 0;
for (int i = 0; i < count; i++) {
printf("%-20s %12.2f %12.2f %12.2f %6d\n",
r[i].category, r[i].income, r[i].expense,
r[i].balance, r[i].count);
total_inc += r[i].income;
total_exp += r[i].expense;
}
printf("%-20s %12.2f %12.2f %12.2f\n",
"TOTAL", total_inc, total_exp, total_inc - total_exp);
}Fungsi laporan bulanan:
void report_print_monthly(const Finance *f) {
if (f == NULL || f->count == 0) {
printf("Tidak ada data untuk laporan bulanan.\n");
return;
}
printf("\n=== Laporan Bulanan ===\n");
printf("%-8s %12s %12s %12s\n",
"Bulan", "Pemasukan", "Pengeluaran", "Saldo");
char current[8] = "";
double m_inc = 0, m_exp = 0;
for (int i = 0; i < f->count; i++) {
const Transaction *t = &f->items[i];
char month[8];
snprintf(month, sizeof(month), "%.7s", t->date);
if (strcmp(month, current) != 0) {
if (current[0] != '\0') {
printf("%-8s %12.2f %12.2f %12.2f\n",
current, m_inc, m_exp, m_inc - m_exp);
}
strcpy(current, month);
m_inc = 0;
m_exp = 0;
}
if (t->type == TXN_INCOME) {
m_inc += t->amount;
} else {
m_exp += t->amount;
}
}
if (current[0] != '\0') {
printf("%-8s %12.2f %12.2f %12.2f\n",
current, m_inc, m_exp, m_inc - m_exp);
}
}Verifikasi laporan:
/* demo_report.c - Uji coba laporan */
#include <stdio.h>
#include "finance.h"
#include "report.h"
int main(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_INCOME, 5000000.0, "Gaji", "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");
finance_add(&f, TXN_EXPENSE, 20000.0, "Cafe", "makan", "2024-03-03");
finance_add(&f, TXN_INCOME, 1000000.0, "Bonus", "Gaji", "2024-03-15");
finance_add(&f, TXN_EXPENSE, 75000.0, "Bensin", "Transport", "2024-04-05");
finance_add(&f, TXN_EXPENSE, 40000.0, "Makan", "Makan", "2024-04-05");
/* Laporan per kategori. */
CategoryReport reports[16];
int n = report_by_category(&f, reports, 16);
report_print_category(reports, n);
/* Laporan bulanan. */
report_print_monthly(&f);
return 0;
}Output yang Dihasilkan:
=== Laporan per Kategori ===
Kategori Pemasukan Pengeluaran Saldo #Txn
Gaji 6000000.00 0.00 6000000.00 2
Transport 0.00 125000.00 -125000.00 2
Makan 0.00 90000.00 -90000.00 3
TOTAL 6000000.00 215000.00 5785000.00
=== Laporan Bulanan ===
Bulan Pemasukan Pengeluaran Saldo
2024-03 6000000.00 100000.00 5900000.00
2024-04 0.00 115000.00 -115000.00Mari kita telaah baris-baris kode di atas:
- Baris 1-7 (
cat_report_init): Helper inisialisasi entry baru. Pakaimemset(r, 0, sizeof(*r))agar semua field numerik mulai dari 0.strncpydengan terminator eksplisit memastikan string aman. - Baris 9-33 (
report_by_category): Linear search untuk kategori yang sudah ada diout. Bila tidak ada, buat entri baru di posisiunique. Variabelfoundberfungsi sebagai indeks ke kategori yang akan diakumulasi. - Baris 25-28 (out_cap check): Bila kategori baru tidak muat di
out,continueke transaksi berikutnya. Kategori tersebut tidak dimasukkan ke laporan. Pemanggil tahu bahwa laporan mungkin tidak lengkap bilaout_capterlalu kecil. - Baris 30-34 (akumulasi): Tambah amount ke field yang sesuai berdasarkan
type.transaction_signed_valuedipakai untukbalanceagar otomatis positif untuk income, negatif untuk expense. - Baris 51-59 (subtotal per kategori): Format
%-20s(left-aligned, lebar 20) untuk nama kategori.%12.2f(right-aligned, lebar 12, 2 desimal) untuk angka uang. Baris terakhir cetak TOTAL di kolom kategori. - Baris 78-80 (extract month):
snprintf(month, sizeof(month), "%.7s", t->date)mengambil 7 karakter pertama tanggal. Hasilnya2024-03dari2024-03-15. Pola ini bekerja karena format tanggal sudah diperbaiki (YYYY-MM-DD). - Baris 82-90 (deteksi pergantian bulan): Bandingkan month saat ini dengan
current. Bila beda, cetak subtotal bulan sebelumnya dan reset counter. Pola “state machine” sederhana. - Baris 100-103 (flush terakhir): Setelah loop selesai, cetak subtotal bulan terakhir yang belum sempat dicetak di dalam loop. Tanpa langkah ini, bulan terakhir akan hilang dari laporan.
Kode Lengkap #
Kode lengkap untuk report.h dan report.c ada di direktori proyek. Fungsi eq_ci dipakai ulang dari finance.c (atau bisa ditaruh di file helper bersama).
Pengujian #
Edge case yang harus dipastikan:
- Finance kosong → cetak “Tidak ada data”.
- Finance dengan satu kategori → laporan hanya punya satu baris + TOTAL.
- Finance dengan kategori yang sama persis dengan kapitalisasi berbeda (“Makan” vs “makan”) → dianggap satu kategori karena
eq_ci. - Laporan bulanan dengan transaksi yang tanggal tidak berurutan → grouping bisa salah. Solusi: sort by date dulu sebelum cetak laporan bulanan.
gcc -std=c11 -Wall -Wextra -o /tmp/demo_report demo_report.c \
report.c finance.c transaction.c -lm
/tmp/demo_report