skip to content

Why can't you add 'one month' or 'one day across a DST boundary' using kotlin.time.Duration, and what does kotlinx-datetime offer instead?

level: seniorimportance: should knowfreq 40%

answer

  1. Months/years vary; Duration is fixed seconds
  2. DST day = 23h or 25h, not always 24
  3. DatePeriod/DateTimePeriod for calendar spans
  4. Instant.plus(amount, DateTimeUnit, zone) is zone-aware
  5. End-of-month clamps (Jan31 +1mo -> Feb28)

basics

~20 s

A month isn't a fixed length and a 'day' can be 23 or 25 hours during daylight-saving switches. Duration only knows exact time amounts, so you use kotlinx-datetime's calendar arithmetic (DatePeriod, plus with a TimeZone) instead.

solid answer

~40 s

kotlin.time.Duration is a fixed quantity of elapsed time (seconds/nanos). Calendar units are not fixed: months vary (28-31 days), and a civil day can be 23 or 25 hours across a DST transition. Adding 1.days to an Instant adds exactly 24 hours, which lands on the wrong wall-clock time when DST changes. kotlinx-datetime separates these: use DatePeriod (e.g. DatePeriod(months = 1, days = 2)) and DateTimePeriod for calendar-aware spans, applied via LocalDate.plus(period) or Instant.plus(period, timeZone). The zone-aware overload re-anchors to the calendar so 'same time next day' is preserved across DST. For exact elapsed time you still subtract Instants to get a Duration. Rule: Duration for physics-time; DatePeriod/DateTimePeriod for human-calendar time. Use unit-based helpers like LocalDate.plus(1, DateTimeUnit.MONTH) when adding a single unit.

code

kotlin · 12 lines
kotlin
import kotlinx.datetime.*
import kotlin.time.Clock

val zone = TimeZone.of("Europe/Paris")
val now = Clock.System.now()

// calendar-correct across DST: keeps same local wall time
val tomorrow = now.plus(1, DateTimeUnit.DAY, zone)
val nextMonth = now.plus(DateTimePeriod(months = 1), zone)

// month clamping on a LocalDate
val clamped = LocalDate(2026, 1, 31).plus(DatePeriod(months = 1)) // 2026-02-28

go deeper

for a junior

Recognizes a month isn't a fixed number of days.

for a middle

Uses DatePeriod/plus for calendar arithmetic instead of Duration.

for a senior

Explains DST-driven 23h/25h days, zone-aware Instant.plus, and end-of-month clamping.

for a principal

Designs APIs that keep physics-time and calendar-time separate, injects zones, and reasons about gap/overlap correctness at boundaries.

## Two kinds of 'adding time' - **Exact elapsed time** (physics): seconds tick uniformly. This is `kotlin.time.Duration`. - **Calendar time** (human): 'next month', 'tomorrow at the same hour' depend on calendar rules and time zones. ## Why Duration can't express calendar units - A **month** has no fixed length (28-31 days). `Duration` is just seconds; there is no `30.days == 1.month` truth. - A **civil day** is usually 24h but is **23h or 25h** across a daylight-saving transition. So `instant + 1.days` (exactly 24h) lands an hour off the intended wall-clock time on a DST day. ```kotlin // WRONG for calendar intent across DST: val next = instant + 1.days // exactly 24h, wall clock may shift ``` ## What kotlinx-datetime provides ### DatePeriod and DateTimePeriod Calendar-aware spans: - `DatePeriod(years, months, days)` — whole-date arithmetic. - `DateTimePeriod(... hours, minutes, ...)` — mixed date + time spans. ```kotlin import kotlinx.datetime.* val d = LocalDate(2026, 1, 31) val m = d.plus(DatePeriod(months = 1)) // 2026-02-28, clamps end-of-month ``` ### Zone-aware Instant arithmetic To move an `Instant` by a calendar amount you supply the zone so the rule is applied correctly: ```kotlin val zone = TimeZone.of("Europe/Paris") val tomorrowSameTime = instant.plus(1, DateTimeUnit.DAY, zone) val inOneMonth = instant.plus(DateTimePeriod(months = 1), zone) ``` The zone-aware overload re-anchors through the calendar, preserving 'same local time' across DST instead of blindly adding fixed seconds. ### DateTimeUnit `DateTimeUnit` distinguishes **time-based** units (`HOUR`, `MINUTE`, fixed seconds) from **date-based** units (`DAY`, `MONTH`, `YEAR`, calendar-dependent). Date-based units require a zone for `Instant` arithmetic; time-based units don't (they're exact). ## End-of-month clamping Calendar addition also clamps overflow: Jan 31 + 1 month -> Feb 28/29, not Mar 3. `Duration` math would never do this; it has no concept of month boundaries. ## Computing differences - Exact elapsed: `instantB - instantA` -> `Duration`. - Calendar difference: `startDate.periodUntil(endDate)` -> `DatePeriod`, or `until(..., DateTimeUnit, zone)` for whole-unit counts. ## Rule of thumb - **Physics-time** (timeouts, benchmarks, 'in 30 seconds') -> `Duration`. - **Human-calendar-time** ('next billing month', 'tomorrow 9am') -> `DatePeriod` / `DateTimePeriod` / `DateTimeUnit` with a `TimeZone`.

  • Does instant.plus(1, DateTimeUnit.HOUR, zone) need the zone?
    HOUR is a fixed time-based unit, so the result is the same as exact arithmetic; the zone matters for date-based units like DAY/MONTH that vary across DST and month length.
  • How would you count whole months between two dates?
    Use startDate.periodUntil(endDate) for a DatePeriod, or until(end, DateTimeUnit.MONTH) for the whole-month count.

Duration is a stopwatch (exact seconds); DatePeriod is a wall calendar you flip page by page, where pages aren't all the same size.

saying these in an interview costs you the question

  • Adding 30.days and calling it 'one month'
  • Using instant + 1.days and expecting same wall-clock time across DST
  • Claiming every day is exactly 24 hours
  • Mixing Duration and DatePeriod as if interchangeable
  • Ignoring the zone argument for date-based Instant arithmetic

context