250 строк кода, распознающих дату на русском языке

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



    Пример и код в jsFiddle.

    Код не претендует на образец, является отъявленной лапшой человека, который занимается программированием в качестве хобби. Но уже год спокойно работает и выполняет свои функции в 3х проектах.

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

    Зайдя в пример на jsFiddle, вы можете попробовать разные варианты и легко разобраться, как он работает и поиграться разными фразами. Все основные варианты человеческого написания он распознаёт.

    Используется в Javascript очень просто:
    jsParseDate("Через 10 дней купить подарки Жене на Новый год в 12:30 напомнить"); 
    //Результат:  {"title":"+10 дн. [12:30] | Напомнить за 15 м","date":"2023-12-13T06:30:00.205Z","sms":" | Напомнить за 15 м"}
    


    Надеюсь, хоть кому то это пригодится. А ещё больше надеюсь, что найдётся энтузиаст, который напишет нормальный плагин, который будет уметь тоже самое и, в том числе, будет понимать английский язык. Я бы этим плагином с удовольствием бы воспользовался.

    Плагин найдёт широкое применение в планировщиках. Я, например, использую его для быстрого занесения дел в своей программе, в которую можно добавлять дела при помощи SMS на определённый номер телефона или записывая подобную строку в теме письма, которое я отправляю в свою программу. Менеджеры, которые используют одну из моих программ, планируют дела связанные с клиентами в корпоративной CRM. Распознавание происходит налету, поэтому оно быстрее, чем кликать в календарике.

    PS: Профи, ещё раз, простите за качество кода. И можете показать нам класс, «форкнув» проект в Fiddle и указав ссылку в примере.
    PS: В примере используется Angular.js и те, кто с ним не знаком, могут оценить, как легко на нём писать примеры.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 77
    • +3
      Классно получилось, спасибо! Форкнул, на досуге попробую сделать такое на PHP :)
      • +7
        Делал подобный парсер на php для своей домашнего xmpp-бота. Бот умел напоминать о событиях, делать записи в гугл-календарь и прочие приятные мелочи.
        Так вот для php мне показалось удобнее использовать готовую функцию strtotime.
        Чтобы обучить strtotime парсить даты на русском языке, просто переводил ключевые слова с русского на английский.
        Вот так:
        $keywords = array(
        	"понедельник" => "monday", 
        	"воскресенье" => "sunday", 
        	"послезавтра" => "+2 days",
        	"месяцев" => "months",
        	"вторник" => "tuesday", 
        ...
        	$result = strtotime(
        			str_replace(array_keys($keywords), 
        					array_values($keywords), 
        					self::strtolower($time) )
        					);
        
        

        Работает неплохо.
        • +4
          Раньше тоже так делал, но потом весь мой мозг переехал на node.js
          • 0
            хорошая мысль! как раз для этого и хотел сделать парсер)))
            • 0
              $result = strtotime(strtr(self::strtolower($time), $keywords));
              
            • 0
              Мой код на PHP в 448 строк, генерирующий числительное в любом падеже: 123 => «сто двадцать три». Не пинайте особо, коду больше лет, чем коммиту. Передаю этот код в общественное достояние. Может быть, кому-то пригодится.
            • +7
              «Проснуться 2 января» выдает 2 января 2013 года.
              Еще неплохо было бы, если бы распознавало что-то типа «7 вечера».
              • +2
                «Проснуться 2 января 2014» — можно так,
                а можно зайти в код и подправить определение года, зная текущую дату. Добавил себе в планы.
            • +1
              У вас баг в распозновании «двадцать» и т.д., а «двадцать пять», я так понял вообще не реализовано.
              Баг фиксится прсто — replace двадцати надо поставить раньше replace'а двух.
              • +1
                «двадцать пять» не реализовывал. А ваше замечание в примере исправил. Спасибо.
              • +4
                «в 21часов 30 минут» — о_О
                А если написать «Сегодня в 21час 30 минут», то получим «Распознано как: + сегодня [21 30 ми:00]»
                Ну, и сам код причиняет боль, да…
                • +3
                  «днюха в следующий вторник»
                  • +18
                    «Третий четверг каждого нечётного месяца — санитарный день»
                    • +4
                      Мы, кстати, спорили с женой, если сегодня понедельник, то что означает фраза: «следующий вторник»: завтрашний вторник, или через 8 дней?
                      • +11
                        Не могу не вспомнить людей, говорящих фразу «на той неделе». У меня она ассоциируется с прошлой неделей, а у энного количества людей старшего поколения по каким-то неведомым мне причинам со следующей.
                        • +8
                          Потому что «та» — противоположность «этой». «На той неделе» = «На другой неделе».
                          • 0
                            А «другая» — это какая неделя, прошлая или будущая?
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Проблема не в неопределенности — тут, вы правы, контекст решает. Проблема исключительно холиварного характера — вроде бы и правильно, но все равно коробит, когда будущую неделю называют «той».
                                • +1
                                  вроде бы и правильно, но все равно коробит, когда будущую неделю называют «той».
                                  И не одного вас коробит!
                                • +2
                                  — А у Васи день рождения уже прошел или будет только?
                                  — На той неделе.
                          • +3
                            Думаю, тут как с «остановите на следующей остановке» — чем ближе к остановке это сказано, тем менее вероятно что имеется ввиду ближайшая.
                            • 0
                              Специально раньше дожидался, когда подъедем к остановке, чтобы сказать «остановите на следующей» :)
                              • 0
                                Теперь можно водителя потестировать и определить на каком расстоянии какую «следующую» он понимает))
                            • +1
                              У меня мысль развивалась вот таким образом.

                              1. Какая разница, какой сегодня день? Следующий вторник — это ближайший.

                              2. Когда мы (или, в частности, я) подразумеваем действительно ближайший вторник, мы говорим «в этот вторник» или просто «во вторник». Значит, «в следующий вторник» всё-таки означает не ближайший вторник, а через один.

                              3. «Этот» и «следующий» возможно привязываются к календарной неделе. Например, сегодня четверг. Тогда: «в [этот] понедельник» = -3 дня, «в следующий понедельник» = +4 дня, «в [это] воскресенье» = +3 дня, «в следующее воскресенье» = +10 дней.

                              4. Я склоняюсь к третьему варианту, но нужно учитывать контекст. Если сегодня среда, и я говорю: «Напомнить в понедельник съесть пирожное», то, очевидно, понедельник текущей недели уже прошёл, значит речь всё-таки о другом понедельнике; в данном случае «в понедельник» = «в следующий понедельник».

                              Как-то так… :)
                            • +1
                              Именно так мы ломали стандартную хваталку даты в календарь в айфоне. Там если писать сообщение в imes, то он хватает дату и предлагает занести в календарь. Если дату не писать как «седьмая суббота от сегодня, в час следующий за часом, когда солне в зените».
                              • 0
                                пойти на выборы в последнюю субботу мая
                                • +2
                                  … последнего мая

                                  Извините, не удержался :)
                              • +1
                                самое и, в том числе, будет понимать английский язык.

                                Для английского такое обычно совершенно не нужно. Методы конвертации человекопонятной строки содержащей дату в дату есть во всех язык. Там ноги выросли от сишной getdate.

                                Конкретно в JS конструктор объекта Date поддерживает строки такого формата.
                                • 0
                                  «Мыть машину в следующий четверг» не понимает
                                  • 0
                                    image
                                    Юридически и буквально верно.
                                  • 0
                                    В коде распознается «одиНадцать», хотя по правилам русского языка должно писаться «одиННадцать». Правильное написание «одиннадцать» распознается как «+1 день», а не «+11 дней» (:
                                    • +4
                                      Исправил и в коде и в голове. Спасибо.
                                      • 0
                                        У меня до сих пор в голове, как учительница в начальных классах объясняла, хитрость как запомнить правописание таких чисел.
                                        Один на дцать, две на дцать, три на дцать, и так далее, единственное исключение четырнадцать.
                                        • 0
                                          А почему «один» в мужском роде, а «две» в женском? :)
                                    • +2
                                      Спасибо за код!

                                      Рацпредложение: и день, и месяц следует записывать двумя цифрами.
                                      Верно: 05.01.2013.
                                      Неверно: 5.1.2013.
                                      (См. ГОСТ ИСО 8601-2001.)
                                      • 0
                                        Там дата передаётся объектом Date(), можете конвертировать её в любой вид. А так не по ГОСТу в дату превращает встроенная функция, использованная в отображении: (new Date()).getLocaleString().
                                        • 0
                                          Вообще-то ISO 8601 предписывает YYYY-MM-DD (или то же самое, но без разделителей), если речь про даты, а никак не DD.MM.YYYY.
                                          • 0
                                            Да, вы правы насчет стандарта. Но такой порядок записи даты является стандартным для научно-технической документации. Цитата из Мильчина:
                                            Форма дат XX в. в справочных и особо компактных изданиях: 05.08.85 (форма написания в современных документах, кроме научно-технических).
                                            Другие формы: 02.03.1975...
                                            (См. «7.2.1. Даты из числа месяца, порядкового номера месяца и года», ред. 1998 г.)

                                            В любом случае, суть в том, что нули перед однозначными номерами дня/месяца в русском языке убирать не следует.
                                      • +1
                                        Может стоит добавить распознавание текста вида «полвторого», «половина двенадцатого», «полпятого вечера», «полседьмого утра»?
                                        • +3
                                          Порадовало:
                                          ДР жены. Напомнить за 15 минут.
                                          • 0
                                            Не, там:
                                            Напомнить за 15м

                                            что означает: за 15 месяцев.
                                            • +1
                                              Вот вам юзкесы:
                                              in: Напомнить про завтрак за 10 минут
                                              out: + завтра [10 ми] | Напомнить за 15 м

                                              in: напомнить про субботник в пятницу
                                              out: субб | Напомнить за 15 м

                                              in: напомнить про вчерашнее завтра
                                              out: + вчера | Напомнить за 15 м
                                          • 0
                                            Супер!
                                            Но вот такой пример, некорректно прошел

                                            «Сегодня посмотреть 2 часть Матрицы в 21 час 30 минут »: Распознано как: + сегодня [2 ч 21 30 ми:00]
                                            • +1
                                              Интересно! Попробую сделать подобный парсинг в своей библиотечке habrahabr.ru/post/204162/ когда буду в отпуске (в конце декабря). Тоже увлекался в свое время написанием ботов/парсеров текстов, очень интересная тема.
                                              Спасибо за код и идею!
                                              • 0
                                                Делал парсинг TimeSpan (не DateTime) на C# для ABCat. Задача — парсинг строки TimeSpan, написанной на русском языке в свободной форме, парсилось время звучания аудиокниги в топиках на рутрекере.
                                                Парсер съедает порядка 99% процентов всех вариантов написания, принятых на рутрекере, правда из-за большого количества записей тяжело оценить сколько из них понимаются правильно. На первый взгляд — большинство. Если кому нужно — спрашивайте или ищите в исходниках на codeplex, строка «public static TimeSpan ToTimeSpan(this string lenght)».
                                                • –1
                                                  «Дней через пять» велик и могуч русский язык)
                                                  • 0
                                                    Упс. Промазал веткой, показалось что это ответ к моему коменту — сбила с толку ваша прозрачная иконка пользователя.

                                                    Такого варианта там нет, ваша правда. Заточено на длительность, и на конкретный сайт. Но добавить поддержку такого варианта совсем несложно. К «примерно» и «около» добавить «через».

                                                  • +2
                                                    31 декабря
                                                    Распознано как: 31 дек
                                                    Дата: 31.12.2013 17:50:00

                                                    32 декабря
                                                    Распознано как: 32 дек
                                                    Дата: 1.12.2014 17:50:00
                                                    • 0
                                                      Первым делом ввёл не сработавший вариант, который я бы голосом озвучил той же Siri / Google Now: «таймер на полчаса». «Полчаса»/«Час» не работают, «один час» и «тридцать минут» — нормально.

                                                      Upd: «будильник завтра в 9 вечера» тоже работает некорректно :)
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                        • –2
                                                          Перезагрузить сервер в воскресение в 2 распознает как воскр [2:00] — мне кажется, что в повседневной жизни мы скорее оперирует дневными часами, нежели ночными, и следует распознавать такую фразу как 14:00
                                                          • 0
                                                            В повседневной жизни обычно зависит от контекста, как ситуационного («обычно я ложусь в 2, а встаю в 8» — ночь и утро по умолчанию), так и временного («перезагрузить сервер в 2» произнесенное вечером значит в 2 ночи, утром — в 2 дня).
                                                            • +5
                                                              Некоторых увольняют за перезагрузку сервера в 2 дня)
                                                              • +2
                                                                Что-то часто вас увольняют :)
                                                                • 0
                                                                  Может он засмотрелся в обед на девушек и случайно перезагрузил сервер :)
                                                            • +1
                                                              «завтра в 7 вечера»
                                                              Распознано как: + завтра [7:00]
                                                              Дата: 4 Декабрь 2013 г. 7:00:00
                                                              • +12
                                                                Код некрасив в мелочах, несколько примеров:
                                                                plus = "-";
                                                                


                                                                я не ожидаю получить минус в переменной, которая называется плюс. Почему бы не назвать ее modifier, например?

                                                                shablon
                                                                


                                                                Рвет шаблон.

                                                                        var matches2 = title.match(/\d{1,4}/g); //все двух-значные цифры
                                                                


                                                                У нас такое не проходит code review. Почему бы не назвать так, чтобы ее имя символизировало ее содержание? digits, numbers, etc.

                                                                • +7
                                                                  Не понял, за что комментарию поставили минусы. Вообще-то человек прав, над говнокодом нужно работать.
                                                                • 0
                                                                  Еще подкину для теста: «Через две недели в шесть тридцать вечера по Екатеринбургу» и «29 февраля»
                                                                  • –4
                                                                    «Без пятнадцати полтретьего через четверг напомни купить еды»
                                                                    Такое не парсит.
                                                                    • +6
                                                                      Жена тоже
                                                                      • –4
                                                                        Без пятнадцати полтретьего → 14:15
                                                                        Через четверг → в четверг через неделю

                                                                        Ну и «Семнадцать минут четвёртого» распознало как «четв».
                                                                        • 0
                                                                          Бог с ней, с женой, я сам распарсил это небезошибочно :).
                                                                          • 0
                                                                            Фичреквест же.
                                                                        • +1
                                                                          Да это просто праздник какой-то!
                                                                          Удобно встанет в Google Scripts для Google Tasks.
                                                                          • +1
                                                                            Проверить почту через тридцать минут — + 3 minute

                                                                            Для решения используйте
                                                                            .replace(/\s*?три[^дцать]/, " 3")

                                                                            вместо
                                                                            .replace(" три", " 3")

                                                                            Или заменяйте «тридцать», до «трех»
                                                                            • +1
                                                                              Потрясающе! Выложите на гитхаб!

                                                                              Не понимает «Вечером сделать ужин жене»
                                                                              • +1
                                                                                <зануда>Не распознает «прямо сейчас»</зануда>
                                                                                • 0
                                                                                  Делал что-то подобное для себя на C# в виде десктопной программы. С хоткеями, напоминалками и т.п. xminderapp.blogspot.ru/2010/01/blog-post.html
                                                                                  Но внутри у меня подход чуть другой: используется грамматика LL с предпросмотром вперед. Просто было интересно, можно ли что-то такое сделать с помощью грамматик и конкретно Coco/R. Парсер занял 90 строк кода на ATG 53605.selcdn.ru/dlxeon_public/habr/ATG/XMinder.ATG потом из него автоматом генерируется C#. Чего нет — так поддержки дат. Но вряд ли уже руки дойдут до этого.

                                                                                  • 0
                                                                                    «в субботу в 7 вечера»
                                                                                    Распознано как: субб [7:00]

                                                                                    Хорошо бы в этом случае распознавать как [19:00]
                                                                                    • +1
                                                                                      можно скукожить код

                                                                                      if (matches3[0] == «янв») var mymonth = 1;
                                                                                      if (matches3[0] == «фев») var mymonth = 2;
                                                                                      if (matches3[0] == «мар») var mymonth = 3;
                                                                                      if (matches3[0] == «апр») var mymonth = 4;
                                                                                      if (matches3[0] == «мая») var mymonth = 5;
                                                                                      if (matches3[0] == «май») var mymonth = 5;
                                                                                      if (matches3[0] == «июн») var mymonth = 6;
                                                                                      if (matches3[0] == «июл») var mymonth = 7;
                                                                                      if (matches3[0] == «авг») var mymonth = 8;
                                                                                      if (matches3[0] == «сен») var mymonth = 9;
                                                                                      if (matches3[0] == «окт») var mymonth = 10;
                                                                                      if (matches3[0] == «ноя») var mymonth = 11;
                                                                                      if (matches3[0] == «дек») var mymonth = 12;

                                                                                      в

                                                                                      var mymonth = 0, month_list = «янв|фев|мар|апр|ма[яй]|июн|июл|авг|сен|окт|ноя|дек».match(/[^|]+/g);
                                                                                      for (var i=1; i<month_list.length && mymonth === 0; ++i )
                                                                                      if ( new RegExp(month_list[i]).test( matches3[0] ) ) month_num = i+1;
                                                                                      • 0
                                                                                        Вот всегда приходится метаться между вариантом где сходу всё понятно, но больше букв и вариантом с красивым кодом и изящностью, но с не мгновенно считываемой логикой.
                                                                                        • 0
                                                                                          Неверное у Вас суждение. Можно получить преимущества обеих методов просто изолировав определение номера месяца в функцию:

                                                                                          get_month_number( s: String ) -> Number;

                                                                                          А там уже пиши как хочешь, хоть изящно, хоть «в лоб», хоть с сотней грязных хаков.
                                                                                      • 0
                                                                                        Кажется тут надо ввести какие то ограничения на разные варианты фраз и привести пример не верных фраз, которые дадут не корректный ответ.
                                                                                        Либо сделать некое обучения перед использованием.

                                                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.