JavaRush /Courses /Kotlin SELF /Current time and comparisons: Clock.System.now(), sorting...

Current time and comparisons: Clock.System.now(), sorting, and Duration

Kotlin SELF
Level 51, Lesson 2
Available

1. Why we need “current time”, and why it is not a string

When you first want to add time to a program, the usual thought is: “I’ll just print text like ‘12:34’ and that’s it.” But as soon as questions appear like “what happened earlier,” “how much time passed,” “sort by time,” “show the latest 10 events,” a string stops being your friend and becomes that coworker who “will just hold the cable for a second,” and then disappears along with the cable.

In this lecture, we will think like this: time is data that you can compare, sort, and subtract—not decoration in the console. For these tasks, kotlinx.datetime gives us Instant (a point on the time scale), and the Kotlin standard library gives us kotlin.time.Duration (a length of time between points).

Imagine a timeline as a straight line:

flowchart LR
  A["Instant A"] -->|elapsed| D["Duration"] --> B["Instant B"]

Instant is a “point”, and Duration is the “distance between points”.

2. Clock.System.now(): getting the current Instant

There is a very human desire: “I need the current time” → “so I’ll just paste Clock.System.now() everywhere.” It is roughly like cooking: “need salt” → “so I will salt at every step.” In the end, you either oversalt, or you get an unpredictable taste.

The right habit is this: if “current time” is needed as a reference point, take it once and store it in a val. This makes behavior more stable and the code clearer: you can see exactly which “now” you meant.

The simplest scenario: “freeze now”

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

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

The output is usually in ISO format. At this stage, what matters is not how it looks, but that it is an Instant type that you can compare and subtract.

Why calling now() twice gives two different values

import kotlinx.datetime.Clock

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

    println(a == b) // most likely: false
    println(b > a)  // most likely: true
}

Between the two calls, at least a microscopic fraction of time has passed. Sometimes it is so tiny that you start doubting reality, but the CPU does not doubt it.

Good style: “take now once and pass it along”

Suppose we are creating an event record. It is better to do this:

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

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

fun main() {
    val createdAt = createTimestamp()
    println(createdAt) // example: 2026-01-14T18:22:10.456Z
}

Yes, it looks “too simple”, but as soon as the code grows, you will be grateful that time is taken in one place instead of being smeared across the program like jam across a keyboard.

3. Comparing Instant: earlier/later and checks

Comparisons are where the real benefits of Instant begin. When you have a “moment in time” type, comparisons with >, <, >=, <= become meaningful: you are comparing points on a time scale, not letters in a string.

In this section, we will learn how to write “earlier/later” checks, write readable conditions, and be careful with the idea that “time can go backwards” (for example, due to system clock adjustments). We will not dig deep into the last one, but it is useful to know it can happen—at least so you are not surprised later by weird negative durations.

Check: “the event happened after the program started”

import kotlinx.datetime.Clock

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

    // ... there could be some code here ...

    val eventAt = Clock.System.now()

    if (eventAt >= startedAt) {
        println("The event happened after startup") // most likely true
    }
}

There is an important semantic point here: we compare moments, not local clock time.

Timeout check: “did more than N seconds pass?”

We are not building any complex timers yet, but the pattern is very common: you want to understand that “enough time has passed” and you can do something (or, conversely, it is time to stop).

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) // most often: false
}

This is where Duration shows up. For now it is enough to understand: the difference of two Instant values is a duration.

Nuance: compare the same “now”

Sometimes beginners write a condition like this (and yes, I did it too when I was young and compiled faster):

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

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

    // Bad idea: now is re-read each time
    if (Clock.System.now() - startedAt > 2.seconds) {
        println("More than 2 seconds have passed")
    }
}

Formally it works. In practice it hurts readability: you cannot see where the moment for the check is “fixed”. It is better to do val now = ... and use it further.

4. The difference between two Instant values as a Duration

When people first hear the word Duration, they often think “oh, it’s like a date, just smaller.” No. A duration is not a “small date”; it is a time interval: 5 seconds, 120 milliseconds, 3 minutes. A duration is convenient for measuring “how long it took”, “how much time passed”, “how late we are”.

In this section, we will learn how to get a Duration as the difference between two Instant values, extract numbers from it (seconds/milliseconds), and not confuse durations with calendar arithmetic. Calendars are a different beast, and we will not touch them today.

Basic example: end - start

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

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

    // suppose there was some code here

    val end = Clock.System.now()

    val elapsed: Duration = end - start
    println(elapsed) // example: 12ms
}

Important: elapsed is not a number. It is a duration object that you can convert to the units you need.

Getting milliseconds, seconds, and nanoseconds

import kotlinx.datetime.Clock

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

    val elapsed = end - start
    println(elapsed.inWholeMilliseconds) // example: 0
    println(elapsed.inWholeNanoseconds)  // example: 50000 (numbers are illustrative)
}

Yes, “0 milliseconds” is a normal result: less than a millisecond may have passed between the two calls.

Negative duration: not a bug, just life

Sometimes (rarely, but it happens) you can end up in a situation where “the end is earlier than the start”—for example, if the system clock shifts. For a basic course, it is enough to remember: Duration can be negative, and if you need guarantees, you must design the logic carefully (but that is an architecture topic for time measurements, not today’s mechanics).

import kotlin.time.Duration.Companion.seconds

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

