kotlinx-datetime: мультиплатформенная работа с датой и временем в Kotlin
1. Что это и зачем нужна
kotlinx-datetime — это официальная библиотека от JetBrains для работы с датой и временем в экосистеме Kotlin. Её главная особенность — мультиплатформенность: один и тот же код работает на JVM, JS (в браузере и Node.js), Native (iOS, macOS, Windows, Linux) и Wasm. Это избавляет разработчиков от необходимости писать платформо-зависимые обёртки или использовать разные библиотеки для разных целей.
Зачем она нужна? Стандартная библиотека Kotlin/JVM использует java.time, который недоступен на других платформах. Для Kotlin/Native и Kotlin/JS приходилось либо использовать платформенные API (NSDate, Date), либо подключать сторонние библиотеки. kotlinx-datetime решает эту проблему, предоставляя единый API, основанный на спецификации ISO-8601. Она включает классы для представления моментов времени (Instant), дат без времени (LocalDate), времени без даты (LocalTime), даты со временем (LocalDateTime), часовых поясов (TimeZone) и периодов (DateTimePeriod).
Библиотека идеально подходит для проектов, использующих Kotlin Multiplatform (KMP), серверных приложений на Ktor, Android-приложений (как замена java.time) и даже для скриптов и консольных утилит. Она легковесна, не имеет внешних зависимостей и активно поддерживается сообществом.
2. Установка
Библиотека распространяется через Maven Central. Для её подключения необходимо добавить зависимость в файл build.gradle.kts (или build.gradle). Ниже приведён точный код для Gradle Kotlin DSL.
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
// Основная библиотека
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// Если вы используете мультиплатформенный проект, добавляйте в commonMain:
// commonMain.dependencies {
// implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// }
}
Важно: Версия 0.6.1 — последняя стабильная на момент написания статьи. Проверяйте актуальную версию на GitHub-релизах. Для Gradle Groovy DSL команда будет аналогичной, но без кавычек у строк.
Если вы используете Maven, добавьте в pom.xml:
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-datetime</artifactId>
<version>0.6.1</version>
</dependency>
Для Gradle Kotlin DSL в мультиплатформенном проекте (KMP) добавьте в commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
}
}
}
3. Быстрый старт
Минимальный рабочий пример: создание текущего момента времени, его форматирование и вывод на консоль.
import kotlinx.datetime.*
fun main() {
// Получаем текущий момент времени в UTC
val now: Instant = Clock.System.now()
println("Текущий момент в UTC: $now")
// Преобразуем в локальную дату и время (используя системный часовой пояс)
val localDateTime: LocalDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault())
println("Локальная дата и время: $localDateTime")
// Создаём конкретную дату
val specificDate: LocalDate = LocalDate(2025, 3, 15)
println("Конкретная дата: $specificDate")
}
Вывод (зависит от текущего времени):
Текущий момент в UTC: 2025-03-15T12:30:45.123Z
Локальная дата и время: 2025-03-15T15:30:45.123
Конкретная дата: 2025-03-15
4. Основные методы, функции и классы
Рассмотрим 8 ключевых элементов библиотеки. Все примеры предполагают импорт import kotlinx.datetime.*.
4.1. Clock.System.now()
Сигнатура: fun now(): Instant
Что делает: Возвращает текущий момент времени в виде Instant (момент на временной шкале, обычно в UTC). Это аналог System.currentTimeMillis() в Java, но с наносекундной точностью.
val currentInstant: Instant = Clock.System.now()
println("Сейчас: $currentInstant") // Например: 2025-03-15T12:30:45.123456Z
4.2. Instant.toLocalDateTime()
Сигнатура: fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime
Что делает: Преобразует момент времени (Instant) в локальную дату и время (LocalDateTime) для указанного часового пояса. Учитывает смещение и переход на летнее время.
val instant = Instant.parse("2025-03-15T12:00:00Z")
val tz = TimeZone.of("Europe/Moscow") // UTC+3
val localDT = instant.toLocalDateTime(tz)
println(localDT) // 2025-03-15T15:00:00
4.3. LocalDate.plus() и minus()
Сигнатура: operator fun LocalDate.plus(other: DateTimePeriod): LocalDate (аналогично для minus)
Что делает: Позволяет прибавлять или вычитать временные периоды (DateTimePeriod) из даты. Поддерживает дни, месяцы, годы.
val today = LocalDate(2025, 3, 15)
val nextWeek = today.plus(DateTimePeriod(days = 7))
val lastMonth = today.minus(DateTimePeriod(months = 1))
println("Через неделю: $nextWeek") // 2025-03-22
println("Месяц назад: $lastMonth") // 2025-02-15
4.4. LocalDateTime.toInstant()
Сигнатура: fun LocalDateTime.toInstant(timeZone: TimeZone): Instant
Что делает: Преобразует локальную дату и время в момент времени (Instant) в заданном часовом поясе. Обратная операция к toLocalDateTime.
val localDT = LocalDateTime(2025, 3, 15, 15, 0, 0)
val tz = TimeZone.of("Europe/Moscow")
val instant = localDT.toInstant(tz)
println(instant) // 2025-03-15T12:00:00Z (так как Moscow UTC+3)
4.5. DateTimePeriod
Сигнатура: data class DateTimePeriod(val days: Int, val months: Int, val years: Int, val hours: Int, val minutes: Int, val seconds: Int, val nanoseconds: Int)
Что делает: Представляет собой период времени в календарных единицах (дни, месяцы, годы) и точных единицах (часы, минуты, секунды, наносекунды). Используется для арифметики с датами и временем.
val period = DateTimePeriod(days = 30, hours = 12)
val start = LocalDateTime(2025, 1, 1, 0, 0)
val end = start.plus(period)
println(end) // 2025-01-31T12:00
4.6. TimeZone.currentSystemDefault()
Сигнатура: fun currentSystemDefault(): TimeZone
Что делает: Возвращает часовой пояс, установленный в операционной системе. Это удобно для отображения времени в локальном формате.
val systemTz: TimeZone = TimeZone.currentSystemDefault()
println("Системный часовой пояс: ${systemTz.id}") // Например: "Europe/Moscow"
4.7. Instant.periodUntil()
Сигнатура: fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod
Что делает: Вычисляет разницу между двумя моментами времени в виде DateTimePeriod с учётом календаря (дни, месяцы, годы) для заданного часового пояса.
val start = Instant.parse("2025-01-01T00:00:00Z")
val end = Instant.parse("2025-03-15T12:30:00Z")
val diff = start.periodUntil(end, TimeZone.UTC)
println("Разница: ${diff.months} месяцев, ${diff.days} дней, ${diff.hours} часов")
// Разница: 2 месяцев, 14 дней, 12 часов
4.8. LocalDate.dayOfWeek
Сигнатура: val LocalDate.dayOfWeek: DayOfWeek (свойство)
Что делает: Возвращает день недели для даты в виде перечисления DayOfWeek (Monday, Tuesday, ..., Sunday). Полезно для календарных расчётов.
val date = LocalDate(2025, 3, 15) // 15 марта 2025
val dayOfWeek: DayOfWeek = date.dayOfWeek
println("День недели: ${dayOfWeek.name}") // SATURDAY
// Можно получить числовое представление (1 = Monday, 7 = Sunday)
println("Номер дня: ${dayOfWeek.isoDayNumber}") // 6
5. Реальный практический пример
Напишем приложение — планировщик встреч. Оно будет:
- Принимать список встреч с датой, временем и часовым поясом.
- Проверять, не пересекаются ли встречи.
- Вычислять длительность встречи.
- Выводить расписание в локальном времени пользователя.
import kotlinx.datetime.*
data class Meeting(
val name: String,
val startInstant: Instant,
val endInstant: Instant
)
class MeetingScheduler {
private val meetings = mutableListOf()
fun addMeeting(name: String, startLocal: LocalDateTime, endLocal: LocalDateTime, timeZone: TimeZone) {
val start = startLocal.toInstant(timeZone)
val end = endLocal.toInstant(timeZone)
if (start >= end) {
println("Ошибка: время начала должно быть раньше времени окончания")
return
}
// Проверка на пересечение
for (existing in meetings) {
if (start < existing.endInstant && end > existing.startInstant) {
println("Ошибка: встреча '$name' пересекается с '${existing.name}'")
return
}
}
meetings.add(Meeting(name, start, end))
println("Встреча '$name' добавлена")
}
fun printSchedule(userTimeZone: TimeZone) {
if (meetings.isEmpty()) {
println("Расписание пусто")
return
}
meetings.sortBy { it.startInstant }
println("Расписание встреч (в часовом поясе ${userTimeZone.id}):")
for ((index, meeting) in meetings.withIndex()) {
val startLocal = meeting.startInstant.toLocalDateTime(userTimeZone)
val endLocal = meeting.endInstant.toLocalDateTime(userTimeZone)
val duration = meeting.startInstant.periodUntil(meeting.endInstant, userTimeZone)
println("${index + 1}. ${meeting.name}")
println(" Начало: $startLocal")
println(" Конец: $endLocal")
println(" Длительность: ${duration.hours} ч ${duration.minutes} мин")
}
}
fun getMeetingsForDate(date: LocalDate, userTimeZone: TimeZone): List {
val dayStart = date.atStartOfDayIn(userTimeZone)
val dayEnd = date.plus(DateTimePeriod(days = 1)).atStartOfDayIn(userTimeZone)
return meetings.filter { it.startInstant >= dayStart && it.startInstant < dayEnd }
}
}
fun main() {
val