Aplikasi pemrosesan data membaca berkas transaksi penjualan, memeriksa setiap baris, mengubah data yang sah menjadi bentuk siap analisis, lalu merangkum semuanya menjadi laporan. Kebutuhan seperti ini biasanya tumbuh lebih cepat daripada kemampuan satu thread, sehingga paralelisasi menjadi keharusan — tetapi paralelisasi tanpa rancangan hanya menukar lambat dengan galat yang sulit dilacak. Bagian 1 karena itu sepenuhnya sekuensial: kita merancang lima tahap pipeline, menetapkan kontrak data antar tahap lewat record dan interface sealed, lalu menyimpan keluarannya sebagai baseline yang kelak dipakai membuktikan versi paralel menghasilkan hasil identik.
Lima Tahap dan Kontrak Datanya #
Pipeline kita membagi pekerjaan menjadi tahap bernama: baca memberi nomor urut pada tiap baris berkas, validasi memeriksa format dan aturan bisnis, transformasi membakukan kota serta menghitung nilai berjasa, agregasi merangkum seluruh kejadian menjadi peta terurut, dan tulis menyusun laporan akhir. Setiap tahap menerima satu bentuk data dan mengembalikan bentuk berikutnya; kesepakatan itulah kontrak data. Kontrak yang tegas membuat tiap tahap bisa diuji terpisah dan dipindahkan ke thread mana pun tanpa menyusulkan tahap tetangganya.
| Tahap | Masukan | Keluaran | Aturan utama |
|---|---|---|---|
| baca | List<String> | List<BarisData> | nomor baris unik mulai 1 |
| validasi | BarisData | EventPenjualan atau alasan tolakan | 5 segmen, tanggal valid, kategori dikenal, nilai positif |
| transformasi | EventPenjualan | KejadianTerproses | kota kapital, nilai berjasa 2% |
| agregasi | daftar kejadian | RingkasanAgregasi | peta TreeMap agar terurut |
| tulis | ringkasan + hasil | List<String> | penolakan dicetak terurut |
Kontrak Sebagai Record dan Interface Sealed #
Kontrak paling penting justru tidak terlihat pada tabel: hasil validasi kita bentuk sebagai nilai data, bukan exception. Baris cacat tetap mengalir sebagai TidakSah yang membawa nomor baris dan alasan, sehingga laporan galat tinggal dikumpulkan dari hasil tanpa try/catch bersarang:
/**
* Kontrak hasil pemrosesan satu baris: nilai data, bukan exception.
* Varian terkunci lewat sealed sehingga compiler memaksa setiap switch
* menangani semua kemungkinan.
*/
public sealed interface HasilBaris permits HasilBaris.Sah, HasilBaris.TidakSah {
/** Nomor baris sumber — dipakai pelaporan galat dan pengurutan hasil. */
int nomorBaris();
/** Baris selesai diproses dan membawa kejadian siap agregasi. */
record Sah(KejadianTerproses kejadian) implements HasilBaris {
@Override
public int nomorBaris() {
return kejadian.nomorBaris();
}
}
/** Baris ditolak validator; alasan ditulis untuk laporan galat. */
record TidakSah(int nomorBaris, String alasan) implements HasilBaris {
@Override
public int nomorBaris() {
return nomorBaris;
}
}
}Dua hal membuat HasilBaris layak disebut kontrak keras. Pertama, sealed mengunci daftar varian: hanya Sah dan TidakSah yang boleh mengimplementasikan interface ini, dan compiler menolak switch yang lupa menangani salah satunya. Kedua, kedua varian menyediakan nomorBaris() sehingga siapa pun yang memegang hasil selalu tahu asal barisnya. Kontrak antar validasi dan transformasi memakai record biasa:
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* Kontrak keluaran tahap validasi: event penjualan yang lolos seluruh pemeriksaan.
* Tahap transformasi menerima record ini tanpa memeriksa format lagi.
*/
public record EventPenjualan(int nomorBaris, LocalDate tanggal, String kota,
String pelanggan, String kategori, BigDecimal nilai) {}EventPenjualan hanya berisi data yang sudah lolos pemeriksaan; tahap transformasi yang menerimanya bebas memikirkan format. Pemisahan inilah yang nanti memungkinkan satu tahap diparalelkan tanpa mengubah pihak lain.
Pipeline Sekuensial sebagai Baseline #
Kelas Pipeline merangkai kelima tahap sebagai method static; prosesSatuBaris menuntaskan validasi lalu transformasi untuk satu baris dan mengembalikan HasilBaris:
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** Pipeline sekuensial: baca -> validasi -> transformasi -> agregasi -> tulis. */
public final class Pipeline {
/** Kategori yang dikenal validator; di luar daftar ini baris ditolak. */
private static final Set<String> KATEGORI_DIKENAL =
Set.of("Elektronik", "Pakaian", "Buku", "Peralatan");
/** Faktor biaya layanan 2% yang dihitung tahap transformasi. */
private static final BigDecimal BIAYA_LAYANAN = new BigDecimal("1.02");
private Pipeline() {
// Kelas utilitas: seluruh anggota static.
}
/** Tahap baca: memberi nomor urut pada tiap baris berkas. */
public static List<BarisData> baca(List<String> isiBerkas) {
List<BarisData> baris = new ArrayList<>(isiBerkas.size());
for (int i = 0; i < isiBerkas.size(); i++) {
baris.add(new BarisData(i + 1, isiBerkas.get(i)));
}
return baris;
}
/** Tahap validasi lalu transformasi untuk satu baris. */
public static HasilBaris prosesSatuBaris(BarisData baris) {
String[] segmen = baris.teks().split(";", -1);
if (segmen.length != 5) {
return new HasilBaris.TidakSah(baris.nomor(),
"jumlah segmen " + segmen.length + ", harus 5");
}
LocalDate tanggal;
try {
tanggal = LocalDate.parse(segmen[0]);
} catch (DateTimeParseException e) {
return new HasilBaris.TidakSah(baris.nomor(),
"tanggal tidak terurai: '" + segmen[0] + "'");
}
String kota = segmen[1].trim();
String pelanggan = segmen[2].trim();
String kategori = segmen[3].trim();
if (kota.isEmpty()) {
return new HasilBaris.TidakSah(baris.nomor(), "kota kosong");
}
if (pelanggan.isEmpty()) {
return new HasilBaris.TidakSah(baris.nomor(), "nama pelanggan kosong");
}
if (!KATEGORI_DIKENAL.contains(kategori)) {
return new HasilBaris.TidakSah(baris.nomor(),
"kategori tidak dikenal: '" + kategori + "'");
}
BigDecimal nilai;
try {
nilai = new BigDecimal(segmen[4].trim());
} catch (NumberFormatException e) {
return new HasilBaris.TidakSah(baris.nomor(),
"nilai tidak terurai: '" + segmen[4].trim() + "'");
}
if (nilai.signum() <= 0) {
return new HasilBaris.TidakSah(baris.nomor(),
"nilai harus positif: " + nilai.toPlainString());
}
EventPenjualan event = new EventPenjualan(baris.nomor(), tanggal, kota,
pelanggan, kategori, nilai);
return new HasilBaris.Sah(ubah(event));
}
/** Tahap transformasi: bakukan kota dan hitung nilai berjasa 2%. */
public static KejadianTerproses ubah(EventPenjualan event) {
BigDecimal berjasa = event.nilai().multiply(BIAYA_LAYANAN)
.setScale(2, RoundingMode.HALF_EVEN);
return new KejadianTerproses(event.nomorBaris(), event.tanggal(),
event.kota().toUpperCase(Locale.ROOT), event.pelanggan(),
event.kategori(), event.nilai(), berjasa);
}
/** Tahap agregasi: ringkasan terurut dari seluruh kejadian. */
public static RingkasanAgregasi agregasi(List<KejadianTerproses> kejadian) {
Map<String, BigDecimal> perKota = kejadian.stream()
.collect(Collectors.groupingBy(KejadianTerproses::kota,
TreeMap::new,
Collectors.reducing(BigDecimal.ZERO,
KejadianTerproses::nilaiBerjasa, BigDecimal::add)));
Map<String, Long> perKategori = kejadian.stream()
.collect(Collectors.groupingBy(KejadianTerproses::kategori,
TreeMap::new, Collectors.counting()));
BigDecimal totalNilai = kejadian.stream()
.map(KejadianTerproses::nilai)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalBerjasa = kejadian.stream()
.map(KejadianTerproses::nilaiBerjasa)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new RingkasanAgregasi(kejadian.size(), totalNilai, totalBerjasa,
perKota, perKategori);
}
/** Tahap tulis: susun laporan akhir dari ringkasan dan hasil per baris. */
public static List<String> tulis(RingkasanAgregasi ringkasan, List<HasilBaris> hasil) {
int sah = 0;
for (HasilBaris h : hasil) {
if (h instanceof HasilBaris.Sah) {
sah++;
}
}
List<String> laporan = new ArrayList<>();
laporan.add("=== Laporan pipeline penjualan ===");
laporan.add("baris dibaca : " + hasil.size());
laporan.add("baris sah : " + sah);
laporan.add("baris ditolak : " + (hasil.size() - sah));
laporan.add(String.format(Locale.US, "total nilai : %,14.2f", ringkasan.totalNilai()));
laporan.add(String.format(Locale.US, "total berjasa : %,14.2f", ringkasan.totalBerjasa()));
laporan.add("nilai berjasa per kota:");
ringkasan.nilaiBerjasaPerKota().forEach((kota, nilai) ->
laporan.add(String.format(Locale.US, " %-12s %,14.2f", kota, nilai)));
laporan.add("jumlah transaksi per kategori:");
ringkasan.jumlahPerKategori().forEach((kategori, jumlah) ->
laporan.add(String.format(Locale.US, " %-12s %6d", kategori, jumlah)));
laporan.add("baris ditolak (terurut nomor):");
for (HasilBaris h : hasil) {
if (h instanceof HasilBaris.TidakSah(int nomor, String alasan)) {
laporan.add(String.format(Locale.US, " baris %2d: %s", nomor, alasan));
}
}
return laporan;
}
/** Menjalankan seluruh tahapan secara sekuensial. */
public static HasilPipeline jalankanSekuensial(List<String> isiBerkas) {
List<BarisData> baris = baca(isiBerkas);
List<HasilBaris> hasil = new ArrayList<>(baris.size());
List<KejadianTerproses> kejadian = new ArrayList<>(baris.size());
for (BarisData b : baris) {
HasilBaris h = prosesSatuBaris(b);
hasil.add(h);
if (h instanceof HasilBaris.Sah(var k)) {
kejadian.add(k);
}
}
RingkasanAgregasi ringkasan = agregasi(kejadian);
return new HasilPipeline(baris.size(), kejadian.size(),
baris.size() - kejadian.size(),
ringkasan.nilaiBerjasaPerKota().size(),
ringkasan.jumlahPerKategori().size(),
tulis(ringkasan, hasil));
}
}Mari kita bedah bagian-bagian kuncinya:
split(";", -1): argumen-1mempertahankan segmen kosong di akhir, sehingga baris dengan segmen kurang terhitung benar dan tertolak dengan alasan yang jujur, bukanArrayIndexOutOfBoundsExceptiondi kemudian hari.- Urutan pemeriksaan: segmen → tanggal → kota → pelanggan → kategori → nilai. Urutan tetap menentukan alasan yang dilaporkan ketika satu baris bercacat di banyak tempat, sehingga keluaran selalu sama untuk data yang sama.
ubahtanpa efek samping: method ini hanya membacaEventPenjualandan membangunKejadianTerprosesbaru — sifat murni inilah yang membuat method aman dipanggil banyak thread sekaligus di Bagian 2.groupingBy(..., TreeMap::new, reducing(...)): agregasi memakaiTreeMapsehingga laporan per kota tampil terurut tanpa pengurutan tambahan;reducing(BigDecimal.ZERO, ..., BigDecimal::add)menjadi penjumlah nominal uang.
Menjalankan Baseline #
Pintu masuk demo menjalankan pipeline sekuensial lalu mencetak statistik per tahap dan laporan akhir:
import java.util.List;
/** Bagian 1: pipeline sekuensial — baseline untuk seluruh bagian berikutnya. */
public class DemoPipeline {
public static void main(String[] args) {
HasilPipeline hasil = Pipeline.jalankanSekuensial(DataContoh.isiBerkas());
System.out.println("== Tahapan pipeline (sekuensial) ==");
System.out.println("tahap baca : " + hasil.jumlahBaris() + " baris");
System.out.println("tahap validasi : " + hasil.jumlahBaris() + " baris diperiksa, "
+ hasil.jumlahSah() + " sah, " + hasil.jumlahDitolak() + " ditolak");
System.out.println("tahap transformasi : " + hasil.jumlahSah() + " kejadian");
System.out.println("tahap agregasi : " + hasil.jumlahKota() + " kota, "
+ hasil.jumlahKategori() + " kategori");
System.out.println("tahap tulis : " + hasil.laporan().size() + " baris laporan");
System.out.println();
hasil.laporan().forEach(System.out::println);
}
}Output yang Dihasilkan:
== Tahapan pipeline (sekuensial) ==
tahap baca : 30 baris
tahap validasi : 30 baris diperiksa, 26 sah, 4 ditolak
tahap transformasi : 26 kejadian
tahap agregasi : 5 kota, 4 kategori
tahap tulis : 22 baris laporan
=== Laporan pipeline penjualan ===
baris dibaca : 30
baris sah : 26
baris ditolak : 4
total nilai : 19,574,000.00
total berjasa : 19,965,480.00
nilai berjasa per kota:
BANDUNG 3,350,700.00
MAKASSAR 1,405,560.00
MEDAN 5,981,280.00
SURABAYA 4,173,840.00
YOGYAKARTA 5,054,100.00
jumlah transaksi per kategori:
Buku 6
Elektronik 8
Pakaian 7
Peralatan 5
baris ditolak (terurut nomor):
baris 7: nilai harus positif: -150000
baris 12: jumlah segmen 4, harus 5
baris 14: kategori tidak dikenal: 'Camilan'
baris 30: tanggal tidak terurai: '2026-13-31'Tiga pengamatan dari keluaran tersebut:
- Statistik per tahap menelusuri alur data: 30 baris dibaca, validasi menolak 4, transformasi menghasilkan 26 kejadian, agregasi memetakan 5 kota dan 4 kategori, tulis merangkai 22 baris laporan.
- Kota tercetak seragam huruf kapital meskipun tiga baris sumber menuliskannya dengan huruf kecil — transformasi telah membakukan
bandung,surabaya, danmedan. - Keempat penolakan tampil terurut nomor dengan alasan spesifik. Tidak ada exception yang bocor ke pemanggil; semuanya nilai
TidakSahyang datang dari kontrak.
Baseline sekuensial ini sengaja disimpan sebagai patokan. Bagian 2 memparalelkan tahap validasi dan transformasi dengan ExecutorService dan CompletableFuture, lalu membuktikan hasilnya identik baris demi baris — pengujian yang hanya mungkin karena hari ini kita sudah memegang keluaran sekuensial yang pasti.
Referensi #
- Oracle: Class Record (Java SE 25 API)
- Oracle: Class Collectors (Java SE 25 API)
- Oracle: Class LocalDate (Java SE 25 API)
- OpenJDK: JEP 409 — Sealed Classes
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BarisData.java
Unduh berkas/** Satu baris berkas mentah: nomor urut (mulai dari 1) dan teks aslinya. */
public record BarisData(int nomor, String teks) {}
DataContoh.java
Unduh berkasimport java.util.List;
/**
* Isi berkas penjualan contoh: 30 baris data kuartal pertama 2026.
* Dua puluh enam baris sah dan empat baris sengaja cacat untuk menguji validator.
*/
public final class DataContoh {
private DataContoh() {
// Kelas utilitas: seluruh anggota static.
}
/** Isi berkas contoh; pada aplikasi nyata berasal dari Files.readAllLines. */
public static List<String> isiBerkas() {
return List.of(
"2026-01-05;Bandung;Budi;Elektronik;1250000",
"2026-01-07;Surabaya;Siti;Pakaian;340000",
"2026-01-09;bandung;Dewi;Buku;95000",
"2026-01-12;Yogyakarta;Budi;Peralatan;780000",
"2026-01-15;Medan;Siti;Elektronik;2100000",
"2026-01-18;Bandung;Dewi;Buku;128000",
"2026-01-20;Medan;Siti;Pakaian;-150000",
"2026-01-21;Makassar;Budi;Pakaian;450000",
"2026-01-24;surabaya;Siti;Peralatan;620000",
"2026-01-27;Yogyakarta;Dewi;Elektronik;1850000",
"2026-02-02;Bandung;Siti;Pakaian;295000",
"2026-02-05;Bandung;Dewi;Buku",
"2026-02-08;Surabaya;Budi;Elektronik;990000",
"2026-02-11;Makassar;Siti;Camilan;88000",
"2026-02-14;Yogyakarta;Dewi;Pakaian;380000",
"2026-02-17;Medan;Budi;Elektronik;1720000",
"2026-02-20;bandung;Siti;Buku;112000",
"2026-02-23;Surabaya;Dewi;Peralatan;690000",
"2026-02-26;Makassar;Budi;Pakaian;410000",
"2026-02-28;Yogyakarta;Siti;Elektronik;1580000",
"2026-03-04;Medan;Dewi;Buku;64000",
"2026-03-07;Bandung;Budi;Peralatan;830000",
"2026-03-10;Surabaya;Siti;Elektronik;1350000",
"2026-03-13;Makassar;Dewi;Buku;88000",
"2026-03-16;Yogyakarta;Budi;Pakaian;365000",
"2026-03-19;Bandung;Siti;Peralatan;575000",
"2026-03-22;medan;Dewi;Elektronik;1980000",
"2026-03-25;Surabaya;Budi;Buku;102000",
"2026-03-28;Makassar;Siti;Pakaian;430000",
"2026-13-31;Yogyakarta;Dewi;Peralatan;715000");
}
}
DemoPipeline.java
Unduh berkasimport java.util.List;
/** Bagian 1: pipeline sekuensial — baseline untuk seluruh bagian berikutnya. */
public class DemoPipeline {
public static void main(String[] args) {
HasilPipeline hasil = Pipeline.jalankanSekuensial(DataContoh.isiBerkas());
System.out.println("== Tahapan pipeline (sekuensial) ==");
System.out.println("tahap baca : " + hasil.jumlahBaris() + " baris");
System.out.println("tahap validasi : " + hasil.jumlahBaris() + " baris diperiksa, "
+ hasil.jumlahSah() + " sah, " + hasil.jumlahDitolak() + " ditolak");
System.out.println("tahap transformasi : " + hasil.jumlahSah() + " kejadian");
System.out.println("tahap agregasi : " + hasil.jumlahKota() + " kota, "
+ hasil.jumlahKategori() + " kategori");
System.out.println("tahap tulis : " + hasil.laporan().size() + " baris laporan");
System.out.println();
hasil.laporan().forEach(System.out::println);
}
}
EventPenjualan.java
Unduh berkasimport java.math.BigDecimal;
import java.time.LocalDate;
/**
* Kontrak keluaran tahap validasi: event penjualan yang lolos seluruh pemeriksaan.
* Tahap transformasi menerima record ini tanpa memeriksa format lagi.
*/
public record EventPenjualan(int nomorBaris, LocalDate tanggal, String kota,
String pelanggan, String kategori, BigDecimal nilai) {}
HasilBaris.java
Unduh berkas/**
* Kontrak hasil pemrosesan satu baris: nilai data, bukan exception.
* Varian terkunci lewat sealed sehingga compiler memaksa setiap switch
* menangani semua kemungkinan.
*/
public sealed interface HasilBaris permits HasilBaris.Sah, HasilBaris.TidakSah {
/** Nomor baris sumber — dipakai pelaporan galat dan pengurutan hasil. */
int nomorBaris();
/** Baris selesai diproses dan membawa kejadian siap agregasi. */
record Sah(KejadianTerproses kejadian) implements HasilBaris {
@Override
public int nomorBaris() {
return kejadian.nomorBaris();
}
}
/** Baris ditolak validator; alasan ditulis untuk laporan galat. */
record TidakSah(int nomorBaris, String alasan) implements HasilBaris {
@Override
public int nomorBaris() {
return nomorBaris;
}
}
}
HasilPipeline.java
Unduh berkasimport java.util.List;
/** Hasil pipeline utuh: statistik tiap tahap plus laporan akhir siap cetak. */
public record HasilPipeline(int jumlahBaris,
int jumlahSah,
int jumlahDitolak,
int jumlahKota,
int jumlahKategori,
List<String> laporan) {}
KejadianTerproses.java
Unduh berkasimport java.math.BigDecimal;
import java.time.LocalDate;
/**
* Kontrak keluaran tahap transformasi: data siap agregasi.
* Kota sudah dibakukan (huruf kapital) dan nilai berjasa sudah dihitung.
*/
public record KejadianTerproses(int nomorBaris, LocalDate tanggal, String kota,
String pelanggan, String kategori, BigDecimal nilai,
BigDecimal nilaiBerjasa) {}
Pipeline.java
Unduh berkasimport java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** Pipeline sekuensial: baca -> validasi -> transformasi -> agregasi -> tulis. */
public final class Pipeline {
/** Kategori yang dikenal validator; di luar daftar ini baris ditolak. */
private static final Set<String> KATEGORI_DIKENAL =
Set.of("Elektronik", "Pakaian", "Buku", "Peralatan");
/** Faktor biaya layanan 2% yang dihitung tahap transformasi. */
private static final BigDecimal BIAYA_LAYANAN = new BigDecimal("1.02");
private Pipeline() {
// Kelas utilitas: seluruh anggota static.
}
/** Tahap baca: memberi nomor urut pada tiap baris berkas. */
public static List<BarisData> baca(List<String> isiBerkas) {
List<BarisData> baris = new ArrayList<>(isiBerkas.size());
for (int i = 0; i < isiBerkas.size(); i++) {
baris.add(new BarisData(i + 1, isiBerkas.get(i)));
}
return baris;
}
/** Tahap validasi lalu transformasi untuk satu baris. */
public static HasilBaris prosesSatuBaris(BarisData baris) {
String[] segmen = baris.teks().split(";", -1);
if (segmen.length != 5) {
return new HasilBaris.TidakSah(baris.nomor(),
"jumlah segmen " + segmen.length + ", harus 5");
}
LocalDate tanggal;
try {
tanggal = LocalDate.parse(segmen[0]);
} catch (DateTimeParseException e) {
return new HasilBaris.TidakSah(baris.nomor(),
"tanggal tidak terurai: '" + segmen[0] + "'");
}
String kota = segmen[1].trim();
String pelanggan = segmen[2].trim();
String kategori = segmen[3].trim();
if (kota.isEmpty()) {
return new HasilBaris.TidakSah(baris.nomor(), "kota kosong");
}
if (pelanggan.isEmpty()) {
return new HasilBaris.TidakSah(baris.nomor(), "nama pelanggan kosong");
}
if (!KATEGORI_DIKENAL.contains(kategori)) {
return new HasilBaris.TidakSah(baris.nomor(),
"kategori tidak dikenal: '" + kategori + "'");
}
BigDecimal nilai;
try {
nilai = new BigDecimal(segmen[4].trim());
} catch (NumberFormatException e) {
return new HasilBaris.TidakSah(baris.nomor(),
"nilai tidak terurai: '" + segmen[4].trim() + "'");
}
if (nilai.signum() <= 0) {
return new HasilBaris.TidakSah(baris.nomor(),
"nilai harus positif: " + nilai.toPlainString());
}
EventPenjualan event = new EventPenjualan(baris.nomor(), tanggal, kota,
pelanggan, kategori, nilai);
return new HasilBaris.Sah(ubah(event));
}
/** Tahap transformasi: bakukan kota dan hitung nilai berjasa 2%. */
public static KejadianTerproses ubah(EventPenjualan event) {
BigDecimal berjasa = event.nilai().multiply(BIAYA_LAYANAN)
.setScale(2, RoundingMode.HALF_EVEN);
return new KejadianTerproses(event.nomorBaris(), event.tanggal(),
event.kota().toUpperCase(Locale.ROOT), event.pelanggan(),
event.kategori(), event.nilai(), berjasa);
}
/** Tahap agregasi: ringkasan terurut dari seluruh kejadian. */
public static RingkasanAgregasi agregasi(List<KejadianTerproses> kejadian) {
Map<String, BigDecimal> perKota = kejadian.stream()
.collect(Collectors.groupingBy(KejadianTerproses::kota,
TreeMap::new,
Collectors.reducing(BigDecimal.ZERO,
KejadianTerproses::nilaiBerjasa, BigDecimal::add)));
Map<String, Long> perKategori = kejadian.stream()
.collect(Collectors.groupingBy(KejadianTerproses::kategori,
TreeMap::new, Collectors.counting()));
BigDecimal totalNilai = kejadian.stream()
.map(KejadianTerproses::nilai)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalBerjasa = kejadian.stream()
.map(KejadianTerproses::nilaiBerjasa)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new RingkasanAgregasi(kejadian.size(), totalNilai, totalBerjasa,
perKota, perKategori);
}
/** Tahap tulis: susun laporan akhir dari ringkasan dan hasil per baris. */
public static List<String> tulis(RingkasanAgregasi ringkasan, List<HasilBaris> hasil) {
int sah = 0;
for (HasilBaris h : hasil) {
if (h instanceof HasilBaris.Sah) {
sah++;
}
}
List<String> laporan = new ArrayList<>();
laporan.add("=== Laporan pipeline penjualan ===");
laporan.add("baris dibaca : " + hasil.size());
laporan.add("baris sah : " + sah);
laporan.add("baris ditolak : " + (hasil.size() - sah));
laporan.add(String.format(Locale.US, "total nilai : %,14.2f", ringkasan.totalNilai()));
laporan.add(String.format(Locale.US, "total berjasa : %,14.2f", ringkasan.totalBerjasa()));
laporan.add("nilai berjasa per kota:");
ringkasan.nilaiBerjasaPerKota().forEach((kota, nilai) ->
laporan.add(String.format(Locale.US, " %-12s %,14.2f", kota, nilai)));
laporan.add("jumlah transaksi per kategori:");
ringkasan.jumlahPerKategori().forEach((kategori, jumlah) ->
laporan.add(String.format(Locale.US, " %-12s %6d", kategori, jumlah)));
laporan.add("baris ditolak (terurut nomor):");
for (HasilBaris h : hasil) {
if (h instanceof HasilBaris.TidakSah(int nomor, String alasan)) {
laporan.add(String.format(Locale.US, " baris %2d: %s", nomor, alasan));
}
}
return laporan;
}
/** Menjalankan seluruh tahapan secara sekuensial. */
public static HasilPipeline jalankanSekuensial(List<String> isiBerkas) {
List<BarisData> baris = baca(isiBerkas);
List<HasilBaris> hasil = new ArrayList<>(baris.size());
List<KejadianTerproses> kejadian = new ArrayList<>(baris.size());
for (BarisData b : baris) {
HasilBaris h = prosesSatuBaris(b);
hasil.add(h);
if (h instanceof HasilBaris.Sah(var k)) {
kejadian.add(k);
}
}
RingkasanAgregasi ringkasan = agregasi(kejadian);
return new HasilPipeline(baris.size(), kejadian.size(),
baris.size() - kejadian.size(),
ringkasan.nilaiBerjasaPerKota().size(),
ringkasan.jumlahPerKategori().size(),
tulis(ringkasan, hasil));
}
}
RingkasanAgregasi.java
Unduh berkasimport java.math.BigDecimal;
import java.util.Map;
/**
* Kontrak keluaran tahap agregasi: ringkasan seluruh kejadian.
* Peta memakai TreeMap sehingga iterasi selalu terurut (deterministik).
*/
public record RingkasanAgregasi(int jumlahKejadian,
BigDecimal totalNilai,
BigDecimal totalBerjasa,
Map<String, BigDecimal> nilaiBerjasaPerKota,
Map<String, Long> jumlahPerKategori) {}