Продвинутая работа с датами и временем в Kotlin
В современной разработке на Kotlin работа с датами и временем выходит далеко за рамки простого вывода текущей даты. На продвинутом уровне требуется учитывать часовые пояса, выполнять сложные вычисления с периодами, парсить нестандартные форматы и обрабатывать временные метки в распределённых системах. В этой статье мы рассмотрим ключевые библиотеки и техники, которые помогут вам писать надёжный и эффективный код для работы с датами и временем на Kotlin.
1. Выбор библиотеки: java.time vs kotlinx-datetime
Kotlin полностью совместим с Java, поэтому вы можете использовать мощный API java.time (JSR-310), который является стандартом для новых проектов. Однако для мультиплатформенных проектов (KMP) лучше выбрать kotlinx-datetime от JetBrains.
java.time (JVM)
Основные классы: LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period.
import java.time.*
fun main() {
val today = LocalDate.now()
val meetingTime = LocalDateTime.of(2025, 3, 15, 14, 30)
val zonedMeeting = ZonedDateTime.of(meetingTime, ZoneId.of("Europe/Moscow"))
println("Встреча в Москве: $zonedMeeting")
}
kotlinx-datetime (Multiplatform)
Добавьте зависимость в build.gradle.kts:
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
import kotlinx.datetime.*
fun main() {
val now = Clock.System.now()
val date = now.toLocalDateTime(TimeZone.of("Europe/Moscow")).date
println("Сегодня: $date")
}
2. Парсинг и форматирование дат
Корректный парсинг — основа работы с внешними данными. Рассмотрим продвинутые приёмы.
Парсинг с нестандартными форматами
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
fun parseCustomDate(dateString: String): LocalDateTime? {
val formatters = listOf(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("dd/MM/yyyy'T'HH:mm"),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
)
for (formatter in formatters) {
try {
return LocalDateTime.parse(dateString, formatter)
} catch (e: DateTimeParseException) {
// пробуем следующий
}
}
return null
}
fun main() {
println(parseCustomDate("2025-03-15 14:30:00")) // 2025-03-15T14:30
println(parseCustomDate("15/03/2025T14:30")) // 2025-03-15T14:30
}
Локализованное форматирование
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
fun main() {
val date = LocalDate.of(2025, 3, 15)
val usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withLocale(Locale.US)
val ruFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withLocale(Locale("ru"))
println("USA: ${date.format(usFormatter)}") // Saturday, March 15, 2025
println("RU: ${date.format(ruFormatter)}") // суббота, 15 марта 2025 г.
}
3. Сложные вычисления с Duration и Period
Duration — для точных интервалов времени (часы, минуты, секунды), Period — для календарных периодов (годы, месяцы, дни).
Разница между датами с учётом бизнес-логики
import java.time.*
import java.time.temporal.ChronoUnit
fun businessDaysBetween(start: LocalDate, end: LocalDate): Long {
var count = 0L
var current = start
while (current.isBefore(end)) {
if (current.dayOfWeek != DayOfWeek.SATURDAY &&
current.dayOfWeek != DayOfWeek.SUNDAY) {
count++
}
current = current.plusDays(1)
}
return count
}
fun main() {
val start = LocalDate.of(2025, 3, 10) // понедельник
val end = LocalDate.of(2025, 3, 17) // понедельник
println("Рабочих дней: ${businessDaysBetween(start, end)}") // 5
}
Работа с Instant и временными метками
import java.time.*
fun main() {
val start = Instant.now()
Thread.sleep(1500) // имитация работы
val end = Instant.now()
val duration = Duration.between(start, end)
println("Прошло: ${duration.seconds} секунд")
println("В миллисекундах: ${duration.toMillis()}")
// Сложение Duration с Instant
val future = start.plus(Duration.ofHours(2))
println("Через 2 часа: $future")
}
4. Таймзоны и переход на летнее время
Одна из самых сложных тем — корректная обработка часовых поясов, особенно при переводе часов.
Безопасное преобразование между таймзонами
import java.time.*
fun convertTimeZone(
dateTime: LocalDateTime,
fromZone: ZoneId,
toZone: ZoneId
): ZonedDateTime {
val zonedFrom = ZonedDateTime.of(dateTime, fromZone)
return zonedFrom.withZoneSameInstant(toZone)
}
fun main() {
val meeting = LocalDateTime.of(2025, 3, 30, 2, 30) // ночь перехода на летнее время
val moscow = ZoneId.of("Europe/Moscow")
val london = ZoneId.of("Europe/London")
val result = convertTimeZone(meeting, moscow, london)
println("В Москве: $meeting MSK -> В Лондоне: $result")
// Обратите внимание: 30 марта 2025 в 2:00 MSK -> 3:00 MSK (переход)
// Kotlin корректно обработает этот случай
}
Получение смещения для конкретной даты
import java.time.*
fun printOffsetInfo(zoneId: String) {
val zone = ZoneId.of(zoneId)
val now = LocalDateTime.now()
val zoned = ZonedDateTime.of(now, zone)
val offset = zoned.offset
println("Текущее смещение для $zoneId: $offset")
// Проверка, действует ли летнее время
val rules = zone.r