Files
skills/docs/foundation/timing/datetime/src/relative/relative.md
2026-05-29 08:20:56 +01:00

3.9 KiB
Raw Blame History

relative

(no submodules)

Purpose

The relative module provides human-readable relative-time formatting for @suaveplan/datetime. It converts the distance between two moments into concise English phrases like "3 days ago", "in 2 hours", or "just now". The clock injection point makes all functions deterministic in tests, while the unified formatRelative entry point subsumes the common timeAgo / timeUntil aliases.

The module also exposes diffInUnits, a lower-level utility that returns the signed numeric difference between two DateInput values in any DurationUnit. This is useful when callers need the raw integer delta rather than a formatted string.

Features

  • diffInUnits — signed numeric difference in any DurationUnit
  • formatRelative — threshold-based English string (past and future)
  • timeAgo — convenience alias for past-relative formatting
  • timeUntil — convenience alias for future-relative formatting
  • Pluggable Clock for deterministic rendering in tests
  • "just now" threshold for differences under 45 seconds

Usage: Basic Example

import { timeAgo, timeUntil } from "@suaveplan/datetime/relative";

// Using wall clock
console.log(timeAgo(new Date(Date.now() - 3 * 60_000))); // "3 minutes ago"
console.log(timeUntil(new Date(Date.now() + 2 * 3600_000))); // "in 2 hours"

Usage: Advanced Example

import { diffInUnits, formatRelative } from "@suaveplan/datetime/relative";

// Deterministic with a fake clock
const clock = () => new Date("2024-06-01T12:00:00Z").getTime();

const pastDate = "2024-05-28T12:00:00Z";
console.log(formatRelative(pastDate, { clock })); // "4 days ago"
console.log(diffInUnits(pastDate, "2024-06-01T12:00:00Z", "days")); // 4

// Sub-unit precision
const oneHourAgo = new Date(clock() - 3723_000); // 1h 2m 3s ago
console.log(formatRelative(oneHourAgo, { clock })); // "1 hour ago"
console.log(diffInUnits(oneHourAgo, new Date(clock()), "seconds")); // 3723

API Reference

diffInUnits(a: DateInput, b: DateInput, unit: DurationUnit): number

Returns floor((b − a) / unitMs) truncated toward zero. Positive when b > a.

  • a — start date (any DateInput)
  • b — end date (any DateInput)
  • unit — one of the DurationUnit values

formatRelative(date: DateInput, opts?: RelativeTimeOptions): string

Format the difference between date and opts.clock() (default: Date.now()).

  • Distances under 45 seconds → "just now"
  • Distances under 45 minutes → "N seconds ago" / "in N seconds"
  • Distances under 22 hours → "N minutes ago" / "in N minutes"
  • Distances under 26 days → "N hours ago" / "in N hours"
  • Distances under 11 months → "N days ago" / "in N days"
  • Otherwise → "N months ago" / "in N months" or "N years ago" / "in N years"

timeAgo(date: DateInput, opts?: RelativeTimeOptions): string

Alias for formatRelative. Intended for past dates, but works symmetrically.

timeUntil(date: DateInput, opts?: RelativeTimeOptions): string

Alias for formatRelative. Intended for future dates, but works symmetrically.

Implementation Notes

Average month (30.4375 days) and average year (365.25 days) constants are used for the coarse months / years thresholds. These are approximations intentionally — precise calendar arithmetic for display strings would add complexity without improving user experience. Use diffInUnits("months") together with calendar arithmetic when you need exact month counts.

The threshold table mirrors the conventions used by popular libraries like date-fns and moment.js so that migrating code produces the same user-visible strings.

The precision field in RelativeTimeOptions is reserved for future use to cap the label at a specific unit (e.g. only show up to "days", never "hours"). Currently it is accepted but not applied in the implementation.