Обновить

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

Уровень сложностиСредний
Время на прочтение5 мин
Охват и читатели6.7K
Всего голосов 4: ↑4 и ↓0+6
Комментарии15

Комментарии 15

Идея классная, сам что то подобное пробовал создать. Но в итоге стало понятно что, если я пишу с обикшами, то у меня нет шансов угадать, что закодировано в парсере. В итоге текст все равно идёт в ллм.

Кстати, с ноября 2025го модель (gpt > 4.xx) нормально так угадывает даты или диапазон. @mrprolopstar , а можно узнать на какие модели есть нарекания?

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

Бенчмарка по конкретным моделям у меня нет, поэтому называть модели не стану. Речь не о том, что модели плохо понимают даты, а о нескольких местах, где ошибка молчаливая и правдоподобная:
- 0 0 1 * 1 в cron означает «каждый понедельник или 1 число», а не «1 число, если это понедельник»; модели нередко генерируют такое для второго смысла;
- концы диапазонов: «каждые 15 минут с 9 до 18» это 9-17 в поле часов, а не 9-18;
- 12/24: «в семь» без уточнения.
Ответ модели при этом выглядит корректным cron, и глазами ошибку не видно.

Поэтому я бы не противопоставлял, а комбинировал: LLM переводит свободный текст в cron, а describe() показывает пользователю, как это расписание будет понято («по будням в 9:30»). Если модель ошиблась, человек увидит это сразу. Для агентов есть MCP-сервер ровно для этого.

Слабовато пока что. Не понимает:
в полвторого ежедневно
в четверть третьего
в час дня
в пять минут седьмого
без пяти шестнадцать

В половину третьего // и вообще, любого часа
Без шести семь // кстати, как он будет выбирать формат 12-24 и угадывать, какого семь?
Да, и общеупотребительные слова-маркеры часов русского языка он не парсит:
Без четверти пополудни // точное указание на 12:00
каждое десятилетие / каждый век // ЕМНИП, то крон умеет в годах
в час ночи / в три утра / в три часа утра // Эээ? Не может распознать числительное "три"?

Хабратестирование.

Спасибо за «хабратестирование», всё из списка уже в релизе 1.2.0:

в половину третьего     → 30 2 * * *   (любого часа, с первого по двенадцатый)
без шести семь          → 54 6 * * *
без четверти пополудни  → 45 11 * * *
в час ночи              → 0 1 * * *
в три утра              → 0 3 * * *
в три часа утра         → 0 3 * * *

Про 12/24 хороший вопрос. По умолчанию часы читаются как сказаны: «без шести семь» это 6:54, так же как «в 7» цифрами. «Утра», «дня», «вечера», «ночи» и «пополудни» переключают половину суток, а числа от 13 до 23 однозначны сами по себе. Для случаев, где ошибка дорого стоит, например в ботах-напоминалках, в 1.2.0 появился строгий режим strictHours. В нём «без шести семь» без уточнения даёт ошибку с просьбой указать утро или вечер, а угадывания нет.

Про годы: в стандартном cron пять полей, поля года там нет (оно есть в Quartz). Поэтому «каждое десятилетие» и «каждый век» теперь дают понятную ошибку «интервалы длиннее года cron не выражает».

Песочница: https://mrprolopstar.github.io/cronsense/

Добрый день! Спасибо, учёл, проработал, выпустил патч 1.1.0, теперь он должен был закрыть такие косяки.

Ок, продолжаем тест :)
раз в полгода
ежеквартально
каждый чётный час
каждый третий час начиная с часа ночи
Ну и, возможно, стоит добавить формат таймера systemd.

Спасибо за продолжение теста! Всё из списка сделал в 1.7.0:

раз в полгода                          → 0 0 1 */6 *
ежеквартально                          → 0 0 1 */3 *
каждый чётный час                      → 0 */2 * * *
каждый третий час начиная с часа ночи  → 0 1-23/3 * * *

«Чётный» учитывается и внутри окна: «каждый чётный час с 9 до 18» начинается с 10.

