Setelah mampu merepresentasikan durasi, kebutuhan berikutnya adalah mengulang durasi tersebut di sepanjang rentang: menagih tagihan setiap bulan, menjadwalkan rapat setiap pekan, atau menyusun laporan harian selama satu bulan penuh. Menulis perulangan manual dengan penghitung hari berarti kita sendiri yang menanggung beban logika kalender — jumlah hari tiap bulan yang berbeda, tahun kabisat, dan sebagainya. Kelas DatePeriod menyelesaikan masalah ini dengan menggabungkan tiga bahan yang sudah kita kuasai: tanggal mulai, objek DateInterval , dan batas akhir, lalu menyajikannya sebagai iterator yang siap dipakai foreach. Chapter ini membahas bentuk-bentuk konstruktornya, perilaku batas akhir yang eksklusif, bentuk jumlah perulangan, flag pengecualian tanggal mulai, hingga studi kasus menyusun daftar hari kerja.

Mengiterasi Rentang Tanggal #

DatePeriod mengimplementasikan interface Traversable, sehingga objeknya bisa langsung dilempar ke foreach tanpa konversi apa pun. Konstruktor pada bentuknya yang paling umum menerima tiga argumen: objek tanggal mulai, interval antar-tanggal, dan objek tanggal akhir. Setiap putaran iterasi menghasilkan satu objek waktu — bertipe DateTimeImmutable bila titik mulainya DateTimeImmutable, atau DateTime bila titik mulainya DateTime. Berikut iterasi harian pada rentang empat hari:

php
<?php
$mulai = new DateTimeImmutable("2026-01-12 08:00:00", new DateTimeZone("Asia/Jakarta"));
$berakhir = new DateTimeImmutable("2026-01-16 08:00:00", new DateTimeZone("Asia/Jakarta"));

$periode = new DatePeriod($mulai, new DateInterval("P1D"), $berakhir);

foreach ($periode as $tanggal) {
    echo $tanggal->format("Y-m-d l") . "\n";
}

Output yang Dihasilkan:

text
2026-01-12 Monday
2026-01-13 Tuesday
2026-01-14 Wednesday
2026-01-15 Thursday

Mari kita telaah baris-baris kode di atas:

  1. Baris 5: objek DatePeriod dibuat dengan titik mulai 12 Januari, interval satu hari, dan batas akhir 16 Januari 2026.
  2. Baris 7-9: foreach meminta satu per satu tanggal dari period; setiap tanggal otomatis bertambah satu hari dari tanggal sebelumnya, dan iterasi berhenti sebelum batas akhir tercapai.
  3. Hasilnya empat baris mulai 2026-01-12 Monday hingga 2026-01-15 Thursday — tanggal 16 Januari sengaja tidak muncul, dan kita buktikan perilaku tersebut secara khusus berikutnya.

Batas Akhir Bersifat Eksklusif #

Aturan paling penting sekaligus paling sering menjebak: tanggal akhir tidak pernah ikut dihasilkan. Iterasi hanya mengeluarkan tanggal yang lebih awal daripada batas akhir, meskipun batas akhir itu sendiri jatuh tepat pada urutan kandidat. Mari kita buktikan dengan rentang yang batas akhirnya merupakan kandidat yang sah:

php
<?php
$mulai = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$berakhir = new DateTimeImmutable("2026-01-18 08:00:00", new DateTimeZone("Asia/Jakarta"));

$periode = new DatePeriod($mulai, new DateInterval("P1D"), $berakhir);

foreach ($periode as $tanggal) {
    echo $tanggal->format("Y-m-d") . "\n";
}

echo "Batas akhir: " . $berakhir->format("Y-m-d") . "\n";

Output yang Dihasilkan:

text
2026-01-15
2026-01-16
2026-01-17
Batas akhir: 2026-01-18

Urutan kandidat tanggal adalah 15, 16, 17, dan 18 Januari. Tiga kandidat pertama tercetak; kandidat 18 Januari ditolak karena nilainya sama dengan batas akhir, sehingga baris terakhir keluaran justru berasal dari echo pada baris 11 yang mencetak Batas akhir: 2026-01-18. Desain eksklusif ini membuat rentang dapat dirantai tanpa tumpang tindih — batas akhir satu rentang sekaligus menjadi titik mulai rentang berikutnya.

Menentukan Jumlah Perulangan dengan Recurrences #

Bentuk konstruktor kedua menggantikan tanggal akhir dengan angka jumlah perulangan (recurrences): new DatePeriod($mulai, $interval, $recurrences). Angka tersebut menghitung pengulangan setelah tanggal mulai, sehingga total tanggal yang dihasilkan selalu $recurrences + 1. Bentuk ini cocok untuk kejadian berulang yang jumlahnya diketahui di muka — misalnya program latihan empat sesi:

php
<?php
$jadwal = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));

$rapat = new DatePeriod($jadwal, new DateInterval("P1W"), 3);

foreach ($rapat as $pertemuan) {
    echo $pertemuan->format("Y-m-d") . "\n";
}

echo "Jumlah tanggal: " . iterator_count($rapat) . "\n";
echo "Nilai recurrences: " . var_export($rapat->getRecurrences(), true) . "\n";

Output yang Dihasilkan:

text
2026-01-15
2026-01-22
2026-01-29
2026-02-05
Jumlah tanggal: 4
Nilai recurrences: 3

Empat tanggal tercetak: sesi pembuka pada 15 Januari ditambah tiga pengulangan mingguan pada 22, 29 Januari, dan 5 Februari. Baris 10 memakai fungsi iterator_count() yang menghitung elemen objek Traversable apa pun — hasilnya 4. Baris 11 membaca kembali konfigurasi period lewat getRecurrences() yang mengembalikan 3, menegaskan beda antara jumlah pengulangan dan jumlah tanggal. Dua pembaca konfigurasi lainnya, getStartDate() dan getEndDate(), menyediakan cara serupa untuk memeriksa penyusun sebuah period.

