kotlinintermediate

Extension Properties and Receiver Functions

Add properties and functions to existing classes: extension receivers, generic extensions, and DSL patterns.

kotlin
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// Extension properties
val String.isEmail: Boolean
    get() = matches(Regex("""[\w.+-]+@[\w-]+\.[\w.-]+"""))

val String.wordCount: Int
    get() = trim().split("""\s+""".toRegex()).filter { it.isNotEmpty() }.size

val String.titleCase: String
    get() = split(" ").joinToString(" ") { word ->
        word.replaceFirstChar { it.uppercase() }
    }

val Int.isEven: Boolean get() = this % 2 == 0
val Int.isOdd: Boolean get() = this % 2 != 0
val Int.factorial: Long
    get() {
        require(this >= 0) { "Factorial not defined for negative numbers" }
        return if (this <= 1) 1L else (2..this).fold(1L) { acc, i -> acc * i }
    }

// Duration extensions
val Int.seconds: Long get() = this * 1000L
val Int.minutes: Long get() = this * 60.seconds
val Int.hours: Long get() = this * 60.minutes
val Int.days: Long get() = this * 24.hours

// Date extensions
val LocalDate.isWeekend: Boolean
    get() = dayOfWeek.value >= 6

val LocalDateTime.formatted: String
    get() = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

// Collection extensions
val <T> List<T>.tail: List<T>
    get() = if (isEmpty()) emptyList() else subList(1, size)

val <T> List<T>.head: T?
    get() = firstOrNull()

fun <T> List<T>.second(): T? = getOrNull(1)

// Safe math extensions
fun Double.roundTo(decimals: Int): Double {
    var multiplier = 1.0
    repeat(decimals) { multiplier *= 10 }
    return kotlin.math.round(this * multiplier) / multiplier
}

// String extensions
fun String.truncate(maxLength: Int, suffix: String = "..."): String =
    if (length <= maxLength) this
    else take(maxLength - suffix.length) + suffix

fun String.removeHtml(): String =
    replace(Regex("<[^>]*>"), "")

fun String.toSlug(): String =
    lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "")
        .replace(Regex("\\s+"), "-")
        .trim('-')

// Generic extension
fun <T : Comparable<T>> T.clamp(min: T, max: T): T = when {
    this < min -> min
    this > max -> max
    else -> this
}

// Extension with receiver
fun <T> buildIf(condition: Boolean, builder: () -> T): T? =
    if (condition) builder() else null

// Nullable extensions
fun String?.orDefault(default: String = ""): String = this ?: default
fun <T> List<T>?.orEmpty(): List<T> = this ?: emptyList()

fun main() {
    // String extensions
    println("test@email.com isEmail: ${"test@email.com".isEmail}")
    println("not-email isEmail: ${"not-email".isEmail}")
    println("\"hello world\".wordCount: ${"hello world".wordCount}")
    println("\"hello world\".titleCase: ${"hello world".titleCase}")

    // Truncate
    val long = "This is a very long string that needs truncation"
    println("Truncated: ${long.truncate(20)}")

    // Slug
    println("Slug: ${"Hello World! This is Cool".toSlug()}")

    // Number extensions
    println("\n5.isOdd: ${5.isOdd}")
    println("6.isEven: ${6.isEven}")
    println("5! = ${5.factorial}")

    // Duration
    println("\n5.minutes = ${5.minutes}ms")
    println("2.hours = ${2.hours}ms")

    // Math
    println("\n3.14159.roundTo(2) = ${3.14159.roundTo(2)}")

    // Clamp
    println("150.clamp(0, 100) = ${150.clamp(0, 100)}")

    // List extensions
    val list = listOf(1, 2, 3, 4, 5)
    println("\nHead: ${list.head}")
    println("Tail: ${list.tail}")
    println("Second: ${list.second()}")

    // Date
    val now = LocalDateTime.now()
    println("\nFormatted: ${now.formatted}")
    println("Weekend: ${LocalDate.now().isWeekend}")

    // Nullable
    val nullStr: String? = null
    println("\nDefault: ${nullStr.orDefault("N/A")}")

    // HTML
    println("Clean: ${"<p>Hello <b>World</b></p>".removeHtml()}")
}

Use Cases

  • Adding utility methods to existing types
  • Domain-specific language sugar
  • Null-safe operations on nullable types

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.