Парсинг дат в 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


ссылка на оригинал статьи https://habr.com/ru/post/676026/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *