159 lines
4 KiB
TypeScript
159 lines
4 KiB
TypeScript
import * as Moo from 'moo';
|
||
import { CalendarEvent } from '../models/calendar-event';
|
||
import * as Luxon from 'luxon';
|
||
import { Logger } from '@nestjs/common';
|
||
|
||
export const DEFAULT_PARAMS: Moo.Rules = {
|
||
WS: /[ \t]+/,
|
||
delimiter: /(?: - |: )/,
|
||
delimiter2: /(?:^\* |^- |(?!\d)T(?!>\d))/,
|
||
date: /\b(?:(?:\d{2}\.\d{2}\.(?:\d{2}|\d{4}))|(?:\d{4}-\d{2}-\d{2}))\b/,
|
||
time: /\b(?:\d{2}\b:\d{2}:\d{2}|\d{2}:\d{2})\b/,
|
||
word: /[\wА-Яа-я]+/,
|
||
other: { match: /./, lineBreaks: true },
|
||
NL: { match: /\n/, lineBreaks: true },
|
||
};
|
||
|
||
export const DEFAULT_DATE_FORMATS = ['yyyy-MM-dd', 'dd.MM.yyyy', 'dd.MM.yy'];
|
||
|
||
export const DEFAULT_TIME_FORMATS = ['HH:mm:ss', 'HH:mm'];
|
||
|
||
export const DEFAULT_EVENT_DURATION = 60 * 60 * 1000; // 1 hour in millis
|
||
|
||
const logger = new Logger('string-with-dates-parser');
|
||
|
||
export function parse(str: string, params?: Moo.Rules): Moo.Token[] {
|
||
if (!params) params = DEFAULT_PARAMS;
|
||
try {
|
||
const lexer = Moo.compile(params);
|
||
lexer.reset(str);
|
||
const res: Moo.Token[] = [];
|
||
let token = lexer.next();
|
||
while (token) {
|
||
res.push(token);
|
||
token = lexer.next();
|
||
}
|
||
return res;
|
||
} catch (ex) {
|
||
logger.error(
|
||
`Error at parse str=${str} with params=${params}, error message - ${ex}`,
|
||
);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export type ParserOpts = {
|
||
rules?: Moo.Rules;
|
||
dateFormats?: string[];
|
||
timeFormats?: string[];
|
||
};
|
||
|
||
export const DEFAULT_PARSER_OPTS: ParserOpts = {
|
||
rules: DEFAULT_PARAMS,
|
||
dateFormats: DEFAULT_DATE_FORMATS,
|
||
timeFormats: DEFAULT_TIME_FORMATS,
|
||
};
|
||
|
||
export function parseDate(
|
||
str: string,
|
||
formats?: string[],
|
||
): Luxon.DateTime | null {
|
||
if (!str) return null;
|
||
if (!formats) formats = DEFAULT_DATE_FORMATS;
|
||
let res: Luxon.DateTime;
|
||
for (let i = 0; i < formats.length; i++) {
|
||
const format = formats[i];
|
||
res = Luxon.DateTime.fromFormat(str, format);
|
||
if (res.isValid) return res;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function parseTime(
|
||
str: string,
|
||
formats?: string[],
|
||
): Luxon.Duration | null {
|
||
if (!str) return null;
|
||
if (!formats) formats = DEFAULT_TIME_FORMATS;
|
||
let res: Luxon.DateTime;
|
||
for (let i = 0; i < formats.length; i++) {
|
||
const format = formats[i];
|
||
res = Luxon.DateTime.fromFormat(str, format);
|
||
if (res.isValid) {
|
||
const startOfDay = res.set({
|
||
hour: 0,
|
||
minute: 0,
|
||
second: 0,
|
||
millisecond: 0,
|
||
});
|
||
return res.diff(startOfDay).shiftToAll();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function parseToCalendarEvent(
|
||
str: string,
|
||
opts?: ParserOpts,
|
||
): CalendarEvent | null {
|
||
if (!opts) opts = DEFAULT_PARSER_OPTS;
|
||
const tokens = parse(str, opts?.rules);
|
||
|
||
const words = tokens.filter((i) => i.type === 'word');
|
||
|
||
const dates = tokens.filter((i) => i.type === 'date');
|
||
if (dates.length < 0) return null;
|
||
const date1 = parseDate(dates[0]?.value, opts?.dateFormats);
|
||
const date2 = parseDate(dates[1]?.value, opts?.dateFormats);
|
||
|
||
const times = tokens.filter((i) => i.type === 'time');
|
||
const time1 = parseTime(times[0]?.value, opts?.timeFormats);
|
||
const time2 = parseTime(times[1]?.value, opts?.timeFormats);
|
||
|
||
let from: Luxon.DateTime;
|
||
let to: Luxon.DateTime;
|
||
let fullDay: boolean;
|
||
|
||
if (date1 && time1) {
|
||
from = date1.set({
|
||
hour: time1.hours,
|
||
minute: time1.minutes,
|
||
second: time1.seconds,
|
||
});
|
||
fullDay = false;
|
||
} else if (date1) {
|
||
from = date1;
|
||
fullDay = true;
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
if (date2 && time2) {
|
||
to = date2.set({
|
||
hour: time2.hours,
|
||
minute: time2.minutes,
|
||
second: time2.seconds,
|
||
});
|
||
} else if (date2) {
|
||
to = date2.plus({ day: 1 });
|
||
} else if (fullDay) {
|
||
to = from.plus({ day: 1 });
|
||
} else if (time2) {
|
||
to = date1.set({
|
||
hour: time2.hours,
|
||
minute: time2.minutes,
|
||
second: time2.seconds,
|
||
});
|
||
} else {
|
||
to = from.plus(Luxon.Duration.fromMillis(DEFAULT_EVENT_DURATION));
|
||
}
|
||
|
||
return {
|
||
from: from.toISO(),
|
||
fromTimestamp: from.toMillis(),
|
||
to: to.toISO(),
|
||
toTimestamp: to.toMillis(),
|
||
description: words.join(' '),
|
||
fullDay: fullDay,
|
||
};
|
||
}
|