🎯 Kotlin

Arbeiten mit Datum und Uhrzeit in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — организатор школьного концерта. Тебе нужно знать: когда начинается репетиция, сколько она длится, какого числа нужно сдать костюмы, и через сколько дней состоится выступление. Без умения работать с датами и временем ты бы просто запутался: «Встречаемся в пятницу» — а какое сегодня число? «Через 3 дня» — а это какой день недели?

В программировании всё то же самое. Любое приложение, которое работает с расписанием, историей действий, сроками или календарём, должно уметь:

  • Запоминать текущий момент (сейчас 15:30 12 марта)
  • Сравнивать даты (сегодня раньше, чем завтра)
  • Вычислять разницу (сколько дней до каникул)
  • Форматировать дату для человека («12 марта 2025 года» вместо «2025-03-12»)

В Kotlin для этого есть специальные инструменты — библиотеки java.time (из Java) и встроенные функции. Они работают как календарь и часы одновременно, только гораздо точнее и умнее.

💡 Важно: Работа с датами — одна из самых сложных тем в программировании из-за високосных годов, часовых поясов и разных календарей. Kotlin (через Java Time API) делает это максимально простым и безопасным.

Теория: как это работает изнутри

Компьютер не понимает «12 марта 2025 года». Для него дата — это просто число — количество миллисекунд, прошедших с 1 января 1970 года (эта точка называется Unix-эпоха). Почему именно 1970? Так исторически сложилось в операционных системах.

Когда ты пишешь «2025-03-12», компьютер переводит это в число: 1 741 276 800 000 миллисекунд. А когда ты просишь показать дату «красиво», он делает обратное преобразование.

В Kotlin (и Java) есть несколько ключевых классов для работы с датами:

Класс Что хранит Пример
LocalDate Только дату (год, месяц, день) 2025-03-12
LocalTime Только время (часы, минуты, секунды) 15:30:45
LocalDateTime Дату и время вместе 2025-03-12T15:30:45
Instant Момент времени (миллисекунды от 1970) 1741276800000
Duration Промежуток времени (часы, минуты) 2 часа 30 минут
Period Промежуток в днях/месяцах/годах 3 месяца 5 дней

Все эти классы — неизменяемые (immutable). Это значит, что если ты создал дату «12 марта», ты не можешь её изменить. Ты можешь только создать новую дату на основе старой. Это защищает от случайных ошибок.

Синтаксис в Kotlin: все формы и варианты

В Kotlin для работы с датами используется Java Time API (пакет java.time). Он появился в Java 8 и считается лучшим стандартом. Kotlin полностью поддерживает его, а также добавляет свои удобные расширения.

1. Создание даты и времени

// Текущая дата
val today = LocalDate.now()
println(today) // 2025-03-12

// Конкретная дата
val birthday = LocalDate.of(2010, Month.MAY, 15)
// или так:
val birthday2 = LocalDate.of(2010, 5, 15)

// Текущее время
val now = LocalTime.now()
println(now) // 15:30:45.123

// Конкретное время
val meeting = LocalTime.of(14, 30) // 14:30
val exactTime = LocalTime.of(10, 15, 30, 500_000_000) // 10:15:30.5

// Дата + время
val moment = LocalDateTime.of(2025, 3, 12, 15, 30)
println(moment) // 2025-03-12T15:30

2. Разбор строки в дату

// Стандартный формат ISO (2025-03-12)
val dateFromString = LocalDate.parse("2025-03-12")

// Свой формат
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
val customDate = LocalDate.parse("12.03.2025", formatter)
println(customDate) // 2025-03-12

3. Форматирование даты

val date = LocalDate.of(2025, 3, 12)

// Встроенные форматы
println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)) // 2025-03-12
println(date.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"))) // 12 марта 2025
println(date.format(DateTimeFormatter.ofPattern("EEEE, d MMMM"))) // среда, 12 марта

// Символы форматирования:
// yyyy - год (2025)
// yy - год (25)
// MM - месяц (03)
// MMM - месяц (мар)
// MMMM - месяц (март)
// dd - день (12)
// EEEE - день недели (среда)

4. Арифметика с датами

val today = LocalDate.now()

// Прибавить дни
val nextWeek = today.plusDays(7)
val nextMonth = today.plusMonths(1)
val nextYear = today.plusYears(1)

// Отнять дни
val yesterday = today.minusDays(1)

// Работа с временем
val time = LocalTime.of(10, 30)
val later = time.plusHours(2).plusMinutes(15) // 12:45

5. Сравнение дат

val date1 = LocalDate.of(2025, 3, 12)
val date2 = LocalDate.of(2025, 6, 1)

println(date1.isBefore(date2)) // true (раньше)
println(date1.isAfter(date2))  // false (позже)
println(date1.isEqual(date2))  // false (равны)
println(date1.compareTo(date2)) // отрицательное число (date1 < date2)

6. Разница между датами

val start = LocalDate.of(2025, 1, 1)
val end = LocalDate.of(2025, 12, 31)

// Period - для дней/месяцев/лет
val period = Period.between(start, end)
println("${period.years} лет, ${period.months} месяцев, ${period.days} дней")
// 0 лет, 11 месяцев, 30 дней

// ChronoUnit - для точного количества
val daysBetween = ChronoUnit.DAYS.between(start, end)
println("$daysBetween дней") // 364 дня

// Duration - для времени
val time1 = LocalTime.of(10, 0)
val time2 = LocalTime.of(14, 30)
val duration = Duration.between(time1, time2)
println("${duration.toHours()} часов ${duration.toMinutes() % 60} минут")
// 4 часов 30 минут

7. Часовые пояса

// ZonedDateTime - дата и время с часовым поясом
val moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))
val newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"))

println("Москва: $moscowTime")
println("Нью-Йорк: $newYorkTime")

// Преобразование между поясами
val meetingInMoscow = ZonedDateTime.of(
    LocalDateTime.of(2025, 3, 12, 15, 0),
    ZoneId.of("Europe/Moscow")
)
val meetingInNY = meetingInMoscow.withZoneSameInstant(ZoneId.of("America/New_York"))
println("В Москве: $meetingInMoscow, в Нью-Йорке: $meetingInNY")

Примеры кода — от простого к сложному

Пример 1: Сколько дней до дня рождения?

import java.time.LocalDate
import java.time.Month
import java.time.temporal.ChronoUnit

fun main() {
    val today = LocalDate.now()
    // Задаём день рождения (15 мая)
    val birthday = LocalDate.of(today.year, Month.MAY, 15)
    
    // Если день рождения уже прошёл в этом году — берём следующий год
    val nextBirthday = if (birthday.isBefore(today) || birthday.isEqual(today)) {
        birthday.plusYears(1)
    } else {
        birthday
    }
    
    val daysUntil = ChronoUnit.DAYS.between(today, nextBirthday)
    println("До твоего дня рождения осталось $daysUntil дней!")
}

Пример 2: Проверка, является ли год високосным

import java.time.Year

fun main() {
    val year = 2024
    val isLeap = Year.isLeap(year.toLong())
    
    if (isLeap) {
        println("$year — високосный год! В феврале 29 дней.")
    } else {
        println("$year — не високосный. В феврале 28 дней.")
    }
    
    // Альтернатива через LocalDate
    val date = LocalDate.of(year, 1, 1)
    println("Дней в году: ${date.lengthOfYear()}") // 366 для високосного
}

Пример 3: Таймер обратного отсчёта до Нового года

import java.time.LocalDateTime
import java.time.Duration

fun main() {
    val now = LocalDateTime.now()
    val newYear = LocalDateTime.of(now.year + 1, 1, 1, 0, 0)
    
    val duration = Duration.between(now, newYear)
    
    val days = duration.toDays()
    val hours = duration.toHours() % 24
    val minutes = duration.toMinutes() % 60
    val seconds = duration.seconds % 60
    
    println("До Нового года осталось:")
    println("$days дней, $hours часов, $minutes минут, $seconds секунд")
}

Пример 4: Определение дня недели по дате

import java.time.LocalDate
import java.time.format.TextStyle
import java.util.Locale

fun main() {
    val date = LocalDate.of(2025, 3, 12)
    val dayOfWeek = date.dayOfWeek
    
    // Получаем название на русском
    val russianName = dayOfWeek.getDisplayName(TextStyle.FULL, Locale("ru"))
    val shortName = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale("ru"))
    
    println("$date — это $russianName ($shortName)")
    // Вывод: 2025-03-12 — это среда (ср)
    
    // Номер дня недели (1 = понедельник, 7 = воскресенье)
    println("Номер дня недели: ${dayOfWeek.value}")
}

Пример 5: Работа с часовыми поясами — конвертер времени

import java.time.ZonedDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun main() {
    // Время в Москве
    val moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))
    
    // Конвертируем в другие города
    val londonTime = moscowTime.withZoneSameInstant(ZoneId.of("Europe/London"))
    val tokyoTime = moscowTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
    val newYorkTime = moscowTime.withZoneSameInstant(ZoneId.of("America/New_York"))
    
    val formatter = DateTimeFormatter.ofPattern("HH:mm dd.MM.yyyy")
    
    println("Сейчас в мире:")
    println("Москва: ${moscowTime.format(formatter)}")
    println("Лондон: ${londonTime.format(formatter)}")
    println("Токио: ${tokyoTime.format(formatter)}")
    println("Нью-Йорк: ${newYorkTime.format(formatter)}")
}

Пример 6: Парсинг даты из пользовательского ввода

import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

fun main() {
    // Представим, что пользователь ввёл дату
    val userInput = "15/03/2025"
    
    // Создаём форматтер для нашего формата
    val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
    
    try {
        val date = LocalDate.parse(userInput, formatter)
        println("Распознана дата: $date")
        
        // Проверяем, не будущая ли это дата
        if (date.isAfter(LocalDate.now())) {
            println("Это будущая дата!")
        } else {
            println("Это прошедшая или сегодняшняя дата.")
        }
    } catch (e: DateTimeParseException) {
        println("Ошибка! Неверный формат даты. Используйте ДД/ММ/ГГГГ")
    }
}

Пример 7: Полный календарь на месяц

import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth

fun printCalendar(year: Int, month: Int) {
    val yearMonth = YearMonth.of(year, month)
    val firstDay = yearMonth.atDay(1)
    val lastDay = yearMonth.atEndOfMonth()
    
    println("Календарь на ${yearMonth.month} $year")
    println("Пн Вт Ср Чт Пт Сб Вс")
    
    // Смещение для первого дня (понедельник = 1)
    val offset = firstDay.dayOfWeek.value - 1
    print("   ".repeat(offset))
    
    for (day in 1..lastDay.dayOfMonth) {
        print("%2d ".format(day))
        val currentDate = LocalDate.of(year, month, day)
        if (currentDate.dayOfWeek == DayOfWeek.SUNDAY) {
            println() // Перенос на новую строку после воскресенья
        }
    }
    println()
}

fun main() {
    printCalendar(2025, 3)
}

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Вот основные различия:

Операция Kotlin (Java Time API) Python (datetime)
Текущая дата LocalDate.now() datetime.date.today()
Создание даты LocalDate.of(2025, 3, 12) datetime.date(2025, 3, 12)
Разница в днях ChronoUnit.DAYS.between(d1, d2) (d2 - d1).days
Форматирование date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) date.strftime("%d.%m.%Y")
Парсинг строки LocalDate.parse("12.03.2025", formatter) datetime.strptime("12.03.2025", "%d.%m.%Y")
Часовые пояса ZonedDateTime (встроен) pytz или zoneinfo (требует установки)

Главные отличия:

  • Неизменяемость: В Kotlin все объекты дат неизменяемые. В Python datetime тоже неизменяемый, но есть нюансы.
  • Типобезопасность: Kotlin строже — ты не сможешь случайно сложить дату с числом, не вызвав специальный метод.
  • Duration vs timedelta: В Kotlin есть Duration (для часов/минут) и Period (для дней/месяцев). В Python всё делает timedelta.
  • Форматтеры: В Kotlin используются DateTimeFormatter с паттернами, похожими на Java. В Python — strftime/strptime с символами %.
💡 Совет: Если ты переходишь с Python на Kotlin, запомни главное правило: в Kotlin почти всегда нужно явно указывать, что ты хочешь сделать (прибавить дни, отнять часы), в то время как Python позволяет больше «магии» с операторами.

