# Relative Time

Formats JavaScript dates to localized **relative time strings** (e.g., "3 hours ago", "yesterday", "in 2 weeks").
It selects a **best‑fit unit** for you (seconds → minutes → hours → days → weeks → months → years) **and** uses the platform’s `Intl.RelativeTimeFormat` for localization.

- **High‑level**: `RelativeTime` → _resolve unit_ → format
- **Low‑level**: `RelativeTimeResolver` → _resolve only_ the `{ value, unit }`

Built on **Temporal** for precise date math and **Intl.RelativeTimeFormat** for i18n. That means you get the **same CLDR data** your runtime ships with—no locale bundles to download.

> **Why not just `Intl.RelativeTimeFormat`?**
> `Intl.RelativeTimeFormat` is intentionally **low level**: you must choose the unit yourself (e.g., day vs week) and then ask it to format the pair `{value, unit}`. This library adds the **unit resolver** layer on top—by design that’s out of scope for the built‑in API. (see more details at [tc39/proposal-intl-relative-time#14](https://github.com/tc39/proposal-intl-relative-time/issues/14))

## Install

```bash
npm install relative-time
```

### Temporal polyfill

This library expects **Temporal**. Use it where Temporal is available, or load the official polyfill:

```bash
npm install @js-temporal/polyfill
```

When using `@js-temporal/polyfill`, **inject** `Temporal` when constructing `RelativeTime`/`RelativeTimeResolver`:

```js
import RelativeTime, { RelativeTimeResolver } from "relative-time";
import { Temporal } from "@js-temporal/polyfill";

const rt = new RelativeTime({ Temporal });
// or:
const resolver = new RelativeTimeResolver({ Temporal });
```

## Quick start

```js
import RelativeTime from "relative-time";
// If needed:
// import { Temporal } from "@js-temporal/polyfill";

const rt = new RelativeTime(); // locale inferred from the runtime

const threeHoursAgo = Temporal.Now.plainDateTimeISO().subtract({ hours: 3 });
rt.format(threeHoursAgo);
// Output: "3 hours ago"

const pt = new RelativeTime("pt");
const oneHourAgo = Temporal.Now.plainDateTimeISO().subtract({ hours: 1 });
pt.format(oneHourAgo);
// Output: "há 1 hora"
```

### Time‑zone aware

```js
const rt = new RelativeTime();

const laDate = Temporal.ZonedDateTime.from(
  "2016-04-09T17:00:00-07:00[America/Los_Angeles]"
);
rt.format(laDate);
// Output: "yesterday"
// Assuming now is "2016-04-10T05:00:00-07:00[America/Los_Angeles]"

const berlinDate = Temporal.ZonedDateTime.from(
  "2016-04-10T02:00:00+02:00[Europe/Berlin]"
);
rt.format(berlinDate, { now: berlinNow });
// Output: "12 hours ago"
// Assuming now is "2016-04-10T14:00:00+02:00[Europe/Berlin]"
```

Use `Temporal.PlainDateTime` when you want to **ignore time‑zone rules** (local calendar math), and `Temporal.ZonedDateTime` when offset and DST **must** be respected.

### Resolver only

Use the resolver when you need just the unit/value.

```js
import { RelativeTimeResolver } from "relative-time";

const resolver = new RelativeTimeResolver();
const event = Temporal.Now.plainDateTimeISO().subtract({ minutes: 3 });
const { value, unit } = resolver.resolve(event);
// Output: { value: -3, unit: "minute" }

// You can format this yourself or with Intl.RelativeTimeFormat
new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(value, unit);
// Output: "3 minutes ago"
```

## API

### `class RelativeTime` (default export)

High‑level formatter that **resolves the unit** and **formats** the string.

#### Constructor

```ts
new RelativeTime(
  locales?: string | string[],
  options?: Intl.RelativeTimeFormatOptions
)
```

- `locales` — same semantics as other `Intl` constructors (e.g., `"en"`, `["fr", "en"]`).
- `options` — forwarded to `Intl.RelativeTimeFormat` (e.g., `{ style: "short", numeric: "auto" }`).
  Tip: `numeric: "auto"` yields strings like “yesterday/tomorrow”; `numeric: "always"` gives “1 day ago / in 1 day”.

#### `format(date, options?) => string`

```ts
format(
  date: Temporal.PlainDateTime | Temporal.ZonedDateTime,
  options?: {
    now?: Temporal.PlainDateTime | Temporal.ZonedDateTime,
    unit?: "best-fit" | "second" | "minute" | "hour" | "day" | "week" | "month" | "year"
  }
): string
```

- `date` — the target moment.
- `options.now` — reference moment (defaults to “now”, matched to `date`’s type).
- `options.unit` — force a specific unit or let the library decide with `"best-fit"` (default).

**How “best‑fit” works (conceptually)**

The resolver promotes units using _calendar boundaries_ and _configurable step-up thresholds_ (e.g., sec → min → hr → day → month). If the difference crosses a calendar day, you'll get _"yesterday"_ (instead of "20 hours ago"). But if it crosses midnight _without_ clearing the threshold, it stays in hours, e.g., _"3 hours ago"_ (instead of "yesterday").

---

### `class RelativeTimeResolver` (named export)

Low‑level **unit chooser**. Use it if you want the `{ value, unit }` pair to feed into your own formatter (including `Intl.RelativeTimeFormat` directly).

#### Constructor

```ts
new RelativeTimeResolver();
```

#### `resolve(date, options?) => { value: number, unit: RTFUnit }`

```ts
type RTFUnit = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";

resolve(
  date: Temporal.PlainDateTime | Temporal.ZonedDateTime,
  options?: {
    now?: Temporal.PlainDateTime | Temporal.ZonedDateTime,
    unit?: "best-fit" | RTFUnit  // "best-fit" (default) or force a unit
  }
): { value: number; unit: RTFUnit }
```

**Example**

```js
import { RelativeTimeResolver } from "relative-time";

const r = new RelativeTimeResolver();
const target = Temporal.Now.plainDateTimeISO().add({ days: 6, hours: 5 });

const { value, unit } = r.resolve(target); // e.g., { value: 1, unit: "week" }

new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(value, unit);
// "next week"
```

## Using `Intl.RelativeTimeFormat` options

You can customize style and numeric behavior at construction:

```js
const rtShort = new RelativeTime("en", { style: "short", numeric: "always" });
rtShort.format(Temporal.Now.plainDateTimeISO().subtract({ day: 1 }));
// "1 day ago"
```

> `RelativeTime` is literally a thin wrapper over `Intl.RelativeTimeFormat` once a `{ value, unit }` has been resolved. You get the same pluralization and grammar your engine provides.

## When to use which

| You need…                         | Use                                | Why                                                           |
| --------------------------------- | ---------------------------------- | ------------------------------------------------------------- |
| “Just give me a localized string” | `RelativeTime`                     | Picks a sensible unit and formats it                          |
| `{ value, unit }` only            | `RelativeTimeResolver`             | Feed another formatter, build custom UIs, analytics, etc.     |
| Full control over unit and i18n   | `Intl.RelativeTimeFormat` directly | Low‑level by design; no unit resolution provided by the spec. |

## Examples

**Force a unit**

```js
const rt = new RelativeTime("en");
rt.format(Temporal.Now.plainDateTimeISO().subtract({ hours: 27 }), {
  unit: "hour",
});
// "27 hours ago"
```

**Different locales**

```js
new RelativeTime(["fr", "en"]).format(
  Temporal.Now.plainDateTimeISO().add({ minutes: 5 })
);
// "dans 5 minutes"
```

## Notes on accuracy

This project uses **Temporal** for differences (calendar‑aware) and **`Intl.RelativeTimeFormat`** for localization. That combination prevents common errors around DST, month lengths, and week boundaries that “approximate threshold” libraries can exhibit.

## Browser / runtime support

- Works wherever **Temporal** is available; otherwise include **`@js-temporal/polyfill`**.
- Uses your runtime’s **`Intl.RelativeTimeFormat`**, which has wide support in modern environments.

## FAQ

**Does this support quarters?**
No—units are: `second`, `minute`, `hour`, `day`, `week`, `month`, `year`.

**Why do I sometimes see “yesterday” instead of “1 day ago”?**
That comes from `numeric: "auto"` in `Intl.RelativeTimeFormat`. Switch to `numeric: "always"` if you prefer numbers only.

**How is this different from the TC39 proposal?**
This library’s resolver is the high‑level part **omitted on purpose** from the `Intl.RelativeTimeFormat` spec; the built‑in API only formats a supplied `{ value, unit }`. See the historical discussion around “best‑fit” in the proposal repo.

## Contributing

PRs and issues welcome!

## Appendix

### Relative time

In this library, we'll define relative time as what makes sense for expressions like "now", "2 days ago", "in 3 months", "last year", "yesterday", and so on. In a more formal definition, _relative time_ is an approximate date distance given a unit. This is, _relative time_ is the date distance of _a_ and _b_ ± error, where error < unit. Please, see the below examples of each unit for clarity.

#### second

```
                8:31:38.000         8:31:39.000         8:31:40.000         8:31:41.000
ms  | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
sec             |   e             d |   c             b |       a     N     |
                8:31:38             8:31:39             8:31:40             8:31:41

N: The assumed now
a: now / 0 seconds ago
b: 1 second ago
c: 1 second ago
d: 2 seconds ago
e: 2 seconds ago
```

#### minute

```
          8:28:00           8:29:00           8:30:00           8:31:00           8:32:00
sec |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
min       |     g        f  |     e        d  |     c        b  |        a  N     |
          8:28              8:29              8:30              8:31              8:32

N: The assumed now
a: 0 minutes ago
b: 1 minute ago
c: 1 minute ago
d: 2 minutes ago
e: 2 minutes ago
f: 3 minutes ago
g: 3 minutes ago
```

#### hour

```
          5:00              6:00              7:00              8:00              9:00
min |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
hr.       |     g        f  |     e        d  |     c        b  |     a  N        |
          5                 6                 7                 8                 9

N: The assumed now
a: 0 hours ago
b: 1 hour ago
c: 1 hour ago
d: 2 hours ago
e: 2 hours ago
f: 3 hours ago
g: 3 hours ago
```

#### day

```
       Mar 21, 00:00           Mar 22, 00:00           Mar 23, 00:00           Mar 24, 00:00
hr.  | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
day    | e                   d | c                   b |     a N               |
       Mar 21                  Mar 22                  Mar 23                  Mar 24

N: The assumed now
a: today / 0 days ago
b: yesterday / 1 day ago
c: yesterday / 1 day ago
d: 2 days ago
e: 2 days ago
```

#### week

```
                     Wk. 11, Sun, Mar 12  Wk. 12, Sun, Mar 19  Wk. 13, Sun, Mar 26
day   |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
wk.   e           d  |     c             b|       a    N       |
                     Wk 11                Wk 12                Wk 13

N: The assumed now
a: this week
b: last week
c: last week
d: 2 weeks ago
e: 2 weeks ago
```

#### month

```
      Wk. 1       Wk. 5       Wk. 9          Wk. 14
wk.   |  |  |  |  |  |  |  |  |  |  |  |  |  |
mo.   |e         d | c        b|     a   N   |
      Jan          Feb         Mar           Apr

N: The assumed now
a: this month / 0 months ago
b: last month / 1 month ago
c: last month / 1 month ago
d: 2 months ago
e: 2 months ago
```

Note the months distances doesn't match weeks distance or days distance uniformly.

#### year

```
          Jan               Jan               Jan               Jan               Jan
mo. |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
yr.       |g             f  |e             d  |c             b  |a  N             |
          2013              2014              2015              2016              2017

N: The assumed now
a: this year / 0 years ago
b: last year / 1 year ago
c: last year / 1 year ago
d: 2 years ago
e: 2 years ago
f: 3 years ago
g: 3 years ago
```

Note that (although not shown by the above ruler), the years distances doesn't match weeks distance or days distance uniformly.

## License

MIT © Rafael Xavier de Souza
