Парсинг дат в JS — добавляем русский язык в библиотеку Chrono
Crono это парсер дат на естественном языке. Кроме формальных ISO 8601 или dd.MM.yyyy, распознает варианты а-ля «в среду утром», «с 10 до 11 вечера»,»2 часа 5 минут назад» и т.п. Поддерживает 8 языков, в том числе, теперь, и русский.
Использую Chrono в своем проекте начиная с 2016 года. Русский язык библиотека не поддерживала, но удалось найти форк, который сносно парсил даты на русском. Прошло 6 лет, библиотека развивалась, в 2020 автор переписал её на TypeScript, переработав архитектуру, а поддержки русского языка в официальном репозитории так и не появилось. Решил это исправить. PR вмёрджили, можно и статью написать.
Добавление нового языка
Для добавления нового языка нужно добавить специфичные для него парсеры (parsers) и рефайнеры (refiners). Из них состоит пайплайн в Chrono.
Парсеры
Их задача извлечь из строки соответствующие форматы дат. Идеально один, или смежные форматы — один парсер. Для русского их сейчас 10. Например, RUCasualDateParser
находит даты вида сегодня|вчера|завтра|послезавтра|позавчера.
Интерфейс парсера:
interface Parser {
pattern: (context: ParsingContext) => RegExp для поиска дат,
extract: (context: ParsingContext, match: RegExpMatchArray) =>
Результат извлечения
}
Рефайнеры
Задача —фильтрация полученных результатов, их сортировка, объединение, обогащение дополнительной информацией. Например, строка «в пятницу в 19:00». Парсер дней недели извлёк «в пятницу», парсер времени «в 19:00», задача рефайнеров это объединить.
Основную работу делает набор общих для всех языков рефайнеров. Специфичных для языка обычно 2 или 3. Наследуются от стандартных классов, и выглядят, как правило, так:
export default class RUMergeDateRangeRefiner
extends AbstractMergeDateRangeRefiner {
patternBetween(): RegExp {
return /^\s*(и до|и по|до|по|-)\s*$/i;
}
}
Интерфейс:
interface Refiner {
refine: (context: ParsingContext, results: ParsingResult[])
=> ParsingResult[]
}
Использование
По умолчанию используется английский, к русской локали обращаемся через chrono.ru
.
import * as chrono from 'chrono-node';
chrono.ru.parseDate('Встреча 12 сентября');
// 2022-09-12T08:00:00.000Z
chrono.ru.parse('Встреча 12 сентября');
/* [{
index: 18,
text: '12 сентября',
start: ...
}] */
parseDate
вернёт вам первую встретившуюся дату как стандартный Date
, parse
вернёт все найденные даты как ParsedResult
.
export interface ParsedResult {
/**
* Дата относительно которой производился поиск, подробнее ниже.
*/
readonly refDate: Date;
/**
* Индекс найденного вхождения.
* Для строки 'Встреча 12 сентября' будет 8.
*/
readonly index: number;
/**
* Текст найденной датой. Для строки 'Встреча 12 сентября'
* будет '12 сентября'.
*/
readonly text: string;
/**
* Либо найденную дату, если, например, на вход подать '12 сентября'.
* Либо начало интервала, если строка была,
* например, '10 - 22 августа 2012'.
*/
readonly start: ParsedComponents;
/**
* Конец интервала, если найден интервал, а не единичная дата.
*/
readonly end?: ParsedComponents;
/**
* Создает экземпляр Date из result.start.
*/
date(): Date;
}
Свойства start
и end
имеют тип ParsedComponents
. Содержат не только дату, но и немного метаинформации. Например, метод isCertain
.
chrono.ru.parse('утром')[0].start.isCertain('day');
//false — тут день предполагается
chrono.ru.parse('12 сентября утром')[0].start.isCertain('day');
//true — тут чётко понятно, какой день
Оба метода принимают дополнительные опции:
function parse(text: string, ref?: Date, option?: ParsingOption)
: ParsedResult[] {
...
}
function parseDate(text: string, ref?: Date, option?: ParsingOption)
: Date {
...
}
Параметр ref
нужен для того, чтобы задать контекст. Если сказать «сегодня в 21:45» сейчас и через месяц, будут иметься ввиду разные моменты времени. По умолчанию new Date()
.
В ParsingOption
есть 2 полезных свойства:
export interface ParsingOption {
/**
* Дает понять, что результат должен быть после reference date.
*/
forwardDate?: boolean;
/**
* Можно переопределить смещение временных зон
*/
timezones?: { [tzKeyword: string]: number };
}
Пример:
const referenceDate = new Date(2012, 7, 25);
// 25 августа 2012, суббота
chrono.ru.parseDate('в пятницу', referenceDate);
// 24 августа 2012
chrono.ru.parseDate('в пятницу', referenceDate, { forwardDate: true });
// 31 августа 2012, пятница
Есть строгая версия парсера — chrono.ru.strict
. Состоит из парсеров, понимающих только формальные паттерны.
chrono.ru.strict.parseDate('сегодня');
// null
chrono.ru.strict.parseDate('06.07.2020');
// 6 июля 2022
Примеры поддерживаемых форматов
прошлым вечером
10 августа — 12 сентября 2013
24 го октября, 9:00
в 12
полчаса назад
через месяц
в прошлый четверг
06.09.2012
10:00:00 — 21:45:01
и т.д.
PRs are welcome
За образец брал парсеры английского, поэтому какие-то специфичные для русского языка паттерны могли просто не прийти в голову. Если придут вам, будет супер, если создадите PR, или хотя бы ишью. Если ишью, можете отмечать меня.
GitHub
npm