JavaRush/Courses/Kotlin SELF/Calendar arithmetic and time zones: DatePeriod and start ...

Calendar arithmetic and time zones: DatePeriod and start of day

Open

1. Calendar and timeline

When you first start working with dates, it’s very tempting to think that date and time are the same thing, just “with different precision”. But in programming these are two different axes of meaning: the calendar answers “which day?”, and the time scale answers “at what moment?”. If you don’t separate them, you’ll get bugs that show up only twice a year (and, as luck would have it, in production).

Let’s imagine two typical requirements:

  • If you are doing “pay by 2026-02-01”, that’s a calendar date. LocalDate fits here, because what matters is the day in the calendar, not seconds and not the time zone.
  • If you are doing “the event happened at 2026-02-01T10:15:30Z”, that’s a point in time. You need Instant here, because you care about a point on the time scale that is the same for everyone.

The core idea of the lecture in one sentence is: do calendar shifts via LocalDate + DatePeriod, and measure elapsed time between instants via Instant and Duration. And yes, these are different things, even if it feels to a person that “a day is 24 hours”.

2. DatePeriod and Duration

If Duration is “how much time has passed” in seconds/milliseconds/hours, then DatePeriod is “by how much to shift the calendar” in years/months/days. And here it’s important not just to memorize the names, but to feel that these are two different kinds of math.

  • With Duration the world is flat: 1 hour is always 60 minutes, 1 minute is always 60 seconds. It’s like a ruler.
  • With the calendar the world is uneven: a month can have 28, 29, 30, or 31 days. Plus there are time zone transitions and DST (daylight saving time), because of which a particular calendar “date + time” may behave unexpectedly. It’s like a ruler that sometimes turns into an accordion.

It’s useful to keep a table like this in mind:

Task Correct type Correct “arithmetic”
“How long did execution take?”
Instant
end - start → Duration
“Show expenses for the last 7 days (calendar)”
LocalDate
today - DatePeriod(days = 6)
“Extend the subscription in 1 month”
LocalDate
date + DatePeriod(months = 1)
“Send a notification in 24 hours”
Instant
instant + 24.hours (via Duration)

A small but important note about coding style. When you write functions, Kotlin encourages readable signatures and neat formatting of multi-line parameters — it reduces the chance of an “off-by-one-argument” mistake and makes the code friendlier to your future self. This style also aligns well with the official function formatting conventions.

3. Calendar shifts in LocalDate

When you want “plus one day” or “minus two months”, the right tool is DatePeriod. In kotlinx.datetime (at the usage level in this course) it looks very nice: you create DatePeriod(...) and add or subtract it from LocalDate.

Let’s start with the simplest: plus one day.

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate

fun main() {
    val d = LocalDate.parse("2026-01-31")
    val next = d + DatePeriod(days = 1)

    println(d)       // 2026-01-31
    println(next)    // 2026-02-01
}

Here a clear “calendar logic” works: January 31 + 1 day = February 1. No hours, minutes, or “how many seconds are in this day”.

Now “minus a week” (a calendar week is just 7 days on the calendar):

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate

fun main() {
    val d = LocalDate.parse("2026-01-14")
    val weekAgo = d - DatePeriod(days = 7)

    println(weekAgo) // 2026-01-07
}

And with months we get to the reason calendar arithmetic is separated into its own tool at all.

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate

fun main() {
    val d = LocalDate.parse("2026-01-31")
    val nextMonth = d + DatePeriod(months = 1)

    println(nextMonth) // (result depends on calendar rules)
}

Why didn’t I write a comment with the exact date here? Because it’s more important to understand the idea: “plus a month” is not “plus 30 days”. It means “move to the next month, keeping the day-of-month as much as possible according to the calendar rules”. In practice, this is where you must understand the requirements: the business may want “the last day of the next month”, “the same day number if it exists”, or “a shift by 30 days” — and these are three different logics.

Days vs duration: two different meanings

When you write a program, the brain often makes a sneaky substitution: if you see “in 1 day”, your hand reaches for Duration. Because “day” sounds like 24 hours. But for the calendar, “day” is a date change, not 24*60*60 seconds.

  • The scenario “deadline by the end of tomorrow” is almost always calendar-based: you want it to be “tomorrow” on the calendar, regardless of how many hours are in the user’s day (spoiler: sometimes not 24).
  • The scenario “wait a day and make a request” is duration-based. Duration is appropriate there.

To better feel the difference, let’s compare two functions: one does “plus 7 calendar days”, the other does “plus 7 * 24 hours”.

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate

fun add7CalendarDays(date: LocalDate): LocalDate {
    return date + DatePeriod(days = 7)
}
import kotlinx.datetime.Instant
import kotlin.time.Duration.Companion.days

fun add7x24Hours(instant: Instant): Instant {
    return instant + 7.days
}

Both functions are useful. They just answer different questions. If you mix them up, you’ll get a bug that shows up “sometimes”. And “sometimes” in programming translates to “the user will definitely hit it”.

4. Start of day and conversion to Instant

As soon as you want to turn a calendar date (LocalDate) into a point in time (Instant), you must choose a time zone. Because “2026-01-14 00:00” in Tokyo and “2026-01-14 00:00” in New York are different moments on the time scale.

In everyday speech we say “start of day” as if it were a universal point. In programming it’s not a universal point, but a rule: “in this zone, the start of this calendar day corresponds to such-and-such an instant”.

In kotlinx.datetime there’s a convenient function for this: atStartOfDayIn(timeZone).

import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn

fun main() {
    val date = LocalDate.parse("2026-01-14")

    val startUtc = date.atStartOfDayIn(TimeZone.UTC)
    println(startUtc) // 2026-01-14T00:00:00Z
}

Notice the elegance: LocalDate by itself doesn’t contain a time of day, and atStartOfDayIn(...) says: “OK, we assume the start of day is 00:00 in this zone, and convert it to an Instant”.

It’s very useful to visualize it like this:

flowchart LR
    A["LocalDate: 2026-01-14
calendar date"] -->|"atStartOfDayIn(TimeZone)"| B["Instant
point in time on the timeline"] B -->|"toLocalDateTime(TimeZone)"| C["LocalDateTime
local clock time"]

The key idea here: TimeZone is a bridge. Without the bridge you’re standing on the shore feeling sad (or writing hacks, and then feeling even sadder).

From LocalDateTime to Instant

There’s a common situation: you get local “wall clock time” from the user, for example 2026-01-14T10:00:00. This is LocalDateTime. But to turn it into an “instant”, you need to understand: 10 AM where?

In kotlinx.datetime the conversion is done like this: localDateTime.toInstant(timeZone).

import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant

fun main() {
    val ldt = LocalDateTime.parse("2026-01-14T10:00:00")

    val instantUtc = ldt.toInstant(TimeZone.UTC)
    println(instantUtc) // 2026-01-14T10:00:00Z
}

If you plug in the system zone, you’ll get the instant corresponding to “10:00 according to the local rules of the machine where the program is running”.

import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant

fun main() {
    val ldt = LocalDateTime.parse("2026-01-14T10:00:00")
    val tz = TimeZone.currentSystemDefault()

    val instant = ldt.toInstant(tz)
    println(instant) // depends on tz
}

Why does this matter? Because the “system time zone” is a hidden dependency. Today the program runs on your laptop, tomorrow on a server in a different zone, the day after tomorrow on a user’s device while traveling. If the zone affects the meaning, it’s better to pass TimeZone explicitly as a parameter rather than hoping it will “somehow work out”.

DST and why a day is not always 24 hours

This part is not to make you an expert on time zones. It’s to make you stop treating time as linear where it’s not linear.

DST (daylight saving time) is a situation where in some regions, on a certain day, the clock “jumps” forward or backward. Because of this, in local time there can be “short days” where an hour disappears, and “long days” where an hour repeats.

What does this mean in practice? If you take a LocalDate, compute atStartOfDayIn(tz) for two neighboring dates, and subtract them as Instants, you may get a Duration that is not exactly 24 hours. And that is normal, because the calendar lives by local rules.

For the purposes of this course, it’s enough to remember the rule: if you want “the next calendar day”, use LocalDate + DatePeriod(days = 1); if you want “24 hours”, use Instant + 24.hours. Don’t try to replace one with the other — it’s like trying to replace “go to the next page” with “scroll 300 pixels”: sometimes it looks similar, but the meaning is different.

When you really need “start of day”

A question may come up now: if we store expenses as LocalDate, why do we need atStartOfDayIn() and conversion to Instant at all? It usually appears exactly when you start doing “real” sorting and boundaries.

Imagine you want to export expenses into a log where everything is stored as instants (for example, for synchronization or audit). Or you want to compare expenses “strictly starting at midnight” in a particular zone. Then the “date” has to become an “instant”, and that’s where the start-of-day rule comes in.

For example, let’s make a function: “get the instant for the start of the expense day” in a given zone.

import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn

fun expenseStartInstant(expense: Expense, timeZone: TimeZone): Instant {
    return expense.date.atStartOfDayIn(timeZone)
}

And you can sort expenses by these instants (although for LocalDate sorting is usually simple anyway). But the point is that you now make the dependency on the zone explicit and don’t try to “guess” where that midnight is.

5. Practical example: dates in BudgetBuddy

Now let’s take a practical (and quite real-life) step: we’ll add the “calendar date of the transaction” to our training console expense tracking app. Let’s call it modestly and without pathos: BudgetBuddy. In previous chapters of the course we already have an expense model, an expense list, commands, and reports. Now we want an expense to be not just “amount and category”, but also “on which day”.

Model: an expense with LocalDate

Let’s start with a simple model. Note: we use LocalDate because for expenses in everyday sense the calendar day usually matters (“I spent money on January 14”), not the exact Instant.

import kotlinx.datetime.LocalDate

data class Expense(
    val id: Int,
    val amountCents: Int,
    val category: String,
    val date: LocalDate,
)

Yes, we store money in cents (Int) so we don’t argue with Double about where it puts the decimal separator (Double usually puts it exactly where you least want it).

“Today” as LocalDate

We need “today” as a calendar date. The most direct route: take an Instant and convert it to LocalDate via the system zone.

import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

fun todayLocalDate(timeZone: TimeZone): kotlinx.datetime.LocalDate {
    val now = Clock.System.now()
    return now.toLocalDateTime(timeZone).date
}

There’s an important idea here: even “today” depends on the time zone. When in one zone it’s already January 14, in another it’s still the 13th. So we accept timeZone as a parameter and make the dependency explicit.

Safe parsing of an input date

Now let’s add a date reading function. The user enters a string in ISO format yyyy-MM-dd (because LocalDate.parse understands it), and we return LocalDate?.

import kotlinx.datetime.LocalDate

fun parseLocalDateOrNull(input: String): LocalDate? {
    val prepared = input.trim()
    return runCatching { LocalDate.parse(prepared) }.getOrNull()
}

The contract is simple: either the date is valid, or null. No program crashes just because someone typed 2026-13-40.

Expense date: empty → take “today”

Let’s assemble a piece of the adding logic. Here we use guard clauses (early returns) so the code reads linearly. And yes, style-wise such functions are usually better formatted neatly, especially if parameters start to grow — this matches the general Kotlin coding conventions.

import kotlinx.datetime.TimeZone

fun readExpenseDateOrToday(timeZone: TimeZone): kotlinx.datetime.LocalDate {
    print("Date (yyyy-MM-dd), empty = today: ")
    val s = readln().trim()

    if (s.isEmpty()) return todayLocalDate(timeZone)

    return parseLocalDateOrNull(s) ?: todayLocalDate(timeZone)
}

Yes, there is a compromise here: if the date is invalid, we silently take today. In a real product it’s better to tell the user about the error, but for a learning example this is fine: we focus on types and arithmetic.

Report “last 7 calendar days”

Now the most interesting part: we want to print expenses for the last 7 days, including today. This is pure calendar logic: “today” and “six days ago”.

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.TimeZone

fun last7DaysRange(timeZone: TimeZone): ClosedRange<kotlinx.datetime.LocalDate> {
    val end = todayLocalDate(timeZone)
    val start = end - DatePeriod(days = 6)
    return start..end
}

Now let’s apply the range to the list.

import kotlinx.datetime.TimeZone

fun filterByDateRange(
    expenses: List<Expense>,
    timeZone: TimeZone,
): List<Expense> {
    val range = last7DaysRange(timeZone)
    return expenses.filter { it.date in range }
}

Note: LocalDate is compared as a calendar date, and the start..end range reads very human-friendly.

6. Common mistakes

Mistake #1: doing calendar shifts via Duration.
Very often a beginner sees “plus one day” and writes + 24.hours. Sometimes this matches expectations, but it’s a completely different semantics: you shift a point in time along a linear scale. If the task needs “the next calendar day”, use LocalDate + DatePeriod(days = 1), otherwise time zone and DST effects will start surfacing.

Mistake #2: assuming that “a month” equals “30 days”.
The calendar has no such rule. Months have different lengths, and sometimes February brings a surprise. If the requirement is “in a month”, use DatePeriod(months = 1) and clarify the business rule for dates like the 29th/30th/31st (what to do if that day doesn’t exist in the next month).

Mistake #3: trying to get an Instant from a LocalDate or LocalDateTime without a time zone.
LocalDate and LocalDateTime don’t contain zone information, so they cannot unambiguously become a “moment”. If you need an Instant, choose the zone explicitly: date.atStartOfDayIn(tz) or ldt.toInstant(tz). This isn’t pedantry; it’s protection from hidden bugs.

Mistake #4: using TimeZone.currentSystemDefault() as a “magic constant” everywhere.
The system zone is an environment dependency; it changes between computers and servers. If the zone matters for meaning (for example, a report must be built “by the user’s time”), pass TimeZone as a parameter. This makes the code testable and predictable.

Mistake #5: mixing “date” and “instant” in one variable.
For example, storing the expense date as the string "2026-01-14T00:00:00Z" and then trying to cut off substring(0, 10) “to get the date”. That takes you back into the world of string-based hacks. If the entity is a calendar date, store LocalDate. If the entity is a point in time, store Instant.

Mistake #6: expecting the difference between the starts of neighboring days to always equal 24 hours.
In local zones, due to time transitions, there are days that are “shorter” or “longer” by an hour. This is normal. So don’t use “start of day + 24 hours” as a universal formula for “the next day”. For calendar logic use DatePeriod, for duration use Duration.

1
Task
Kotlin SELF,  51 level,  4 lecture
Locked
Deadline shift
Deadline shift
1
Task
Kotlin SELF,  51 level,  4 lecture
Locked
Report window
Report window
1
Task
Kotlin SELF,  51 level,  4 lecture
Locked
Start of the Day Worldwide
Start of the Day Worldwide
1
Task
Kotlin SELF,  51 level,  4 lecture
Locked
Day length
Day length
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet