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 usually makes 23 or 25 hours long — the shift is whatever the zone moves by. 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 and native in Firefox 139+, Chrome/Edge 144+ and Node 26+; a polyfill covers Safari and older runtimes.
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, and the snippet above shows every answer. Some clamp to the last day of the month (Feb 28/29). Some roll over into the next month: Go’s AddDate, C’s tm_mon + 1 through mktime, and legacy JavaScript’s setUTCMonth all return March 3. C++20 <chrono> hands back 2026-02-31 as a year_month_day whose .ok() is false, leaving the resolution to you. Some throw an error. Roll-over is the dangerous one, because nothing signals it. 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.