JavaRush /Курсы /Kotlin SELF /Текущее время и сравнения: Clock.System.now(), сортировка...

Текущее время и сравнения: Clock.System.now(), сортировка и Duration

Kotlin SELF
51 уровень , 2 лекция
Открыта

1. Зачем нам «текущее время» и почему это не строка

Когда вы впервые хотите добавить в программу время, обычно мысль такая: «ну я выведу текст “12:34” и всё». Но как только появляются вопросы «что было раньше», «сколько прошло», «отсортировать по времени», «показать последние 10 событий», строка перестаёт быть другом и становится тем самым коллегой, который “просто подержит провод”, а потом исчезает вместе с проводом.

В этой лекции мы будем мыслить так: время — это данные, которые можно сравнивать, сортировать и вычитать, а не украшение в консоли. Для таких задач kotlinx.datetime даёт нам Instant (момент на шкале времени), а стандартная библиотека Kotlin — kotlin.time.Duration (длительность между моментами).

Представьте линию времени как прямую:

flowchart LR
  A["Instant A"] -->|прошло| D["Duration"] --> B["Instant B"]

Instant — это “точка”, Duration — это “расстояние между точками”.

2. Clock.System.now(): получаем текущий Instant

Есть очень человеческое желание: “мне нужно текущее время” → “значит, я просто везде вставлю Clock.System.now()”. Это примерно как в кулинарии: “нужно соли” → “значит, я буду солить на каждом шаге”. В итоге вы либо пересолите, либо получите непредсказуемый вкус.

Правильная привычка такая: если “текущее время” нужно как опорная точка, то мы берём его один раз и кладём в val. Это делает поведение стабильнее и код — понятнее: видно, какое именно “сейчас” вы имели в виду.

Самый простой сценарий: «зафиксировать сейчас»

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

fun main() {
    val now: Instant = Clock.System.now()
    println(now) // пример: 2026-01-14T18:22:10.123Z
}

Вывод обычно будет в ISO-представлении. На этом этапе нам важно не то, как оно выглядит, а то, что это тип Instant, который можно сравнивать и вычитать.

Почему now() два раза — это два разных значения

import kotlinx.datetime.Clock

fun main() {
    val a = Clock.System.now()
    val b = Clock.System.now()

    println(a == b) // скорее всего: false
    println(b > a)  // скорее всего: true
}

Между двумя вызовами прошла хотя бы микроскопическая доля времени. Иногда настолько маленькая, что вы начинаете сомневаться в реальности, но процессор — не сомневается.

Хороший стиль: «берём now один раз и передаём дальше»

Допустим, мы делаем запись события. Лучше сделать так:

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

fun createTimestamp(): Instant {
    return Clock.System.now()
}

fun main() {
    val createdAt = createTimestamp()
    println(createdAt) // пример: 2026-01-14T18:22:10.456Z
}

Да, это кажется “слишком просто”, но как только код разрастётся, вы будете благодарны себе за то, что время берётся в одном месте, а не размазано по всей программе как джем по клавиатуре.

3. Сравнение Instant: раньше/позже и проверки

Сравнения — это момент, где начинаются настоящие преимущества Instant. Когда у вас есть тип “момент времени”, сравнения >, <, >=, <= становятся осмысленными: это сравнение точек на временной шкале, а не сравнение букв в строке.

В этом разделе мы научимся делать проверки “раньше/позже”, писать читаемые условия и аккуратно относиться к идее “время может идти назад” (например, из-за корректировки системных часов). Последнее мы глубоко разбирать не будем, но знать, что такое бывает, полезно — хотя бы чтобы потом не удивляться странным отрицательным длительностям.

Проверка: «событие произошло после старта программы»

import kotlinx.datetime.Clock

fun main() {
    val startedAt = Clock.System.now()

    // ... тут мог быть какой-то код ...

    val eventAt = Clock.System.now()

    if (eventAt >= startedAt) {
        println("Событие произошло после старта") // скорее всего так и будет
    }
}

Тут важный смысловой момент: мы сравниваем моменты, а не локальное время на часах.

Проверка таймаута: «прошло больше N секунд?»

Пока мы не вводим никаких сложных таймеров, но сам паттерн очень частый: вы хотите понять, что прошло “достаточно времени”, и можно что-то делать (или наоборот — пора остановиться).

import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.seconds

fun main() {
    val startedAt = Clock.System.now()

    val now = Clock.System.now()
    val elapsed = now - startedAt

    println(elapsed > 2.seconds) // чаще всего: false
}

Здесь появляется Duration. Пока достаточно понимать: разница двух Instant — это длительность.

Нюанс: сравниваем одно и то же «сейчас»

Иногда новички пишут условие в таком стиле (и да, я тоже так делал, когда был молод и компилировался быстрее):

import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.seconds

fun main() {
    val startedAt = Clock.System.now()

    // Плохая идея: now берётся каждый раз заново
    if (Clock.System.now() - startedAt > 2.seconds) {
        println("Прошло больше 2 секунд")
    }
}