5. Time-stamped events: sorting and a mini-example

As soon as you have a list of events with timestamps, you almost inevitably want: “show the latest”, “sort from newest to oldest”, “find the earliest”, “find the latest”. And this is where Instant suddenly becomes an ideal candidate for sorting, because it is comparable: Instant has a natural ordering “earlier → later”.

In this section, we will apply familiar list-sorting functions, but to moments in time. We will use sorted(), sortedDescending(), sortedBy { ... }, and friends. These functions in the standard library build a new list and do not mutate the original, which makes code more predictable.

Sorting a list of 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)
}

Here we use Instant.parse(...) purely as a quick way to get test moments. In a real application you will more often have live Clock.System.now() values.

Sorting “newest first”

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()) // the later moment first
}

Meaning: if an Instant represents a moment, then “greater” means “later”.

Sorting objects by the createdAt field

Now let’s take one step closer to an application. Suppose we have records (for example, expenses), and each one has a creation time.

import kotlinx.datetime.Instant

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

Then the sorting will look like this:

import kotlinx.datetime.Instant

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

fun main() {
    val xs = listOf(
        Expense("Coffee", 250, Instant.parse("2026-01-06T10:00:00Z")),
        Expense("Pizza", 900, Instant.parse("2026-01-06T09:00:00Z"))
    )

    val sorted = xs.sortedBy { it.createdAt }
    println(sorted.map { it.title }) // [Pizza, Coffee]
}

Functions like sortedBy() are the standard style: we sort a list by a key.

Mini practical example: adding time to an expense tracker

Now comes the important “why do we need all this” moment. In learning examples, it is easy to get the feeling that Instant is just a new type that prints as a nice string. But the real power starts when you add time to the application state and start building logic: sorting, “the last expense was N minutes ago”, “show the latest 5”.

In this mini-example we will take a simple “expense” model and add a createdAt: Instant field that is populated via Clock.System.now(). This does not require any new architectural tricks: just one more field, but it immediately opens many possibilities.

Expense creation function “right 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)
}

Notice the style: we take now once so that the “creation moment” is exactly one moment, not “slightly changing while the constructor is thinking”.

Add an expense to a list and print

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("Coffee", 250, Clock.System.now()))
    expenses.add(Expense("Subway", 60, Clock.System.now()))

    println(expenses.size) // 2
}

We use MutableList because we add elements. The fact that the collection is mutable does not require var: you can change the contents even with a val, because val protects the reference, not the object’s internals.

“Latest expenses”: sorting by time and take(n)

We do not dive into “top-N” as a separate topic, but the technique is very natural: first sort by time, then take the first few.

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)
}

Sorting here returns a new list and does not change the original, which reduces the number of “unexpected surprises” when reading code.

“How much time has passed since the last expense?”

This is where Duration becomes maximally practical.

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
}

The function contract is simple: if there are no expenses, return null; otherwise return the duration from the last event to the current moment.

And an example of usage:

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("Coffee", 250, Clock.System.now())
    )

    val d = Clock.System.now() - xs[0].createdAt
    println(d.inWholeSeconds) // most likely: 0
}

6. Common mistakes

Mistake #1: calling Clock.System.now() many times and thinking it is “the same now”.
If you read now() in several places in a row, you get several different moments. Sometimes the difference is microscopic, but logically it is already “different clocks”. Good style is to take val now = Clock.System.now() once within an operation and use it afterward.

Mistake #2: mixing up Duration and “calendar” shifts.
Duration is “how much time passed”, not “add one calendar day”. Even if a duration equals 24 hours, that does not mean “the next date” in a calendar sense, because calendar rules live separately from durations. For measurement tasks, stick with Instant + Duration, and do not try to imitate calendar logic with durations.

Mistake #3: sorting events by string time instead of Instant.
As soon as you store time as a string and do sorted() on strings, you are forced to rely on format and data discipline. That is too fragile. With Instant, sorting works by meaning and uses the natural ordering of time.

Mistake #4: “accidentally mutating” the original list during sorting.
In Kotlin there are sorts that return a new list (sorted*), and there are “in-place” sorts for MutableList. At the beginning, it is simpler and safer to stick to the “get a new list” style, because it reduces unexpected side effects: the original data stays as it was, and the sorted version is a separate result.

Mistake #5: trying to measure time via String/split/“manual parsing”.
If you catch yourself thinking “I’ll parse hours and minutes from a string and subtract them”, stop. This almost guaranteed leads to bugs at boundaries (minutes roll over 60, days roll over months, and so on). For measurements, use the difference of Instant values, which immediately gives you a Duration.

Mistake #6: treating Duration as a number and losing units.
Duration carries the meaning “duration” and lets you safely convert it to the units you need (inWholeMilliseconds, inWholeSeconds). If you convert it to a Long right away and forget whether it is seconds or milliseconds, a week later you will get the classic bug “1000x slower/faster”—almost like a forgotten break in a when in languages where it exists at all.

1
Task
Kotlin SELF, level 51, lesson 2
Locked
One timestamp
One timestamp
1
Task
Kotlin SELF, level 51, lesson 2
Locked
Which is earlier
Which is earlier
1
Task
Kotlin SELF, level 51, lesson 2
Locked
Enter Stopwatch
Enter Stopwatch
1
Task
Kotlin SELF, level 51, lesson 2
Locked
Activity feed
Activity feed
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION