ESPNThe only MLB playoff preview you need: World Series odds, likely MVPs and how far all 12 teams will goThe Jerusalem PostHezbollah's Nasrallah commemoration exposes its lost standing in Lebanon, expert saysPunchAI should complement, not replace, judicial officers – A’Ibom CJESPN DeportesAl Rojas Vivo: Álvarez, Sánchez, Durán, Stewart y Espada, los latinos de 2026Inquirer‘Most unique’ birds documented in Apayao biosphereColliderNew James Bond Release Officially Adds a 'Game of Thrones' StarAnime News NetworkStalled Despera Anime Project Gets MangaPopular ScienceBald eagles Shadow and Kaydee share a peaceful morning, Jackie’s memorial service approachesVariety‘Stillwater’: Amazon Series Based on Graphic Novel Casts Ben Hardy in Lead RoleBBC عربي"محادثات مرتقبة غير مباشرة" بين واشنطن وطهران في نيويورك، وخامئني يقول إن "القوات الأمريكية ستُجبر على مغادرة بحر العرب"20 Minuten«Bin 100 Meter reingelaufen»: Bodensee-Pegel so tief wie noch nieBBC NewsBest thing we can offer young people is a job, not benefits, says chancellor
The Daily Newsstand · Free, Always
Monday, September 28, 2026

«По будням в 9:30» → 30 9 * * 1–5: парсер расписаний на русском, который отказывается угадывать

Translate

Выражение */15 9-17 * * 1-5 компьютер понимает однозначно, а человек читает с трудом. Особенно это мешает, когда расписание задаёт пользователь: в боте‑напоминалке, в админке, в настройках отчётов. Человек хочет написать «каждые 15 минут с 9 до 18 по будням» и увидеть, что его поняли правильно. Писать для этого форму с пятью выпадающими списками не хочется никому.

Готовых решений для русского языка я не нашёл, поэтому написал своё.

Библиотека называется cronsense. Она переводит расписание, записанное обычными словами на русском или английском, в cron‑выражение и обратно:

по будням в 9:30                      →  30 9 * * 1-5
каждые 15 минут с 9 до 18 по будням   →  */15 9-17 * * 1-5
1 и 15 числа в полдень                →  0 12 1,15 * *
с ноября по февраль в 7 утра          →  0 7 * 1,2,11,12 *

Попробовать можно в браузере: https://mrprolopstar.github.io/cronsense/

Ниже про то, как это устроено внутри и какие грабли встретились по дороге.

Почему не взять готовое

Для направления «cron → текст» давно есть cRonstrue, и у него есть русская локаль. В обратную сторону, из свободного текста в cron, я нашёл в основном билдеры вида schedule.onDays(...).at('09:00'). Это удобный API, но пользователь бота не пишет код, он пишет «напоминай по будням в девять».

Можно отдать фразу LLM. Для прототипа это работает, но модель может ошибиться в cron уверенно и молча, например перепутать день недели с числом месяца или не учесть, что 9-18 в поле часов включает 18:00. Мне нужен был результат, который одинаков при каждом запуске и который можно покрыть тестами.

Отсюда и главный принцип библиотеки: если cron не может выразить фразу точно, она возвращает ошибку с объяснением и не подставляет что‑то похожее.

Как устроен разбор

Конвейер обычный: лексер, словарь, парсер, модель полей cron.

Лексер режет строку на числа, время (9:30, 9.30), слова и разделители. Он помнит позицию каждого токена в исходной строке. Это пригодилось для ошибок: библиотека показывает, какое именно слово ей не понравилось.

по будням кроме пятницы
          ^^^^^
UNSUPPORTED: Not expressible in cron: exclusions

Словарь сопоставляет слова с регулярками по основам, словоформы в нём не перечислены. Для дней недели это выглядит так:

[/^(?:понедельник(?:а|ам|и|ов|у)?|пн|mondays?|mon)$/, dow(1)],
[/^(?:сред(?:а|у|ам|ы|е)|ср|wednesdays?|wed|weds)$/, dow(3)],
[/^(?:будн[а-я]*|рабоч[а-я]*|weekdays?|workdays?)$/, { t: 'dow', days: WEEKDAYS }],

Словарь морфологии здесь избыточен: предметная область маленькая, слов около сотни. Регулярки по основам покрывают «понедельник», «понедельникам», «с понедельника» и английские формы одной строкой.

Интереснее с неоднозначными словами. «Дня» бывает единицей измерения («каждые 3 дня») и указанием времени суток («в 3 часа дня»). В словаре это один токен с двумя смыслами, а выбирает контекст: после «каждые N» это интервал, после часа это «после полудня».

