Ranges & Progressions

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"]
RangeProgression
DefinisiRentang antara dua nilaiUrutan nilai dengan langkah
Contoh1..101..10 step 2
Bisa iterasiHanya tipe tertentuSelalu
Keanggotaanin / !inin / !in
TipeIntRange, CharRange, dllIntProgression, 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 n setara dengan 0..n-1, tapi jauh lebih aman — tidak ada risiko underflow saat n = 0. Dengan 0..n-1 ketika n = 0, kamu mendapat 0..-1 yang menghasilkan range kosong secara tidak intuitif. Selalu gunakan until untuk 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)
forEach dan forEachIndexed tidak mendukung break atau continue — mereka menggunakan lambda. Jika kamu butuh menghentikan loop di tengah atau melewati iterasi tertentu, gunakan for loop biasa dengan range, atau gunakan first { }, find { }, atau any { } 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). until eksklusif di akhir (0 until 10 → 0 sampai 9). Gunakan until untuk range berbasis indeks — lebih aman dari 0..size-1.
  • downTo untuk iterasi mundur (10 downTo 1). Range biasa 10..1 tidak menghasilkan error tapi menghasilkan range kosong saat diiterasi — ini adalah jebakan yang umum.
  • step mengubah range menjadi progression dengan langkah kustom (1..20 step 3 → 1, 4, 7, …, 19). Bisa dikombinasikan dengan downTo.
  • in dan !in untuk pengecekan keanggotaan. Jauh lebih bersih dari x >= 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 daripada for (i in 0 until n) yang tidak memakai i.
  • coerceIn membatasi nilai dalam range; coerceAtLeast dan coerceAtMost untuk batasan satu sisi. Gunakan ini daripada if-else manual untuk clamping.
  • Range bisa dibuat pada tipe Comparable apapun — Char, String, atau kelas kustom yang mengimplementasikan Comparable. Custom progression bisa dibuat dengan mengimplementasikan Iterable dan operator rangeTo.
  • indices pada List/Array adalah shortcut untuk 0 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 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact