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, или хотя бы ишью. Если ишью, можете отмечать меня.
