Topik ini mewujudkan rancangan proyek rekap nilai ujian menjadi kode yang lengkap dan dapat dijalankan. Empat berkas dibuat secara utuh, yaitu modul RekapNilai.pm sebagai penyedia logika, skrip rekap.pl sebagai antarmuka command line, berkas data nilaipercobaan.txt, dan berkas pengujian nilai-uji.t. Setiap kontrak rancangan arsitektur diwujudkan satu per satu, mulai dari nilai balik subrutin hingga kebijakan kode keluar. Setelah topik ini diikuti, setiap kriteria selesai proyek dapat diperiksa melalui perintah.
Berkas data contoh#
Berkas nilaipercobaan.txt berisi enam baris data untuk empat siswa, dan isinya sama persis dengan berkas contoh yang menjadi acuan format masukan sepanjang modul. Konsistensi data lintas topik memungkinkan setiap angka laporan diperiksa ulang dengan hitungan tangan. Ali menyumbang dua nilai, sedangkan tiga siswa lainnya menyumbang satu nilai, sehingga perhitungan rata-rata dari lebih dari satu nilai langsung teruji.
Ali;80
Budi;75
Ali;90
Citra;88
Budi;65
Dewi;79Berkas ini tidak memuat baris judul kolom. Subrutin baca_data memperlakukan setiap baris sebagai data, sehingga baris judul apa pun dilaporkan sebagai baris tidak valid dan menaikkan penghitungnya. Keputusan ini mengikuti lingkup proyek, yaitu berkas kecil dari satu sumber dengan format yang sudah disepakati. Nama siswanya berupa empat nama fiktif yang umum, sehingga laporannya mudah dibaca dan tidak memuat data pribadi nyata.
Format barisnya tetap sederhana sebagaimana lingkup proyek, yaitu nama diikuti tanda titik koma dan satu nilai. Berkas ini menjadi pembanding bagi berkas data kedua yang ditulis pengujian pada bagian akhir topik. Dengan masukan yang tidak berubah, perubahan perilaku program antarversi kode mudah dikenali.
Modul RekapNilai dan tiga subrutinnya#
Modul RekapNilai.pm memuat seluruh logika proyek dalam tiga subrutin yang kontraknya ditetapkan pada rancangan. Deklarasi package RekapNilai; mengikuti konvensi nama berkas, sedangkan use Exporter 'import'; bersama @EXPORT_OK menawarkan tiga subrutin kepada pemanggil yang memintanya secara eksplisit. Bentuk ini lebih disiplin daripada @EXPORT karena simbol hanya diimpor oleh pemanggil yang menuliskannya pada daftar use RekapNilai qw(...). Ketiga subrutin tidak mencetak apa pun, dan seluruh hasil dikembalikan sebagai nilai balik sesuai batasan rancangan.
package RekapNilai;
use strict;
use warnings;
use Exporter 'import';
# subrutin yang boleh diimpor pemanggil lewat use RekapNilai qw(...)
our @EXPORT_OK = qw(baca_data hitung_rata ringkas);
# Membaca berkas berisi baris "nama;nilai", mengelompokkan nilai per nama,
# dan menghitung baris yang tidak valid. Nilai baliknya berupa dua nilai:
# referensi hash nama => array nilai, lalu jumlah baris tidak valid.
sub baca_data {
my ($jalur) = @_;
open my $fh, '<', $jalur or die "$jalur: $!\n";
my %nilai_per_siswa;
my $tidak_valid = 0;
while ( my $baris = <$fh> ) {
chomp $baris;
# baris kosong: dilewati dan dihitung
if ( $baris !~ /\S/ ) {
warn "$jalur baris $.: baris kosong dilewati\n";
$tidak_valid++;
next;
}
my ( $nama, $nilai ) = split /;/, $baris, 2;
# format rusak: tanpa tanda titik koma atau nama kosong
if ( !defined $nilai || $nama !~ /\S/ ) {
warn "$jalur baris $.: format baris bukan nama;nilai\n";
$tidak_valid++;
next;
}
# nilai harus berupa angka pada rentang 0 sampai 100
if ( $nilai !~ /^\d+(\.\d+)?$/ || $nilai > 100 ) {
warn "$jalur baris $.: nilai \"$nilai\" bukan angka 0-100\n";
$tidak_valid++;
next;
}
push @{ $nilai_per_siswa{$nama} }, $nilai;
}
close $fh;
return ( \%nilai_per_siswa, $tidak_valid );
}
# Menghitung rata-rata sebuah array nilai, atau 0 bila arraynya kosong.
sub hitung_rata {
my ($ref_array) = @_;
my $banyak = scalar @{$ref_array};
return 0 if $banyak == 0;
my $jumlah = 0;
foreach my $n ( @{$ref_array} ) {
$jumlah += $n;
}
return $jumlah / $banyak;
}
# Menyusun rata-rata per siswa beserta statistik kelas dari hash nilai.
# Nilai baliknya berupa referensi hash dengan kunci:
# siswa -> array berisi { nama, banyak, rata } terurut abjad
# banyak_siswa -> jumlah kunci hash masukan
# banyak_nilai -> jumlah seluruh elemen array nilai
# rata_kelas -> rata-rata seluruh nilai, bukan rata-rata dari rata-rata
# tertinggi -> { nama, nilai } pemilik nilai tertinggi kelas
# terendah -> { nama, nilai } pemilik nilai terendah kelas
sub ringkas {
my ($ref_hash) = @_;
my @siswa;
my $banyak_nilai = 0;
my $jumlah_semua = 0;
my $tertinggi;
my $terendah;
foreach my $nama ( sort keys %{$ref_hash} ) {
my @nilai = @{ $ref_hash->{$nama} };
push @siswa, {
nama => $nama,
banyak => scalar @nilai,
rata => hitung_rata( \@nilai ),
};
$banyak_nilai += scalar @nilai;
foreach my $n (@nilai) {
$jumlah_semua += $n;
if ( !defined $tertinggi || $n > $tertinggi->{nilai} ) {
$tertinggi = { nama => $nama, nilai => $n };
}
if ( !defined $terendah || $n < $terendah->{nilai} ) {
$terendah = { nama => $nama, nilai => $n };
}
}
}
my $rata_kelas = $banyak_nilai > 0 ? $jumlah_semua / $banyak_nilai : 0;
return {
siswa => \@siswa,
banyak_siswa => scalar @siswa,
banyak_nilai => $banyak_nilai,
rata_kelas => $rata_kelas,
tertinggi => $tertinggi,
terendah => $terendah,
};
}
1;Subrutin ringkas mengembalikan satu hash yang memuat dua kelompok informasi. Kunci siswa berupa array berisi satu hash per siswa dengan kunci nama, banyak, dan rata, dan array ini terurut abjad karena penyusunannya menggunakan sort keys. Kunci tingkat kelas melengkapi hasil tersebut, yaitu banyak_siswa, banyak_nilai, rata_kelas, serta tertinggi dan terendah yang berupa hash nama dan nilai. Dua kunci terakhir membawa nama pemiliknya sehingga laporan tidak berhenti pada angka semata. Urutan dari sort keys menjaga nilai balik ini tetap sama untuk masukan yang sama, sehingga pengujian dapat membandingkannya secara keseluruhan.
Dua mekanisme pada tingkat berkas menopang kontrak modul ini. Subrutin ringkas memanggil hitung_rata untuk setiap siswa, sehingga rumus rata-rata hanya ditulis satu kali di seluruh modul. Baris 1; pada akhir berkas menutup modul dengan nilai benar, syarat yang dituntut use ketika memuat modulnya.
Validasi baris pada baca_data#
Subrutin baca_data membaca berkas baris demi baris dan menempatkan tiga pemeriksaan dalam urutan tetap. Baris kosong dikenali dari ketiadaan karakter non-spasi melalui pola /\S/. Baris berformat rusak dikenali setelah split /;/, $baris, 2, yaitu apabila tanda titik koma tidak ditemukan sehingga nilai tidak terdefinisi, atau namanya kosong. Nilai yang tidak sah dikenali dari pencocokan pola ^\d+(\.\d+)?$ yang menolak teks bukan angka, disertai pemeriksaan rentang 0 sampai 100. Setiap baris yang tidak lolos salah satu pemeriksaan dilompati, dihitung pada penghitung $tidak_valid, dan dilaporkan ke STDERR melalui warn lengkap dengan nomor barisnya.
Kegagalan membuka berkas diperlakukan berbeda karena program tidak memiliki data untuk diolah. Pernyataan open ... or die "$jalur: $!\n" menghentikan modul dengan pesan yang memuat jalur berkas dan alasan dari sistem operasi melalui $!. Tanda \n di akhir pesan bukan hiasan, melainkan syarat agar Perl tidak menambahkan teks posisi baris seperti at RekapNilai.pm line 19 pada pesan tersebut. Disiplin yang sama berlaku pada seluruh warn di subrutin ini, sehingga pesan error di STDERR selalu bersih.
Skrip rekap.pl dan laporan yang dihasilkan#
Skrip rekap.pl memuat modul dan mengubah nilai baliknya menjadi laporan, tanpa memuat satu pun logika pengolahan data. Seluruh perilakunya mengikuti tabel kebijakan rancangan. Keluaran berikut dicetak dari berkas data contoh.
use strict;
use warnings;
# use lib '.' memasukkan direktori kerja saat ini ke @INC, sehingga modul
# RekapNilai.pm yang berada di samping skrip ini dapat dimuat.
use lib '.';
use RekapNilai qw(baca_data hitung_rata ringkas);
# satu argumen posisi opsional: jalur berkas data
my $jalur = shift @ARGV;
$jalur = 'nilaipercobaan.txt' unless defined $jalur;
my ( $data, $tidak_valid ) = baca_data($jalur);
# kebijakan kode keluar: tanpa satu pun nilai yang sah, program berhenti
if ( !%{$data} ) {
print STDERR "Tidak ada nilai yang sah pada $jalur\n";
exit 2;
}
my $ringkasan = ringkas($data);
# daftar siswa sudah terurut abjad karena ringkas memakai sort keys
foreach my $s ( @{ $ringkasan->{siswa} } ) {
printf "%s: rata-rata %.2f dari %d nilai\n",
$s->{nama}, $s->{rata}, $s->{banyak};
}
printf "Kelas: %d siswa, %d nilai, rata-rata %.2f\n",
$ringkasan->{banyak_siswa}, $ringkasan->{banyak_nilai}, $ringkasan->{rata_kelas};
printf "Nilai tertinggi: %s (%d)\n",
$ringkasan->{tertinggi}{nama}, $ringkasan->{tertinggi}{nilai};
printf "Nilai terendah: %s (%d)\n",
$ringkasan->{terendah}{nama}, $ringkasan->{terendah}{nilai};
# jumlah baris tidak valid dilaporkan ke STDERR tanpa menggagalkan laporan
print STDERR "Baris tidak valid: $tidak_valid\n" if $tidak_valid > 0;Ali: rata-rata 85.00 dari 2 nilai
Budi: rata-rata 70.00 dari 2 nilai
Citra: rata-rata 88.00 dari 1 nilai
Dewi: rata-rata 79.00 dari 1 nilai
Kelas: 4 siswa, 6 nilai, rata-rata 79.50
Nilai tertinggi: Ali (90)
Nilai terendah: Budi (65)Pernyataan use lib '.'; menambahkan direktori kerja ke @INC, sehingga use RekapNilai menemukan berkas modul yang berada di samping skrip. Argumen posisi dibaca dengan shift @ARGV, dan bila tidak ada, jalurnya diisi nilaipercobaan.txt sesuai antarmuka rancangan. Bila hasil baca_data berupa hash kosong, tidak ada nilai yang sah, sehingga skrip menulis pesan ke STDERR dan berhenti dengan exit 2. Untuk berkas yang gagal dibuka, die pada modul membawa nilai $! sebagai kode keluar, dan nilai itu untuk berkas yang tidak ada bernilai 2, sehingga kedua keadaan gagal berakhir dengan kode keluar yang sama.
Keberhasilan berakhir dengan kode keluar 0 tanpa pernyataan eksplisit. Jumlah baris tidak valid dilaporkan sekali ke STDERR apabila lebih dari nol, sedangkan laporan tetap dicetak dari data yang sah. Pemisahan antara laporan di STDOUT dan pesan error di STDERR memungkinkan skrip dirangkai dengan perintah lain, karena keberhasilan dinilai dari kode keluarnya tanpa membaca isinya.
Empat baris pertama adalah rata-rata per siswa yang terurut abjad, lengkap dengan jumlah nilai yang menjadi dasar rata-ratanya. Baris kelas menyusul dengan tiga angka dari enam nilai, bukan dari rata-rata per siswa. Rata-rata kelasnya adalah 79.50, hasil dari 477 dibagi 6, sedangkan rata-rata dari empat rata-rata siswa bernilai 80.50 karena bobot jumlah nilai diabaikan. Kedua angka berbeda karena Ali menyumbang dua nilai, dan rancangan menetapkan rata_kelas dihitung dari seluruh nilai. Dua baris terakhir menunjukkan manfaat tertinggi dan terendah yang membawa nama pemiliknya.
Perintah berikut menjalankan skrip dari direktori proyek tanpa argumen. Pilihan 2>&1 mengarahkan STDERR ke STDOUT sehingga pesan error ikut terlihat apabila ada.
# menjalankan skrip dari direktori proyek tanpa argumen
perl rekap.pl 2>&1Ali: rata-rata 85.00 dari 2 nilai
Budi: rata-rata 70.00 dari 2 nilai
Citra: rata-rata 88.00 dari 1 nilai
Dewi: rata-rata 79.00 dari 1 nilai
Kelas: 4 siswa, 6 nilai, rata-rata 79.50
Nilai tertinggi: Ali (90)
Nilai terendah: Budi (65)Jalur kedua dari antarmuka rancangan adalah membaca berkas yang disebut sebagai argumen. Berkas berikut sengaja memuat empat baris bermasalah, yaitu baris tanpa titik koma, nilai bukan angka, nilai di luar rentang, dan baris kosong:
Ali;80
Eko
Budi;abc
Citra;150
Dewi;70
Budi;75Pemanggilan dengan argumen menunjukkan keduanya sekaligus, yaitu laporan yang tetap tercetak dari baris yang sah dan pesan error yang keluar dari STDERR:
# membaca berkas data yang memuat baris bermasalah
perl rekap.pl nilaibermasalah.txt 2>&1nilaibermasalah.txt baris 2: format baris bukan nama;nilai
nilaibermasalah.txt baris 3: nilai "abc" bukan angka 0-100
nilaibermasalah.txt baris 4: nilai "150" bukan angka 0-100
nilaibermasalah.txt baris 6: baris kosong dilewati
Baris tidak valid: 4
Ali: rata-rata 80.00 dari 1 nilai
Budi: rata-rata 75.00 dari 1 nilai
Dewi: rata-rata 70.00 dari 1 nilai
Kelas: 3 siswa, 3 nilai, rata-rata 75.00
Nilai tertinggi: Ali (80)
Nilai terendah: Dewi (70)Setiap baris bermasalah dilaporkan satu per satu beserta nomor barisnya, sehingga penyebabnya dapat diperbaiki pada sumber datanya. Statistik kelas pada laporan ini dihitung hanya dari tiga nilai yang sah, dan kode keluar tetap 0 karena minimal ada satu nilai yang sah sesuai kebijakan rancangan.
Pengujian otomatis dengan Test::More#
Kontrak ketiga subrutin menjadi bahan uji langsung pada berkas nilai-uji.t. Berkas ini menetapkan plan tetap berupa tests => 17, dan setiap fungsi perbandingan menghasilkan satu nomor kasus. Kelompok pertama menguji hitung_rata pada tiga keadaan, yaitu kasus biasa, satu nilai, dan array kosong yang harus menghasilkan 0. Kelompok kedua menguji baca_data terhadap berkas yang ditulis pengujian itu sendiri melalui open dengan mode tulis. Berkasnya berisi empat baris sah dan empat baris tidak valid, sehingga jumlah baris tidak valid dan keberadaan pesannya teruji. Kelompok ketiga menguji ringkas dengan is_deeply pada struktur kecil berisi tiga siswa, sehingga seluruh struktur nilai balik dibandingkan sekaligus.
use strict;
use warnings;
use Test::More tests => 17;
use lib '.';
use RekapNilai qw(baca_data hitung_rata ringkas);
# hitung_rata: kasus biasa, satu nilai, dan array kosong
is( hitung_rata( [ 80, 90 ] ), 85, 'hitung_rata menghitung rata-rata dua nilai' );
is( hitung_rata( [88] ), 88, 'hitung_rata mengembalikan satu nilai apa adanya' );
is( hitung_rata( [] ), 0, 'hitung_rata mengembalikan 0 untuk array kosong' );
# baca_data: berkas uji ditulis skrip ini sendiri agar pengujian mandiri
my $nama_uji = 'data-uji-sementara.txt';
open my $fh, '>', $nama_uji or die "data-uji-sementara.txt: $!\n";
print {$fh} "Ali;80\nBudi;75\nAli;90\nCitra;88\n";
print {$fh} "\nBudi;abc\nDewi;150\nrusak tanpa titik koma\n";
close $fh or die "data-uji-sementara.txt: $!\n";
my @peringatan;
my ( $data_uji, $tidak_valid );
{
# pesan warn dari baca_data dikumpulkan agar keluaran TAP tetap bersih
local $SIG{__WARN__} = sub { push @peringatan, $_[0] };
( $data_uji, $tidak_valid ) = baca_data($nama_uji);
}
is( $tidak_valid, 4, 'jumlah baris tidak valid terhitung dengan tepat' );
is( scalar @peringatan, 4, 'setiap baris tidak valid menghasilkan satu pesan' );
is_deeply( $data_uji->{Ali}, [ 80, 90 ], 'nilai Ali terkumpul sesuai urutan baris' );
is_deeply( $data_uji->{Citra}, [88], 'nilai Citra tercatat satu kali' );
ok( !exists $data_uji->{Dewi}, 'baris dengan nilai di luar 0-100 tidak menciptakan kunci' );
# ringkas: struktur kecil berisi tiga siswa
my $ringkasan = ringkas(
{ Ali => [ 80, 90 ], Budi => [ 75, 65 ], Citra => [88] }
);
is_deeply(
$ringkasan->{siswa},
[
{ nama => 'Ali', banyak => 2, rata => 85 },
{ nama => 'Budi', banyak => 2, rata => 70 },
{ nama => 'Citra', banyak => 1, rata => 88 },
],
'daftar siswa terurut abjad beserta banyak nilai dan rata-ratanya',
);
is( $ringkasan->{banyak_siswa}, 3, 'banyak_siswa menghitung jumlah kunci hash' );
is( $ringkasan->{banyak_nilai}, 5, 'banyak_nilai menjumlahkan seluruh elemen array' );
cmp_ok( $ringkasan->{rata_kelas}, '==', 79.6, 'rata_kelas dihitung dari seluruh nilai' );
is( $ringkasan->{tertinggi}{nama}, 'Ali', 'nama siswa dengan nilai tertinggi tercatat' );
is( $ringkasan->{tertinggi}{nilai}, 90, 'nilai tertinggi kelas tercatat' );
is( $ringkasan->{terendah}{nama}, 'Budi', 'nama siswa dengan nilai terendah tercatat' );
is( $ringkasan->{terendah}{nilai}, 65, 'nilai terendah kelas tercatat' );
# pembersihan: berkas uji
ok( unlink($nama_uji), 'berkas uji sementara berhasil dihapus' );1..17
ok 1 - hitung_rata menghitung rata-rata dua nilai
ok 2 - hitung_rata mengembalikan satu nilai apa adanya
ok 3 - hitung_rata mengembalikan 0 untuk array kosong
ok 4 - jumlah baris tidak valid terhitung dengan tepat
ok 5 - setiap baris tidak valid menghasilkan satu pesan
ok 6 - nilai Ali terkumpul sesuai urutan baris
ok 7 - nilai Citra tercatat satu kali
ok 8 - baris dengan nilai di luar 0-100 tidak menciptakan kunci
ok 9 - daftar siswa terurut abjad beserta banyak nilai dan rata-ratanya
ok 10 - banyak_siswa menghitung jumlah kunci hash
ok 11 - banyak_nilai menjumlahkan seluruh elemen array
ok 12 - rata_kelas dihitung dari seluruh nilai
ok 13 - nama siswa dengan nilai tertinggi tercatat
ok 14 - nilai tertinggi kelas tercatat
ok 15 - nama siswa dengan nilai terendah tercatat
ok 16 - nilai terendah kelas tercatat
ok 17 - berkas uji sementara berhasil dihapusAda dua keputusan dalam berkas uji ini yang perlu diperhatikan. Pesan peringatan (warn) dari baca_data ditangkap menggunakan local $SIG{__WARN__}, sehingga keluaran TAP tetap bersih dan jumlah pesan peringatan dapat diuji secara terpisah. Berkas uji sementara dihapus menggunakan unlink pada kasus pengujian terakhir, dan keberhasilan penghapusannya juga diperiksa sebagai bagian dari pengujian. Dengan demikian, tidak ada berkas sementara yang tertinggal. Perintah prove dapat digunakan untuk menjalankan berkas uji yang sama dengan ringkasan keluaran yang lebih singkat. Perintah ini cukup dijalankan dari direktori proyek.
# tidak-dijalankan
# ringkasan prove memuat waktu eksekusi yang tidak tetap
prove nilai-uji.tKesalahan umum#
- Modul ditutup tanpa nilai balik yang benar, misalnya tanpa
1;pada baris terakhirRekapNilai.pm, sehinggause RekapNilaiberhenti dengan pesandid not return a true value. Perbaikannya adalah menutup setiap modul dengan1;. - Skrip dijalankan dari direktori lain sehingga
RekapNilai.pmtidak ditemukan dan pesanCan't locate RekapNilai.pm in @INCmuncul. Perbaikannya adalah menjalankan proyek dari direktori yang memuat kedua berkasnya, karenause lib '.'menunjuk ke direktori kerja, bukan ke lokasi skrip. - Kasus uji baru ditambahkan tanpa menaikkan angka plan, sehingga pengujian berakhir dengan pesan
Bad plan. You planned 17 tests but ran 18meskipun seluruhnya lulus. Perbaikannya adalah menaikkan nilaitests =>setiap kali fungsi perbandingan ditambahkan.
Ringkasan#
- Logika proyek berada sepenuhnya di
RekapNilai.pmdengan tiga subrutin yang nilai baliknya sesuai kontrak rancangan, sedangkanrekap.plhanya menerima argumen dan mencetak laporan. - Setiap baris tidak valid dilaporkan ke STDERR beserta nomor barisnya, dilompati, dan dihitung, sehingga laporan tetap dihasilkan dari data yang sah.
- Kode keluar membedakan keadaan bagi pemanggil, yaitu 0 untuk keberhasilan dan 2 untuk kegagalan, melalui
exiteksplisit maupundieyang membawa nilai$!. - Berkas
nilai-uji.tmengunci perilaku ketiga subrutin dalam 17 kasus uji Test::More yang seluruhnya lulus.
Referensi#
- perlmod — dokumentasi resmi Perl
— Aturan pembuatan modul, dari package hingga nilai balik
1;yang menjadi bentuk dasarRekapNilai.pm. - perlsub — dokumentasi resmi Perl
— Aturan subrutin, argumen melalui
@_, dan nilai balik yang menjadi kontrak ketiga subrutin. - perlfunc — dokumentasi resmi Perl
— Dokumentasi
open,warn,die,printf, danexityang digunakan modul, skrip, dan pengujian. - perldsc — dokumentasi resmi Perl — Resep struktur data kompleks, termasuk hash dari array yang menjadi struktur utama proyek ini.
- Test::More — dokumentasi resmi Perl
— Fungsi
is,is_deeply,ok,cmp_ok, dan pengaturan plan yang menyusun berkas pengujian.