Формально это работает. Практически это ухудшает читаемость: вы не видите, где “фиксируется” момент для проверки. Лучше делать val now = ... и использовать его дальше.

4. Разница двух Instant как Duration

Когда люди впервые слышат слово Duration, они часто думают “о, это как дата, только поменьше”. Нет. Длительность — это не “дата маленького размера”, а промежуток времени: 5 секунд, 120 миллисекунд, 3 минуты. Длительность удобно использовать для измерений “сколько заняло”, “сколько прошло”, “на сколько опоздали”.

В этом разделе мы научимся получать Duration как разницу двух Instant, доставать из неё числа (секунды/миллисекунды) и не путать длительность с календарной арифметикой. Календарь — это отдельный зверь, и мы его сегодня не трогаем.

Базовый пример: end - start

import kotlinx.datetime.Clock
import kotlin.time.Duration

fun main() {
    val start = Clock.System.now()

    // допустим, тут был какой-то код

    val end = Clock.System.now()

    val elapsed: Duration = end - start
    println(elapsed) // пример: 12ms
}

Важно: elapsed — не число. Это объект-длительность, который можно переводить в нужные единицы.

Получаем миллисекунды, секунды и наносекунды

import kotlinx.datetime.Clock

fun main() {
    val start = Clock.System.now()
    val end = Clock.System.now()

    val elapsed = end - start
    println(elapsed.inWholeMilliseconds) // пример: 0
    println(elapsed.inWholeNanoseconds)  // пример: 50000 (числа условные)
}

Да, “0 миллисекунд” — нормальный результат: между двумя вызовами могло пройти меньше миллисекунды.

Отрицательная длительность: это не баг, это жизнь

Иногда (редко, но бывает) можно получить ситуацию, где “конец раньше начала” — например, если системные часы сдвинулись. На уровне базового курса достаточно помнить: Duration может быть отрицательной, и если вам важны гарантии, нужно проектировать логику аккуратно (но это уже тема архитектуры измерений времени, а не сегодняшняя механика).

import kotlin.time.Duration.Companion.seconds

fun main() {
    val d = (-5).seconds
    println(d) // -5s
}

5. События со временем: сортировка и мини‑пример

Как только у вас появляется список событий с метками времени, почти неизбежно хочется: “покажи последние”, “отсортируй от нового к старому”, “найди самое раннее”, “найди самое позднее”. И вот тут Instant внезапно превращается в идеального кандидата для сортировки, потому что он сравним: Instant имеет естественный порядок “раньше → позже”.

В этом разделе мы применим привычные функции сортировки списков, но уже к моментам времени. Мы будем использовать sorted(), sortedDescending(), sortedBy { ... } и друзей. Эти функции в стандартной библиотеке строят новый список и не мутируют исходный, что делает код предсказуемее.

Сортируем список Instant

import kotlinx.datetime.Instant

fun main() {
    val moments = listOf(
        Instant.parse("2026-01-06T10:00:01Z"),
        Instant.parse("2026-01-06T10:00:00Z"),
        Instant.parse("2026-01-06T09:59:59Z")
    )

    val sorted = moments.sorted()
    println(sorted)
}

Мы здесь используем Instant.parse(...) чисто как способ быстро получить тестовые моменты. В реальном приложении чаще будут живые Clock.System.now().

Сортировка «сначала новые»

import kotlinx.datetime.Instant

fun main() {
    val moments = listOf(
        Instant.parse("2026-01-06T10:00:01Z"),
        Instant.parse("2026-01-06T10:00:00Z")
    )

    println(moments.sortedDescending()) // сначала более поздний момент
}

Смысл: если Instant задаёт момент, то “больше” означает “позже”.

Сортируем объекты по полю createdAt

Теперь сделаем шаг ближе к приложению. Допустим, у нас есть записи (например, расходы), и у каждой есть время создания.

import kotlinx.datetime.Instant

data class Expense(
    val title: String,
    val amount: Int,
    val createdAt: Instant
)

Тогда сортировка будет такой:

import kotlinx.datetime.Instant

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun main() {
    val xs = listOf(
        Expense("Кофе", 250, Instant.parse("2026-01-06T10:00:00Z")),
        Expense("Пицца", 900, Instant.parse("2026-01-06T09:00:00Z"))
    )

    val sorted = xs.sortedBy { it.createdAt }
    println(sorted.map { it.title }) // [Пицца, Кофе]
}

Функции вроде sortedBy() — это стандартный стиль: мы сортируем список по ключу.

Мини-практический пример: добавляем время в трекер расходов

Сейчас будет важный момент “зачем это всё”. В учебных примерах легко получить ощущение, что Instant — это просто новый тип, который печатается красивой строкой. Но настоящая сила начинается, когда вы добавляете время в состояние приложения и начинаете строить логику: сортировки, «последний расход был N минут назад», «покажи последние 5».

