Part of Github Copilot with Kotlin

Kotlin: Create a random date

Sandy LaneSandy Lane

Video: Kotlin: Create a random date by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Kotlin: Generate a Random Date Between Two Bounds

Convert both endpoints to epoch days, draw a random integer in that range, convert back to LocalDate. Six lines, fully reproducible if you seed the RNG.

A practical task: produce a random LocalDate between two bounds — say, "any day in 2024." Useful for seeding test databases, generating plausible dummy data, or building simulations.

The Copilot prompt

import java.time.LocalDate

// Generate a random LocalDate between startInclusive and endExclusive
fun randomDate(startInclusive: LocalDate, endExclusive: LocalDate): LocalDate {

Copilot completes:

import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom

fun randomDate(startInclusive: LocalDate, endExclusive: LocalDate): LocalDate {
  val startEpochDay = startInclusive.toEpochDay()
  val endEpochDay = endExclusive.toEpochDay()
  val randomDay = ThreadLocalRandom.current().nextLong(startEpochDay, endEpochDay)
  return LocalDate.ofEpochDay(randomDay)
}

Walkthrough

The trick: convert both LocalDate bounds to epoch days (number of days since 1970-01-01), pick a random integer in that range, convert back.

1. toEpochDay()LocalDate exposes a fast conversion to "days since the Unix epoch." January 1, 1970 → 0; January 2, 1970 → 1; etc. June 15, 2024 → about 19890.

2. nextLong(start, end)ThreadLocalRandom.current().nextLong(a, b) returns a random Long in [a, b) — inclusive lower, exclusive upper. That matches our function signature.

3. LocalDate.ofEpochDay(day) — the inverse of toEpochDay. Given a number of days since epoch, returns the corresponding LocalDate.

The whole thing is one round-trip through "days as numbers." Date math becomes integer math.

Why ThreadLocalRandom?

ThreadLocalRandom.current() is the preferred RNG in modern Java/Kotlin:

  • Thread-local. No contention between threads (unlike the global Random instance).
  • Fast. Designed for high-throughput.
  • Available since Java 7.

For Kotlin-style code, you could also use kotlin.random.Random:

import kotlin.random.Random

fun randomDate(startInclusive: LocalDate, endExclusive: LocalDate): LocalDate {
  val day = Random.nextLong(startInclusive.toEpochDay(), endExclusive.toEpochDay())
  return LocalDate.ofEpochDay(day)
}

Random.nextLong(a, b) has the same semantics. Choose based on context — ThreadLocalRandom if you're already in JVM/Java idioms; kotlin.random.Random for pure-Kotlin code or multiplatform.

Using the function

fun main() {
  val start = LocalDate.of(2024, 1, 1)
  val end = LocalDate.of(2025, 1, 1)   // exclusive

  repeat(5) {
    println(randomDate(start, end))
  }
}

Output (varies):

2024-07-23
2024-02-14
2024-11-08
2024-05-30
2024-09-17

Five random dates in 2024.

Reproducibility

For tests, you want the same "random" date every run. Seed an explicit Random:

import kotlin.random.Random

fun randomDate(startInclusive: LocalDate, endExclusive: LocalDate, rng: Random = Random): LocalDate {
  val day = rng.nextLong(startInclusive.toEpochDay(), endExclusive.toEpochDay())
  return LocalDate.ofEpochDay(day)
}

// In tests:
val rng = Random(seed = 42)
println(randomDate(start, end, rng))   // always the same
println(randomDate(start, end, rng))   // always the same second value

Default to the system RNG, but allow injection. Standard test-friendly Kotlin pattern.

Edge cases

Same start and end. endExclusive == startInclusive makes nextLong(x, x) throw IllegalArgumentException. Guard:

require(endExclusive.isAfter(startInclusive)) {
  "endExclusive must be after startInclusive"
}

Reversed bounds. If the user passes start = 2024-12-31, end = 2024-01-01, nextLong errors. The require above catches it.

Large ranges. Even from year 1 to year 9999, the epoch-day range fits easily in a Long (≈3.65 million days). No overflow concerns.

Random ZonedDateTime

Same trick for time-zoned dates with random time-of-day:

import java.time.*

fun randomDateTime(start: LocalDateTime, end: LocalDateTime, zone: ZoneId): ZonedDateTime {
  val startSec = start.atZone(zone).toEpochSecond()
  val endSec = end.atZone(zone).toEpochSecond()
  val randomSec = ThreadLocalRandom.current().nextLong(startSec, endSec)
  return Instant.ofEpochSecond(randomSec).atZone(zone)
}

Same idea: round-trip through epoch seconds.

Random duration

For a random Duration between two bounds:

import java.time.Duration

fun randomDuration(min: Duration, max: Duration): Duration {
  val nanos = ThreadLocalRandom.current().nextLong(min.toNanos(), max.toNanos())
  return Duration.ofNanos(nanos)
}

The pattern: convert to a numeric type that captures the range, randomize, convert back.

Common mistakes

Inclusive vs exclusive confusion. The convention [start, end) (inclusive lower, exclusive upper) matches Kotlin/Java idioms. If you want both inclusive, use nextLong(start, end + 1).

Forgetting toEpochDay() is Long, not Int. Years past 5,879,000 AD overflow Int. Stick with Long.

Using Math.random() and casting. (Math.random() * (end - start)).toLong() + start works but is awkward. nextLong(a, b) is cleaner.

No reproducibility for tests. Hardcoded Random is unseeded → fresh seed every run → flaky tests. Inject the RNG.

Time-of-day assumed. A LocalDate has no time component. Tests that compare random times need LocalDateTime or Instant.

Common Copilot habits

When asked for "random date":

  • Default: epoch-day technique with ThreadLocalRandom.
  • "Use kotlin.random.Random": switches to the Kotlin idiom.
  • "Make it deterministic for tests": Copilot adds an injected RNG parameter.
  • "Generate N random dates in 2024": Copilot wraps the helper in a List(n) { randomDate(...) }.

What's next

Episode 5: Capitalize the first letter of a string. A string-manipulation puzzle showing the right (and wrong) ways to do title-case conversion.

Recap

LocalDate.toEpochDay()LocalDate.ofEpochDay(day) makes date math integer math. ThreadLocalRandom.current().nextLong(start, end) for [start, end) random Long. Same trick for LocalDateTime/ZonedDateTime (epoch seconds) and Duration (nanos). For tests, inject a seeded Random. Guard against same-or-reversed bounds with require.

Next episode: capitalizing the first letter.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.