Формат systemd тоже добавил, toSystemd и флаг --systemd в CLI. Он умеет больше cron: последний день месяца (*-*~01), первый понедельник (Mon --01..07), число И день недели. Каждое выражение в тестах сверяется с systemd-analyze calendar. Настоящие интервалы вроде «каждые 90 минут» OnCalendar не выражает, там библиотека подскажет OnUnitActiveSec=90min.

Очень надо такое же, только для rrule. Или даже не rrule а самодельная альтернатива даже лучше будет, потому что rrule страдает странными ограничениями.

Спасибо, мысль хорошая. Многое из того, от чего cronsense сейчас отказывается, потому что cron это не выражает («каждые 2 недели», «в последний день месяца», «в первый понедельник месяца», «10 раз»), в RRULE как раз есть. Думаю добавить второй выход toRRule на том же парсере, со всеми русскими формами. А какие ограничения rrule мешают вам? Если это про конкретную библиотеку, а не про стандарт, то лучше учесть это сразу.

Вот с чем я столкнулся в rrule.js. Стандарт rrule якобы требует, чтобы была установлена начальная дата (dtstart). В библиотеке rrule.js, если не указать начальную дату, то по умолчанию будет поставлено "сегодня". Но ставит как-то ппц хитро, потому что события типа "каждую пятницу" при этом работают, даже если сегодня не пятница.
Почему я говорю, что это странно, потому что если установить дату dtstart вручную, скажем, на начало месяца, то это просто не будет работать, если первое число месяца внезапно не пятница. Библиотеку перекашивает и все события плывут. (Как бы логично, если для события "каждую пятницу" начальная дата внезапно понедельник. Тут любой растеряется.)
В итоге проблема не имеет решения. Если я устанавливаю начальную дату на начало месяца, то все события с днями недели ломаются. Если не устанавливаю, то события появляются только в конце календаря ("с сегодняшнего дня и далее"). Выставлять каждому событию начальную дату руками я не хочу, потому что это много бессмысленной работы. Логичнее было бы просто задать "каждый понедельник в 19:00" и мотать календарь назад и вперёд сколько хочу. Без начальной даты.

Вторая хотелка - это пасхальные даты. В rrule.js есть не совсем стандартный параметр byeaster (сделан по аналогии с python-dateutil). Около половины основных христианских праздников привязаны к Пасхе, так что с byeaster можно было бы внести эти праздники в календарь один раз, а не делать это каждый год вручную. Это то, чего мне не хватает, например, в google calendar.

Спасибо, оба пункта сделал в версии 1.4.0.

Про DTSTART. Воспроизвёл вашу ситуацию. Дело не в дне недели начальной даты: «каждая пятница» от четверга rrule.js считает правильно. Проблема в том, что rrule.js работает в UTC, и когда в правиле нет BYHOUR, время берётся из DTSTART. Локальная полночь по Москве - это 21:00 UTC предыдущего дня, поэтому пятничные события уезжают на субботу.

В cronsense это решено двумя способами. toRRule всегда пишет явные BYHOUR/BYMINUTE, поэтому такого сдвига нет. А главное, появился свой вычислитель без начальной даты, как вы и хотели: задаёте правило один раз и «мотаете» календарь куда угодно:

occurrences('по понедельникам в 19:00', { from, to })

Точка отсчёта нужна только для «каждые 2 недели» и подобных: без неё в принципе неизвестно, какие недели считать.

Про Пасху. Есть нюанс: BYEASTER в rrule.js и dateutil считает только западную Пасху, а православная часто с ней не совпадает (в 2026 году 12 апреля против 5-го). Поэтому в cronsense оба календаря:

occurrences('через 49 дней после Пасхи', { from, to })        // Троица: 31 мая 2026
occurrences('за 46 дней до католической Пасхи', { from, to }) // Пепельная среда
toRRule('49 days after easter')                               // FREQ=YEARLY;...;BYEASTER=49

«Пасха» по умолчанию православная, «Easter» западная, можно уточнить словами или опцией. Для православной toRRule честно отказывает и предлагает occurrences, потому что в RRULE её не выразить.

Песочница: https://mrprolopstar.github.io/cronsense/. Если какая-то фраза не разберётся, пришлите её, добавлю в тесты.

Спасибо! Прямо спасение для меня.

каждые 30 минут понимает, а каждые полчаса - нет.

Добрый день! Спасибо, поправил!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации