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:00

One 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

ExpressionResultWhy
31 Jan + 1 month28 FebClamped to the last day of the target month
28 Feb + 1 month28 MarNo clamping needed
28 Feb + 1 day1 MarDuration, exact
1 Mar - 1 month1 FebPeriod
2024-02-29 + 1 year2025-02-28Leap day clamped
90 minutes after 01:3003:00Duration 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 order

Business 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 ruleMeaningUsed for
Truncate (floor)Drop the smaller unitsBucket labels, grouping by day or hour
CeilRound up to the next unitBilling periods that charge a partial interval
Round half upNearest unit, ties upwardHuman-facing display
Round half evenNearest unit, ties to evenNumeric 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.

Daylight saving and its edge cases Date libraries compared

Last refreshed 2026-09-18.