relativeTime method
Returns an English relative-time phrase between this DateTime and now
(defaults to DateTime.now).
isDescriptive— verbose phrasing ("a moment","about an hour","5 minutes") whentrue; terse tokens ("now","~1h","5 min") whenfalse.isDescriptiveTimeSuffix— append relativeTimeSuffixPast / relativeTimeSuffixFuture ("ago"/"from now") whentrue.roundUp— round the numeric unit (89.6 min→2 hrband-permitting) instead of flooring.
Year-level spans use date-based calendar arithmetic (subtract years, back
off one if the anniversary has not yet passed) rather than days / 365.25
to avoid off-by-one errors near anniversary boundaries.
Returns null only on the degenerate path where every band produces an
empty string; for ordinary inputs a non-empty phrase is always returned.
Example:
final now = DateTime(2024, 6, 15, 12);
DateTime(2024, 6, 15, 11, 55).relativeTime(now: now); // "5 minutes ago"
DateTime(2024, 6, 20).relativeTime(now: DateTime(2024, 6, 15));
// // "5 days from now"
Audited: 2026-06-12 11:26 EDT
Implementation
@useResult
String? relativeTime({
DateTime? now,
bool isDescriptive = true,
bool isDescriptiveTimeSuffix = true,
bool roundUp = false,
}) {
// Exact-instant short-circuit must run before defaulting now, so that the
// documented `this == now` contract holds even when now is passed null.
if (this == now) {
return relativeNowTime;
}
// Resolve once into a local (not a param reassignment) so a clock tick
// mid-computation cannot shift the bucket and the original arg is preserved.
final DateTime resolvedNow = now ?? DateTime.now();
// Millisecond granularity; microsecond ties are intentionally ignored.
final int signedElapsed = millisecondsSinceEpoch - resolvedNow.millisecondsSinceEpoch;
// isBefore (not `signedElapsed < 0`) keeps direction correct at microsecond
// ties where the millisecond delta can be zero yet the instants still differ.
final bool isPast = isBefore(resolvedNow);
final int elapsed = isPast ? signedElapsed.abs() : signedElapsed;
final String? body = _relativePhraseBody(
from: this,
now: resolvedNow,
elapsed: elapsed,
isPast: isPast,
isDescriptive: isDescriptive,
roundUp: roundUp,
);
// Guard before interpolating: only the degenerate empty-band path is null.
if (body == null || body.isEmpty) {
return null;
}
if (isDescriptiveTimeSuffix) {
return '$body ${isPast ? relativeTimeSuffixPast : relativeTimeSuffixFuture}';
}
return body;
}