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 / yy | Year of era, 4 / 2 digits | 2009 / 09 |
| u | Proleptic year (signed; safe across BC/AD) — not y | 2009 |
| Y | Week-based year — differs from y near New Year | 2009 |
| MMMM | Month, full name | June |
| MMM | Month, abbreviated name | Jun |
| MM | Month number, 2 digits — month, not minute | 06 |
| M | Month number, no leading zero | 6 |
| dd / d | Day of month, 2 digits / no leading zero | 15 / 15 |
| D | Day of year — not day of month | 166 |
| EEEE | Day of week, full name | Monday |
| EEE | Day of week, abbreviated | Mon |
| HH / H | Hour, 24-hour clock, 2 digits / no zero | 13 / 13 |
| hh / h | Hour, 12-hour clock, 2 digits / no zero | 01 / 1 |
| mm / m | Minutes, 2 digits / no zero — minute, not month | 45 / 45 |
| ss / s | Seconds, 2 digits / no zero | 30 / 30 |
| SSS | Fraction of second, 3 digits (milliseconds) | 617 |
| a | AM/PM of day | PM |
| VV | Time-zone ID (IANA) | America/Los_Angeles |
| zzz | Time-zone name | PDT |
| XXX | Offset, ISO 8601 — Z when zero | -07:00 |
| xxx | Offset — +00:00 when zero (never Z) | -07:00 |
| O | Localized zone-offset | GMT-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-digit | yyyy | year() |
| Month number, 2-digit | MM | monthNumber() |
| Month, full name | MMMM | monthName(MonthNames.ENGLISH_FULL) |
| Day of month, 2-digit | dd | day() |
| Day of week, full | EEEE | dayOfWeek(DayOfWeekNames.ENGLISH_FULL) |
| Hour, 24-hour, 2-digit | HH | hour() |
| Minute, 2-digit | mm | minute() |
| Second, 2-digit | ss | second() |
| Literal character | (literal in the string) | char('-') |
Nothing matches that search. .
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.
InstantandClockmoved tokotlin.time(stable in Kotlin 2.3) — update imports offkotlinx.datetime.Instant.- Serializing a
kotlin.time.Instantneeds kotlinx.serialization ≥ 1.9; older versions only knew the removedkotlinx.datetime.Instant. - Store an
Instant(an absolute point), not aLocalDateTime— a wall value with no zone is ambiguous. epochSecondsvstoEpochMilliseconds()— APIs disagree on the unit; ×1000 against Unix seconds.
Reference:
kotlin.time.Instant
and kotlinx.serialization.
Showing 0 of 0 answers
No answers match that topic. .
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 moment → Instant (or ZonedDateTime); a wall date/time → LocalDate/LocalDateTime; an amount → Duration (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 type — LocalDate? 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.timeon Android needs API 26+ or core-library desugaring — the “Call requires API level 26” error.SimpleDateFormatis not thread-safe — use an immutableDateTimeFormatter, never a sharedSimpleDateFormat.MMis month butmmis minute;HHis 24-hour buthhis 12-hour.- In
java.time,y/Y/uare three different years andd/Dare day-of-month vs day-of-year. - kotlinx-datetime calendar arithmetic on an
Instantrequires aTimeZone— a day isn’t a fixed span. Instant/Clockmoved tokotlin.time(stable in Kotlin 2.3) — update imports; serialize with kotlinx.serialization ≥ 1.9.epochSecondsvstoEpochMilliseconds()— mind the unit; ×1000 against Unix seconds.kotlinx-datetimeis pre-1.0 — its calendar API can still change; pin the version.- Never measure elapsed time with
Clock.System.now()— usemeasureTime/TimeSource.Monotonic. - Represent “no date” with a nullable
LocalDate?, not a sentinel likeMIN/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