Ranges & Progressions #
Kotlin memiliki cara yang sangat ekspresif untuk merepresentasikan rentang nilai: 1..10, 'a'..'z', "apple".."mango". Ini bukan sekadar sintaks manis — range adalah tipe data nyata di Kotlin yang bisa diiterasi, dicek keanggotaannya, dan dikombinasikan dengan berbagai operator. Di baliknya, ada dua konsep yang berbeda: range (rentang nilai) dan progression (urutan nilai dengan langkah tertentu). Memahami keduanya membuka cara berpikir yang lebih deklaratif — daripada menulis for (i = 0; i < n; i++), kamu cukup menulis for (i in 0 until n). Artikel ini membahas seluruh ekosistem range dan progression di Kotlin, dari penggunaan dasar hingga custom progression dan pola idiomatik yang membuat kode lebih bersih.
Range vs Progression #
Sebelum masuk ke detail, penting memahami perbedaan mendasar antara keduanya.
flowchart TD
A["Range"] --> B["Rentang antara dua nilai\n(start dan endInclusive)\nContoh: 1..10"]
A --> C["Bisa dicek keanggotaan\n5 in 1..10 → true"]
A --> D["Bisa diiterasi jika tipenya\nmendukung (Int, Long, Char)"]
E["Progression"] --> F["Urutan nilai dengan langkah\n(start, end, step)\nContoh: 1..10 step 2"]
E --> G["Selalu bisa diiterasi\n→ 1, 3, 5, 7, 9"]
E --> H["Turunan dari Range\ndengan informasi step"]| Range | Progression | |
|---|---|---|
| Definisi | Rentang antara dua nilai | Urutan nilai dengan langkah |
| Contoh | 1..10 | 1..10 step 2 |
| Bisa iterasi | Hanya tipe tertentu | Selalu |
| Keanggotaan | in / !in | in / !in |
| Tipe | IntRange, CharRange, dll | IntProgression, dll |
Membuat Range #
Operator .. — Range Inklusif
#
Operator .. membuat range yang menyertakan kedua ujungnya (inklusif di kedua sisi).
val angka = 1..10 // 1, 2, 3, ..., 10 (10 termasuk)
val huruf = 'a'..'z' // 'a', 'b', ..., 'z'
val teks = "apple".."mango" // range String (hanya untuk perbandingan, tidak bisa diiterasi)
// Pengecekan keanggotaan
println(5 in 1..10) // true
println(11 in 1..10) // false
println('e' in 'a'..'z') // true
// Tipe yang dihasilkan
val r: IntRange = 1..10
val c: CharRange = 'a'..'z'
val l: LongRange = 1L..1000L
until — Range Eksklusif di Akhir
#
until membuat range yang tidak menyertakan nilai akhir. Ini sangat umum dipakai saat bekerja dengan indeks array atau list, karena indeks valid adalah 0 sampai size - 1.
val daftar = listOf("apel", "jeruk", "mangga", "durian")
// ANTI-PATTERN: pakai .. dengan size - 1, mudah salah
for (i in 0..daftar.size - 1) {
println(daftar[i])
}
// BENAR: until lebih jelas dan aman
for (i in 0 until daftar.size) {
println(daftar[i])
}
// Atau lebih idiomatik lagi: gunakan indices
for (i in daftar.indices) {
println("$i: ${daftar[i]}")
}
// until pada operasi umum
val batasEksklusif = 0 until 100 // 0, 1, ..., 99 (100 tidak termasuk)
println(99 in batasEksklusif) // true
println(100 in batasEksklusif) // false
0 until nsetara dengan0..n-1, tapi jauh lebih aman — tidak ada risiko underflow saatn = 0. Dengan0..n-1ketikan = 0, kamu mendapat0..-1yang menghasilkan range kosong secara tidak intuitif. Selalu gunakanuntiluntuk range berbasis indeks.
downTo — Range Menurun
#
downTo membuat range yang berjalan dari nilai besar ke nilai kecil. Range biasa (..) tidak bisa diiterasi mundur — 10..1 adalah range yang valid tapi kosong saat diiterasi.
// ANTI-PATTERN: 10..1 tidak bisa diiterasi (range kosong saat di-loop)
for (i in 10..1) {
println(i) // tidak pernah dieksekusi!
}
// BENAR: gunakan downTo untuk iterasi mundur
for (i in 10 downTo 1) {
println(i) // 10, 9, 8, ..., 1
}
// downTo dengan until tidak ada — gunakan downTo + stop manual
// atau: (1..10).reversed()
val mundur = (1..10).reversed() // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
// Countdown
for (i in 5 downTo 1) {
println("$i...")
}
println("Mulai!")
Progressions dengan step
#
step mengubah range menjadi progression dengan langkah kustom — bukan satu per satu, tapi melompat sejumlah nilai tertentu.
// Angka genap dari 0 sampai 20
for (i in 0..20 step 2) {
print("$i ") // 0 2 4 6 8 10 12 14 16 18 20
}
// Angka ganjil
for (i in 1..19 step 2) {
print("$i ") // 1 3 5 7 9 11 13 15 17 19
}
// Kombinasi downTo + step
for (i in 100 downTo 0 step 10) {
print("$i ") // 100 90 80 70 60 50 40 30 20 10 0
}
// step pada Char range
for (c in 'a'..'z' step 2) {
print("$c ") // a c e g i k m o q s u w y
}
// step harus positif — tidak ada negatif
// untuk mundur dengan step: downTo + step
for (i in 20 downTo 0 step 5) {
print("$i ") // 20 15 10 5 0
}
flowchart LR
A["1..10"] -->|"step 1 (default)"| B["1 2 3 4 5 6 7 8 9 10"]
A -->|"step 2"| C["1 3 5 7 9"]
A -->|"step 3"| D["1 4 7 10"]
E["10 downTo 1"] -->|"step 1"| F["10 9 8 7 6 5 4 3 2 1"]
E -->|"step 2"| G["10 8 6 4 2"]Range di Loop #
Ini adalah penggunaan range yang paling sering ditemui — sebagai kontrol iterasi di for loop.
Iterasi Standar #
// Loop sederhana
for (i in 1..5) print("$i ") // 1 2 3 4 5
for (i in 1 until 5) print("$i ") // 1 2 3 4
for (i in 5 downTo 1) print("$i ") // 5 4 3 2 1
// Loop dengan index pada collection
val buah = listOf("apel", "jeruk", "mangga")
// ANTI-PATTERN: loop manual dengan indeks
for (i in 0 until buah.size) {
println("$i: ${buah[i]}")
}
// BENAR: gunakan indices atau withIndex
for (i in buah.indices) {
println("$i: ${buah[i]}")
}
for ((index, nama) in buah.withIndex()) {
println("$index: $nama")
}
repeat — Alternatif untuk Loop Sederhana #
Saat kamu hanya butuh mengulang sesuatu N kali tanpa peduli indeksnya, repeat lebih ekspresif daripada for (i in 0 until n).
// ANTI-PATTERN: for loop dengan variabel yang tidak dipakai
for (i in 0 until 5) {
println("Halo!")
}
// BENAR: repeat lebih jelas intentnya
repeat(5) {
println("Halo!")
}
// repeat dengan indeks jika dibutuhkan
repeat(5) { i ->
println("Iterasi ke-$i")
}
forEachIndexed vs Range Loop #
val produk = listOf("Laptop", "Mouse", "Keyboard", "Monitor")
// Range loop dengan indeks
for (i in produk.indices) {
println("${i + 1}. ${produk[i]}")
}
// forEachIndexed: lebih idiomatik untuk collection
produk.forEachIndexed { index, nama ->
println("${index + 1}. $nama")
}
// Keduanya ekuivalen — pilih yang lebih jelas untuk konteksnya
// forEachIndexed lebih cocok saat sudah dalam konteks functional
// range loop lebih cocok saat butuh kontrol flow (break, continue)
forEachdanforEachIndexedtidak mendukungbreakataucontinue— mereka menggunakan lambda. Jika kamu butuh menghentikan loop di tengah atau melewati iterasi tertentu, gunakanforloop biasa dengan range, atau gunakanfirst { },find { }, atauany { }sesuai kebutuhan.
Range di when
#
Range bisa digunakan sebagai kondisi di when expression — ini adalah salah satu fitur yang membuat when Kotlin jauh lebih ekspresif dari switch Java.
// Klasifikasi nilai ujian
fun klasifikasiNilai(nilai: Int): String = when (nilai) {
in 90..100 -> "A — Sangat Baik"
in 80 until 90 -> "B — Baik"
in 70 until 80 -> "C — Cukup"
in 60 until 70 -> "D — Kurang"
in 0 until 60 -> "E — Tidak Lulus"
else -> "Nilai tidak valid"
}
// BMI classifier
fun kategoriBMI(bmi: Double): String = when {
bmi < 18.5 -> "Underweight"
bmi in 18.5..24.9 -> "Normal"
bmi in 25.0..29.9 -> "Overweight"
bmi >= 30.0 -> "Obese"
else -> "Tidak valid"
}
// Kategori usia
fun kategoriUsia(usia: Int): String = when (usia) {
in 0..12 -> "Anak-anak"
in 13..17 -> "Remaja"
in 18..25 -> "Dewasa Muda"
in 26..59 -> "Dewasa"
in 60..Int.MAX_VALUE -> "Lansia"
else -> "Tidak valid"
}
// Diskon berdasarkan jumlah pembelian
fun hitungDiskon(jumlah: Int): Double = when (jumlah) {
in 1..4 -> 0.0
in 5..9 -> 0.05
in 10..19 -> 0.10
in 20..49 -> 0.15
in 50..Int.MAX_VALUE -> 0.20
else -> 0.0
}
Range untuk Validasi #
Salah satu penggunaan range yang sangat praktis adalah validasi input — jauh lebih bersih dari kombinasi >= dan <=.
// ANTI-PATTERN: validasi dengan perbandingan eksplisit
fun validasiUsia(usia: Int): Boolean {
return usia >= 0 && usia <= 150
}
fun validasiSuhu(suhu: Double): Boolean {
return suhu >= -273.15 && suhu <= 1000.0
}
// BENAR: gunakan range untuk validasi
fun validasiUsia(usia: Int): Boolean = usia in 0..150
fun validasiSuhu(suhu: Double): Boolean = suhu in -273.15..1000.0
// Validasi dengan !in untuk kasus invalid
fun validasiPort(port: Int): Boolean = port in 1..65535
fun isPortInvalid(port: Int): Boolean = port !in 1..65535
// Validasi karakter
fun isHuruf(c: Char): Boolean = c in 'a'..'z' || c in 'A'..'Z'
fun isAngka(c: Char): Boolean = c in '0'..'9'
fun isAlphanumeric(c: Char): Boolean = isHuruf(c) || isAngka(c)
// Fungsi validasi yang lebih kaya
data class RentangValid(val min: Int, val max: Int) {
val range = min..max
fun valid(nilai: Int) = nilai in range
fun pesanError(nilai: Int) = "Nilai $nilai harus antara $min dan $max"
}
val rentangUsia = RentangValid(0, 120)
val rentangPort = RentangValid(1, 65535)
fun prosesInput(usia: Int, port: Int) {
require(rentangUsia.valid(usia)) { rentangUsia.pesanError(usia) }
require(rentangPort.valid(port)) { rentangPort.pesanError(port) }
// lanjut proses...
}
Range pada Tipe Non-Numerik #
Range tidak terbatas pada angka. Kotlin mendukung range pada Char dan String (untuk perbandingan), serta tipe apapun yang mengimplementasikan Comparable.
Char Range #
// Iterasi huruf
for (c in 'A'..'Z') {
print(c) // ABCDEFGHIJKLMNOPQRSTUVWXYZ
}
// Alphabet generator
val hurufKecil = ('a'..'z').toList()
// ['a', 'b', 'c', ..., 'z']
val hurufBesar = ('A'..'Z').toList()
// ['A', 'B', 'C', ..., 'Z']
// Angka sebagai Char
val digitChar = ('0'..'9').toList()
// ['0', '1', '2', ..., '9']
// Password generator sederhana
val karakter = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun buatPasswordAcak(panjang: Int): String {
return (1..panjang)
.map { karakter.random() }
.joinToString("")
}
// Validasi karakter dengan range
fun isVokal(c: Char): Boolean = c.lowercaseChar() in "aeiou" // trik dengan String
fun isKonsonan(c: Char): Boolean = c in 'a'..'z' && !isVokal(c)
String Range dan Comparable #
// String range: bisa dicek keanggotaan tapi tidak bisa diiterasi
val rentangBuah = "apel".."mangga"
println("jeruk" in rentangBuah) // true (perbandingan lexicographic)
println("semangka" in rentangBuah) // false ('s' > 'm')
// Comparable range: tipe apapun yang implement Comparable
data class Versi(val major: Int, val minor: Int) : Comparable<Versi> {
override fun compareTo(other: Versi): Int {
return if (major != other.major) major - other.major
else minor - other.minor
}
}
val versiDidukung = Versi(2, 0)..Versi(4, 9)
println(Versi(3, 5) in versiDidukung) // true
println(Versi(1, 9) in versiDidukung) // false
println(Versi(5, 0) in versiDidukung) // false
// Tanggal dengan LocalDate (kotlinx-datetime)
// val rentangLiburan = LocalDate(2024, 12, 24)..LocalDate(2025, 1, 1)
// val hariIni = LocalDate.now()
// val sedangLibur = hariIni in rentangLiburan
Operasi pada Range dan Progression #
Range dan progression punya beberapa fungsi utilitas yang berguna.
val range = 1..20
// Konversi ke List
val list = range.toList() // [1, 2, 3, ..., 20]
val listStep = (1..20 step 3).toList() // [1, 4, 7, 10, 13, 16, 19]
// Properti range
println(range.first) // 1
println(range.last) // 20
println(range.step) // 1 (IntProgression)
val prog = 1..20 step 3
println(prog.first) // 1
println(prog.last) // 19 (bukan 20, karena 20 tidak dalam progression)
println(prog.step) // 3
// isEmpty: range terbalik selalu kosong
println((5..1).isEmpty()) // true
println((1..5).isEmpty()) // false
// contains: sama dengan `in`
println(range.contains(10)) // true
println(10 in range) // true (ekuivalen)
// reversed
val rangeReversed = (1..10).reversed() // [10, 9, ..., 1]
// sum, average, count pada progression
val jumlah = (1..100).sum() // 5050
val rata = (1..10).average() // 5.5
val banyak = (1..20 step 2).count() // 10
// any, all, none
val adaYangBesar = (1..100).any { it > 90 } // true
val semuaPositif = (1..100).all { it > 0 } // true
val tidakAdaNol = (1..100).none { it == 0 } // true
Custom Progression #
Kotlin memungkinkan kamu membuat tipe progression sendiri untuk tipe kustom dengan mengimplementasikan Iterable dan operator rangeTo.
// Contoh: progression untuk tanggal sederhana
data class Tanggal(val hari: Int) : Comparable<Tanggal> {
override fun compareTo(other: Tanggal) = hari - other.hari
operator fun plus(n: Int) = Tanggal(hari + n)
}
class TanggalProgression(
override val start: Tanggal,
override val endInclusive: Tanggal,
val langkah: Int = 1
) : Iterable<Tanggal>, ClosedRange<Tanggal> {
override fun iterator(): Iterator<Tanggal> = object : Iterator<Tanggal> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next(): Tanggal {
val result = current
current = current + langkah
return result
}
}
}
// Operator rangeTo untuk Tanggal
operator fun Tanggal.rangeTo(lain: Tanggal) = TanggalProgression(this, lain)
// Extension infix untuk step
infix fun TanggalProgression.langkah(n: Int) =
TanggalProgression(start, endInclusive, n)
// Penggunaan
val awal = Tanggal(1)
val akhir = Tanggal(31)
for (tgl in awal..akhir) {
println("Hari ke-${tgl.hari}")
}
for (tgl in awal..akhir langkah 7) {
println("Minggu ke-${(tgl.hari - 1) / 7 + 1}: hari ${tgl.hari}")
}
Pola Idiomatik #
Beberapa pola rangkuman penggunaan range yang sering muncul dalam kode produksi.
Sampling dan Pembagian Data #
val data = (1..1000).toList()
// Ambil sampel setiap N elemen
val sampel = data.filterIndexed { index, _ -> index % 10 == 0 }
// [1, 11, 21, 31, ..., 991] (setiap elemen ke-10)
// Bagi data menjadi batch
val batch = data.chunked(100)
// [[1..100], [101..200], ..., [901..1000]]
// Ambil 10% pertama dan 10% terakhir
val awal10persen = data.take(data.size / 10)
val akhir10persen = data.takeLast(data.size / 10)
Fibonacci dengan Range #
// Generator Fibonacci menggunakan progression
fun fibonacci(n: Int): List<Long> {
if (n <= 0) return emptyList()
if (n == 1) return listOf(1L)
val hasil = mutableListOf(1L, 1L)
for (i in 2 until n) {
hasil.add(hasil[i - 1] + hasil[i - 2])
}
return hasil
}
println(fibonacci(10))
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Clamp — Batasi Nilai dalam Range #
// Clamp: pastikan nilai ada dalam batas
fun Int.coerceIn(min: Int, max: Int) = when {
this < min -> min
this > max -> max
else -> this
}
// Kotlin sudah punya coerceIn bawaan!
val nilai = 150
val dibatasi = nilai.coerceIn(0, 100) // 100
val normal = 75.coerceIn(0, 100) // 75
val negatif = (-5).coerceIn(0, 100) // 0
// coerceIn juga menerima range langsung
val range = 0..100
val hasilClamp = nilai.coerceIn(range) // 100
// coerceAtLeast dan coerceAtMost
val minSaja = (-5).coerceAtLeast(0) // 0
val maxSaja = 150.coerceAtMost(100) // 100
Tabel Perkalian #
// Tabel perkalian dengan nested range
fun cetakTabelPerkalian(batas: Int = 10) {
for (i in 1..batas) {
for (j in 1..batas) {
print("${(i * j).toString().padStart(4)}")
}
println()
}
}
// Versi functional
val tabel = (1..10).map { i ->
(1..10).map { j -> i * j }
}
Perbandingan dengan Pendekatan Java #
Range Kotlin jauh lebih ekspresif dibanding loop Java konvensional.
// Java style (masih valid di Kotlin tapi tidak idiomatik)
// ANTI-PATTERN:
var i = 0
while (i < 10) {
println(i)
i++
}
// ANTI-PATTERN:
for (i in 0..9) { // padahal maksudnya 0 sampai 9
println(i)
}
// BENAR — idiomatik Kotlin:
for (i in 0 until 10) { // jelas: 0 sampai eksklusif 10
println(i)
}
repeat(10) { i -> // jika hanya iterasi tanpa logika kompleks
println(i)
}
// Kondisi berbasis range — Java butuh && yang verbose
// ANTI-PATTERN:
if (nilai >= 80 && nilai <= 100) println("Baik")
// BENAR:
if (nilai in 80..100) println("Baik")
flowchart TD
A{Apa yang butuh\ndilakukan?} --> B["Iterasi maju\n1 per 1"]
A --> C["Iterasi mundur"]
A --> D["Iterasi dengan\nlompatan"]
A --> E["Cek keanggotaan\nnilai dalam rentang"]
A --> F["Klasifikasi nilai\ndi when"]
A --> G["Validasi batas\nnilai"]
B --> B1["for (i in start..end)\natau\nfor (i in start until end)"]
C --> C1["for (i in end downTo start)"]
D --> D1["for (i in start..end step n)\natau\nfor (i in end downTo start step n)"]
E --> E1["nilai in start..end\nnilai !in start..end"]
F --> F1["when (x) { in a..b -> ... }"]
G --> G1["nilai.coerceIn(min, max)\nrequire(nilai in min..max)"]Ringkasan #
..membuat range inklusif di kedua ujung (1..10→ 1 sampai 10 termasuk).untileksklusif di akhir (0 until 10→ 0 sampai 9). Gunakanuntiluntuk range berbasis indeks — lebih aman dari0..size-1.downTountuk iterasi mundur (10 downTo 1). Range biasa10..1tidak menghasilkan error tapi menghasilkan range kosong saat diiterasi — ini adalah jebakan yang umum.stepmengubah range menjadi progression dengan langkah kustom (1..20 step 3→ 1, 4, 7, …, 19). Bisa dikombinasikan dengandownTo.indan!inuntuk pengecekan keanggotaan. Jauh lebih bersih darix >= a && x <= b— gunakan ini untuk validasi dan kondisi.when+ range adalah pengganti switch-case yang sangat ekspresif untuk klasifikasi nilai:in 80..100 -> "Baik".repeat(n) { }untuk iterasi tanpa keperluan indeks — lebih jelas intentnya daripadafor (i in 0 until n)yang tidak memakaii.coerceInmembatasi nilai dalam range;coerceAtLeastdancoerceAtMostuntuk batasan satu sisi. Gunakan ini daripada if-else manual untuk clamping.- Range bisa dibuat pada tipe
Comparableapapun —Char,String, atau kelas kustom yang mengimplementasikanComparable. Custom progression bisa dibuat dengan mengimplementasikanIterabledan operatorrangeTo.indicespada List/Array adalah shortcut untuk0 until size— selalu gunakan ini daripada mengalkulasi range secara manual.- Progression mendukung operasi collection seperti
sum(),average(),count(),any {},all {},toList()— tidak perlu konversi manual ke List terlebih dahulu.
← Sebelumnya: Scope Functions Berikutnya: Higher-Order Functions →