Kotlin JVM Dates, Time and Time Zones

Choose a time type that matches the domain, convert instants through explicit zones, and make time-dependent code reproducible.

Lesson 40 of 43Intermediate9 min read

A timestamp, a birthday and a shop's opening time describe different kinds of information. Treating them all as one string or one number makes later calculations ambiguous. A timestamp identifies a moment on a timeline; a birthday is normally a calendar date; an opening time needs a local schedule and often a location's zone rules.

Review functions and the original testing lesson first. This guide uses the Java 8+ java.time API from Kotlin/JVM. These imports are JVM-specific. Shared multiplatform projects should choose an appropriate multiplatform time library and follow its own versioned API rather than copying Java imports into common code.

Choose the value before formatting it

Instant represents a point on the timeline and works well for event timestamps. LocalDate represents a date without a time of day or zone. LocalTime represents a local wall-clock time. LocalDateTime combines local date and time but still does not identify a unique global moment without additional zone or offset information.

ZonedDateTime combines local fields with a named ZoneId and the applicable offset. OffsetDateTime records an offset, but an offset alone does not describe a location's future daylight-saving rules. Use the domain's information explicitly: a scheduled meeting in a location and a historical receipt timestamp have different storage needs.

Formatting creates presentation text; it should not replace your internal model. Compare Instants when ordering global events, and compare LocalDates when evaluating date-only business rules. Do not compare localized display strings lexicographically and assume the result is chronological. Their field order and language can vary.

Calendar arithmetic differs from elapsed time

Duration models an elapsed amount in seconds and nanoseconds. Period models a date-based amount such as months or days. Adding one calendar day to a zoned schedule can differ from adding twenty-four elapsed hours when daylight-saving transitions change the local clock.

Similarly, a month is not a fixed number of seconds or thirty universally valid days. A billing rule saying one month later should use calendar operations with an explicit end-of-month policy. Inspect the resulting date for edge cases such as January thirty-first rather than silently converting a month to a guessed duration.

An appointment's local date and time can fall into a zone transition gap or overlap. The standard API has resolution rules, but a booking system may need a more explicit business decision. Explain ambiguous choices to the user instead of assuming every local wall-clock value maps to exactly one instant. Test representative transitions for the zones the application supports.

Parse carefully and control the clock

ISO parsing is useful for stable machine-facing formats. Invalid input can raise DateTimeParseException, which should become a useful validation result at the input boundary. Human-facing formats may need a selected Locale and strict parsing policy. Avoid assuming that a slash-separated date means the same order in every region.

Calls to Instant.now or LocalDate.now obtain time from a clock and, for local dates, a zone. Hard-coding those calls inside every function makes tests depend on midnight and the developer's machine. Pass a Clock into time-dependent logic. Clock.fixed provides a deterministic instant and zone for examples and tests.

Time-zone rules can change through political decisions and are supplied by the runtime's zone database. Keep supported runtimes updated and avoid replacing named zones with permanent guessed offsets. For elapsed performance measurements, a monotonic timing mechanism is preferable to subtracting wall-clock timestamps, which can be adjusted by the system.

Official references

The java.time package defines the major types. Read ZonedDateTime for transition behavior and Clock for replaceable time sources. A good time test names the instant, zone, calendar rule and boundary rather than depending on today's environment.

Kotlin JVM Dates, Time and Time Zones Examples

Each example shows the code, the output it produces and what each part does.

Example 1

One instant, two local clocks

Both displayed clocks describe the same instant. The formatting pattern uses uuuu for a proleptic year and XXX for an offset. The Z in the parsed ISO timestamp denotes UTC. This example fixes its input, so the expected output does not depend on the machine's current time or default zone.

Kotlin
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun main() {
    val instant = Instant.parse("2025-01-15T10:00:00Z")
    val format = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm XXX")
    println(instant.atZone(ZoneId.of("UTC")).format(format))
    println(instant.atZone(ZoneId.of("Asia/Karachi")).format(format))
    println(instant)
}
Output
2025-01-15 10:00 Z
2025-01-15 15:00 +05:00
2025-01-15T10:00:00Z

Kotlin JVM Dates, Time and Time Zones in Real Projects

Worked exercise: Calculate a date-only payment deadline

A shop gives customers seven calendar days to pay. Return the due LocalDate from a supplied Clock, then report whether an existing due date is already overdue. Today itself is not overdue under this explicitly chosen rule.

Kotlin
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId

fun paymentDue(clock: Clock): LocalDate = LocalDate.now(clock).plusDays(7)
fun isOverdue(due: LocalDate, clock: Clock): Boolean = due.isBefore(LocalDate.now(clock))

fun main() {
    val clock = Clock.fixed(Instant.parse("2025-01-15T20:30:00Z"), ZoneId.of("Asia/Karachi"))
    println(LocalDate.now(clock))
    println(paymentDue(clock))
    println(isOverdue(LocalDate.of(2025, 1, 15), clock))
    println(isOverdue(LocalDate.of(2025, 1, 16), clock))
}
Output
2025-01-16
2025-01-23
true
false

The local shop date is already January sixteen because the fixed instant is after midnight in Karachi. A UTC-based date would produce a different business answer. Tests should cover the boundary date, the preceding date and a clock near local midnight. State the shop's zone in configuration rather than inherit the server's default.

Kotlin JVM Dates, Time and Time Zones FAQ

Can LocalDateTime identify an event globally?

Not by itself. It contains local date and clock fields without a zone or offset. The same fields can describe different moments in different locations, and transition overlaps can make even one location ambiguous. Use an Instant for an event already anchored on the timeline, or retain the schedule's zone information when the local meaning matters. Converting to a global timestamp requires an explicit interpretation, not merely a prettier format string.

Why not store every birthday as midnight UTC?

A birthday is normally a date-only fact, not a moment. Converting an invented midnight instant for display in another zone can shift its calendar date and show the wrong birthday. LocalDate expresses the intended information without that artificial conversion. Store an actual birth timestamp separately when the domain genuinely needs it. Choosing a type that preserves meaning prevents errors that cannot be fixed reliably by adjusting the display format later.

Are one day and twenty-four hours always equivalent?

They serve different models. A date-based day advances the calendar, while a twenty-four-hour Duration advances elapsed time. In zones with daylight-saving changes, a local day can span a different elapsed duration. Use calendar operations for rules such as tomorrow at the same local time and duration operations for rules such as expire after exactly twenty-four hours. Write the policy explicitly and test transition dates instead of relying only on ordinary weekdays.

Why pass Clock when now is easy to call?

A Clock makes the source of time and its zone explicit. Production code can receive a system clock, while tests receive a fixed clock near a carefully chosen boundary. This avoids failures caused by a test crossing midnight or running on a server in another zone. It also allows you to test overdue rules without waiting. Passing the dependency is a small design choice that makes temporal behavior reviewable and reproducible.

Kotlin JVM Dates, Time and Time Zones Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

Which design makes a due-date test independent of the machine's current day?

Kotlin JVM Dates, Time and Time Zones Quiz

2 questions to check what you learned.

  1. Which type best represents a date-only birthday?

    • An arbitrary UTC midnight Instant.
    • LocalDate.
    • A formatted server timestamp string.
  2. What does converting an Instant with atZone do?

    • Changes the original event to a different moment.
    • Removes the need for zone rules.
    • Gives local fields and an offset for the same moment in the chosen zone.