Типичные ошибки и подводные камни

Ошибка 1: Забыть про неизменяемость

// ❌ НЕПРАВИЛЬНО
val date = LocalDate.of(2025, 3, 12)
date.plusDays(7) // Результат никуда не сохранён!
println(date) // Всё ещё 2025-03-12

// ✅ ПРАВИЛЬНО
val date = LocalDate.of(2025, 3, 12)
val newDate = date.plusDays(7)
println(newDate) // 2025-03-19

Ошибка 2: Путаница между Duration и Period

// ❌ НЕПРАВИЛЬНО: Duration для дней
val d1 = LocalDate.of(2025, 1, 1)
val d2 = LocalDate.of(2025, 1, 10)
val wrong = Duration.between(d1, d2) // Ошибка компиляции!

// ✅ ПРАВИЛЬНО: Period для дат, Duration для времени
val correct = Period.between(d1, d2) // P9D
val days = ChronoUnit.DAYS.between(d1, d2) // 9

Ошибка 3: Игнорирование часовых поясов

// ❌ НЕПРАВИЛЬНО: Использовать LocalDateTime для跨часовых операций
val eventInMoscow = LocalDateTime.of(2025, 3, 12, 15, 0)
// Если пользователь в Нью-Йорке, он увидит 15:00, а не 7:00

// ✅ ПРАВИЛЬНО: Использовать ZonedDateTime
val eventInMoscow = ZonedDateTime.of(
    LocalDateTime.of(2025, 3, 12, 15, 0),
    ZoneId.of("Europe/Moscow")
)

Ошибка 4: Неверный формат при парсинге

// ❌ НЕПРАВИЛЬНО: Путаница между MM и mm
val formatter = DateTimeFormatter.ofPattern("dd.mm.yyyy") // mm = минуты!
val date = LocalDate.parse("12.03.2025", formatter) // Ошибка!

// ✅ ПРАВИЛЬНО: MM = месяц, mm = минуты
val correctFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")

Ошибка 5: Високосные годы

// ❌ НЕПРАВИЛЬНО: Считаем, что в феврале всегда 28 дней
val feb29 = LocalDate.of(2023, 2, 29) // Ошибка! 2023 не високосный

// ✅ ПРАВИЛЬНО: Проверяем
val year = 2024
if (Year.of(year).isLeap) {
    val feb29 = LocalDate.of(year, 2, 29)
    println("Есть 29 февраля!")
}

Ошибка 6: Сравнение дат без учёта времени

// ❌ НЕПРАВИЛЬНО: Сравнение LocalDateTime с LocalDate
val dateTime = LocalDateTime.now()
val date = LocalDate.now()
// dateTime.isBefore(date) // Ошибка компиляции!

// ✅ ПРАВИЛЬНО: Приводим к одному типу
val dateFromDateTime = dateTime.toLocalDate()
println(dateFromDateTime.isEqual(date)) // true

Лучшие практики и советы профессионалов

1. Всегда используйте Java Time API (не устаревшие классы)

Избегайте java.util.Date, java.util.Calendar и SimpleDateFormat. Они устарели, неудобны и содержат ошибки проектирования. Java Time API (java.time.*) — современный стандарт.

2. Храните даты в UTC, показывайте в локальном времени

В базе данных и в логике приложения всегда храните даты в UTC (всемирное координированное время). Преобразуйте в часовой пояс пользователя только при отображении.

// Хранение
val utcNow = Instant.now() // Всегда UTC

// Отображение для пользователя
val userZone = ZoneId.of("Europe/Moscow")
val localTime = utcNow.atZone(userZone)
println(localTime.format(formatter))

3. Используйте ChronoUnit для точных расчётов

ChronoUnit даёт точное количество единиц между датами, учитывая все нюансы:

val start = LocalDate.of(2025, 1, 1)
val end = LocalDate.of(2025, 12, 31)

// Точное количество дней
val days = ChronoUnit.DAYS.between(start, end) // 364

// Точное количество месяцев
val months = ChronoUnit.MONTHS.between(start, end) // 11

4. Никогда не парсите даты без try-catch

Пользовательский ввод всегда может быть неверным. Всегда оборачивайте парсинг в try-catch:

