From 6b23322423dc8b2e334d0c3aee321373a4d0367a Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Mon, 15 Apr 2024 19:46:33 +0200 Subject: [PATCH] feat: custom parser for relative date input (#53) --- src/datemath/datemath.test.ts | 15 ++---- src/datemath/datemath.ts | 87 +++++++++++++---------------------- src/index.ts | 10 ++-- src/parser/parser.test.ts | 46 +++++++++++++++++- src/parser/parser.ts | 34 ++++++++++++-- src/settings/settings.ts | 16 +++++-- src/settings/types.ts | 25 ++++++++++ 7 files changed, 154 insertions(+), 79 deletions(-) diff --git a/src/datemath/datemath.test.ts b/src/datemath/datemath.test.ts index 31846c2..34bc019 100644 --- a/src/datemath/datemath.test.ts +++ b/src/datemath/datemath.test.ts @@ -52,7 +52,7 @@ describe('DateMath', () => { expected.setSeconds(0); expected.setMilliseconds(0); - const startOfDay = dateMath.parse('now/d', false)?.valueOf(); + const startOfDay = dateMath.parse('now/d', {roundUp: false})?.valueOf(); expect(startOfDay).toBe(expected.getTime()); }); @@ -62,7 +62,7 @@ describe('DateMath', () => { Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0), ); - const startOfDay = dateMath.parse('now/d', false, 'utc')?.valueOf(); + const startOfDay = dateMath.parse('now/d', {roundUp: false, timeZone: 'utc'})?.valueOf(); expect(startOfDay).toBe(expected.getTime()); }); @@ -114,7 +114,7 @@ describe('DateMath', () => { }); it('should round now to the end of the ' + span, () => { - expect(dateMath.parse('now/' + span, true)?.format(format)).toEqual( + expect(dateMath.parse('now/' + span, {roundUp: true})?.format(format)).toEqual( now.endOf(span).format(format), ); }); @@ -125,15 +125,6 @@ describe('DateMath', () => { }); }); - describe('isValid', () => { - it('should return false when invalid date text', () => { - expect(dateMath.isValid('asd')).toBe(false); - }); - it('should return true when valid date text', () => { - expect(dateMath.isValid('now-1h')).toBe(true); - }); - }); - describe('Parsing part after now', () => { it('should handle negative time', () => { const date = dateMath.parseDateMath('-2d', dateTime({input: [2014, 1, 5]})); diff --git a/src/datemath/datemath.ts b/src/datemath/datemath.ts index 2c6e1ad..0d3f333 100644 --- a/src/datemath/datemath.ts +++ b/src/datemath/datemath.ts @@ -2,80 +2,59 @@ // Copyright 2021 YANDEX LLC import includes from 'lodash/includes'; -import isDate from 'lodash/isDate'; -import {dateTime, isDateTime} from '../dateTime'; +import {dateTime} from '../dateTime'; import type {DateTime, DurationUnit, TimeZone} from '../typings'; const units: DurationUnit[] = ['y', 'Q', 'M', 'w', 'd', 'h', 'm', 's']; -/** - * Checks if value is a valid date which in this context means that it is either - * a Dayjs instance or it can be parsed by parse function. - * @param value value to parse. - */ -export function isValid(value?: string | DateTime): boolean { - const date = parse(value); - - if (!date) { - return false; - } - - if (isDateTime(date)) { - return date.isValid(); - } +export function isLikeRelative(text: string) { + return text.startsWith('now'); +} - return false; +export interface ParseOptions { + roundUp?: boolean; + timeZone?: TimeZone; } -export function parse( - text?: string | DateTime | Date | null, - roundUp?: boolean, - timeZone?: TimeZone, -): DateTime | undefined { +export function parse(text: string, options: ParseOptions = {}): DateTime | undefined { if (!text) { return undefined; } - if (typeof text === 'string') { - let time; - let mathString = ''; - let index; - let parseString; - - if (text.substring(0, 3) === 'now') { - time = dateTime({timeZone}); - mathString = text.substring('now'.length); - } else { - index = text.indexOf('||'); - - if (index === -1) { - parseString = text; - mathString = ''; - } else { - parseString = text.substring(0, index); - mathString = text.substring(index + 2); - } - - time = dateTime({input: parseString, timeZone}); - } + const {roundUp, timeZone} = options; - if (!mathString.length) { - return time; - } + let time; + let mathString = ''; + let index; + let parseString; - return parseDateMath(mathString, time, roundUp); + if (text.substring(0, 3) === 'now') { + time = dateTime({timeZone}); + mathString = text.substring('now'.length); } else { - if (isDateTime(text)) { - return text; - } + index = text.indexOf('||'); - if (isDate(text)) { - return dateTime({input: text, timeZone}); + if (index === -1) { + parseString = text; + mathString = ''; + } else { + parseString = text.substring(0, index); + mathString = text.substring(index + 2); } + time = dateTime({input: parseString, timeZone}); + } + + if (!time.isValid()) { return undefined; } + + if (!mathString.length) { + return time; + } + + return parseDateMath(mathString, time, roundUp); } export function parseDateMath( diff --git a/src/index.ts b/src/index.ts index 231100f..c3cad98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,11 @@ +import {settings as innerSettings} from './settings'; +import type {PublicSettings} from './settings/types'; + +export const settings = innerSettings as PublicSettings; + export {dateTime, dateTimeUtc, isDateTime} from './dateTime'; -export {isValid} from './datemath'; -export {dateTimeParse} from './parser'; +export {parse as defaultRelativeParse, isLikeRelative as defaultIsLikeRelative} from './datemath'; +export {dateTimeParse, isValid, isLikeRelative} from './parser'; export {getTimeZonesList, guessUserTimeZone, isValidTimeZone, timeZoneOffset} from './timeZone'; export type {DateTime, DateTimeInput} from './typings'; -export {settings} from './settings'; export {UtcTimeZone} from './constants'; diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index dce5e7e..98f24d3 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -1,10 +1,15 @@ import MockDate from 'mockdate'; import {DEFAULT_SYSTEM_DATE_FORMAT} from '../constants'; +import { + dateTimeParse, + defaultIsLikeRelative, + defaultRelativeParse, + isValid, + settings, +} from '../index'; import type {DateTime} from '../typings'; -import {dateTimeParse} from './parser'; - const TESTED_DATE_STRING = '2021-08-07'; const TESTED_TIMESTAMP = 1621708204063; const MOCKED_DATE = '2021-08-07T12:10:00'; @@ -68,3 +73,40 @@ describe('Parser', () => { expect(date?.toISOString()).toEqual(expected); }); }); + +describe('custom parser', () => { + afterEach(() => { + settings.setRelativeParser({ + parse: defaultRelativeParse, + isLikeRelative: defaultIsLikeRelative, + }); + }); + + it('should return DateTime in case of using custom parser', () => { + settings.setRelativeParser({ + isLikeRelative: (text) => { + return text.startsWith('test'); + }, + parse: (text, options) => { + const t = text.replace(/^test/, 'now'); + return defaultRelativeParse(t, options); + }, + }); + + const date = dateTimeParse('test-1h'); + expect(date?.toISOString()).toEqual(new Date(Date.now() - 3600000).toISOString()); + }); +}); + +describe('isValid', () => { + it('should return false when invalid date text', () => { + expect(isValid('asd')).toBe(false); + }); + it('should return true when valid date text', () => { + expect(isValid('now-1h')).toBe(true); + }); + it('should return false when value is falsy', () => { + expect(isValid(undefined)).toBe(false); + expect(isValid('')).toBe(false); + }); +}); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 32ebd62..7450c9c 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,22 +1,27 @@ // Copyright 2015 Grafana Labs // Copyright 2021 YANDEX LLC -import {dateTime} from '../dateTime'; -import {isValid, parse} from '../datemath'; +import {dateTime, isDateTime} from '../dateTime'; +import {settings} from '../settings'; import type {DateTime, DateTimeOptionsWhenParsing, DateTimeParser} from '../typings'; +export function isLikeRelative(text: unknown): text is string { + return typeof text === 'string' && settings.getRelativeParser().isLikeRelative(text); +} + const parseInput: DateTimeParser = ( input, options, ): DateTime | undefined => { - if (typeof input === 'string' && input.indexOf('now') !== -1) { + if (isLikeRelative(input)) { const allowRelative = options?.allowRelative ?? true; - if (!isValid(input) || !allowRelative) { + if (!allowRelative) { return undefined; } - return parse(input, options?.roundUp, options?.timeZone); + const parser = settings.getRelativeParser(); + return parser.parse(input, options); } const {format, lang} = options || {}; @@ -49,3 +54,22 @@ export const dateTimeParse: DateTimeParser = ( return date; }; + +/** + * Checks if value is a valid date which in this context means that it is either + * a DateTime instance or it can be parsed by parse function. + * @param value value to parse. + */ +export function isValid(value?: string | DateTime): boolean { + if (isDateTime(value)) { + return value.isValid(); + } + + const date = dateTimeParse(value, {allowRelative: true}); + + if (!date) { + return false; + } + + return date.isValid(); +} diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 7e614c2..a4daa1e 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -1,16 +1,18 @@ import cloneDeep from 'lodash/cloneDeep'; +import {isLikeRelative, parse} from '../datemath'; import dayjs from '../dayjs'; import {normalizeTimeZone} from '../timeZone'; import {localeLoaders} from './locales'; -import type {UpdateLocaleConfig} from './types'; +import type {Parser, PublicSettings, UpdateLocaleConfig} from './types'; -class Settings { +class Settings implements PublicSettings { // 'en' - preloaded locale in dayjs private loadedLocales = new Set(['en']); private defaultLocale = 'en'; private defaultTimeZone = 'system'; + private parser: Parser = {parse, isLikeRelative}; constructor() { this.updateLocale({ @@ -76,6 +78,14 @@ class Settings { return this.defaultTimeZone; } + setRelativeParser(parser: Parser) { + this.parser = parser; + } + + getRelativeParser() { + return this.parser; + } + private isLocaleLoaded(locale: string) { const localeInLowerCase = locale.toLocaleLowerCase(); return this.loadedLocales.has(localeInLowerCase); @@ -83,6 +93,6 @@ class Settings { } /** - * Settings to manage Dayjs customization + * Settings to manage DateTime customization */ export const settings = new Settings(); diff --git a/src/settings/types.ts b/src/settings/types.ts index 16276ba..d961156 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -1,4 +1,29 @@ +import type {ParseOptions} from '../datemath'; import type dayjs from '../dayjs'; +import type {DateTime} from '../typings'; // https://dayjs.gitee.io/docs/ru/customization/customization export type UpdateLocaleConfig = Parameters[1] | Partial; + +export interface Parser { + parse: (text: string, options?: ParseOptions) => DateTime | undefined; + isLikeRelative: (text: string) => boolean; +} + +export interface PublicSettings { + loadLocale(locale: string): Promise; + + getLocale(): string; + + getLocaleData(): ILocale; + + setLocale(locale: string): void; + + updateLocale(config: UpdateLocaleConfig): void; + + setDefaultTimeZone(zone: 'system' | (string & {})): void; + + getDefaultTimeZone(): string; + + setRelativeParser(parser: Parser): void; +}