Okay, here is a detailed article covering Kotlin DateTime handling, aiming for approximately 5000 words.
Kotlin DateTime Tutorial: Mastering Dates and Times
Handling dates and times is a fundamental requirement in almost any software application. Whether you’re scheduling tasks, logging events, calculating durations, displaying user-specific information, or working with data that has temporal relevance, you need a robust and reliable way to manage date and time information. Historically, this has been a notoriously tricky area in programming, plagued by confusing APIs, mutability issues, and complex timezone handling.
Fortunately, the Java ecosystem (which Kotlin heavily leverages on the JVM and Android) underwent a major overhaul with the introduction of the java.time
package (JSR-310) in Java 8. This package provides a vastly improved, immutable, and more intuitive API for date and time manipulation. Kotlin, with its excellent Java interoperability, fully embraces this modern API. Additionally, for Kotlin Multiplatform projects or those preferring a pure Kotlin solution, the kotlinx-datetime
library offers a native alternative.
This comprehensive tutorial will guide you through effectively handling dates and times in Kotlin, focusing primarily on the standard java.time
package due to its widespread use and inclusion in the Java standard library (available since Java 8 and largely available on modern Android versions via desugaring or native support). We will also touch upon the kotlinx-datetime
library.
Target Audience: This tutorial is aimed at Kotlin developers (beginner to intermediate) who want to understand and effectively use modern date and time APIs in their applications (primarily JVM/Android). Basic familiarity with Kotlin syntax is assumed.
What We Will Cover:
- Why Modern DateTime APIs? (Brief look at legacy issues)
- Setting Up: (Dependencies, Android considerations)
- Core Concepts of
java.time
: (Immutability, Time Zones, Class Overview) - Key Classes:
LocalDate
: Representing dates without time or timezone.LocalTime
: Representing time without date or timezone.LocalDateTime
: Representing date and time without timezone.Instant
: Representing a point on the timeline (UTC).ZonedDateTime
: Representing date and time with a specific timezone.OffsetDateTime
: Representing date and time with a fixed offset from UTC.Duration
: Representing a time-based amount (seconds, nanoseconds).Period
: Representing a date-based amount (years, months, days).ZoneId
&ZoneOffset
: Representing timezones and offsets.Clock
: Abstracting the current time for testability.
- Common Operations:
- Getting the Current Date/Time.
- Creating Specific Dates/Times.
- Parsing Strings into Date/Time Objects.
- Formatting Date/Time Objects into Strings (
DateTimeFormatter
). - Manipulating Dates and Times (Adding, Subtracting, Modifying).
- Comparing Dates and Times.
- Calculating Differences (Durations and Periods).
- Handling Time Zones Effectively.
- Introduction to
kotlinx-datetime
: (Multiplatform alternative) - Best Practices:
- Conclusion:
1. Why Modern DateTime APIs? (A Brief History Lesson)
Before Java 8, developers primarily relied on java.util.Date
and java.util.Calendar
. These APIs suffered from several significant drawbacks:
- Mutability:
java.util.Date
objects were mutable. This meant that passing aDate
object to a method could result in its value being changed unexpectedly, leading to bugs that were hard to track down. Imagine passing a start date to a function, only to have that function inadvertently modify it. - Confusing API Design: The API was often unintuitive. For example, months in
java.util.Calendar
were 0-indexed (January = 0), while days were 1-indexed, a common source of errors. Methods had confusing names, and performing simple operations often required cumbersome code. - Poor Time Zone Handling: Managing time zones with
Calendar
was complex and error-prone. - Lack of Type Safety:
java.util.Date
represented a specific instant in time (milliseconds since the epoch) but was often misused to represent just a date or just a time. There wasn’t a clear separation of concepts like date-only, time-only, or date-with-timezone. - Not Thread-Safe:
SimpleDateFormat
, used for formatting and parsing, was notoriously not thread-safe, requiring developers to implement synchronization or use thread-local instances in concurrent environments.
These issues led to the widespread adoption of third-party libraries like Joda-Time. Recognizing the need for a better standard solution, the JSR-310 expert group, led by Stephen Colebourne (the creator of Joda-Time), designed the java.time
package, heavily inspired by Joda-Time.
Advantages of java.time
:
- Immutability: All core classes (
LocalDate
,LocalTime
,LocalDateTime
, etc.) are immutable. Operations like adding a day return a new instance, leaving the original unchanged. This makes the API inherently thread-safe and easier to reason about. - Clarity and Separation of Concerns: Provides distinct classes for different concepts:
LocalDate
for dates,LocalTime
for times,LocalDateTime
for date-times,ZonedDateTime
for timezoned date-times,Instant
for machine timestamps,Period
for date-based durations, andDuration
for time-based durations. - Fluent API: Offers a clean, fluent API (e.g.,
now.plusDays(1).minusHours(2)
). - Improved Time Zone Handling: Robust support for time zones (
ZoneId
,ZonedDateTime
) makes handling complexities like Daylight Saving Time (DST) more manageable. - Better Performance: Generally offers better performance compared to the legacy APIs.
Given these advantages, using java.time
(or kotlinx-datetime
) is strongly recommended for all new Kotlin development.
2. Setting Up
Kotlin/JVM Projects (Gradle/Maven):
The java.time
package is part of the Java Standard Library since Java 8. If your project targets Java 8 or higher, you don’t need any additional dependencies to use it. Your standard Kotlin project setup is sufficient.
“`kotlin
// No extra dependencies needed for java.time if using Java 8+
// build.gradle.kts example snippet
plugins {
kotlin(“jvm”) version “1.9.20” // Or your Kotlin version
application // Or other relevant plugin
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin(“stdlib”))
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8 // Or higher
targetCompatibility = JavaVersion.VERSION_1_8 // Or higher
}
“`
Android Projects:
Android’s adoption of Java 8+ features, including java.time
, has evolved.
- Android API Level 26 (Android 8.0 Oreo) and higher: The
java.time
APIs are available directly. - Below API Level 26: You need to enable API desugaring. This feature, integrated into the Android Gradle plugin, brings newer Java language APIs (including most of
java.time
) to older Android versions by rewriting your code and including a support library.
To enable desugaring (if needed, check current Android Gradle Plugin documentation for the latest recommendations):
“`kotlin
// app/build.gradle.kts
android {
compileOptions {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
dependencies {
// Add the desugaring library dependency
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") // Use the latest version
}
}
“`
Always consult the official Android Developers documentation on Java 8+ API desugaring for the most up-to-date instructions.
kotlinx-datetime
Dependency:
If you choose to use the multiplatform kotlinx-datetime
library, you need to add its dependency:
“`kotlin
// build.gradle.kts (for a JVM project)
dependencies {
implementation(“org.jetbrains.kotlinx:kotlinx-datetime:0.5.0”) // Use the latest version
}
// For multiplatform projects, add it to the commonMain source set dependencies
// commonMain source set in build.gradle.kts
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(“org.jetbrains.kotlinx:kotlinx-datetime:0.5.0”) // Use the latest version
}
}
}
}
“`
For the rest of this tutorial, we will primarily focus on java.time
, assuming a compatible environment (Java 8+ JVM or appropriately configured Android project).
3. Core Concepts of java.time
Before diving into specific classes, let’s understand some fundamental principles:
- Immutability: As mentioned, instances of
java.time
classes cannot be changed after creation. Any operation that seems to modify an object (like adding days) actually returns a new object with the modified value. This eliminates a whole class of bugs related to shared mutable state and makes the API thread-safe by design. - Time Zones: Time zones are critical. A simple time like “9:00 AM” is ambiguous without knowing where it’s 9:00 AM. Is it New York, London, or Tokyo? The
java.time
API provides robust classes (ZoneId
,ZonedDateTime
) to handle this complexity, including rules for Daylight Saving Time (DST). Always be conscious of whether you need timezone information. Storing timestamps in UTC (Instant
) and converting to a local timezone only for display is a common best practice. - Separation of Concerns: The API distinguishes between different temporal concepts:
- Human Time: Dates and times as humans perceive them (
LocalDate
,LocalTime
,LocalDateTime
,ZonedDateTime
). - Machine Time: A specific point on the global timeline, typically represented as an offset from a fixed point (the epoch: January 1, 1970, UTC).
Instant
is the primary class for this. - Amounts of Time: Durations (
Duration
– second/nanosecond precision) and Periods (Period
– year/month/day precision).
- Human Time: Dates and times as humans perceive them (
- Based on ISO 8601: The API design and default string representations largely follow the ISO 8601 standard for representing dates and times (e.g.,
YYYY-MM-DD
for dates,HH:MM:SS
for times, combined formats likeYYYY-MM-DDTHH:MM:SSZ
). This promotes consistency and interoperability. - Static Factory Methods: Objects are typically created using static factory methods like
now()
,of(...)
, andparse(...)
rather than constructors. This allows for more flexible object creation and caching strategies (though caching is less relevant for the core date/time classes themselves).
4. Key Classes
Let’s explore the most important classes in the java.time
package with Kotlin examples.
(Note: For brevity, explicit imports like import java.time.LocalDate
will often be omitted in code snippets, but remember to include them in your actual code.)
java.time.LocalDate
Represents a date (year, month, day) without time or timezone information. Useful for representing birthdays, holidays, or any event where the specific time of day or timezone isn’t relevant.
“`kotlin
import java.time.LocalDate
import java.time.Month
import java.time.temporal.ChronoUnit
fun main() {
// 1. Get the current date
val today: LocalDate = LocalDate.now()
println(“Today: $today”) // Example output: Today: 2023-10-27
// 2. Create a specific date using of()
val independenceDay: LocalDate = LocalDate.of(1776, Month.JULY, 4)
// Alternative using integer for month:
val specificDate: LocalDate = LocalDate.of(2024, 1, 31) // Year, Month (1-12), Day
println("Independence Day: $independenceDay") // Independence Day: 1776-07-04
println("Specific Date: $specificDate") // Specific Date: 2024-01-31
// 3. Parse a date string (ISO 8601 format YYYY-MM-DD is default)
val parsedDate: LocalDate = LocalDate.parse("2023-12-25")
println("Parsed Date: $parsedDate") // Parsed Date: 2023-12-25
// 4. Get components of the date
val year: Int = today.year
val month: Month = today.month // Returns the Month enum
val monthValue: Int = today.monthValue // Returns the month as int (1-12)
val dayOfMonth: Int = today.dayOfMonth
val dayOfYear: Int = today.dayOfYear
val dayOfWeek = today.dayOfWeek // Returns the DayOfWeek enum (e.g., FRIDAY)
println("Year: $year, Month: $month ($monthValue), Day: $dayOfMonth")
println("Day of Year: $dayOfYear, Day of Week: $dayOfWeek")
// 5. Manipulate the date (returns a new instance)
val tomorrow: LocalDate = today.plusDays(1)
val nextMonth: LocalDate = today.plusMonths(1)
val lastYear: LocalDate = today.minusYears(1)
val tenWeeksLater = today.plus(10, ChronoUnit.WEEKS) // Using ChronoUnit
println("Tomorrow: $tomorrow")
println("Next Month: $nextMonth")
println("Last Year Same Day: $lastYear")
println("Ten Weeks Later: $tenWeeksLater")
// 6. Modify specific fields using with() (returns a new instance)
val firstDayOfYear: LocalDate = today.withDayOfYear(1)
val lastDayOfMonth: LocalDate = today.withDayOfMonth(today.lengthOfMonth())
println("First day of this year: $firstDayOfYear")
println("Last day of this month: $lastDayOfMonth")
// 7. Compare dates
val isBefore: Boolean = independenceDay.isBefore(today)
val isAfter: Boolean = specificDate.isAfter(today)
val isEqual: Boolean = today.isEqual(LocalDate.now()) // Could be false if run across midnight
println("Is Independence Day before today? $isBefore")
println("Is 2024-01-31 after today? $isAfter")
// 8. Check leap year
val isLeap: Boolean = today.isLeapYear
println("Is $year a leap year? $isLeap")
}
“`
java.time.LocalTime
Represents a time (hour, minute, second, nanosecond) without date or timezone information. Useful for representing opening/closing times, daily alarms, etc.
“`kotlin
import java.time.LocalTime
import java.time.temporal.ChronoUnit
fun main() {
// 1. Get the current time
val now: LocalTime = LocalTime.now()
println(“Current Time: $now”) // Example output: Current Time: 15:30:45.123456789
// 2. Create a specific time using of()
val specificTime: LocalTime = LocalTime.of(10, 15) // Hour, Minute
val timeWithSeconds: LocalTime = LocalTime.of(10, 15, 30) // Hour, Minute, Second
val timeWithNanos: LocalTime = LocalTime.of(10, 15, 30, 987654321) // H, M, S, Nanos
println("Specific Time: $specificTime") // Specific Time: 10:15
println("Time with Seconds: $timeWithSeconds") // Time with Seconds: 10:15:30
println("Time with Nanos: $timeWithNanos") // Time with Nanos: 10:15:30.987654321
// 3. Parse a time string (ISO 8601 format HH:MM:SS.nano is default)
val parsedTime: LocalTime = LocalTime.parse("23:59:59.999")
println("Parsed Time: $parsedTime") // Parsed Time: 23:59:59.999
// 4. Get components of the time
val hour: Int = now.hour
val minute: Int = now.minute
val second: Int = now.second
val nano: Int = now.nano
println("Hour: $hour, Minute: $minute, Second: $second, Nano: $nano")
// 5. Manipulate the time (returns a new instance)
val oneHourLater: LocalTime = now.plusHours(1)
val thirtyMinutesBefore: LocalTime = now.minusMinutes(30)
val truncatedToMinutes: LocalTime = now.truncatedTo(ChronoUnit.MINUTES) // Sets smaller units to 0
println("One hour later: $oneHourLater")
println("Thirty minutes before: $thirtyMinutesBefore")
println("Truncated to minutes: $truncatedToMinutes") // Example: 15:30
// 6. Modify specific fields using with() (returns a new instance)
val timeAtHour18: LocalTime = now.withHour(18)
val timeAtZeroSeconds: LocalTime = now.withSecond(0).withNano(0)
println("Time at hour 18: $timeAtHour18")
println("Time at zero seconds/nanos: $timeAtZeroSeconds")
// 7. Compare times
val openingTime = LocalTime.of(9, 0)
val closingTime = LocalTime.of(17, 30)
val isBeforeOpening = now.isBefore(openingTime)
val isAfterClosing = now.isAfter(closingTime)
println("Is current time before 09:00? $isBeforeOpening")
println("Is current time after 17:30? $isAfterClosing")
// 8. Constants for midnight and noon
println("Midnight: ${LocalTime.MIDNIGHT}") // 00:00
println("Noon: ${LocalTime.NOON}") // 12:00
println("Min Time: ${LocalTime.MIN}") // 00:00
println("Max Time: ${LocalTime.MAX}") // 23:59:59.999999999
}
“`
java.time.LocalDateTime
Represents a combination of date and time (year, month, day, hour, minute, second, nanosecond) without any timezone information. Useful for storing timestamps when the timezone context is handled elsewhere or is irrelevant (e.g., a user’s scheduled event in their local time before timezone conversion).
“`kotlin
import java.time.LocalDateTime
import java.time.Month
import java.time.LocalDate
import java.time.LocalTime
fun main() {
// 1. Get the current date and time
val currentDateTime: LocalDateTime = LocalDateTime.now()
println(“Current DateTime: $currentDateTime”) // Example: 2023-10-27T15:45:10.123456789
// 2. Create a specific LocalDateTime using of()
val specificDateTime: LocalDateTime = LocalDateTime.of(2024, Month.JANUARY, 1, 10, 30, 0)
println("Specific DateTime: $specificDateTime") // 2024-01-01T10:30
// 3. Combine LocalDate and LocalTime
val datePart: LocalDate = LocalDate.of(2023, 11, 20)
val timePart: LocalTime = LocalTime.of(14, 0)
val combinedDateTime1: LocalDateTime = LocalDateTime.of(datePart, timePart)
val combinedDateTime2: LocalDateTime = datePart.atTime(timePart)
val combinedDateTime3: LocalDateTime = datePart.atTime(14, 0, 0) // Overload
val combinedDateTime4: LocalDateTime = timePart.atDate(datePart)
println("Combined 1: $combinedDateTime1") // 2023-11-20T14:00
println("Combined 2 (date.atTime(time)): $combinedDateTime2")
println("Combined 3 (date.atTime(h,m,s)): $combinedDateTime3")
println("Combined 4 (time.atDate(date)): $combinedDateTime4")
// 4. Parse a LocalDateTime string (ISO 8601 format YYYY-MM-DDTHH:MM:SS.nano is default)
val parsedDateTime: LocalDateTime = LocalDateTime.parse("2023-12-31T23:59:59")
println("Parsed DateTime: $parsedDateTime") // 2023-12-31T23:59:59
// 5. Get components (access via date/time parts or directly)
val date: LocalDate = currentDateTime.toLocalDate()
val time: LocalTime = currentDateTime.toLocalTime()
val year: Int = currentDateTime.year
val hour: Int = currentDateTime.hour
println("Date part: $date, Time part: $time")
println("Year: $year, Hour: $hour")
// 6. Manipulate (similar to LocalDate/LocalTime, returns new instance)
val twoHoursLater: LocalDateTime = currentDateTime.plusHours(2)
val nextDaySameTime: LocalDateTime = currentDateTime.plusDays(1)
println("Two hours later: $twoHoursLater")
println("Next day same time: $nextDaySameTime")
// 7. Compare LocalDateTime instances
val eventStart = LocalDateTime.of(2024, 1, 1, 9, 0)
val eventEnd = LocalDateTime.of(2024, 1, 1, 17, 0)
val isNowBetweenEvents = currentDateTime.isAfter(eventStart) && currentDateTime.isBefore(eventEnd)
println("Is $currentDateTime between $eventStart and $eventEnd? $isNowBetweenEvents")
}
“`
Important: LocalDateTime
does not store timezone information. 2023-11-15T10:00
could be 10 AM in London, New York, or Tokyo. It’s just a date and time. This makes it unsuitable for representing a specific instant on the global timeline without additional context.
java.time.Instant
Represents a specific point in time on the UTC timeline, typically measured as nanoseconds from the epoch of 1970-01-01T00:00:00Z. This is the ideal class for representing timestamps, logging events, and storing points in time in databases. It’s unambiguous and independent of local time zones.
“`kotlin
import java.time.Instant
import java.time.Duration
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit
fun main() {
// 1. Get the current instant (UTC)
val now: Instant = Instant.now()
println(“Current Instant (UTC): $now”) // Example: 2023-10-27T14:50:30.123456789Z (Z indicates UTC)
// 2. Create an Instant from epoch seconds or milliseconds
val epochSecond: Instant = Instant.ofEpochSecond(1678886400) // Example: Seconds since 1970-01-01T00:00:00Z
val epochMilli: Instant = Instant.ofEpochMilli(System.currentTimeMillis()) // Like legacy new Date().getTime()
println("From epoch second: $epochSecond")
println("From epoch milli: $epochMilli")
// Create with offset from epoch second (allows nanoseconds)
val specificInstant: Instant = Instant.ofEpochSecond(1678886400, 500_000_000) // 0.5 seconds adjustment
println("Specific Instant: $specificInstant")
// 3. Parse an Instant string (ISO 8601 format)
val parsedInstant: Instant = Instant.parse("2023-10-27T10:15:30.00Z")
println("Parsed Instant: $parsedInstant")
// 4. Get epoch values
val secondsFromEpoch: Long = now.epochSecond
val millisFromEpoch: Long = now.toEpochMilli()
println("Seconds from epoch: $secondsFromEpoch")
println("Millis from epoch: $millisFromEpoch")
// 5. Manipulate (returns a new instance)
val tenSecondsLater: Instant = now.plusSeconds(10)
val oneHourBefore: Instant = now.minus(Duration.ofHours(1)) // Use Duration for time-based amounts
println("Ten seconds later: $tenSecondsLater")
println("One hour before: $oneHourBefore")
// 6. Compare Instants
val isAfterEpoch = now.isAfter(Instant.EPOCH) // Instant.EPOCH is 1970-01-01T00:00:00Z
println("Is current instant after epoch? $isAfterEpoch")
// 7. Convert Instant to/from ZonedDateTime (applying timezone)
val systemZone: ZoneId = ZoneId.systemDefault()
val zonedDateTimeNow: ZonedDateTime = now.atZone(systemZone)
println("Instant $now in zone $systemZone: $zonedDateTimeNow")
val someZonedDateTime: ZonedDateTime = ZonedDateTime.of(
2024, 1, 1, 12, 0, 0, 0, ZoneId.of("America/New_York")
)
val instantFromZoned: Instant = someZonedDateTime.toInstant()
println("$someZonedDateTime as Instant (UTC): $instantFromZoned")
}
“`
Instant
is often the best choice for storing timestamps in databases (usually as a TIMESTAMP WITH TIME ZONE
type, which typically stores UTC).
java.time.ZonedDateTime
Represents a date and time with a fully specified time zone and its rules (including DST). This is the class to use when you need to represent a specific event that occurs in a particular geographic location, considering local time changes.
“`kotlin
import java.time.ZonedDateTime
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.Instant
import java.time.Month
fun main() {
// 1. Get the current date/time in the system default timezone
val nowDefaultZone: ZonedDateTime = ZonedDateTime.now()
println(“Now (Default Zone): $nowDefaultZone”) // Example: 2023-10-27T16:05:15.123+01:00[Europe/London]
// 2. Get the current date/time in a specific timezone
val zoneNY: ZoneId = ZoneId.of("America/New_York")
val nowInNY: ZonedDateTime = ZonedDateTime.now(zoneNY)
println("Now (New York): $nowInNY") // Example: 2023-10-27T11:05:15.456-04:00[America/New_York]
// 3. Create a specific ZonedDateTime using of()
val specificDateTime = LocalDateTime.of(2024, Month.MARCH, 10, 1, 30) // Before DST change in NY
val zoneLA = ZoneId.of("America/Los_Angeles")
val zonedDateTimeLA = ZonedDateTime.of(specificDateTime, zoneLA)
println("Specific time in LA: $zonedDateTimeLA") // 2024-03-10T01:30-08:00[America/Los_Angeles]
// 4. Create from an Instant
val instant = Instant.now()
val zonedDateTimeParis = instant.atZone(ZoneId.of("Europe/Paris"))
println("Instant $instant in Paris: $zonedDateTimeParis")
// 5. Parse a ZonedDateTime string (ISO 8601 format)
// Requires zone information in the string
val parsedZoned = ZonedDateTime.parse("2023-11-05T01:30:00-07:00[America/Denver]")
println("Parsed ZonedDateTime: $parsedZoned")
// 6. Get components (includes offset and zone)
val localPart: LocalDateTime = nowDefaultZone.toLocalDateTime()
val zone: ZoneId = nowDefaultZone.zone
val offset = nowDefaultZone.offset // ZoneOffset: e.g., +01:00
println("Local part: $localPart, Zone: $zone, Offset: $offset")
// 7. Manipulate (Handles DST correctly!)
// Example: DST spring forward in New York (usually 2nd Sunday in March)
// Let's take a time just before the change (e.g., 1:30 AM)
val beforeDstChange = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0, zoneNY)
println("Before DST change NY: $beforeDstChange") // 2024-03-10T01:30-05:00[America/New_York]
val oneHourLater = beforeDstChange.plusHours(1)
println("Adding one hour (crosses DST): $oneHourLater") // 2024-03-10T03:30-04:00[America/New_York] - Note hour jumps from 1 to 3, offset changes
// Example: DST fall back (usually 1st Sunday in November)
// Time occurs twice (e.g., 1:30 AM happens first in DST, then again in standard time)
val beforeFallBack = ZonedDateTime.of(2024, 11, 3, 1, 30, 0, 0, zoneNY)
.withEarlierOffsetAtOverlap() // Ensure we get the DST instance
println("Before Fall back NY (DST): $beforeFallBack") // 2024-11-03T01:30-04:00[America/New_York]
val oneHourAfterFallBack = beforeFallBack.plusHours(1)
println("Adding one hour (during fall back): $oneHourAfterFallBack") // 2024-11-03T01:30-05:00[America/New_York] - Note hour stays 1:30, offset changes
// Use withLaterOffsetAtOverlap() to get the second occurrence if needed
val secondOccurrence = ZonedDateTime.of(2024, 11, 3, 1, 30, 0, 0, zoneNY)
.withLaterOffsetAtOverlap()
println("Second occurrence of 1:30 AM: $secondOccurrence") // 2024-11-03T01:30-05:00[America/New_York]
// 8. Change Time Zone while keeping the same Instant
val sameInstantDifferentZone = nowDefaultZone.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
println("Same instant in Tokyo: $sameInstantDifferentZone")
// 9. Change Time Zone while keeping the same Local Date and Time
// (This changes the actual instant in time!)
val sameLocalDifferentZone = nowDefaultZone.withZoneSameLocal(ZoneId.of("Asia/Tokyo"))
println("Same local time in Tokyo: $sameLocalDifferentZone (different instant!)")
// 10. Convert to Instant (losing zone info, keeping UTC point in time)
val backToInstant: Instant = nowDefaultZone.toInstant()
println("Back to Instant: $backToInstant")
}
“`
ZonedDateTime
is powerful but complex due to DST rules. Use it when the wall-clock time in a specific region is paramount.
java.time.OffsetDateTime
Represents a date and time with a fixed offset from UTC (e.g., -05:00
). Unlike ZonedDateTime
, it doesn’t store the full timezone rules (ZoneId
), only the offset that was valid at that specific date and time.
Use cases:
* Sometimes used in protocols or formats (like some XML schemas or logging) that specify an offset but not a full timezone ID.
* When you need to represent a time with an offset but don’t need or can’t determine the full region rules (less common for application logic, more for data representation).
“`kotlin
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
fun main() {
// 1. Get current time with system default offset
// Note: This captures the current offset, not the full zone rules
val nowDefaultOffset: OffsetDateTime = OffsetDateTime.now()
println(“Now (Default Offset): $nowDefaultOffset”) // Example: 2023-10-27T16:15:30.123+01:00
// 2. Get current time with a specific offset
val offsetMinus5 = ZoneOffset.ofHours(-5)
val nowOffsetMinus5 = OffsetDateTime.now(offsetMinus5) // Instant converted to this offset
println("Now (Offset -05:00): $nowOffsetMinus5") // Example: 2023-10-27T11:15:30.456-05:00
// 3. Create from LocalDateTime and ZoneOffset
val localDT = LocalDateTime.of(2024, 1, 1, 10, 0)
val offsetPlus2 = ZoneOffset.ofHoursMinutes(2, 30)
val offsetDateTime = OffsetDateTime.of(localDT, offsetPlus2)
println("Specific OffsetDateTime: $offsetDateTime") // 2024-01-01T10:00+02:30
// 4. Parse an OffsetDateTime string
val parsedOffset = OffsetDateTime.parse("2023-12-25T08:00:00-08:00")
println("Parsed OffsetDateTime: $parsedOffset")
// 5. Get components
val localDateTimePart: LocalDateTime = offsetDateTime.toLocalDateTime()
val offset: ZoneOffset = offsetDateTime.offset
println("Local part: $localDateTimePart, Offset: $offset")
// 6. Manipulate (adds duration, keeps the SAME offset)
val twoHoursLater = offsetDateTime.plusHours(2)
println("Two hours later (same offset): $twoHoursLater") // 2024-01-01T12:00+02:30
// 7. Convert to Instant ( unambiguous point in time)
val instant = offsetDateTime.toInstant()
println("As Instant: $instant")
// 8. Convert to ZonedDateTime (requires providing a ZoneId)
// Best Practice: Usually convert from ZonedDateTime -> OffsetDateTime if needed,
// converting OffsetDateTime -> ZonedDateTime can be ambiguous if offset doesn't map uniquely
// to a zone rule at that time.
val zone = ZoneId.of("Europe/Bucharest") // A zone that might have +02:30 offset sometimes
val zonedFromOffset = offsetDateTime.atZoneSameInstant(zone)
// Or maybe more common: get ZonedDateTime first, then convert
val zdt = ZonedDateTime.now(zone)
val odtFromZdt = zdt.toOffsetDateTime()
println("Zoned from Offset (same instant): $zonedFromOffset")
println("Offset from Zoned: $odtFromZdt")
// Comparison: OffsetDateTime vs ZonedDateTime
// ZonedDateTime knows DST rules, OffsetDateTime does not.
// If you add 6 months to an OffsetDateTime, the offset remains fixed.
// If you add 6 months to a ZonedDateTime, the offset might change due to DST.
val winterTime = OffsetDateTime.of(2024, 1, 15, 12, 0, 0, 0, ZoneOffset.ofHours(-5)) // e.g., EST
val summerTime = winterTime.plusMonths(6)
println("OffsetDateTime Winter: $winterTime") // 2024-01-15T12:00-05:00
println("OffsetDateTime + 6 Months: $summerTime") // 2024-07-15T12:00-05:00 (Offset unchanged!)
val winterZoned = ZonedDateTime.of(2024, 1, 15, 12, 0, 0, 0, ZoneId.of("America/New_York"))
val summerZoned = winterZoned.plusMonths(6)
println("ZonedDateTime Winter: $winterZoned") // 2024-01-15T12:00-05:00[America/New_York]
println("ZonedDateTime + 6 Months: $summerZoned") // 2024-07-15T12:00-04:00[America/New_York] (Offset changed due to EDT!)
}
“`
Generally, prefer ZonedDateTime
when dealing with user-facing times in specific regions or scheduling future events, and prefer Instant
for machine timestamps. OffsetDateTime
is more for data interchange or specific scenarios where only the offset is known/relevant.
java.time.Duration
Represents a time-based amount of time measured in seconds and nanoseconds. It’s suitable for measuring machine-based time differences (e.g., how long a process took).
“`kotlin
import java.time.Duration
import java.time.Instant
import java.time.LocalTime
import java.time.temporal.ChronoUnit
fun main() {
// 1. Create Durations
val tenSeconds: Duration = Duration.ofSeconds(10)
val twoMinutes: Duration = Duration.ofMinutes(2)
val fiveHours: Duration = Duration.ofHours(5)
val fiftyMillis: Duration = Duration.ofMillis(50)
val complexDuration: Duration = Duration.ofSeconds(3665, 123_456_789) // 1 hour, 1 min, 5 secs, plus nanos
println("10 Seconds: $tenSeconds") // PT10S (ISO-8601 duration format)
println("2 Minutes: $twoMinutes") // PT2M
println("5 Hours: $fiveHours") // PT5H
println("50 Millis: $fiftyMillis") // PT0.05S
println("Complex: $complexDuration") // PT1H1M5.123456789S
// 2. Create using standard units (more readable)
val durationFromUnits = Duration.of(3, ChronoUnit.HALF_DAYS) // 3 * 12 hours = 36 hours
println("From Units (3 half-days): $durationFromUnits") // PT36H
// 3. Parse from ISO-8601 string
val parsedDuration: Duration = Duration.parse("PT15M30.5S") // 15 minutes, 30.5 seconds
println("Parsed Duration: $parsedDuration") // PT15M30.5S
// 4. Calculate Duration between Instants or LocalTimes
val startInstant = Instant.now()
// Simulate work
Thread.sleep(1500) // Sleep for 1.5 seconds
val endInstant = Instant.now()
val elapsed: Duration = Duration.between(startInstant, endInstant)
println("Elapsed time: $elapsed") // Example: PT1.50876...S
val startTime = LocalTime.of(9, 0)
val endTime = LocalTime.of(17, 30)
val workDuration: Duration = Duration.between(startTime, endTime)
println("Work duration: $workDuration") // PT8H30M
// Cannot use Duration.between with LocalDate or LocalDateTime directly across DST changes
// Use ZonedDateTime for accurate duration calculation across potential DST boundaries.
// 5. Get components (seconds and nanoseconds)
val totalSeconds: Long = elapsed.seconds
val nanoPart: Int = elapsed.nano
println("Elapsed: $totalSeconds seconds and $nanoPart nanoseconds")
// Get as other units (truncates)
val elapsedMillis: Long = elapsed.toMillis()
val elapsedMinutes: Long = elapsed.toMinutes()
println("Elapsed millis: $elapsedMillis, Elapsed minutes: $elapsedMinutes")
// 6. Manipulate Durations
val doubledDuration: Duration = workDuration.multipliedBy(2)
val halfDuration: Duration = workDuration.dividedBy(2)
val addedDuration: Duration = workDuration.plus(Duration.ofMinutes(45)) // Add 45 mins
val subtractedDuration: Duration = workDuration.minusSeconds(900) // Subtract 15 mins
val absoluteDuration: Duration = Duration.ofSeconds(-10).abs() // PT10S
println("Doubled work duration: $doubledDuration")
println("Added 45 mins: $addedDuration")
println("Absolute of -10s: $absoluteDuration")
// 7. Add/Subtract Duration from temporal objects
val meetingStart = LocalTime.of(14, 0)
val meetingEnd = meetingStart.plus(Duration.ofMinutes(90)) // Add 1.5 hours
println("Meeting ends at: $meetingEnd") // 15:30
val eventInstant = Instant.now()
val reminderInstant = eventInstant.minus(Duration.ofHours(24)) // 24 hours before
println("Reminder time: $reminderInstant")
}
“`
java.time.Period
Represents a date-based amount of time measured in years, months, and days. Suitable for differences between LocalDate
objects or human-centric periods (e.g., “2 years, 3 months, and 5 days”).
Crucially: A Period
does not have a fixed duration in seconds because months and years vary in length (and leap years). Period.ofMonths(1)
added to 2023-01-31
results in 2023-02-28
, while added to 2023-03-31
results in 2023-04-30
.
“`kotlin
import java.time.Period
import java.time.LocalDate
import java.time.Month
import java.time.temporal.ChronoUnit
fun main() {
// 1. Create Periods
val tenDays: Period = Period.ofDays(10)
val threeMonths: Period = Period.ofMonths(3)
val twoYears: Period = Period.ofYears(2)
val complexPeriod: Period = Period.of(1, 6, 15) // 1 Year, 6 Months, 15 Days
println("10 Days: $tenDays") // P10D (ISO-8601 duration format)
println("3 Months: $threeMonths") // P3M
println("2 Years: $twoYears") // P2Y
println("Complex: $complexPeriod") // P1Y6M15D
// 2. Create using weeks (converted to days)
val twoWeeks: Period = Period.ofWeeks(2)
println("2 Weeks: $twoWeeks") // P14D
// 3. Parse from ISO-8601 string
val parsedPeriod: Period = Period.parse("P2Y3M4W5D") // 2 Years, 3 Months, 4 Weeks (28 days), 5 Days
println("Parsed Period: $parsedPeriod") // P2Y3M33D (Weeks are normalized to days)
// 4. Calculate Period between LocalDates
val startDate = LocalDate.of(2022, Month.JANUARY, 15)
val endDate = LocalDate.of(2024, Month.MARCH, 20)
val periodBetween: Period = Period.between(startDate, endDate)
println("Period between $startDate and $endDate: $periodBetween") // P2Y2M5D
// 5. Get components (Years, Months, Days)
val years: Int = periodBetween.years
val months: Int = periodBetween.months
val days: Int = periodBetween.days
println("Components: $years Years, $months Months, $days Days")
// Get total months (estimated, careful!)
val totalMonths: Long = periodBetween.toTotalMonths()
println("Total months (approx): $totalMonths") // 26 (2*12 + 2)
// 6. Manipulate Periods
val doubledPeriod: Period = complexPeriod.multipliedBy(2) // P2Y12M30D -> Normalized: P3Y30D
val normalizedPeriod = doubledPeriod.normalized() // Normalizes months > 11 into years
val subtractedPeriod: Period = complexPeriod.minusDays(10) // P1Y6M5D
val addedPeriod: Period = complexPeriod.plusMonths(7) // P1Y13M15D -> Normalized: P2Y1M15D
println("Doubled Complex Period: $doubledPeriod") // P2Y12M30D (Before normalization)
println("Normalized Doubled: $normalizedPeriod") // P3Y30D
println("Subtracted 10 days: $subtractedPeriod")
println("Added 7 months (normalized): ${addedPeriod.normalized()}") // P2Y1M15D
// 7. Add/Subtract Period from LocalDate or LocalDateTime
val today = LocalDate.now()
val futureDate = today.plus(complexPeriod)
val pastDate = today.minus(Period.ofMonths(6))
println("Today: $today")
println("Today plus $complexPeriod: $futureDate")
println("Today minus 6 months: $pastDate")
// Example of month length difference:
val jan31 = LocalDate.of(2023, 1, 31)
val feb28 = jan31.plusMonths(1) // Adjusts to end of February
val mar31 = LocalDate.of(2023, 3, 31)
val apr30 = mar31.plusMonths(1) // Adjusts to end of April
println("$jan31 plus 1 month: $feb28") // 2023-02-28
println("$mar31 plus 1 month: $apr30") // 2023-04-30
// Cannot directly add Period to Instant (doesn't make sense without a timezone context for month/day lengths)
// Can add Period to ZonedDateTime (handles DST and varying lengths correctly)
val zonedNow = ZonedDateTime.now()
val zonedFuture = zonedNow.plus(Period.ofMonths(6))
println("Zoned now: $zonedNow")
println("Zoned plus 6 months: $zonedFuture") // Offset might change!
}
“`
Duration vs. Period:
* Use Duration
for exact, time-based amounts (seconds/nanos) – good for machine time, elapsed times.
* Use Period
for date-based, human-centric amounts (years/months/days) – good for birthdays, subscription lengths.
java.time.ZoneId
& java.time.ZoneOffset
ZoneId
: Represents a time zone identifier, such asEurope/Paris
orAsia/Tokyo
. It contains the rules defining the offset from UTC for that region, including historical changes and Daylight Saving Time transitions. UseZoneId.systemDefault()
to get the JVM’s default time zone.ZoneId.getAvailableZoneIds()
provides a set of all available region IDs (based on the IANA Time Zone Database).ZoneOffset
: Represents a fixed offset from UTC, like+02:00
or-05:00
. It doesn’t have DST rules.ZonedDateTime
has aZoneId
and aZoneOffset
(the offset currently applicable for that date/time within that zone).OffsetDateTime
only has aZoneOffset
.
“`kotlin
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
fun main() {
// 1. Get system default ZoneId
val systemZone: ZoneId = ZoneId.systemDefault()
println(“System Default ZoneId: $systemZone”)
// 2. Get a specific ZoneId
val parisZone: ZoneId = ZoneId.of("Europe/Paris")
val tokyoZone: ZoneId = ZoneId.of("Asia/Tokyo")
println("Paris ZoneId: $parisZone")
// 3. Get available ZoneIds (large set!)
val availableZoneIds: Set<String> = ZoneId.getAvailableZoneIds()
println("Number of available Zone IDs: ${availableZoneIds.size}")
// Example: Print zones containing "America/Los"
availableZoneIds.filter { it.contains("America/Los") }.forEach { println(it) }
// 4. Get rules for a ZoneId (includes DST info)
val parisRules = parisZone.rules
val nowInstant = Instant.now()
val isDstInParis = parisRules.isDaylightSavings(nowInstant)
val offsetInParisNow = parisRules.getOffset(nowInstant)
println("Is DST currently active in Paris? $isDstInParis")
println("Current offset in Paris: $offsetInParisNow")
// 5. Create ZoneOffsets
val offsetPlus2: ZoneOffset = ZoneOffset.ofHours(2)
val offsetMinus5_30: ZoneOffset = ZoneOffset.ofHoursMinutes(-5, -30)
val offsetFromString: ZoneOffset = ZoneOffset.of("+05:45")
val utcOffset: ZoneOffset = ZoneOffset.UTC // Constant for +00:00
println("Offset +2 hours: $offsetPlus2")
println("Offset -5:30: $offsetMinus5_30")
println("Offset from string: $offsetFromString")
println("UTC Offset: $utcOffset")
// 6. Get offset from a ZonedDateTime
val zonedDateTime = ZonedDateTime.now(parisZone)
val currentParisOffset: ZoneOffset = zonedDateTime.offset
println("Current offset for ZonedDateTime in Paris: $currentParisOffset")
}
“`
java.time.Clock
This is an abstract class that provides access to the current instant, date, and time using a time zone. Its primary purpose is dependency injection and testing. By using a Clock
instead of calling Instant.now()
or LocalDate.now()
directly in your application logic, you can inject a fixed or controllable clock during tests.
“`kotlin
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.LocalDate
// Example Service using Clock
class EventScheduler(private val clock: Clock) {
fun scheduleEvent(daysFromNow: Long): Instant {
val now = Instant.now(clock) // Use the injected clock!
val eventTime = now.plus(Duration.ofDays(daysFromNow))
println(“Scheduling event at: $eventTime (Current time via clock: $now)”)
// … actual scheduling logic …
return eventTime
}
fun isTodayWeekend(): Boolean {
val today = LocalDate.now(clock) // Use clock here too
val dayOfWeek = today.dayOfWeek
return dayOfWeek == java.time.DayOfWeek.SATURDAY || dayOfWeek == java.time.DayOfWeek.SUNDAY
}
}
fun main() {
// — In Production Code —
// Use the system clock
val systemClock: Clock = Clock.systemDefaultZone()
val schedulerProd = EventScheduler(systemClock)
println(“— Production Run —“)
schedulerProd.scheduleEvent(7)
println(“Is today weekend (Prod)? ${schedulerProd.isTodayWeekend()}”)
// --- In Test Code ---
// Use a fixed clock
val fixedInstant = Instant.parse("2023-11-15T10:00:00Z")
val fixedZone = ZoneId.of("UTC")
val fixedClock: Clock = Clock.fixed(fixedInstant, fixedZone)
val schedulerTest = EventScheduler(fixedClock)
println("\n--- Test Run with Fixed Clock ---")
val scheduledTime = schedulerTest.scheduleEvent(7)
// Assertion in a real test: assertEquals(Instant.parse("2023-11-22T10:00:00Z"), scheduledTime)
println("Is 'today' (Nov 15) weekend (Test)? ${schedulerTest.isTodayWeekend()}") // Will be false
// Use an offset clock (ticks relative to system clock)
val offsetClock: Clock = Clock.offset(systemClock, Duration.ofHours(-2)) // Clock is 2 hours behind system
val schedulerOffset = EventScheduler(offsetClock)
println("\n--- Test Run with Offset Clock (-2 Hours) ---")
schedulerOffset.scheduleEvent(7) // Will use a time 2 hours behind the actual current time
// You can also create custom Clock implementations for more complex test scenarios.
}
“`
Using Clock
significantly improves the testability of code that relies on the current time.
5. Common Operations
Let’s consolidate common tasks using the classes we’ve learned.
Getting the Current Date/Time
“`kotlin
val nowInstant: Instant = Instant.now() // UTC timestamp
val today: LocalDate = LocalDate.now() // System default zone date
val currentTime: LocalTime = LocalTime.now() // System default zone time
val currentDateTime: LocalDateTime = LocalDateTime.now()// System default zone date/time
val currentZoned: ZonedDateTime = ZonedDateTime.now() // System default zone date/time with zone
// Get current time in a specific zone
val zoneId = ZoneId.of(“America/New_York”)
val todayNY = LocalDate.now(zoneId)
val currentTimeNY = LocalTime.now(zoneId)
val currentDateTimeNY = LocalDateTime.now(zoneId)
val currentZonedNY = ZonedDateTime.now(zoneId)
// Use Clock for testability
val clock: Clock = Clock.systemUTC() // Example: UTC clock
val nowInstantViaClock = Instant.now(clock)
val todayViaClock = LocalDate.now(clock)
// … and so on
“`
Creating Specific Dates/Times
Use the of(...)
factory methods:
“`kotlin
val date = LocalDate.of(2024, 12, 25) // Year, Month (int), Day
val dateEnum = LocalDate.of(2024, Month.DECEMBER, 25) // Year, Month (enum), Day
val time = LocalTime.of(14, 30) // Hour, Minute
val timeSec = LocalTime.of(14, 30, 15) // H, M, S
val timeNano = LocalTime.of(14, 30, 15, 500_000_000) // H, M, S, Nano
val dateTime = LocalDateTime.of(date, time) // Combine LocalDate and LocalTime
val dateTimeOf = LocalDateTime.of(2024, 1, 1, 0, 0) // Y, M, D, H, M
val instant = Instant.ofEpochSecond(1704067200) // From epoch seconds
val instantMilli = Instant.ofEpochMilli(1704067200000L) // From epoch milliseconds
val zone = ZoneId.of(“Europe/Berlin”)
val zonedDateTime = ZonedDateTime.of(dateTime, zone) // Combine LocalDateTime + ZoneId
val zonedDateTimeOf = ZonedDateTime.of(2024, 1, 1, 9, 0, 0, 0, zone) // Y,M,D,H,M,S,nano,ZoneId
val offset = ZoneOffset.ofHours(-7)
val offsetDateTime = OffsetDateTime.of(dateTime, offset) // Combine LocalDateTime + ZoneOffset
“`
Parsing Strings into Date/Time Objects
The parse()
method is used. By default, it expects ISO 8601 format.
“`kotlin
// Default ISO formats
val date = LocalDate.parse(“2023-11-21”)
val time = LocalTime.parse(“10:15:30.123”)
val dateTime = LocalDateTime.parse(“2023-11-21T10:15:30.123”)
val instant = Instant.parse(“2023-11-21T09:15:30.123Z”) // Z or offset required
val zonedDateTime = ZonedDateTime.parse(“2023-11-21T10:15:30.123+01:00[Europe/Paris]”)
val offsetDateTime = OffsetDateTime.parse(“2023-11-21T08:15:30.123-05:00”)
println(“Parsed Date: $date”)
println(“Parsed DateTime: $dateTime”)
println(“Parsed Instant: $instant”)
println(“Parsed ZonedDateTime: $zonedDateTime”)
// For other formats, use DateTimeFormatter (see next section)
“`
Formatting Date/Time Objects into Strings (DateTimeFormatter
)
The format()
method of temporal objects takes a DateTimeFormatter
. This powerful class handles converting date/time objects to strings in various formats.
“`kotlin
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
fun main() {
val dateTime = LocalDateTime.of(2024, 7, 4, 14, 30, 5)
val zonedDateTime = ZonedDateTime.of(dateTime, ZoneId.of(“America/New_York”))
// 1. Predefined ISO Formatters (often the default toString() output)
println("ISO_DATE_TIME: ${dateTime.format(DateTimeFormatter.ISO_DATE_TIME)}")
// Output: ISO_DATE_TIME: 2024-07-04T14:30:05
println("ISO_DATE: ${dateTime.format(DateTimeFormatter.ISO_DATE)}")
// Output: ISO_DATE: 2024-07-04
println("ISO_ZONED_DATE_TIME: ${zonedDateTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)}")
// Output: ISO_ZONED_DATE_TIME: 2024-07-04T14:30:05-04:00[America/New_York]
// 2. Localized Formatters (FormatStyle: FULL, LONG, MEDIUM, SHORT)
val formatterFull = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)
.withLocale(Locale.US)
val formatterLong = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
.withLocale(Locale.FRANCE)
val formatterMedium = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.GERMANY)
val formatterShortDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.UK)
// Note: Localized formatters generally require full date/time info (or just date/time)
println("Full (US): ${zonedDateTime.format(formatterFull)}")
// Output: Full (US): Thursday, July 4, 2024 at 2:30:05 PM Eastern Daylight Time
println("Long (France): ${zonedDateTime.format(formatterLong)}")
// Output: Long (France): 4 juillet 2024 à 14:30:05 EDT
println("Medium (Germany): ${zonedDateTime.format(formatterMedium)}")
// Output: Medium (Germany): 04.07.2024, 14:30:05
println("Short Date (UK): ${dateTime.format(formatterShortDate)}") // Using LocalDateTime here
// Output: Short Date (UK): 04/07/2024
// 3. Custom Patterns (Most flexible)
// See DateTimeFormatter documentation for pattern letters (y, M, d, H, h, m, s, a, z, Z, etc.)
val pattern1 = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
val pattern2 = DateTimeFormatter.ofPattern("EEE, MMM dd, yyyy 'at' h:mm a") // EEE=Short day name, MMM=Short month name, a=AM/PM
val pattern3 = DateTimeFormatter.ofPattern("yyyyMMdd")
val pattern4 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z (zzzz)") // Z=Offset, zzzz=Zone Name
println("Custom dd/MM/yyyy HH:mm:ss: ${dateTime.format(pattern1)}")
// Output: Custom dd/MM/yyyy HH:mm:ss: 04/07/2024 14:30:05
println("Custom EEE, MMM dd...: ${zonedDateTime.format(pattern2.withLocale(Locale.US))}") // Locale affects names
// Output: Custom EEE, MMM dd...: Thu, Jul 04, 2024 at 2:30 PM
println("Custom yyyyMMdd: ${dateTime.format(pattern3)}")
// Output: Custom yyyyMMdd: 20240704
println("Custom with Zone/Offset: ${zonedDateTime.format(pattern4)}")
// Output: Custom with Zone/Offset: 2024-07-04 14:30:05 -0400 (Eastern Daylight Time)
// 4. Using Formatters for Parsing
val dateString1 = "15/08/2023 10:00:00"
val parsedDateTime1 = LocalDateTime.parse(dateString1, pattern1)
println("Parsed '$dateString1': $parsedDateTime1")
val dateString2 = "Mon, Aug 14, 2023 at 9:05 AM"
// Need locale for parsing month/day names correctly
val parsedDateTime2 = LocalDateTime.parse(dateString2, pattern2.withLocale(Locale.US))
println("Parsed '$dateString2': $parsedDateTime2")
// Tip: Create DateTimeFormatter instances once and reuse them (they are immutable and thread-safe).
companion object {
val APP_DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val APP_TIMESTAMP_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
}
// Usage: val formatted = myLocalDate.format(APP_DATE_FORMATTER)
}
“`
Manipulating Dates and Times
Use plus*
, minus*
, and with*
methods. Remember they return new instances.
“`kotlin
val dateTime = LocalDateTime.now()
// Adding/Subtracting Durations/Periods
val twoWeeksLater = dateTime.plus(Period.ofWeeks(2))
val ninetyMinutesBefore = dateTime.minus(Duration.ofMinutes(90))
// Adding/Subtracting specific units
val nextYear = dateTime.plusYears(1)
val prevHour = dateTime.minusHours(1)
val tenSecondsAdded = dateTime.plusSeconds(10)
// Using ChronoUnit for flexibility
val threeMonthsLater = dateTime.plus(3, ChronoUnit.MONTHS)
// Setting specific fields using ‘with’
val startOfDay = dateTime.with(LocalTime.MIN) // Set time to 00:00:00.0
val endOfMonth = dateTime.withDayOfMonth(dateTime.toLocalDate().lengthOfMonth())
val forcedToJune = dateTime.withMonth(Month.JUNE.value) // Set month to June
val timeSetToNoon = dateTime.withHour(12).withMinute(0).withSecond(0).withNano(0)
println(“Original: $dateTime”)
println(“Two weeks later: $twoWeeksLater”)
println(“90 mins before: $ninetyMinutesBefore”)
println(“Start of Day: $startOfDay”)
println(“End of Month: $endOfMonth”)
println(“Set to June: $forcedToJune”)
println(“Set to Noon: $timeSetToNoon”)
// TemporalAdjusters for common adjustments
import java.time.temporal.TemporalAdjusters
val firstDayOfNextMonth = dateTime.with(TemporalAdjusters.firstDayOfNextMonth())
val lastSundayInMonth = dateTime.with(TemporalAdjusters.lastInMonth(java.time.DayOfWeek.SUNDAY))
println(“First day of next month: $firstDayOfNextMonth”)
println(“Last Sunday in this month: $lastSundayInMonth”)
“`
Comparing Dates and Times
Use isBefore()
, isAfter()
, isEqual()
, and compareTo()
.
“`kotlin
val date1 = LocalDate.of(2023, 10, 27)
val date2 = LocalDate.of(2024, 1, 1)
val dateTime1 = LocalDateTime.now()
val dateTime2 = dateTime1.plusHours(1)
val instant1 = Instant.now()
val instant2 = instant1.minusSeconds(10)
println(“$date1 is before $date2: ${date1.isBefore(date2)}”) // true
println(“$date1 is after $date2: ${date1.isAfter(date2)}”) // false
println(“$date1 equals $date1: ${date1.isEqual(date1)}”) // true
println(“$dateTime1 compareTo $dateTime2: ${dateTime1.compareTo(dateTime2)}”) // < 0 (negative)
println(“$dateTime2 compareTo $dateTime1: ${dateTime2.compareTo(dateTime1)}”) // > 0 (positive)
println(“$dateTime1 compareTo $dateTime1: ${dateTime1.compareTo(dateTime1)}”) // = 0 (zero)
println(“$instant1 is after $instant2: ${instant1.isAfter(instant2)}”) // true
// Remember: equals() checks type and value. isEqual() checks chronology (can compare Instant and ZonedDateTime).
val zdt = ZonedDateTime.now()
val inst = zdt.toInstant()
println(“zdt.isEqual(inst): ${zdt.isEqual(inst)}”) // Likely true
// println(zdt == inst) // Compile error – different types
“`
Calculating Differences (Durations and Periods)
Use Duration.between()
for time-based differences and Period.between()
for date-based differences.
“`kotlin
// Time difference
val time1 = LocalTime.of(9, 15)
val time2 = LocalTime.of(10, 0)
val duration = Duration.between(time1, time2) // PT45M (45 minutes)
println(“Duration between $time1 and $time2: $duration”)
val instant1 = Instant.now()
// … some time passes …
Thread.sleep(550)
val instant2 = Instant.now()
val elapsed = Duration.between(instant1, instant2) // e.g., PT0.55…S
println(“Elapsed time: $elapsed (${elapsed.toMillis()} ms)”)
// Date difference
val date1 = LocalDate.of(2023, 1, 1)
val date2 = LocalDate.of(2024, 6, 15)
val period = Period.between(date1, date2) // P1Y5M14D (1 year, 5 months, 14 days)
println(“Period between $date1 and $date2: $period”)
// Using ChronoUnit.between for differences in a single unit (truncates)
val daysBetween = ChronoUnit.DAYS.between(date1, date2)
val monthsBetween = ChronoUnit.MONTHS.between(date1, date2)
val hoursBetween = ChronoUnit.HOURS.between(time1, time2) // Requires Temporal, works with LocalTime, LocalDateTime etc.
println(“Days between: $daysBetween”) // e.g., 530
println(“Months between: $monthsBetween”) // e.g., 17
println(“Hours between: $hoursBetween”) // e.g., 0 (as it’s less than 1 full hour)
// Be careful with ChronoUnit.between across DST with LocalDateTime – use ZonedDateTime for accuracy.
val zoned1 = ZonedDateTime.of(2024, 3, 10, 1, 0, 0, 0, ZoneId.of(“America/New_York”)) // Before DST
val zoned2 = zoned1.plusHours(3) // Ends up at 5 AM EDT due to DST jump
val hours = ChronoUnit.HOURS.between(zoned1, zoned2)
println(“Hours between $zoned1 and $zoned2: $hours”) // Correctly prints 3
“`
Handling Time Zones Effectively
- Identify Need: Does this date/time represent a global instant (use
Instant
), or a time specific to a user/location (useZonedDateTime
)? Is it a future schedule independent of zone for now (useLocalDateTime
, but plan for conversion)? - Store in UTC: Store timestamps (
Instant
) in your database, ideally in a column type that preserves timezone information (likeTIMESTAMP WITH TIME ZONE
in PostgreSQL, which stores UTC and converts on retrieval). - Convert for Display: Convert
Instant
or UTCZonedDateTime
to the user’s localZoneId
only when displaying it to the user. - Use
ZoneId
: PreferZoneId
(e.g., “Europe/London”) over fixed offsets (ZoneOffset
) unless the offset is all you have or specifically required by a protocol.ZoneId
handles DST correctly. - Be Explicit: Don’t rely on the system default timezone (
ZoneId.systemDefault()
) unless you are certain the server’s timezone is appropriate and stable. PassZoneId
explicitly. - Testing: Test edge cases around DST transitions (
ZonedDateTime
), usingClock.fixed(...)
to set specific instants before, during, and after transitions.
“`kotlin
// Example: Storing an event time and displaying it to users in different zones
// 1. Record event time as Instant (UTC)
val eventInstantUtc: Instant = ZonedDateTime.of(
2024, 11, 15, // Year, Month, Day
18, 0, 0, 0, // Hour, Min, Sec, Nano
ZoneId.of(“Europe/London”) // Event occurs at 6 PM London time
).toInstant()
println(“Event stored as UTC Instant: $eventInstantUtc”) // e.g., 2024-11-15T18:00:00Z (assuming London is GMT in Nov)
// — Store eventInstantUtc in database —
// — Later, retrieve eventInstantUtc from database —
// 2. Display for a user in New York
val userZoneNY = ZoneId.of(“America/New_York”)
val eventTimeInNY: ZonedDateTime = eventInstantUtc.atZone(userZoneNY)
val formatterNY = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.US)
println(“Event time for NY user: ${eventTimeInNY.format(formatterNY)}”) // e.g., Nov 15, 2024, 1:00:00 PM
// 3. Display for a user in Tokyo
val userZoneTokyo = ZoneId.of(“Asia/Tokyo”)
val eventTimeInTokyo: ZonedDateTime = eventInstantUtc.atZone(userZoneTokyo)
val formatterTokyo = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.JAPAN)
println(“Event time for Tokyo user: ${eventTimeInTokyo.format(formatterTokyo)}”) // e.g., 2024/11/16 3:00:00
“`
6. Introduction to kotlinx-datetime
While java.time
is excellent for JVM/Android, Kotlin Multiplatform (KMP) projects targeting platforms like iOS, WebAssembly, or Native require a common date/time library. kotlinx-datetime
is the official Kotlin-native solution for this.
Its API is heavily inspired by java.time
, making the transition relatively smooth.
Key Concepts & Classes (Similarities):
- Immutability: Core principle, just like
java.time
. Instant
: Represents a point on the UTC timeline (nanosecond precision). Very similar tojava.time.Instant
.LocalDate
,LocalTime
,LocalDateTime
: Represent date, time, and date-time without timezone. Analogous to theirjava.time
counterparts.TimeZone
: Equivalent tojava.time.ZoneId
. Represents timezone rules (e.g.,TimeZone.of("Europe/Paris")
,TimeZone.currentSystemDefault()
).DateTimePeriod
: Similar role tojava.time.Period
(years, months, days) andjava.time.Duration
combined, but can hold components of both date and time units.Duration
: (From Kotlin stdlibkotlin.time.Duration
) Used for fixed lengths of time (seconds, nanoseconds), similar role tojava.time.Duration
.kotlinx-datetime
integrates well with it.
Key Differences/Additions:
- Multiplatform: The primary reason for its existence. Works across JVM, Native, JS, Wasm.
Clock
Interface: Defined directly within the library (kotlinx.datetime.Clock
) for testability, similar concept tojava.time.Clock
.Clock.System
is the default.DateTimePeriod
: A single class for date and time components (years, months, days, hours, minutes, seconds, nanoseconds).java.time
separates this intoPeriod
andDuration
.- Formatting/Parsing: Relies on expected patterns passed directly to
parse
andformat
functions, rather than a separateDateTimeFormatter
class. The patterns are similar but might have minor differences. Currently, localization support is more limited compared tojava.time
.
Example Usage (kotlinx-datetime
):
“`kotlin
// Make sure you have the dependency: implementation(“org.jetbrains.kotlinx:kotlinx-datetime:0.5.0”)
import kotlinx.datetime.* // Import necessary symbols
import kotlin.time.Duration.Companion.hours // Use kotlin.time.Duration
fun main() {
// 1. Get current Instant and LocalDateTime (using system clock by default)
val nowInstant: Instant = Clock.System.now()
val systemTimeZone: TimeZone = TimeZone.currentSystemDefault()
val nowLocalDateTime: LocalDateTime = nowInstant.toLocalDateTime(systemTimeZone)
println("kotlinx Now Instant: $nowInstant")
println("kotlinx Now LocalDateTime ($systemTimeZone): $nowLocalDateTime")
// 2. Create specific instances
val date: LocalDate = LocalDate(2024, Month.MARCH, 15)
val time: LocalTime = LocalTime(10, 30, 0, 0)
val dateTime: LocalDateTime = LocalDateTime(date, time)
val instantFromEpoch: Instant = Instant.fromEpochSeconds(1700000000)
// 3. TimeZone and conversion
val londonTZ: TimeZone = TimeZone.of("Europe/London")
val dateTimeInLondon: Instant = dateTime.toInstant(londonTZ) // Convert LocalDateTime to Instant via Zone
val dateTimeFromInstant: LocalDateTime = dateTimeInLondon.toLocalDateTime(londonZone)
println("$dateTime in London is Instant: $dateTimeInLondon")
println("$dateTimeInLondon as LocalDateTime in London: $dateTimeFromInstant")
// 4. Parsing and Formatting (Basic - relies on ISO 8601 or specific patterns)
val parsedInstant: Instant = Instant.parse("2023-10-27T10:30:00Z")
val parsedLocalDateTime: LocalDateTime = LocalDateTime.parse("2023-11-20T14:00:15.123")
// Formatting often uses toString() for ISO format or requires custom logic for other formats.
// More advanced formatting might need platform-specific implementations or helper libraries.
println("Parsed Instant: $parsedInstant")
println("Parsed LocalDateTime: $parsedLocalDateTime")
// 5. Manipulation using Duration (kotlin.time) and DateTimePeriod
val tomorrow: LocalDate = date.plus(1, DateTimeUnit.DAY)
val oneHourLaterInstant: Instant = nowInstant.plus(1.hours) // Using kotlin.time extension
val complexPeriod = DateTimePeriod(years = 1, months = 2, days = 3)
val futureDate = date.plus(complexPeriod)
println("Tomorrow: $tomorrow")
println("One hour later: $oneHourLaterInstant")
println("$date plus $complexPeriod: $futureDate")
// 6. Difference calculation
val date1 = LocalDate(2023, 1, 1)
val date2 = LocalDate(2024, 4, 1)
val periodDiff: DateTimePeriod = date1.periodUntil(date2) // P1Y3M
val daysDiff: Int = date1.daysUntil(date2) // 456
val durationDiff: kotlin.time.Duration = instantFromEpoch - nowInstant // Using minus operator
println("Period until $date2: $periodDiff")
println("Days until $date2: $daysDiff")
println("Duration difference: $durationDiff")
}
“`
When to use kotlinx-datetime
?
- Kotlin Multiplatform Projects: It’s the standard choice for sharing date/time logic across platforms.
- Pure Kotlin Preference: If you prefer a Kotlin-first library over a Java one, even on the JVM.
Keep in mind that java.time
is more mature and feature-rich, especially regarding localization and formatting, on the JVM/Android platform. For JVM-only or Android-only projects, java.time
remains the most common and often recommended choice.
7. Best Practices
- Use
java.time
orkotlinx-datetime
: Avoid the legacyjava.util.Date
andjava.util.Calendar
APIs entirely in new code. If interacting with old APIs, convert to the modern types as soon as possible. - Be Explicit About Time Zones: Don’t rely on the system default unless intended. Store timestamps in UTC (
Instant
). Convert to/from specific time zones (ZoneId
/TimeZone
) only when necessary (e.g., for user display, handling region-specific logic). - Choose the Right Class:
Instant
: Machine timestamps, specific points on the global timeline (UTC). Ideal for storage.ZonedDateTime
: Date and time in a specific region, accounting for DST. For user-facing times, future schedules tied to a location.LocalDateTime
: Date and time without zone context. Use when zone is irrelevant or handled separately (be careful!).LocalDate
/LocalTime
: Date-only or Time-only values (birthdays, opening hours).Duration
: Exact time-based amounts (seconds/nanos).Period
/DateTimePeriod
: Date-based amounts (years/months/days).
- Embrace Immutability: Understand that all operations create new objects. This prevents side effects and ensures thread safety.
- Prefer Standard Formats (ISO 8601): Use ISO 8601 (e.g.,
2023-10-27T18:30:00Z
) for data interchange (APIs, serialization) whenever possible. It’s unambiguous and widely supported. - Use
Clock
for Testability: InjectClock
into your classes instead of callingInstant.now()
,LocalDate.now()
, etc., directly. This allows you to control time during tests. - Handle Parsing Errors: Parsing external date strings can fail. Use
try-catch
blocks or Kotlin’srunCatching
to handle potentialDateTimeParseException
. - Validate User Input: When accepting date/time input from users, validate it rigorously. Provide clear formats or use date/time pickers.
- Database Mapping: Use modern JDBC drivers (4.2+) which typically map
java.time
types directly to appropriate SQL types (DATE
,TIME
,TIMESTAMP
,TIMESTAMP WITH TIME ZONE
). Consult your database and ORM/library documentation. StoringInstant
usually maps well toTIMESTAMP WITH TIME ZONE
(often stored as UTC). - Understand DST: Be aware of Daylight Saving Time transitions if using
ZonedDateTime
. Test behavior around the “spring forward” (gap) and “fall back” (overlap) periods. Use methods likewithEarlierOffsetAtOverlap()
orwithLaterOffsetAtOverlap()
if needed during overlaps.
8. Conclusion
Handling dates and times in Kotlin, primarily leveraging the java.time
package, is significantly more pleasant and robust than using legacy APIs. By understanding the core concepts of immutability, time zones, and the distinct purpose of each class (Instant
, LocalDate
, ZonedDateTime
, Duration
, Period
, etc.), you can write clearer, safer, and more maintainable code.
We’ve covered creating, parsing, formatting, manipulating, and comparing temporal values, along with the crucial aspects of time zone handling and best practices. We also briefly introduced kotlinx-datetime
as the go-to solution for Kotlin Multiplatform projects.
Mastering these APIs is essential for many applications. While the nuances, especially around time zones and DST, can still be challenging, the structure and clarity provided by java.time
and kotlinx-datetime
give you the right tools to tackle these complexities effectively. Remember to choose the right class for the job, be explicit about time zones, leverage immutability, and test your temporal logic thoroughly.