Kotlin reference · Dates & times

Kotlin Date & Time Handling

The Kotlin answer to “how do I do dates?” in one place — the java.time vs kotlinx-datetime + kotlin.time choice, formatting, parsing, time zones, UTC, arithmetic, Unix epoch, the Android desugaring floor, and Multiplatform.

Which do I reach for?

Kotlin’s date story is a fork keyed to your target, and knowing which side you’re on is the whole game. Every code example below is tagged JVM · java.time or multiplatform so it’s never ambiguous.

java.time

The default on the JVM and Android — Java’s mature JSR-310 set (LocalDate, ZonedDateTime, Instant, Duration, DateTimeFormatter). Rich and battle-tested. Android needs API 26+ or desugaring.

kotlinx-datetime + kotlin.time

For Kotlin Multiplatform shared code (JS, Native, Wasm), where java.time doesn’t exist — or when you want a cleaner Kotlin-idiomatic API. The calendar API is still pre-1.0.

kotlin.time

The stdlib types — Duration, Clock, and (since Kotlin 2.3) Instant — usable everywhere. Reach for its Duration and measureTime for durations and elapsed timing on any target.

Not the legacy set

Avoid java.util.Date, Calendar, and SimpleDateFormat — mutable, error-prone, and SimpleDateFormat is not thread-safe. They’re what java.time replaced.

Formatting reference

The most-searched date answer: “how do I format the date?” On the JVM and Android the answer is java.time’s pattern-letter mini-language — pass a pattern to DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"). In multiplatform code, kotlinx-datetime deliberately favors a type-safe format DSL over pattern strings; the second table maps one to the other.

0 of 0 rows

Example output is the canonical instant — Monday, June 15, 2009 1:45:30.617 PM at offset -07:00 (i.e. 2009-06-15T20:45:30.617Z) — a ZonedDateTime in America/Los_Angeles formatted with Locale.US, so the wall clock reads 1:45:30 PM. Month/weekday names are locale-sensitive.

JVM — java.time.DateTimeFormatter pattern letters

Pattern Meaning Example
yyyy / yyYear of era, 4 / 2 digits2009 / 09
uProleptic year (signed; safe across BC/AD) — not y2009
YWeek-based year — differs from y near New Year2009
MMMMMonth, full nameJune
MMMMonth, abbreviated nameJun
MMMonth number, 2 digits — month, not minute06
MMonth number, no leading zero6
dd / dDay of month, 2 digits / no leading zero15 / 15
DDay of yearnot day of month166
EEEEDay of week, full nameMonday
EEEDay of week, abbreviatedMon
HH / HHour, 24-hour clock, 2 digits / no zero13 / 13
hh / hHour, 12-hour clock, 2 digits / no zero01 / 1
mm / mMinutes, 2 digits / no zero — minute, not month45 / 45
ss / sSeconds, 2 digits / no zero30 / 30
SSSFraction of second, 3 digits (milliseconds)617
aAM/PM of dayPM
VVTime-zone ID (IANA)America/Los_Angeles
zzzTime-zone namePDT
XXXOffset, ISO 8601 — Z when zero-07:00
xxxOffset — +00:00 when zero (never Z)-07:00
OLocalized zone-offsetGMT-7

The predefined ISO formatters. You rarely need a pattern for machine output: DateTimeFormatter.ISO_LOCAL_DATE gives 2009-06-15, ISO_INSTANT gives 2009-06-15T20:45:30.617Z, and ISO_OFFSET_DATE_TIME gives 2009-06-15T13:45:30.617-07:00. Build a DateTimeFormatter once and reuse it — it is immutable and thread-safe (unlike the legacy SimpleDateFormat). Full grammar: DateTimeFormatter pattern letters.

Multiplatform — the kotlinx-datetime format DSL

Unit java.time pattern kotlinx-datetime DSL call
Year, 4-digityyyyyear()
Month number, 2-digitMMmonthNumber()
Month, full nameMMMMmonthName(MonthNames.ENGLISH_FULL)
Day of month, 2-digitddday()
Day of week, fullEEEEdayOfWeek(DayOfWeekNames.ENGLISH_FULL)
Hour, 24-hour, 2-digitHHhour()
Minute, 2-digitmmminute()
Second, 2-digitsssecond()
Literal character(literal in the string)char('-')