Melewati Tanggal Mulai dengan EXCLUDE_START_DATE #

Kadang tanggal mulai adalah “hari nol” yang tidak boleh ikut diproses — langganan aktif mulai besok, atau laporan disusun mulai hari setelah data dikumpulkan. PHP menyediakan konstanta DatePeriod::EXCLUDE_START_DATE sebagai argumen keempat konstruktor:

php
<?php
$mulai = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$berakhir = new DateTimeImmutable("2026-01-19 08:00:00", new DateTimeZone("Asia/Jakarta"));

$semua = new DatePeriod($mulai, new DateInterval("P1D"), $berakhir);
$tanpaAwal = new DatePeriod(
    $mulai,
    new DateInterval("P1D"),
    $berakhir,
    DatePeriod::EXCLUDE_START_DATE
);

echo "Dengan tanggal mulai: " . iterator_count($semua) . " tanggal\n";
echo "Tanpa tanggal mulai: " . iterator_count($tanpaAwal) . " tanggal\n";

foreach ($tanpaAwal as $tanggal) {
    echo $tanggal->format("Y-m-d") . "\n";
}

Output yang Dihasilkan:

text
Dengan tanggal mulai: 4 tanggal
Tanpa tanggal mulai: 3 tanggal
2026-01-16
2026-01-17
2026-01-18

Dua period di atas dibangun dari bahan yang persis sama; satu-satunya perbedaan adalah flag pada baris 10. Period pertama menghasilkan empat tanggal (15-18 Januari), sedangkan period kedua hanya tiga — tanggal 15 dilewati dan iterasi dimulai dari 2026-01-16. Flag yang sama juga berlaku pada bentuk recurrences; perhatikan bahwa flag ini tidak menggeser batas akhir sedikit pun.

Studi Kasus: Hari Kerja dalam Rentang #

Mari kita rangkai seluruh materi menjadi satu kasus nyata: menyusun daftar hari kerja. Bahan bakunya adalah period harian dan pola N — penanda hari ISO-8601 yang diperkenalkan pada Chapter Date/Time Objects dengan Senin = 1 hingga Minggu = 7. Hari kerja berarti nilai N kurang dari 6:

php
<?php
$senin = new DateTimeImmutable("2026-01-12 08:00:00", new DateTimeZone("Asia/Jakarta"));
$sabtu = new DateTimeImmutable("2026-01-17 08:00:00", new DateTimeZone("Asia/Jakarta"));

$mingguKerja = new DatePeriod($senin, new DateInterval("P1D"), $sabtu);

foreach ($mingguKerja as $tanggal) {
    if ((int) $tanggal->format("N") < 6) {
        echo $tanggal->format("Y-m-d l") . "\n";
    }
}

Output yang Dihasilkan:

text
2026-01-12 Monday
2026-01-13 Tuesday
2026-01-14 Wednesday
2026-01-15 Thursday
2026-01-16 Friday

Perhatikan pemilihan batasnya: titik mulai Senin 12 Januari dan batas akhir Sabtu 17 Januari. Karena batas akhir bersifat eksklusif, iterasi mencakup Senin hingga Jumat secara penuh — lima baris keluaran dari 2026-01-12 Monday sampai 2026-01-16 Friday. Pemeriksaan N < 6 pada baris 8 lalu menyaring Sabtu dan Minggu, sebuah pertahanan ganda yang tetap berguna saat rentang dipindah ke tanggal berbeda. Pola “awal bulan ke awal bulan berikutnya” memakai logika yang sama untuk rentang sebulan:

php
<?php
$awalBulan = new DateTimeImmutable("2026-01-01 08:00:00", new DateTimeZone("Asia/Jakarta"));
$awalBulanBerikutnya = new DateTimeImmutable("2026-02-01 08:00:00", new DateTimeZone("Asia/Jakarta"));

$januari = new DatePeriod($awalBulan, new DateInterval("P1D"), $awalBulanBerikutnya);
$jumlahHariKerja = 0;

foreach ($januari as $tanggal) {
    if ((int) $tanggal->format("N") < 6) {
        $jumlahHariKerja++;
    }
}

echo "Hari kerja Januari 2026: {$jumlahHariKerja}\n";

Output yang Dihasilkan:

text
Hari kerja Januari 2026: 22

Batas 1 Februari yang eksklusif membuat iterasi mencakup tepat 31 hari bulan Januari tanpa perlu menghitung jumlah hari bulan itu. Hasil akhirnya 22 hari kerja — 31 hari dikurangi lima Sabtu dan empat Minggu. Untuk kalender kerja sungguhan yang mengenal hari libur nasional, tambahkan pemeriksaan keanggotaan terhadap daftar tanggal libur; kerangka perulangannya tetap sama persis.

Ringkasan #

DatePeriod mengubah tiga bahan — titik mulai, DateInterval, dan batas akhir atau jumlah perulangan — menjadi iterator siap pakai: batas akhir bersifat eksklusif sehingga rentang dapat dirantai, recurrences menghitung pengulangan setelah tanggal mulai, dan EXCLUDE_START_DATE melewati hari nol. Kombinasi period harian dengan pola N menyelesaikan kasus hari kerja tanpa satu pun perhitungan kalender manual. Seluruh contoh kita sejauh ini berpegang pada satu zona waktu Asia/Jakarta; aplikasi nyata berhadapan dengan pengguna di berbagai zona — itulah bahasan Chapter Timezone berikutnya.

Referensi #