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, }; }