So 2009-06-15 13:45 is DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") on the JVM, and on multiplatform: LocalDateTime.Format { year(); char('-'); monthNumber(); char('-'); day(); char(' '); hour(); char(':'); minute() }. kotlinx-datetime has no pattern-string language by default — the builder is the idiom, and Instant.toString() / LocalDate.Formats.ISO cover ISO output. If you must reuse a pattern string, byUnicodePattern("yyyy-MM-dd") is the opt-in escape hatch, and it maps to the same Unicode letters as the table above. Both models share the case traps: MM is month but mm is minute; HH is 24-hour but hh is 12-hour; and in java.time, y/Y/u and d/D are four different fields. DSL grammar: kotlinx-datetime format.

JSON & API timestamps

That 2009-06-15T20:45:30.617Z shape in JSON payloads is ISO 8601 in UTC (the trailing Z, “Zulu,” is the +00:00 offset). Kotlin’s Instant reads and writes it directly — Instant.parse in, toString() out — and both serialization libraries know the type.

Write — to an ISO string

val at: Instant =
    Instant.parse("2009-06-15T20:45:30.617Z")
at.toString()
// 2009-06-15T20:45:30.617Z — always UTC

Instant.toString() is ISO 8601 in UTC — the round-trip partner of parse.

Read — via a serializer

// kotlinx.serialization ≥ 1.9 knows kotlin.time.Instant:
@Serializable data class Event(val at: Instant)
Json.decodeFromString<Event>(text)
// or on the JVM: Jackson's JavaTimeModule

No Kind to guess, no reviver — the type carries its own meaning.

  • Instant and Clock moved to kotlin.time (stable in Kotlin 2.3) — update imports off kotlinx.datetime.Instant.
  • Serializing a kotlin.time.Instant needs kotlinx.serialization ≥ 1.9; older versions only knew the removed kotlinx.datetime.Instant.
  • Store an Instant (an absolute point), not a LocalDateTime — a wall value with no zone is ambiguous.
  • epochSeconds vs toEpochMilliseconds() — APIs disagree on the unit; ×1000 against Unix seconds.

Reference: kotlin.time.Instant and kotlinx.serialization.

Showing 0 of 0 answers

1 · The types & which to use

Which date/time types should I use — and what’s the fork?

Kotlin has two date stories, and which one is correct depends on your target. On the JVM · java.time side you get Java’s JSR-310 set: LocalDate, LocalTime, LocalDateTime (wall values, no zone), ZonedDateTime and OffsetDateTime (a moment with a zone/offset), Instant (an exact point), and Period / Duration for amounts. On the multiplatform side, kotlinx-datetime supplies LocalDate, LocalTime, LocalDateTime, TimeZone, UtcOffset, DatePeriod, DateTimePeriod, DateTimeUnit, Month, and DayOfWeek, paired with the kotlin.time stdlib types Instant, Clock, and Duration.

// JVM / Android — java.time
import java.time.*
val d: LocalDate = LocalDate.of(2009, 6, 15)

// Multiplatform — kotlinx-datetime + kotlin.time
import kotlinx.datetime.*
import kotlin.time.Clock
val now: Instant = Clock.System.now()

The rule of thumb: an absolute momentInstant (or ZonedDateTime); a wall date/timeLocalDate/LocalDateTime; an amountDuration (fixed) or Period/DatePeriod (calendar). And avoid the legacy java.util.Date, Calendar, and SimpleDateFormat — mutable and, in SimpleDateFormat’s case, not thread-safe.

Source: java.time package / kotlinx-datetime.

2 · Getting “now”

How do I get the current date/time?

The multiplatform-portable answer is Clock.System.now() (multiplatform, kotlin.time), which returns an Instant. On the JVM · java.time side, Instant.now() gives the moment and LocalDate.now() / LocalDateTime.now() give the wall value in the system zone. For testable code, inject a Clock rather than calling now() directly — then a test can hand in a fixed clock.

Clock.System.now()          // Instant — multiplatform
Instant.now()                 // java.time Instant (JVM)
LocalDate.now()               // today, system zone (JVM)

// Testable: take a Clock, don't call now() inside
class Service(val clock: Clock = Clock.System) {
    fun stamp() = clock.now()
}

Source: kotlin.time.Clock / Instant.now.

3 · Formatting

How do I format a date to a string?

Two models, per the reference above. On the JVM · java.time, build a DateTimeFormatter from a pattern and call format(...); the formatter is immutable and thread-safe, so declare it once and reuse it. In multiplatform code, use the type-safe Format { } builder, or toString() / LocalDate.Formats.ISO for ISO output.

// JVM — reuse the immutable formatter
val fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
zdt.format(fmt)                 // "2009-06-15 13:45"

// Multiplatform — the DSL builder
val f = LocalDateTime.Format {
    year(); char('-'); monthNumber(); char('-'); day()
}
f.format(ldt)                   // "2009-06-15"

No pattern-string language in kotlinx-datetime by default — the builder is the idiom, with byUnicodePattern as the opt-in escape hatch.

Source: DateTimeFormatter / kotlinx-datetime format.

4 · Parsing

How do I parse a date string?

ISO 8601 is the portable interchange format and every type parses it directly. On the JVM · java.time, LocalDate.parse / Instant.parse read ISO out of the box; for a custom layout pass a DateTimeFormatter, and a bad input throws DateTimeParseException. In multiplatform code, the same Format { } object that formats also parses.

LocalDate.parse("2009-06-15")          // ISO, JVM
Instant.parse("2009-06-15T20:45:30.617Z")

// Custom layout, JVM:
val f = DateTimeFormatter.ofPattern("MM/dd/yyyy")
LocalDate.parse("06/15/2009", f)

// Multiplatform — the DSL parses too:
LocalDate.parse("2009-06-15")          // kotlinx-datetime

Source: LocalDate.parse / kotlinx-datetime LocalDate.

5 · Time zones

How do I work with time zones?

Zones are IANA-based on both sides — ZoneId on the JVM · java.time, TimeZone in multiplatform code — so there is no Windows-vs-IANA zone-id tripwire (the one place Kotlin is simpler than C#). Read the system zone with ZoneId.systemDefault() / TimeZone.currentSystemDefault(), and project an Instant into a zone to get a wall value. Around DST, an hour can be ambiguous (fall-back) or skipped (spring-forward); the libraries resolve these deterministically.

// JVM
val z = ZoneId.of("America/Los_Angeles")
instant.atZone(z)               // ZonedDateTime

// Multiplatform
val tz = TimeZone.of("America/Los_Angeles")
instant.toLocalDateTime(tz)     // LocalDateTime in that zone

Source: ZoneId / kotlinx-datetime TimeZone.

6 · Converting to/from UTC

How do I convert to and from UTC?

An Instant is already an absolute point — UTC is just the zone you read it in. On the JVM · java.time, project it with atZone(ZoneOffset.UTC) / atOffset(...), or go back with ZonedDateTime.toInstant(). In multiplatform code, TimeZone.UTC and instant.toLocalDateTime(TimeZone.UTC) give you the wall clock in UTC, and localDateTime.toInstant(timeZone) converts back.

// JVM
instant.atOffset(ZoneOffset.UTC)     // OffsetDateTime at UTC

// Multiplatform
instant.toLocalDateTime(TimeZone.UTC) // 2009-06-15T20:45:30.617
ldt.toInstant(TimeZone.UTC)          // back to the Instant

Source: Instant / kotlinx-datetime toLocalDateTime.

7 · Arithmetic

How do I add or subtract time?

On the JVM · java.time, every type is immutable and has plusDays / plusMonths / plus(Period) / plus(Duration) returning a new value. In multiplatform code, kotlinx-datetime makes an explicitly-correct design choice: calendar arithmetic on an Instant requires a TimeZone, because “a day” isn’t a fixed span across a DST boundary. Adding a fixed Duration needs no zone; adding a calendar unit does.

// JVM
date.plusDays(1)                    // LocalDate + 1 day
instant.plus(Duration.ofHours(24))

// Multiplatform — a TimeZone is REQUIRED for calendar units
instant.plus(1, DateTimeUnit.DAY, timeZone)
date.plus(DatePeriod(months = 1))
instant.plus(24.hours)             // fixed Duration — no zone

The DST caveat: adding “1 day” can be 23 or 25 hours of elapsed time. Use a calendar unit for “same wall time tomorrow,” a Duration for “exactly 24 hours later.”

Source: LocalDate.plusDays / kotlinx-datetime plus.

8 · Differences between two dates

How do I get the difference between two dates?

On the JVM · java.time, Duration.between gives an exact elapsed span, Period.between gives a calendar breakdown (“1 month, 0 days”), and ChronoUnit.DAYS.between gives a whole-unit count. In multiplatform code, the until family does the same job: daysUntil, monthsUntil, yearsUntil, and periodUntil for a full breakdown.

// JVM
ChronoUnit.DAYS.between(a, b)   // 30
Period.between(a, b)            // P1M
Duration.between(t1, t2)        // exact elapsed

// Multiplatform
a.daysUntil(b)                    // 30
a.periodUntil(b)                  // DatePeriod(months=1)

Source: ChronoUnit / kotlinx-datetime daysUntil.

9 · Comparison & equality

How do I compare two dates — does == work?

Yes — and this is a clean contrast to JavaScript’s === reference-equality trap. Kotlin’s == calls equals(), and every java.time and kotlinx-datetime type has structural equals, so two values built from the same moment are equal. The types are also Comparable, so < / > and isBefore / isAfter / isEqual work. An Instant compares as an absolute point on the timeline, regardless of zone.

val a = Instant.parse("2009-06-15T20:45:30.617Z")
val b = Instant.parse("2009-06-15T20:45:30.617Z")
a == b                    // true — structural equality
a < b                     // false — Comparable
date1.isBefore(date2)     // java.time predicate

Source: LocalDate.isBefore / Instant.

10 · Min / max & “no value”

What are the bounds, and how do I represent “no date”?

On the JVM · java.time the extremes are LocalDate.MIN / MAX and Instant.MIN / MAX; in multiplatform code Instant.DISTANT_PAST / DISTANT_FUTURE and LocalDate.MIN / MAX. But the idiomatic Kotlin answer for “no value” is a nullable typeLocalDate? and null — not a sentinel like C#’s DateTime.MinValue or JavaScript’s Invalid Date. The type system, not a magic value, encodes absence.

var shippedOn: LocalDate? = null   // no date yet — no sentinel
shippedOn?.let { render(it) }         // only if present
val due = shippedOn ?: LocalDate.MAX  // explicit fallback

Source: Kotlin null safety / LocalDate.MIN.

11 · Unix / epoch time

How do I convert to/from a Unix timestamp? Is there a 2038 problem?

Instant is epoch-native. Build one with Instant.fromEpochMilliseconds(...) / fromEpochSeconds(...), and read it back with epochSeconds or toEpochMilliseconds(). The number-one interop bug is the seconds-vs-milliseconds mismatch — epochSeconds is seconds, toEpochMilliseconds() is milliseconds; ×1000 against a Unix-seconds API. On the JVM, System.currentTimeMillis() is the raw epoch-ms reading. There is no year-2038 problem: these are 64-bit.

Instant.fromEpochMilliseconds(1245098730617)
// → 2009-06-15T20:45:30.617Z
instant.epochSeconds              // 1245098730  (SECONDS)
instant.toEpochMilliseconds()     // 1245098730617  (MS)
System.currentTimeMillis()        // JVM epoch ms

Source: Instant.fromEpochMilliseconds.

12 · Precision & measuring elapsed time

How do I time an operation? (Why not Clock.System.now()?)

Use kotlin.time’s measureTime { } (or measureTimedValue { } when you also need the result), or TimeSource.Monotonic.markNow() for manual marks — all backed by a monotonic clock. Never use Clock.System.now() or System.currentTimeMillis() for elapsed time: the wall clock can jump backward on an NTP sync or a manual change, giving a negative or wrong duration. This is the same “what time is it?” vs “how long did that take?” split every language has.

import kotlin.time.*
val took: Duration = measureTime { doWork() }

// or manual marks:
val mark = TimeSource.Monotonic.markNow()
doWork()
val elapsed = mark.elapsedNow()   // monotonic
// Never: Clock.System.now() for elapsed — wall clock jumps

Source: measureTime / TimeSource.

13 · Immutability & thread-safety

Are the types safe to share across threads?

Yes — every java.time and kotlinx-datetime value type is immutable and thread-safe, and so is a DateTimeFormatter: build one and share it freely. The classic bug lives in the legacy API: SimpleDateFormat is not thread-safe, so a shared static instance corrupts under concurrent format/parse calls — a real production incident that DateTimeFormatter was designed to end.

// SAFE — share one immutable formatter
val ISO = DateTimeFormatter.ISO_LOCAL_DATE

// BUG — SimpleDateFormat is NOT thread-safe:
// a shared static instance corrupts under concurrency
// companion object { val f = SimpleDateFormat(...) }  ← don't

Source: DateTimeFormatter (“immutable and thread-safe”).

14 · Serialization

How do dates serialize (kotlinx.serialization, Jackson)?

With multiplatform kotlinx.serialization, a kotlin.time.Instant serializes to its ISO 8601 string out of the box — but you need kotlinx.serialization ≥ 1.9, the version that added support for the stdlib Instant after kotlinx-datetime 0.7.0 removed its own. On the JVM · java.time, register Jackson’s JavaTimeModule so Instant / LocalDate round-trip as ISO strings rather than numeric arrays.

// Multiplatform — kotlinx.serialization ≥ 1.9
@Serializable data class Event(val at: Instant)
Json.encodeToString(Event(instant))
// {"at":"2009-06-15T20:45:30.617Z"}

// JVM — Jackson
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())

Source: kotlinx.serialization / Jackson JavaTimeModule.

15 · Common recipes

Start of day, end of month, age, is-weekend, is-leap-year, next Monday?

The everyday one-liners — mostly JVM · java.time, whose TemporalAdjusters cover the calendar tricks; the kotlinx-datetime equivalents are noted where they differ.

// Start of day in a zone (java.time):
date.atStartOfDay(zone)

// End of month:
date.with(TemporalAdjusters.lastDayOfMonth())

// Age from a birthdate:
Period.between(birth, LocalDate.now()).years

// Is weekend:
date.dayOfWeek in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)

// Is leap year:
Year.isLeap(2009)                 // false  (kotlinx: isLeapYear(2009))

// Next Monday:
date.with(TemporalAdjusters.next(DayOfWeek.MONDAY))

Source: TemporalAdjusters / kotlinx-datetime isLeapYear.

16 · Android & Kotlin Multiplatform

Why doesn’t java.time work on old Android — and what runs in shared KMP code?

This is Kotlin’s signature date dimension. On Android, java.time requires API 26+ (Android 8.0). To use it below that floor, enable core-library desugaring in your Gradle build (the toolchain ships the backing classes in your APK); historically the ThreeTenABP backport did the same job. This is a top real-world Kotlin/Android foot-gun — the “Call requires API level 26” lint error.

// build.gradle.kts — desugar java.time below minSdk 26
compileOptions { isCoreLibraryDesugaringEnabled = true }
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.+")
}

For Kotlin Multiplatform, java.time is JVM-only, so shared code uses multiplatform kotlinx-datetime + kotlin.time. Under the hood each target backs the types differently: JVM delegates to java.time; JS uses the platform (and the emerging Temporal); Native/Wasm use bundled implementations. Your shared code sees one API across all of them.

Source: Android — Java 8+ API desugaring support. Related: Android versions (the API-26 context) and Java versions (where java.time arrived in Java 8).

The foot-guns, in one place

  • java.time on Android needs API 26+ or core-library desugaring — the “Call requires API level 26” error.
  • SimpleDateFormat is not thread-safe — use an immutable DateTimeFormatter, never a shared SimpleDateFormat.
  • MM is month but mm is minute; HH is 24-hour but hh is 12-hour.
  • In java.time, y / Y / u are three different years and d / D are day-of-month vs day-of-year.
  • kotlinx-datetime calendar arithmetic on an Instant requires a TimeZone — a day isn’t a fixed span.
  • Instant / Clock moved to kotlin.time (stable in Kotlin 2.3) — update imports; serialize with kotlinx.serialization ≥ 1.9.
  • epochSeconds vs toEpochMilliseconds() — mind the unit; ×1000 against Unix seconds.
  • kotlinx-datetime is pre-1.0 — its calendar API can still change; pin the version.
  • Never measure elapsed time with Clock.System.now() — use measureTime / TimeSource.Monotonic.
  • Represent “no date” with a nullable LocalDate?, not a sentinel like MIN/MAX.

Every answer links its primary source inline — the java.time documentation for the JVM/Android side, the kotlin.time stdlib and kotlinx-datetime for multiplatform, and the Android desugaring table. The kotlin.time stabilization timeline cross-links the Kotlin version reference. As of July 2026: kotlin.time.Instant / Clock are stable stdlib types (stabilized in Kotlin 2.3.0, Dec 2025), kotlinx-datetime is at 0.8.x and still pre-1.0, and its Instant/Clock moved to the stdlib in 0.7.0.

Mungomash LLC · More on Kotlin