Durations, periods and intervals
Instants versus durations versus calendar periods, why adding one month to 31 January has no single answer, business-day arithmetic, and how to round and truncate consistently.
Three different concepts that share a name
- Instant — a point on the timeline, stored as a timestamp or an ISO string.
- Duration — an exact length of time, such as 3600 seconds, which converts freely between units.
- Period — a calendar amount such as 1 month or 1 year, whose exact length depends on the date it is applied to.
from datetime import datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta
start = datetime(2026, 1, 31, tzinfo=timezone.utc)
# duration: exact, unit-independent
print(start + timedelta(days=30)) # 2026-03-02T00:00:00+00:00
# period: calendar-aware, clamped
print(start + relativedelta(months=1)) # 2026-02-28T00:00:00+00:00One month after 31 January is not a well-defined instant. February has no 31st, so the result must be clamped to the last valid day — 28 February, or 29 in a leap year.
Addition is not associative
| Expression | Result | Why |
|---|---|---|
| 31 Jan + 1 month | 28 Feb | Clamped to the last day of the target month |
| 28 Feb + 1 month | 28 Mar | No clamping needed |
| 28 Feb + 1 day | 1 Mar | Duration, exact |
| 1 Mar - 1 month | 1 Feb | Period |
| 2024-02-29 + 1 year | 2025-02-28 | Leap day clamped |
| 90 minutes after 01:30 | 03:00 | Duration across a DST-free window |
// the order of operations can change the answer
const d = new Date("2026-01-31T00:00:00Z");
const a = new Date(d); a.setUTCMonth(a.getUTCMonth() + 1); a.setUTCDate(a.getUTCDate() + 1);
const b = new Date(d); b.setUTCDate(b.getUTCDate() + 1); b.setUTCMonth(b.getUTCMonth() + 1);
a.toISOString();
b.toISOString();
// both land on 2026-03-01 here, but not for every date — pick and document an orderBusiness days and rounding
import numpy as np
from datetime import date
start = np.datetime64("2026-09-18") # a Friday
print(np.busday_offset(start, 5, roll="forward"))
# 2026-09-25 — five business days later, skipping the weekend
# holidays must be supplied; there is no universal list
holidays = ["2026-12-25", "2026-12-28"]
print(np.busday_offset("2026-12-24", 1, roll="forward",
holidays=holidays)) # 2026-12-29| Rounding rule | Meaning | Used for |
|---|---|---|
| Truncate (floor) | Drop the smaller units | Bucket labels, grouping by day or hour |
| Ceil | Round up to the next unit | Billing periods that charge a partial interval |
| Round half up | Nearest unit, ties upward | Human-facing display |
| Round half even | Nearest unit, ties to even | Numeric stability in finance and statistics |
⚠️
Never implement business-day arithmetic with a loop that adds one day and checks the weekday. Public holidays, weekend definitions and regional calendars differ per country, and a hand-rolled version will be wrong in at least one market.
FAQ
How do I store a billing period?
Store the anchor and the period rule, not precomputed dates. For example an anchor of 31 January plus monthly recurrence reproduces the clamped dates; storing the computed dates loses the intent.
Is a duration ever negative?
Yes, and libraries represent it. Be explicit about what a negative duration means in your domain — before versus after — and validate that a computed interval has the sign you expect.
Related
Daylight saving and its edge cases Date libraries compared
Last refreshed 2026-09-18.