fun parseDateSafely(input: String): LocalDate? {
    return try {
        LocalDate.parse(input, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
    } catch (e: DateTimeParseException) {
        println("Неверный формат даты: $input")
        null
    }
}

5. Используйте YearMonth для работы с месяцами

Если вам нужно работать с целым месяцем (например, «март 2025»), используйте YearMonth:

val march2025 = YearMonth.of(2025, 3)
println("Дней в месяце: ${march2025.lengthOfMonth()}") // 31
println("Первый день: ${march2025.atDay(1)}") // 2025-03-01
println("Последний день: ${march2025.atEndOfMonth()}") // 2025-03-31

6. Для сериализации используйте ISO 8601

При передаче дат через API или сохранении в JSON используйте стандартный формат ISO 8601: 2025-03-12T15:30:00Z. Он понятен всем языкам программирования.

Вопросы для самопроверки

  1. Вопрос: Какой класс в Kotlin используется для хранения только даты (без времени)?
    Ответ: LocalDate.
  2. Вопрос: Что произойдёт, если вызвать LocalDate.of(2023, 2, 29)?
    Ответ: Будет выброшено исключение DateTimeException, так как 2023 год не високосный.
  3. Вопрос: В чём разница между Duration и Period?
    Ответ: Duration измеряет время в часах/минутах/секундах (точное количество), Period — в днях/месяцах/годах (календарные единицы).
  4. Вопрос: Как получить текущую дату и время в часовом поясе «Europe/Moscow»?
    Ответ: ZonedDateTime.now(ZoneId.of("Europe/Moscow")).
  5. Вопрос: Почему нельзя использовать java.util.Date в современном Kotlin?
    Ответ: Он устарел, имеет плохой дизайн (мутабельный, неудобные методы), не поддерживает часовые пояса корректно. Java Time API (java.time.*) — современная замена.
  6. Вопрос: Как узнать, сколько дней осталось до 1 июня 2025 года?
    Ответ: ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.of(2025, 6, 1)).
  7. Вопрос: Что такое ISO 8601 и зачем он нужен?
    Ответ: Это международный стандарт формата даты и времени (например, 2025-03-12T15:30:00Z). Он однозначно интерпретируется во всех языках программирования и системах.
  8. Вопрос: Как отформатировать дату в виде «12 марта 2025 года»?
    Ответ: date.format(DateTimeFormatter.ofPattern("d MMMM yyyy 'года'", Locale("ru"))).
  9. Вопрос: Что такое Unix-эпоха и как она связана с датами?
    Ответ: Unix-эпоха — это 1 января 1970 года 00:00:00 UTC. Все даты в компьютере хранятся как количество миллисекунд (или секунд) от этой точки.
  10. Вопрос: Как безопасно распарсить дату из строки, введённой пользователем?
    Ответ: Использовать try-catch с DateTimeFormatter и обрабатывать DateTimeParseException. Никогда не доверять пользовательскому вводу.

Итог и что изучить дальше

Мы разобрали, как работать с датой и временем в Kotlin. Ты узнал:

  • Что даты хранятся как числа (миллисекунды от 1970 года)
  • Какие классы существуют (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period)
  • Как создавать, форматировать, парсить и сравнивать даты
  • Как работать с часовыми поясами и високосными годами
  • Какие ошибки чаще всего допускают новички
  • Лучшие практики от профессионалов

Что изучить дальше:

  1. Kotlinx-datetime — мультиплатформенная библиотека от JetBrains для работы с датами (если ты пишешь под несколько платформ).
  2. Сериализация дат — как правильно передавать даты через JSON (библиотеки kotlinx.serialization, Gson, Jackson).
  3. Работа с календарями — создание событий, повторяющиеся задачи, расчёт рабочих дней.
  4. Тестирование дат — как писать тесты для кода, который зависит от текущего времени (использование Clock).
  5. Базы данных и даты — как хранить даты в SQLite, PostgreSQL, как работать с JDBC и Exposed.
🎯 Главный совет: Не пытайся запомнить все методы. Запомни главное: в Kotlin есть java.time — мощный и безопасный инструмент. Всегда используй его, и твой код будет надёжным. А если забудешь синтаксис — просто открой эту статью!