From ffcb9c9e2f584f9436c72800bf47b3e04ef829a9 Mon Sep 17 00:00:00 2001 From: Rafael Xavier de Souza Date: Tue, 30 Sep 2025 09:21:29 -0300 Subject: [PATCH 1/3] fix: add prettierrc and prettierignore --- .prettierignore | 11 +++++++++++ .prettierrc.json | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9b210a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Build artifacts +/dist/ + +# Package manager directories +/node_modules/ + +# Generated lock files +package-lock.json + +# Misc +coverage/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..7197da8 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": false, + "trailingComma": "es5" +} From baa36a76f455caaf03e0239499b67bd0f93bd7eb Mon Sep 17 00:00:00 2001 From: Rafael Xavier de Souza Date: Wed, 1 Oct 2025 08:59:00 -0300 Subject: [PATCH 2/3] feat: separate relative time resolution from formatting logic Introduced RelativeTimeResolver to handle unit resolution and difference calculation, while RelativeTime manages formatting. This improves code modularity and allows explicit unit control in relative time computations. Added tests for the resolver class. --- src/relative-time.js | 106 ++++++++++++++++++++++++++---------------- test/relative-time.js | 23 ++++++++- 2 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/relative-time.js b/src/relative-time.js index 0574165..4779d48 100644 --- a/src/relative-time.js +++ b/src/relative-time.js @@ -141,12 +141,13 @@ function differenceInUnit(now, target, unit) { return duration[unit + "s"]; } -export default class RelativeTime { - constructor() { - this.formatters = RelativeTime.initializeFormatters(...arguments); +export class RelativeTimeResolver { + constructor(options = {}) { + this.threshold = options.threshold || this.constructor.threshold; + this.units = options.units || this.constructor.units; } - format(date, { unit = "best-fit", now } = {}) { + resolve(date, { now, unit = "best-fit" } = {}) { const Temporal = getTemporal(); let target; let resolvedNow; @@ -166,54 +167,59 @@ export default class RelativeTime { const diff = Object.create(null); const absDiff = Object.create(null); - const diffUnits = [ - "year", - "month", - /* "week", */ "day", - "hour", - "minute", - "second", - ]; - - diffUnits.forEach(function (currentUnit) { - defineCachedGetter(diff, currentUnit, function () { + const diffUnits = this.units; + + diffUnits.forEach((currentUnit) => { + defineCachedGetter(diff, currentUnit, () => { return differenceInUnit(resolvedNow, target, currentUnit); }); - defineCachedGetter(absDiff, currentUnit, function () { + defineCachedGetter(absDiff, currentUnit, () => { return Math.abs(diff[currentUnit]); }); }); - if (unit === "best-fit") { - unit = RelativeTime.bestFit(absDiff); - } + const resolvedUnit = + unit === "best-fit" + ? this.constructor.bestFit(absDiff, this.threshold) + : unit; - return this.formatters[unit](diff[unit]); + return { + unit: resolvedUnit, + value: diff[resolvedUnit], + }; } -} -RelativeTime.bestFit = function (absDiff) { - const threshold = this.threshold; - switch (true) { - case absDiff.year > 0 && absDiff.month > threshold.month: - return "year"; - case absDiff.month > 0 && absDiff.day > threshold.day: - return "month"; - // case absDiff.month > 0 && absDiff.week > threshold.week: return "month"; - // case absDiff.week > 0 && absDiff.day > threshold.day: return "week"; - case absDiff.day > 0 && absDiff.hour > threshold.hour: - return "day"; - case absDiff.hour > 0 && absDiff.minute > threshold.minute: - return "hour"; - case absDiff.minute > 0 && absDiff.second > threshold.second: - return "minute"; - default: - return "second"; + static bestFit(absDiff, threshold = this.threshold) { + switch (true) { + case absDiff.year > 0 && absDiff.month > threshold.month: + return "year"; + case absDiff.month > 0 && absDiff.day > threshold.day: + return "month"; + // case absDiff.month > 0 && absDiff.week > threshold.week: return "month"; + // case absDiff.week > 0 && absDiff.day > threshold.day: return "week"; + case absDiff.day > 0 && absDiff.hour > threshold.hour: + return "day"; + case absDiff.hour > 0 && absDiff.minute > threshold.minute: + return "hour"; + case absDiff.minute > 0 && absDiff.second > threshold.second: + return "minute"; + default: + return "second"; + } } -}; +} + +RelativeTimeResolver.units = [ + "year", + "month", + /* "week", */ "day", + "hour", + "minute", + "second", +]; -RelativeTime.threshold = { +RelativeTimeResolver.threshold = { month: 2, // week: 4, day: 6, @@ -222,6 +228,26 @@ RelativeTime.threshold = { second: 59, }; +export default class RelativeTime { + constructor() { + this.formatters = RelativeTime.initializeFormatters(...arguments); + this.resolver = new RelativeTimeResolver(); + } + + format(date, options = {}) { + const { unit = "best-fit", now } = options; + const { unit: resolvedUnit, value } = this.resolver.resolve(date, { + unit, + now, + }); + + return this.formatters[resolvedUnit](value); + } +} + +RelativeTime.bestFit = RelativeTimeResolver.bestFit; +RelativeTime.threshold = RelativeTimeResolver.threshold; + RelativeTime.initializeFormatters = function (localesOrFormatter, options) { let locales = localesOrFormatter; let formatOptions = options; diff --git a/test/relative-time.js b/test/relative-time.js index 0c7bbc4..b434488 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -1,4 +1,4 @@ -import RelativeTime from "../src/relative-time"; +import RelativeTime, { RelativeTimeResolver } from "../src/relative-time"; import { Temporal as TemporalPolyfill } from "@js-temporal/polyfill"; function plain(dateTime) { @@ -13,6 +13,7 @@ describe("relative-time", function () { let relativeTime; let originalTemporal; let baseNow; + let resolver; before(function () { originalTemporal = global.Temporal; @@ -25,9 +26,29 @@ describe("relative-time", function () { beforeEach(function () { relativeTime = new RelativeTime(); + resolver = new RelativeTimeResolver(); baseNow = plain("2016-04-10T12:00:00"); }); + describe("resolver", function () { + it("should resolve best-fit unit and value", function () { + const result = resolver.resolve(plain("2016-04-10T11:59:01"), { + now: baseNow, + }); + + expect(result).to.deep.equal({ unit: "second", value: -59 }); + }); + + it("should resolve using an explicit unit", function () { + const result = resolver.resolve(plain("2016-04-10T11:01:00"), { + now: baseNow, + unit: "hour", + }); + + expect(result).to.deep.equal({ unit: "hour", value: -1 }); + }); + }); + describe("bestFit", function () { it("should format seconds-distant dates", function () { expect( From 9937212643c897301aa4cf61100708badfe8fa87 Mon Sep 17 00:00:00 2001 From: Rafael Xavier de Souza Date: Wed, 1 Oct 2025 09:14:46 -0300 Subject: [PATCH 3/3] docs: add RelativeTimeResolver API and improve usage examples in README.md --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 10fd966..34ca0b9 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Note `relative-time` checks for the actual month change instead of counting on a npm install --save relative-time ```js -import RelativeTime from "relative-time"; +import RelativeTime, { RelativeTimeResolver } from "relative-time"; const relativeTime = new RelativeTime(); const threeHoursAgo = Temporal.Now.plainDateTimeISO().subtract({ hours: 3 }); @@ -117,6 +117,14 @@ const relativeTimeInPortuguese = new RelativeTime("pt"); const oneHourAgo = Temporal.Now.plainDateTimeISO().subtract({ hours: 1 }); console.log(relativeTimeInPortuguese.format(oneHourAgo)); // > há 1 hora + +// Use the resolver when you need just the unit/value +const resolver = new RelativeTimeResolver(); +const event = Temporal.Now.plainDateTimeISO().subtract({ minutes: 3 }); +const { value, unit } = resolver.resolve(event); // { value: -3, unit: "minute" } +// You can format this yourself or with Intl.RelativeTimeFormat +new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(value, unit); +// > 3 minutes ago ``` ### Time zone support @@ -145,7 +153,9 @@ relativeTime.format(berlinDate, { now: berlinNow }); ## API -### `format(date{, options})` +### RelativeTime (default export) + +#### `format(date{, options})` ### date @@ -155,7 +165,7 @@ representing the target moment. Use a plain date-time when the relative distance should ignore time zone rules (for example, comparing two local calendar events) and a zoned date-time when offset and daylight-saving changes matter. -### options.unit (optional) +#### options.unit (optional) Unit for formatting. If the unit is not provided, `"best-fit"` is used. @@ -169,17 +179,16 @@ Unit for formatting. If the unit is not provided, `"best-fit"` is used. #### The `"best-fit"` unit -It automatically picks a unit based on the relative time scale. Basically, it looks like this: +It automatically picks a unit based on the relative time scale using thresholds. In short: -- If `absDiffYears > 0 && absDiffMonths > threshold.month`, return `"year"`. -- If `absDiffMonths > 0 && absDiffWeeks > threshold.week`, return `"month"`. -- If `absDiffWeeks > 0 && absDiffDays > threshold.day`, return `"week"`. -- If `absDiffDays > 0 && absDiffHours > threshold.hour`, return `"day"`. -- If `absDiffHours > 0 && absDiffMinutes > threshold.minute`, return `"hour"`. -- If `absDiffMinutes > 0 && absDiffSeconds > threshold.second`, return `"minutes"`. -- Return `"second"`. +- If `absDiff.year > 0 && absDiff.month > threshold.month` → `"year"` +- If `absDiff.month > 0 && absDiff.day > threshold.day` → `"month"` +- If `absDiff.day > 0 && absDiff.hour > threshold.hour` → `"day"` +- If `absDiff.hour > 0 && absDiff.minute > threshold.minute` → `"hour"` +- If `absDiff.minute > 0 && absDiff.second > threshold.second` → `"minute"` +- Otherwise → `"second"` -### options.now (optional) +#### options.now (optional) A [Temporal.PlainDateTime](https://tc39.es/proposal-temporal/docs/plaindatetime.html) or [Temporal.ZonedDateTime](https://tc39.es/proposal-temporal/docs/zoneddatetime.html) @@ -189,10 +198,33 @@ omitted, the current moment is retrieved with either a plain or zoned date-time to match the type of `date`. Passing any other type throws a `TypeError`. -### Return +#### Return Returns the formatted relative time string given `date` and `options`. +### RelativeTimeResolver (named export) + +Resolves the relative difference without formatting, returning `{ value, unit }`. + +#### Constructor + +`new RelativeTimeResolver(options?)` + +- `options.threshold` — override the thresholds used by best-fit. +- `options.units` — override the units considered by the resolver. + +#### `resolve(date, { now, unit = "best-fit" } = {})` + +- `date` — `Temporal.PlainDateTime` or `Temporal.ZonedDateTime`. +- `now` — must match the `date` type; for zoned dates, the time zone must match. If omitted, `Temporal.Now` is used accordingly. +- `unit` — + - `"best-fit"` (default): chooses a unit using thresholds and returns `{ unit, value }`. + - Any supported unit (`second`, `minute`, `hour`, `day`, `month`, `year`): returns `{ unit, value }` using that exact unit (signed, truncated difference). The hour edge-case is handled so very recent past returns `-1` hour instead of `0` hours. + +#### Return + +An object `{ value, unit }` with the signed difference and chosen unit. + ## Appendix ### Relative time