/** * @see https://github.com/date-fns/date-fns/pull/348 * @see https://github.com/date-fns/date-fns/pull/364 * @see https://github.com/date-fns/date-fns/issues/277 * @see https://github.com/date-fns/date-fns/issues/284 * @see https://www.npmjs.com/package/pomeranian-durations * @see https://github.com/webpapaya/zeitgeist.js * @see https://stackoverflow.com/questions/54103133/parse-time-1hr-30m-4s-as-time-ahead-in-javascript * * @see ISO_8601 * @note it accepts `1ssssss`, but not `1ymd` or `1yyyy` (in prod) * * @example valid * - '1w1d' * - '1w' * - '1m' * - '1d' * - '1s' * - '1m1w1d1h1000s' * - '1M1w1D1h1S' * - '10000' * * @example invalid * - '1yyy' * - '10y' (_too big_) * - '5y1m1w1d1h1s' (_too big_) */ import { matchDigitWithNumber, match, hasLetters, hasNumbers, } from '../shared/utils_generic'; import { ErrorKeyKind, TtlErrorKeyType, } from '../shared/constants_domain_specific'; import { TimeLetterKind, MultiplyByKind, VALID_LETTERS, MAX_TTL_API_VALUE, } from './ttl_constants'; /** @perf optimize */ const splitNumberAndLetter = (x: string) => [ +match(/\d+/, x).join(''), match(/[a-z]+/i, x) .join('') .toUpperCase() as TimeLetterKind, ] as [number, string]; export const getNumberLetterPairSeconds = ( matches: [number, string][] ): number => { let inSeconds = 0; for (const [digits, letter] of matches) { switch (letter) { case TimeLetterKind.Second: inSeconds += digits * MultiplyByKind.Second; break; case TimeLetterKind.Hour: inSeconds += digits * MultiplyByKind.Hour; break; case TimeLetterKind.Day: inSeconds += digits * MultiplyByKind.Day; break; case TimeLetterKind.Week: inSeconds += digits * MultiplyByKind.Week; break; case TimeLetterKind.Month: inSeconds += digits * MultiplyByKind.Month; break; case TimeLetterKind.Year: inSeconds += digits * MultiplyByKind.Year; break; } } return inSeconds; }; /** @perf could keep TTL always a string */ export const parseTtl = ( valueInput: ValueType ): [number] | [number | undefined, TtlErrorKeyType] => { if (typeof valueInput === 'number') { return [valueInput]; } else if (typeof valueInput === 'string' && !hasNumbers(valueInput)) { return [, ErrorKeyKind.TtlInvalidFormat]; } const value = valueInput as string; const ttl = parseInt(value, 10); if (!hasLetters(value) && !Number.isNaN(ttl)) { return [ttl]; } else if (Number.isNaN(ttl) || !matchDigitWithNumber.test(value)) { return [, ErrorKeyKind.TtlMissingDefault]; } const matches = match(matchDigitWithNumber, value).map(splitNumberAndLetter); if ( matches.some( ([, letter]) => letter.length > 1 || !VALID_LETTERS.includes(letter) ) ) { return [ttl, ErrorKeyKind.TtlInvalidFormat]; } const seconds = getNumberLetterPairSeconds(matches); if (seconds >= MAX_TTL_API_VALUE) { return [seconds, ErrorKeyKind.TtlOverflow]; } else { return [seconds]; } }; export const getHasValidTtl = (value: string | number): boolean => { const [, error] = parseTtl(value); return !error; };