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 у мовах, де він узагалі буває.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