dydx-v4-web/src/lib/dateTime.ts
James Jia - Test 4b86068d8f
Initial commit
2023-09-08 13:52:13 -07:00

129 lines
4.4 KiB
TypeScript

import { timeUnits, allTimeUnits } from '@/constants/time';
// Given a literal from Intl.RelativeTimeFormat formatToParts,
// strip out words/symbols unrelated to time unit
const isolateTimeUnit = (literal: string) =>
literal
// Remove "future" words/symbols (e.g. "in", positive signs)
// TODO: update with other locales
.replace(
/(?<fr_ru>^[+])|(?<en_de>^in )|(?<es>^dentro de )|(?<zh>后$)|(?<ja>後$)|(?<ko>후$)|(?<tr>[.]? önce$)|(?<pt>^em )/,
''
)
// Remove "past" words/symbols (e.g. "ago", negative signs)
// TODO: update with other locales
.replace(
/(?<fr_ru>^[--])|(?<en>[.]? ago$)|(?<es>^hace )|(?<zh_ja>前$)|(?<ko>전$)|(?<tr>[.]? önce$)|(?<de>^vor )|(?<pt>^há )/,
''
);
// Abbreviate time unit from Intl.RelativeTimeFormat { style: "narrow" }
// (e.g. "day" -> "d", "дн" -> "д")
const toSingleCharacterTimeUnit = (timeUnit: string) =>
timeUnit
// Disambiguate ambiguous prefixes
// TODO: update with other locales
.replace(/(?<en> ?mo)/, ' Mo')
// Remove articles/"counting" words
// TODO: update with other locales
.replace(/(?<zh>个)|(?<ja>か)|(?<ko>개)/, '')
// Strip leading space and naively take just the 1st character
// TODO: take the 1st grapheme instead
.match(/^\s?(.)|/)?.[1];
export const formatRelativeTime = (
timestamp: number,
{
relativeToTimestamp = Date.now(),
locale,
format = 'singleCharacter',
largestUnit = 'year',
resolution = 2,
stripRelativeWords = true,
}: {
locale: string;
relativeToTimestamp?: number;
format?: 'long' | 'short' | 'narrow' | 'singleCharacter';
largestUnit: keyof typeof timeUnits;
resolution?: number;
stripRelativeWords?: boolean;
}
) => {
let elapsed = Math.abs(timestamp - relativeToTimestamp);
const sign = Math.sign(timestamp - relativeToTimestamp);
const unitParts = [];
for (const [unit, amount] of Object.entries(timeUnits).slice(
Object.keys(timeUnits).findIndex((unit) => unit === largestUnit)
))
if (Math.abs(elapsed) >= amount) {
unitParts.push(
new Intl.RelativeTimeFormat(locale, {
style: (
{
long: 'long',
short: 'short',
narrow: 'narrow',
singleCharacter: 'narrow',
} as const
)[format],
numeric: 'always',
}).formatToParts(sign * Math.floor(elapsed / amount), unit as keyof typeof timeUnits)
);
if (--resolution === 0) break;
elapsed %= amount;
}
return unitParts
.map(
(parts) =>
parts
.map(({ value, type }) =>
type === 'literal'
? format === 'singleCharacter'
? toSingleCharacterTimeUnit(stripRelativeWords ? isolateTimeUnit(value) : value)
: stripRelativeWords
? isolateTimeUnit(value)
: value
: /* : type === 'integer' ?
// fr/ru: remove "past" negative signs
Math.abs(Number(value)) */
value
)
.join('')
// ([{ value }, { value: literal }]) => value + literal.replace(/ [^ ]+?$/, '')
)
.join(' ');
};
export const formatAbsoluteTime = (
timestamp: number,
{
locale,
resolutionUnit = 'second',
}: {
locale: string;
resolutionUnit: keyof typeof allTimeUnits;
}
) =>
new Intl.DateTimeFormat(
locale,
({
millisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 3, hour12: false },
centisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 3, hour12: false },
decisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 2, hour12: false },
second: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 1, hour12: false },
minute: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false },
hour: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false },
day: { hour: 'numeric', minute: 'numeric' },
threeDays: { weekday: 'short', hour: 'numeric' },
week: { weekday: 'short', hour: 'numeric' },
month: { month: 'numeric', day: 'numeric', hour: 'numeric' },
year: { year: 'numeric', month: 'numeric', day: 'numeric' },
} as const)[resolutionUnit]
).format(timestamp);