Парсер написан рекурсивным спуском. Самое неприятное место в нём: голые числа. «В 15» означает время, «15 числа» означает день месяца, а просто «15» непонятно что. На последний случай библиотека отвечает ошибкой AMBIGUOUS и подсказывает дописать «в» или «числа».

Сложнее всего оказались как раз такие стыки между правилами. Чтобы новое правило не сломало старую фразу незаметно, на русские и английские формулировки сразу завёл табличные тесты: фраза и ожидаемый cron. Сейчас в проекте 233 теста.

Грабли cron, о которые легко споткнуться

День месяца и день недели объединяются через ИЛИ

Выражение 0 0 1 * 1 многие читают как «первое число, если это понедельник». На деле cron запустит задачу каждый понедельник и каждое первое число. Поэтому фразу «по понедельникам 1 числа» cronsense не превращает в cron, а возвращает ошибку CONFLICT. Явный отказ здесь полезнее молча выданного другого расписания.

Концы диапазонов

«Каждые 15 минут с 9 до 18» люди понимают так: последний запуск в 17:45. Значит, в поле часов должно стоять 9-17, а не 9-18. А вот «каждый час с 9 до 18» обычно включает 18:00, и там 9-18. Эти два правила пришлось закодировать явно.

Интервалы, которых в cron нет

«Каждые 2 недели», «в последний день месяца», «каждые 90 минут» стандартный cron точно не выражает. Всё это даёт ошибку UNSUPPORTED или OUT_OF_RANGE с объяснением. Если число минут кратно 60, например 120, библиотека подсказывает записать интервал в часах.

Несколько времён в одной строке

«В 9:00 и 18:30» одной cron‑строкой не записать: поле минут общее для всех часов, и 0,30 9,18 включит лишние 9:30 и 18:00. Сетку «9:00, 9:30, 18:00, 18:30» записать можно, её парсер принимает.

Обратное направление и гарантия round‑trip

Функция describe делает из cron текст:

describe('0 23 * * 0,1,5,6', { locale: 'ru' }) // 'с пятницы по понедельник в 23:00'
describe('0 9 * * 1-3,5', { locale: 'ru' })    // 'с понедельника по среду и по пятницам в 9:00'
describe('15 * * * *', { locale: 'ru' })       // 'каждый час в 15 минут'

Главное требование к ней: всё, что она пишет, должно разбираться обратно в то же самое расписание. Проверяет это property‑тест. Он генерирует 3000 случайных cron‑выражений, описывает каждое на русском и на английском, разбирает описание и сравнивает результат с исходным расписанием. Сравнивает не строки, а множества минут, часов и месяцев, плюс день месяца и день недели с учётом правила ИЛИ. Строки могут отличаться: 5-59/10 и 5,15,25,35,45,55 означают одно и то же.

Ради этого требования пришлось расширить грамматику парсера. Например, без фразы «каждый час в 15 минут» было нечем описать cron вида 15 * * * *, и её добавили в обе стороны.

Исключение одно: если в cron ограничены и день месяца, и день недели, описание получается «1 числа или по понедельникам». Такую фразу парсер намеренно не принимает, по той же причине с ИЛИ.

Для такого кода property‑тест полезнее десятков ручных примеров. Ручные примеры проверяют то, о чём я подумал, а случайные выражения вроде 5-59/10 22-23,0-5 /2 1,7 проверяют то, о чём я не подумал.

MCP‑сервер для ИИ‑агентов

Раз LLM путаются в cron, логично дать им инструмент. В пакете есть cronsense-mcp: MCP‑сервер с тремя инструментами (to_cron, describe_cron, next_runs). Он без зависимостей: протокол MCP поверх stdio сводится к построчному JSON‑RPC, и отдельный SDK для трёх инструментов не нужен.

claude mcp add cronsense -- npx -y -p github:MrProLopstar/cronsense cronsense-mcp

Как подключить

Пакет опубликован в JSR, работает в Node, Deno, Bun и в браузере:

npx jsr add @mrprolopstar/cronsense
import cron from 'node-cron';
import { toCron } from '@mrprolopstar/cronsense';

cron.schedule(toCron('по будням в 9:30'), sendDailyReport);

Для ботов удобен такой приём: разобрать то, что написал пользователь, и ответить каноническим описанием, чтобы он увидел, как его поняли.

const result = safeParse(message.text);
reply(result.ok ? `Буду напоминать ${describe(result.schedule, { locale: 'ru' })}` : result.error.excerpt);

Код открыт под MIT: https://github.com/MrProLopstar/cronsense. Если какая‑то фраза не разобралась или разобралась неправильно, откройте issue с этой фразой. Такие примеры сразу становятся тестами.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.