Pada Bagian 1 kita mencatat transaksi tetapi tidak tahu berapa banyak yang seharusnya dibelanjakan per kategori. Bagian 2 menambahkan anggaran bulanan per kategori, lalu membandingkannya dengan realisasi. Dengan anggaran, pengguna dapat melihat kategori mana yang melampaui batas dan menyesuaikan kebiasaan belanja.
Konsep Anggaran #
Anggaran adalah pasangan (bulan, kategori) dengan batas nominal. Contoh: anggaran makan bulan 2026-01 adalah 1.000.000. Setiap transaksi pengeluaran dengan kategori makan di bulan tersebut akan mengurangi sisa anggaran. Jika total pengeluaran melebihi batas, status menjadi LEBIH.
Implementasi Buku Keuangan dengan Anggaran #
Kode berikut memperluas BukuKeuangan dengan anggaran dan laporan perbandingan:
// Bagian 2: Kategori transaksi dan anggaran bulanan.
// Mendemonstrasikan pengelompokan transaksi berdasarkan kategori dan
// perbandingan realisasi vs anggaran.
enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }
data class Transaksi(
val id: String,
val jenis: JenisTransaksi,
val jumlah: Long,
val kategori: String,
val bulan: String
)
data class Anggaran(val bulan: String, val kategori: String, val batas: Long)
class BukuKeuangan {
private val transaksi = mutableListOf<Transaksi>()
private val anggaran = mutableMapOf<Pair<String, String>, Long>()
fun tambahTransaksi(t: Transaksi) { transaksi.add(t) }
fun tetapkanAnggaran(bulan: String, kategori: String, batas: Long) {
require(batas > 0) { "Batas anggaran harus positif" }
anggaran[bulan to kategori] = batas
}
fun realisasiPerKategori(bulan: String): Map<String, Long> {
return transaksi
.filter { it.bulan == bulan && it.jenis == JenisTransaksi.PENGELUARAN }
.groupBy { it.kategori }
.mapValues { (_, list) -> list.sumOf { it.jumlah } }
}
fun laporanAnggaran(bulan: String): List<Triple<String, Long, Long>> {
val realisasi = realisasiPerKategori(bulan)
val kategoriTerpakai = (anggaran.keys.map { it.second }.toSet() + realisasi.keys).sorted()
return kategoriTerpakai.map { k ->
Triple(k, anggaran[bulan to k] ?: 0L, realisasi[k] ?: 0L)
}
}
}
fun formatRupiah(nilai: Long): String {
val tanda = if (nilai < 0) "-" else ""
val str = kotlin.math.abs(nilai).toString()
return "${tanda}Rp" + str.reversed().chunked(3).joinToString(".").reversed()
}
fun main() {
val buku = BukuKeuangan()
val bulan = "2026-01"
buku.tambahTransaksi(Transaksi("T001", JenisTransaksi.PEMASUKAN, 5_000_000, "gaji", bulan))
buku.tambahTransaksi(Transaksi("T002", JenisTransaksi.PENGELUARAN, 750_000, "makan", bulan))
buku.tambahTransaksi(Transaksi("T003", JenisTransaksi.PENGELUARAN, 1_200_000, "transport", bulan))
buku.tambahTransaksi(Transaksi("T004", JenisTransaksi.PENGELUARAN, 500_000, "makan", bulan))
buku.tambahTransaksi(Transaksi("T005", JenisTransaksi.PENGELUARAN, 600_000, "hiburan", bulan))
buku.tetapkanAnggaran(bulan, "makan", 1_000_000)
buku.tetapkanAnggaran(bulan, "transport", 1_000_000)
buku.tetapkanAnggaran(bulan, "hiburan", 400_000)
println("=== Laporan Anggaran Bulan $bulan ===")
println("%-12s %-15s %-15s %s".format("Kategori", "Anggaran", "Realisasi", "Sisa"))
buku.laporanAnggaran(bulan).forEach { (k, anggaran, realisasi) ->
val sisa = anggaran - realisasi
val tanda = if (sisa >= 0) "OK" else "LEBIH"
println("%-12s %-15s %-15s %-10s %s".format(
k, formatRupiah(anggaran), formatRupiah(realisasi), formatRupiah(sisa), tanda
))
}
println()
println("=== Kategori tanpa anggaran ===")
buku.realisasiPerKategori(bulan).forEach { (k, v) ->
if (buku.laporanAnggaran(bulan).find { it.first == k }?.second == 0L) {
println("$k: ${formatRupiah(v)} (tanpa anggaran)")
}
}
}Output yang Dihasilkan:
=== Laporan Anggaran Bulan 2026-01 ===
Kategori Anggaran Realisasi Sisa
hiburan Rp400.000 Rp600.000 -Rp200.000 LEBIH
makan Rp1.000.000 Rp1.250.000 -Rp250.000 LEBIH
transport Rp1.000.000 Rp1.200.000 -Rp200.000 LEBIH
=== Kategori tanpa anggaran ===Mari kita bedah komponen kunci:
mutableMapOf<Pair<String, String>, Long>(): key berupaPairbulan dan kategori. Map memungkinkan pencarian cepat berdasarkan kombinasi tersebut.bulan to kategori: bentuk ringkasPair(bulan, kategori)menggunakan infix functionto. Lebih mudah dibaca daripadaPair(bulan, kategori).groupBy { it.kategori }.mapValues { ... }: dua langkah agregasi.groupBymenghasilkanMap<String, List<Transaksi>>,mapValuesmenjumlahkan setiap list.(anggaran.keys.map { it.second }.toSet() + realisasi.keys).sorted(): union kategori dari anggaran dan realisasi, agar laporan menampilkan keduanya meski salah satu tidak punya pasangan.anggaran[bulan to k] ?: 0L: operator Elvis memberi nol jika tidak ada anggaran untuk kategori tersebut."%-12s %-15s".format(...): format string dengan lebar kolom tetap, menghasilkan tabel rapi.
Mengapa Map<Pair, Long> #
Penyimpanan anggaran sebagai Map<Pair<String, String>, Long> membuat lookup cepat: anggaran["2026-01" to "makan"] langsung memberi nilai. Alternatif Map<String, Map<String, Long>> (bulan -> kategori -> batas) lebih nested tetapi memerlukan dua langkah lookup. Pilihan tergantung preferensi; untuk belajar, Pair lebih sederhana.
Laporan Anggaran vs Realisasi #
Laporan menampilkan tiga kolom utama: anggaran, realisasi, dan sisa. Status OK atau LEBIH ditambahkan agar pengguna langsung melihat masalah. Untuk anggaran tanpa realisasi, sisa sama dengan anggaran (tidak ada pengeluaran). Untuk realisasi tanpa anggaran, kolom anggaran kosong dan laporan menandai “tanpa anggaran”.
Referensi Lanjutan #
- Kotlin Collections overview
— pengantar koleksi Kotlin untuk
groupBydansumOfyang dipakai pada anggaran per kategori
Ringkasan Bagian 2 #
Anggaran bulanan per kategori kini berfungsi: tetapkanAnggaran() menyimpan batas, realisasiPerKategori() menghitung pengeluaran aktual, dan laporanAnggaran() menampilkan perbandingan. Status OK/LEBIH memberi sinyal cepat. Pada Bagian 3 kita menambah analisis persentase untuk melihat proporsi pengeluaran per kategori.