Skip to content

Commit

Permalink
feat: custom parser for relative date input (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Apr 15, 2024
1 parent eae9abc commit 6b23322
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 79 deletions.
15 changes: 3 additions & 12 deletions src/datemath/datemath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});

Expand All @@ -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());
});

Expand Down Expand Up @@ -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),
);
});
Expand All @@ -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]}));
Expand Down
87 changes: 33 additions & 54 deletions src/datemath/datemath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
46 changes: 44 additions & 2 deletions src/parser/parser.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
34 changes: 29 additions & 5 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -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<DateTimeOptionsWhenParsing> = (
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 || {};
Expand Down Expand Up @@ -49,3 +54,22 @@ export const dateTimeParse: DateTimeParser<DateTimeOptionsWhenParsing> = (

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();
}
16 changes: 13 additions & 3 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -76,13 +78,21 @@ 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);
}
}

/**
* Settings to manage Dayjs customization
* Settings to manage DateTime customization
*/
export const settings = new Settings();
25 changes: 25 additions & 0 deletions src/settings/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof dayjs.updateLocale>[1] | Partial<ILocale>;

export interface Parser {
parse: (text: string, options?: ParseOptions) => DateTime | undefined;
isLikeRelative: (text: string) => boolean;
}

export interface PublicSettings {
loadLocale(locale: string): Promise<void>;

getLocale(): string;

getLocaleData(): ILocale;

setLocale(locale: string): void;

updateLocale(config: UpdateLocaleConfig): void;

setDefaultTimeZone(zone: 'system' | (string & {})): void;

getDefaultTimeZone(): string;

setRelativeParser(parser: Parser): void;
}

0 comments on commit 6b23322

Please sign in to comment.