В этом мини-примере мы возьмём простую модель “расход” и добавим к ней поле createdAt: Instant, которое заполняется через Clock.System.now(). Это не требует новых архитектурных трюков: просто ещё одно поле, но сразу открывает много возможностей.

Функция создания расхода «прямо сейчас»

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun createExpenseNow(title: String, amount: Int): Expense {
    val now = Clock.System.now()
    return Expense(title = title, amount = amount, createdAt = now)
}

Обратите внимание на стиль: now берём один раз, чтобы “момент создания” был ровно один, а не “немного меняющийся пока конструктор думал”.

Добавляем расход в список и печатаем

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun main() {
    val expenses = mutableListOf<Expense>()

    expenses.add(Expense("Кофе", 250, Clock.System.now()))
    expenses.add(Expense("Метро", 60, Clock.System.now()))

    println(expenses.size) // 2
}

Мы используем MutableList, потому что добавляем элементы. Сам факт, что коллекция мутируемая, не требует var: менять можно содержимое даже у val, потому что val защищает ссылку, а не внутренности объекта.

«Последние расходы»: сортировка по времени и take(n)

Мы не углубляемся в “топ-N” как отдельную тему, но сам приём очень естественный: сначала сортируем по времени, потом берём несколько первых.

import kotlinx.datetime.Instant

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun latest(expenses: List<Expense>, n: Int): List<Expense> {
    return expenses
        .sortedByDescending { it.createdAt }
        .take(n)
}

Сортировка здесь возвращает новый список и не меняет исходный, что снижает количество “неожиданных сюрпризов” при чтении кода.

«Сколько времени прошло с последнего расхода?»

Вот здесь Duration показывает себя максимально практично.

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun timeSinceLast(expenses: List<Expense>): Duration? {
    val last = expenses.maxByOrNull { it.createdAt } ?: return null
    return Clock.System.now() - last.createdAt
}

Контракт функции простой: если расходов нет — возвращаем null, иначе возвращаем длительность от последнего события до текущего момента.

И пример использования:

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

data class Expense(val title: String, val amount: Int, val createdAt: Instant)

fun main() {
    val xs = listOf(
        Expense("Кофе", 250, Clock.System.now())
    )

    val d = Clock.System.now() - xs[0].createdAt
    println(d.inWholeSeconds) // скорее всего: 0
}

6. Типичные ошибки

Ошибка №1: вызывать Clock.System.now() много раз и думать, что это “одно и то же сейчас”.
Если вы берёте now() в нескольких местах подряд, вы получаете несколько разных моментов времени. Иногда разница микроскопическая, но логически это уже “разные часы”. Хороший стиль — взять val now = Clock.System.now() один раз в рамках операции и использовать его дальше.

Ошибка №2: путать Duration и “календарные” сдвиги.
Duration — это “сколько прошло”, а не “прибавить один календарный день”. Даже если длительность равна 24 часам, это ещё не означает “следующая дата” в смысле календаря, потому что календарные правила живут отдельно от длительностей. В задачах измерений держитесь Instant + Duration, а календарные штуки не пытайтесь имитировать длительностями.

Ошибка №3: сортировать события по строковому времени вместо Instant.
Как только вы храните время как строку и делаете sorted() по строкам, вы начинаете надеяться на формат и дисциплину данных. Это слишком хрупко. С Instant сортировка работает по смыслу и использует естественный порядок времени.

Ошибка №4: “случайно мутировать” исходный список при сортировке.
В Kotlin есть сортировки, которые возвращают новый список (sorted*), и есть сортировки “на месте” для MutableList. На старте проще и безопаснее держаться стиля “получили новый список”, потому что так меньше неожиданных эффектов: исходные данные остаются как были, а отсортированная версия — отдельный результат.

Ошибка №5: пытаться измерять время через String/split/“ручной парсинг”.
Если вы ловите себя на мысли “я сейчас распарсю часы и минуты из строки и вычту”, остановитесь. Это почти гарантированно приведёт к ошибкам на границах (минуты переходят через 60, дни — через месяц и так далее). Для измерений используйте разницу Instant, которая сразу даёт Duration.

Ошибка №6: считать, что Duration — это число, и терять единицы измерения.
Duration хранит смысл “длительность” и позволяет безопасно переводить её в нужные единицы (inWholeMilliseconds, inWholeSeconds). Если сразу преобразовать в Long и забыть, это секунды или миллисекунды, то через неделю вы получите баг “в 1000 раз медленнее/быстрее” — классика жанра, почти как забытый break в when в языках, где он вообще бывает.

1
Задача
Kotlin SELF, 51 уровень, 2 лекция
Недоступна
Одна метка
Одна метка
1
Задача
Kotlin SELF, 51 уровень, 2 лекция
Недоступна
Кто раньше
Кто раньше
1
Задача
Kotlin SELF, 51 уровень, 2 лекция
Недоступна
Секундомер Enter
Секундомер Enter
1
Задача
Kotlin SELF, 51 уровень, 2 лекция
Недоступна
Лента событий
Лента событий
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