From 4839c5701a5e8398ff79d08da3518c9be12c3185 Mon Sep 17 00:00:00 2001 From: "hrynevych.romann" Date: Tue, 27 Feb 2024 22:59:36 +0200 Subject: [PATCH 1/2] feat: add \`formatRelative\` function --- src/__tests__/formatRelative.spec.ts | 69 ++++++++++++++++++++++++++++ src/formatRelative.ts | 68 +++++++++++++++++++++++++++ src/offset.ts | 4 +- src/types.ts | 18 +++++++- 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/formatRelative.spec.ts create mode 100644 src/formatRelative.ts diff --git a/src/__tests__/formatRelative.spec.ts b/src/__tests__/formatRelative.spec.ts new file mode 100644 index 0000000..cc8ae4b --- /dev/null +++ b/src/__tests__/formatRelative.spec.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest" +import { formatRelative } from "../formatRelative" +process.env.TZ = "America/New_York" + +describe("format", () => { + it('renders "minutes" relative date', () => { + expect( + formatRelative(new Date(new Date().getTime() - 1000 * 60 * 4), { + unit: "minute", + }), + ).toEqual("4 minutes ago") + }) + + it('renders "long" relative date', () => { + expect( + formatRelative(new Date(new Date().setMonth(new Date().getMonth() - 2)), { + style: "long", + }), + ).toEqual("2 months ago") + }) + + it('renders "short" relative date', () => { + expect( + formatRelative( + new Date(new Date().setFullYear(new Date().getFullYear() - 2)), + { + style: "short", + }, + ), + ).toEqual("2 yr. ago") + }) + + it('renders "narrow" relative date', () => { + expect( + formatRelative(new Date(new Date().setMonth(new Date().getMonth() - 2)), { + style: "narrow", + }), + ).toEqual("2mo ago") + }) + + it("renders ukrainian relative date", () => { + expect( + formatRelative(new Date(new Date().setMonth(new Date().getMonth() - 2)), { + locale: "uk", + }), + ).toEqual("2 місяці тому") + }) + + it("renders relative date with a unit", () => { + expect( + formatRelative( + new Date(new Date().getTime() - 60 * 24 * 60 * 60 * 1000), + { + unit: "days", + }, + ), + ).toEqual("60 days ago") + }) +}) + +// describe("format with a timezone", () => { +// it("can format a date with a timezone", () => { +// expect( +// formatRelative("2024-02-27T20:00:00-0500", { +// tz: "Europe/Amsterdam", +// }), +// ).toBe("7 11:30:10") +// }) +// }) diff --git a/src/formatRelative.ts b/src/formatRelative.ts new file mode 100644 index 0000000..3439589 --- /dev/null +++ b/src/formatRelative.ts @@ -0,0 +1,68 @@ +import { date } from "./date" +import type { DateInput, RelativeFormatOptions } from "./types" +import { deviceLocale } from "./deviceLocale" + +export function formatRelative( + inputDate: DateInput, + options?: RelativeFormatOptions, +) { + let { unit, locale } = options || {} + + if (!locale || locale === "device") { + locale = deviceLocale() + } + + // Remove the 's' from the end of the unit if it exists + // e.g. 'days' -> 'day' + if (unit) + unit = ( + unit.endsWith("s") ? unit.slice(0, -1) : unit + ) as Intl.RelativeTimeFormatUnit + + const date1 = date(inputDate) + + // Allow dates or times to be passed + const timeMs = date1.getTime() + + // Get the amount of seconds between the given date and now + const deltaSeconds = Math.round((timeMs - Date.now()) / 1000) + + // Array reprsenting one minute, hour, day, week, month, etc in seconds + const cutoffs = [ + 60, // 1 minute + 3600, // 1 hour + 86400, // 1 day + 86400 * 7, // 1 week + 86400 * 30, // 1 month + 86400 * 91, // 1 quarter + 86400 * 365, // 1 year + Infinity, // Infinity days + ] + + // Array equivalent to the above but in the string representation of the units + const units: Intl.RelativeTimeFormatUnit[] = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "quarter", + "year", + ] + + // Grab the ideal cutoff unit + const unitIndex = unit + ? units.findIndex((elem) => elem === unit) + : cutoffs.findIndex((cutoff) => cutoff > Math.abs(deltaSeconds)) + + // Get the divisor to divide from the seconds. E.g. if our unit is "day" our divisor + // is one day in seconds, so we can divide our seconds by this to get the # of days + const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1 + + // Intl.RelativeTimeFormat do its magic + const rtf = new Intl.RelativeTimeFormat(locale, options) + const rounder = deltaSeconds < 0 ? Math.ceil : Math.floor + + return rtf.format(rounder(deltaSeconds / divisor), units[unitIndex]) +} diff --git a/src/offset.ts b/src/offset.ts index 6e5e618..303d939 100644 --- a/src/offset.ts +++ b/src/offset.ts @@ -34,7 +34,7 @@ function relativeTime(d: Date, timeZone: string): Date { parts[part.type as keyof typeof parts] = part.value }) return new Date( - `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z` + `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z`, ) } @@ -49,7 +49,7 @@ function relativeTime(d: Date, timeZone: string): Date { export function offset( utcTime: DateInput, tzA = "UTC", - tzB = "device" + tzB = "device", ): string { tzB = tzB === "device" ? deviceTZ() : tzB const d = date(utcTime) diff --git a/src/types.ts b/src/types.ts index ec0dc3c..cf43495 100644 --- a/src/types.ts +++ b/src/types.ts @@ -65,7 +65,7 @@ export type FilledPart = Part & { value: string } export type FormatPattern = [ pattern: FormatToken | string, option: Partial>, - exp?: RegExp + exp?: RegExp, ] /** @@ -159,7 +159,7 @@ export interface FormatOptions { */ genitive?: boolean /** - * A function to filter parts. + * Converts the given date option to the timezone provided. */ tz?: string /** @@ -167,3 +167,17 @@ export interface FormatOptions { */ partFilter?: (part: Part) => boolean } + +export interface RelativeFormatOptions extends Intl.RelativeTimeFormatOptions { + /** + * A unit of time to format the date relative to. + * ```js + * ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'] + * ``` + */ + unit?: Intl.RelativeTimeFormatUnit + /** + * A locale or 'device' by default. + */ + locale?: string +} From ffc942d8aca647a60c76f39161acbd81389ce326 Mon Sep 17 00:00:00 2001 From: "hrynevych.romann" Date: Wed, 27 Mar 2024 21:28:42 +0200 Subject: [PATCH 2/2] fix(relative-time): formatRelative round function on format --- src/formatRelative.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/formatRelative.ts b/src/formatRelative.ts index 3439589..f14458c 100644 --- a/src/formatRelative.ts +++ b/src/formatRelative.ts @@ -5,6 +5,7 @@ import { deviceLocale } from "./deviceLocale" export function formatRelative( inputDate: DateInput, options?: RelativeFormatOptions, + originDate?: DateInput, ) { let { unit, locale } = options || {} @@ -20,12 +21,10 @@ export function formatRelative( ) as Intl.RelativeTimeFormatUnit const date1 = date(inputDate) - - // Allow dates or times to be passed - const timeMs = date1.getTime() + const date2 = originDate ? date(originDate) : date(new Date()) // Get the amount of seconds between the given date and now - const deltaSeconds = Math.round((timeMs - Date.now()) / 1000) + const deltaSeconds = Math.round((date1.getTime() - date2.getTime()) / 1000) // Array reprsenting one minute, hour, day, week, month, etc in seconds const cutoffs = [ @@ -62,7 +61,6 @@ export function formatRelative( // Intl.RelativeTimeFormat do its magic const rtf = new Intl.RelativeTimeFormat(locale, options) - const rounder = deltaSeconds < 0 ? Math.ceil : Math.floor - return rtf.format(rounder(deltaSeconds / divisor), units[unitIndex]) + return rtf.format(Math.round(deltaSeconds / divisor), units[unitIndex]) }