Durations & intervals
Spans vs points — and why “1 month” and “30 days” are not the same thing.
An instant is a point on the timeline. A duration is a length of time — the distance between two instants. Durations come in two flavors that behave very differently.
An exact duration is measured in fixed physical units: seconds, minutes, hours. “3600 seconds” is unambiguous; it is the same length of time regardless of when it starts or what timezone is involved.
A calendar duration is expressed in human calendar units — days, months, years — and its exact length depends on when it is applied. One calendar day spans the local midnight-to-midnight window, which DST can make 23 or 25 hours long. One calendar month is 28, 29, 30, or 31 days. One calendar year is 365 or 366 days. “Add 1 month” is not the same operation as “add 30 days”.
A calendar-aware type keeps the two kinds of unit distinct and clamps to a real day:
const d = Temporal.PlainDate.from('2026-01-31');
d.add({ months: 1 }).toString(); // '2026-02-28' (calendar: clamped to a real day)
d.add({ days: 30 }).toString(); // '2026-03-02' (exact: 30 fixed days)
// Legacy Date's setMonth overflows instead of clamping:
const legacy = new Date(Date.UTC(2026, 0, 31));
legacy.setUTCMonth(legacy.getUTCMonth() + 1);
legacy.toISOString().slice(0, 10); // '2026-03-03', not '2026-02-28'JS examples use Temporal — standardized in ES2026, available via a polyfill today.
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta # third-party
d = date(2026, 1, 31)
d + timedelta(days=30) # date(2026, 3, 2) — exact: 30 fixed days
d + relativedelta(months=1) # date(2026, 2, 28) — calendar: clamped
# (timedelta has no months/years — that's why relativedelta exists.)import java.time.*;
LocalDate d = LocalDate.of(2026, 1, 31);
d.plusMonths(1); // 2026-02-28 (calendar: clamped to a real day)
d.plusDays(30); // 2026-03-02 (exact: 30 fixed days)
// The type system draws the same line: Period is calendar units, Duration is
// exact time — Duration.ofDays(1) is always exactly 24h, DST or not.
d.plus(Period.ofMonths(1)); // 2026-02-28 — same clampJava examples use java.time (JSR-310) — see the Java page for the legacy java.util.Date minefield.
import kotlinx.datetime.*
val d = LocalDate(2026, 1, 31)
d.plus(1, DateTimeUnit.MONTH) // 2026-02-28 (calendar: clamped to a real day)
d.plus(30, DateTimeUnit.DAY) // 2026-03-02 (exact: 30 fixed days)
// Legacy java.util.Calendar also clamps — but months are 0-indexed:
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
cal.clear(); cal.set(2026, 0, 31) // 0 = January
cal.add(java.util.Calendar.MONTH, 1) // -> 2026-02-28 (clamped, unlike JS's overflow)Kotlin examples use kotlinx-datetime, JetBrains’ library — separate from the stdlib.
import Foundation
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "UTC")!
let jan31 = cal.date(from: DateComponents(year: 2026, month: 1, day: 31))!
cal.date(byAdding: .month, value: 1, to: jan31) // 2026-02-28 (calendar: clamped)
cal.date(byAdding: .day, value: 30, to: jan31) // 2026-03-02 (exact: 30 fixed days)Swift examples use Foundation’s Calendar/DateComponents — see the Swift page.
import (
"fmt"
"time"
)
d := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(d.AddDate(0, 1, 0).Format(time.DateOnly)) // 2026-03-03 — overflows, like legacy JS Date!
fmt.Println(d.AddDate(0, 0, 30).Format(time.DateOnly)) // 2026-03-02 (exact: 30 fixed days)
// time.Duration is exact elapsed time only — its "hours" are fixed 3600s and
// there are no month/year units; AddDate is the only calendar arithmetic.Go examples use the standard library’s time package — see the Go page.
use jiff::civil::date;
use jiff::ToSpan;
let d = date(2026, 1, 31);
println!("{}", d.checked_add(1.month())?); // 2026-02-28 (calendar: clamped to a real day)
println!("{}", d.checked_add(30.days())?); // 2026-03-02 (exact: 30 fixed days)Rust examples use jiff, a third-party crate — Rust’s std has no calendar types at all.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// C has no calendar arithmetic. Bumping tm_mon and re-normalizing with mktime
// OVERFLOWS Jan 31 + 1 month to Mar 3 (Feb has 28 days), like legacy JS Date:
setenv("TZ", "UTC", 1);
tzset();
struct tm d = {0};
d.tm_year = 2026 - 1900; d.tm_mon = 0; d.tm_mday = 31; // Jan 31
d.tm_mon += 1; // "+ 1 month"
mktime(&d);
char buf[16];
strftime(buf, sizeof buf, "%Y-%m-%d", &d);
printf("%s\n", buf); // 2026-03-03 — overflow, not clamp
// difftime(a, b) is the only span helper: a double of seconds between time_t.C examples use <time.h> — see the C page. Standard C has no zone-aware or date-only type; the zone is a process-global set by TZ.
#include <chrono>
#include <format>
#include <iostream>
using namespace std::chrono;
// Calendar add: Jan 31 + 1 month is 2026-02-31, which is INVALID. chrono does
// not silently clamp OR overflow — it hands you an un-ok() date to resolve:
year_month_day jan31{January/31/2026y};
year_month_day bad = jan31 + months{1};
std::cout << std::format("{} ok={}", bad, bad.ok()) << "\n"; // 2026-02-31 is not a valid date ok=false
year_month_day clamped{bad.year() / bad.month() / last}; // clamp to the real last day
std::cout << std::format("{}", clamped) << "\n"; // 2026-02-28
// Exact vs calendar: sys_days{...} + days{30} advances 30 fixed days; the month
// add above is calendar-aware. A chrono hours duration is always exactly 3600s.C++ examples use std::chrono (C++20) — see the C++ page. The pre-C++20 world lived on C’s <time.h>.
Intervals
An interval is a concrete span of time with a definite start and end. It can be represented as (start instant, end instant), (start instant, duration), or (duration, end instant). Interval arithmetic is well-defined once both endpoints are resolved to instants.
Pitfall: Adding “1 month” to January 31 is ambiguous. February has fewer than 31 days, so the result is undefined. Different libraries resolve this differently — some clamp to the last day of the month (Feb 28/29), some throw an error. Always check and document your library’s behavior.
See also: DST & the edges for days that aren’t 24 hours, and Recurring events for why a recurrence steps in calendar units, never exact ones.
Go deeper: ISO 8601 duration notation and its traps
ISO 8601 uses the notation P1Y2M3DT4H5M6S to express durations: P followed by date components (years, months, days) then T followed by time components (hours, minutes, seconds). This is supported by many libraries and databases.
However, the mixed form (P1MT30S — one month and thirty seconds) is dangerous to compute with because the month part is calendar-relative while the second part is exact. You cannot convert such a combined duration to a fixed number of seconds without knowing the start instant. Libraries that try to do so silently are a bug source.
A safe rule: keep calendar durations (years/months) separate from exact durations (days/hours/minutes/seconds) in your data model. Only mix them when adding to a specific known start instant.
For the concrete edge cases involving DST and calendar-day length, see DST & the edges.