Мы все ищем работу на профильных сайтах время от времени. Иногда на это уходит значительное время, т.к. мы выполняем множество рутинных действий, которых могли бы избежать. Попробуем автоматизировать этот процесс, так чтобы работа, по-возможности, искалась сама, без непосредственного участия соискателя.
Это средство автоматизации процесса поиска работы, выполненное в форме экспертной системы. Оно умеет обучаться при взаимодействия со своим пользователем. Обучение производится с помощью внесения пользователем правил обработки входящего потока вакансий.
Рассмотрим поиск работы как бизнес-процесс. В этом процессе несколько участников,
самый важный из которых я - Соискатель
:) Еще есть работодатель, но о его интересах
мы думаем меньше, потому что когда я ищу работу, единственное, что я хочу от
работодателя - чтобы он предложил мне Вакансию
. На эту вакансию я откликаюсь своим
Резюме
.
Вакансия
- наиболее часто используемый на первом этапе объект. Она содержит в себе
множество полей, таких как, например, “описание”, но важно определить, что с
Вакансией
можно делать, то есть ее, так называемое “поведение”. Оказывается, что
поведение зависит от “состояния” Вакансии
, например, нельзя еще раз откликнуться на
Вакансию
, работодатель которой пригласил меня на собеседование. Правда, можно
откликнуться другим резюме, и это внезапно говорит нам, что “поведением”, зависящим
от состояния обладает на самом деле пара Вакансия
+ Резюме
.
Как видим, на этом этапе, мы, похоже, уже не вписываемся в классическое
Объектно-Ориентированное Программирование: в ООП поведением обладает класс
, и
выражается оно набором методов класса. Если же мы начнем менять этот набор методов в
зависимости от поля “состояние” в объекте - нам придется либо вставлять проверки в
каждый метод, либо иметь набор классов-наследников и приводить объекты к нужному
классу динамически. И то и другое мне не очень нравится.
Ну а когда у нас поведением обладает пара объектов Вакансия
+ Резюме
проблема
объектной декомпозиции становится совершенно непонятной. Возможно, тот кто это
читает, знает как решить ее, в этом случае прошу написать мне об этом.
Ну а пока я не узнал, как это сделать в классическом ООП, я воспользуюсь для решения этой проблемы обобщенными функциями языка Лисп, где метод может принадлежать одновременно нескольким классам. Для понимания, как это работает я отсылаю всех заинтересовавшихся к главе 16 “Обобщенные функции” книги Practical Common Lisp.
Итак, пара “Вакансия+Резюме” может находится в нескольких состояниях
:
- неотсортирована - т.е. свежеполученная с сайта по поиску работы вакансия
- неинтересная - я даже не собираюсь ее рассматривать
- интересная - вакансия, которая мне интересна, но я не еще отправил на нее отзыв
- отправлен отзыв (мной)
- отзыв просмотрен (работодателем)
- работодатель отказал
- работодатель пригласил на интервью
- прохождение интервью (там возможны тестовые задания)
- получено предложение работы
Состояния образуют ориентированный граф, а множество пар Вакансия+Резюме
в
состояниях после “отзыв отправлен” - фронт работ, в котором понятно что нужно делать
по каждой паре. Ну а интерфейс должен обеспечивать доступ к выборкам пар и действиям
над ними и всякие дополнительные возможности - например, напоминания что по каким-то
вакансиям нет движения уже 3 дня.
Чтобы перевести пару из одного состояния в другое, было бы достаточно просто изменить
значение поля state
. Конечно, чтобы иметь возможность делать что-то, когда это поле
меняется, стоит сделать ему getter и setter, но еще более масштабируемый вариант -
это отправлять паре сообщение. Сообщения можно класть в очередь и обрабатывать
отложенно, и это облегчает разделение системы в соответствии с Service Oriented
Architecture (SOA).
Если по графу состояний понятно, какие возможны действия по каждой вакансии, то часть
этих действий можно автоматизировать - например, так называемый “преселект”, когда мы
выкидываем вакансии, которые соискателю заведомо неинтересны (например, можно
отбросить вакансии без указания зарплаты). Также можно анализировать текст вакансии,
и если стек технологий, оплата и месторасположение устраивает - автоматически
отправлять отзыв. Поскольку к отзыву прикрепляется Резюме
- напрашивается решение
автоматически формировать Резюме
, например, выкидывая из него те куски, в которых
идет речь о технологиях, которые не требуются в целевой вакансии.
Это не очень сложно - вакансии, как правило, пишутся довольно формальным языком, в них всегда есть разделы “Требования”, “Будет плюсом”, “Условия”. От Соискателя в этом случае требуется только узнать о том, что его пригласили на собеседование, ну и пройти его, конечно.
Сценарии использования Работодателя можно автоматизировать похожим образом, с той
лишь разницей, что работодатель осуществляет операции над Отзывом
Соискателя, к
которому прикреплено Резюме
и другие личные данные соискателя (например, портфолио
дизайнера). Этот Отзыв
может находится в таких состояниях:
- непросмотрен
- неинтересный
- интересный
- отправлено приглашение
- приглашение просмотрено
- отправлено тестовое задание
- получено решение тестового задания, но оно еще не проверено
- назначено собеседование
- сделано предложение (с такими-то условиями)
- предложение принято (вакансия закрыта)
- предложение не принято Соискателем
Исходя из всего этого, мы можем написать в первую очередь поддержку сценариев использования для соискателя, а потом и работодателя:
С точки зрения соискателя процесс найма выглядит так:
- Этап составления резюме
- Этап опубликования резюме
- Этап поиска
- Поиск и просмотр вакансий, отсев, ранжирование
- Рассылка откликов
- Этап телефонных переговоров
- Получение звонков, обсуждение деталей по телефону
- Договоренность о еще одном звонке
- Тестовое задание на почту
- Договоренность о skype-интервью
- Этап удаленного тестирования
- Skype-интервью
- Ожидание тестового задания
- Выполнение тестового задания
- Этап очного собеседования
- Приглашение на интервью
- Интервью
- Этап отбора предложений
- Получение предложений
- Выбор предложения
В простейшем случае, Соискатель просто размещает свое резюме. Однако продвинутые соискатели формируют резюме под конкретную вакансию и мы можем автоматизировать этот процесс несколькими способами.
Простейший пример персонализации резюме по отношению к вакансии - это указывать в резюме верхнюю границу той зарплатной вилки, которая указана в вакансии.
Мы можем предложить пользователю составить шаблон резюме и правила, по которым шаблон превращается в готовое резюме. Правила могут анализировать вакансию и выполнять заполнение шаблона в зависимости от того, что находится в требованиях к вакансии. В предельном случае у нас получается Domain Specific Language для написания резюме и анализа вакансий.
Мне нравится этот подход, и он хорошо сработает для меня, но я понимаю, что врядли другие Соискатели будут обрадованы, если им предложить выучить специализированный язык :)
Поэтому в качестве паралельного решения мы можем поиграть с методами искусственного интеллекта: разбить резюме по абзацам, представить каждый абзац как “мешок слов” и попытаться понять, о чем идет речь в каждом абзаце. И если, например, Соискатель знает PHP и JAVA, а вакансия указывает в требованиях только PHP, то можно автоматически выкинуть лишние, не относящиеся к PHP куски.
С точки зрения интерфейса, нужно дать возможность Соискателю прикреплять разные
резюме к вакансиям (т.е. создавать пары Резюме+Вакансия
) и назначать правила
отправки для конкретного резюме.
Таким образом, мы можем определить правила, по которым определяется, будет ли конкретное (возможно сгенерированное тем или иным способом) резюме отправлено работодателю.
Может сущестовать достаточно много сайтов по поиску работы и для наилучшего охвата имеет смысл публиковать резюме на всех. Но у каждого из таких сайтов могут быть свои правила, которые приходится учитывать и обрабатывать все возникающие ошибки.
Нам необязательно парсить все вакансии, которые находятся на сайтах поиска работы. На самом деле мы вполне можем попросить Соискателей указать, какого рода вакансии и в каких областях являются для них интересными. Следуя принципу “eat your own dog food” я ориентируюсь прежде всего на IT-сферу.
Найденные Вакансии полезно упорядочить по зарплате.
Внутри вакансий можно искать по критериям, которые пользователь может задавать сам, в форме правил, выполняющих действия над вакансией, если она совпадает с правилом.
Мне бы хотелось сразу получать представление, насколько свежая вакансия. Наиболее наглядно это делает интерактивный график.
Мне было бы интересно, сколько интервью было проведено и запланировано по вакансии - эту информацию можно узнать из анализа активности по ней других Соискателей. Технически можно даже просить оставить отзыв по Вакансии тех Соискателей, которые дошли до стадии “интервью”
Мне было бы интересно, как менялась вакансия с момента ее размещения компанией. К примеру можно находить и отслеживать похожие вакансии по расстоянию Левенштейна. Динамика изменения зарплатного предложения может многое сказать об отношении к вакансии.
Когда я читаю вакансию, я бы хотел, чтобы она переходила в статус “просмотрено” (и к ней добавлялась дата просмотра)
Читая вакансию, мне бы хотелось устанавливать ей приоритет и вносить заметки, чтобы отслеживать такие моменты, как например: необходимость позвонить позже, или все, что мне сказал hr-специалист по телефону.
Если я отправляю отзыв на вакансию или звоню по телефону - я бы хотел, чтобы эти действия сопровождались временем и изменением статуса, чтобы потом можно было отследить историю взаимодействия с HR.
При этом, мне хотелось бы видеть на дашборде те вакансии, с которыми я договорился о встрече и те, по которым нет движения долгое время, чтобы ничего не забывалось.
Я хочу получать напоминания о моем следующем шаге в отношении тех вакансий, которые мне интересны.
Мне бы хотелось видеть на каком я этапе в тех вакансиях, которые меня интересуют.
Отклик на вакансию - это как раз тот момент, когда вместе связываются Вакансия и Резюме. Часто к отклику можно приложить так называемое “сопроводительное письмо”, которое тоже можно составлять используя те же методы что и для составления Резюме, например, по шаблону или используя специальный язык. Правильный отзыв вероятно может довольно сильно влиять на вероятность приглашения на собеседование.
После отправки отклика звонит работодатель и приглашает на интервью. В этот момент я хочу найти эту вакансию, и в зависимости от того до чего мы договорились с работодателем выставить ей некоторое состояние или внести заметки, поставить тег, и.т.п.
В ряде случаев информация о собеседовании может прийти на email пользователя. Это позволяет вообще исключить человека из этого сценария - единственное что необходимо - уведомить о созданной встрече, добавив ее в календарь.
Здесь можно помечать о том прекрасном мире, где для того чтобы сменить работу вообще не нужно будет ходить по собеседованиям, а о переходе в другую компанию будут договариваться автоматические персональные ассистенты Соискателя и Работодателя :)
Обычно работодатель не слишком заморачивается тестовыми заданиями. Соискатель, выполнивший тестовое задание, может сохранить его, привязав к вакансии. За это ему можно начислять баллы или иным способом поощрять.
Таким образом с вакансиями можно связывать тестовые задания и их решения, что упрощает прохождение собеседований. За такую информацию пользователь может платить (балламы или иным способом)
После прохождения собеседования Соискатель мог бы оставлять отзыв. Другие пользователи могли бы оценивать качество отзыва
Пользователи могли бы использовать интерфейсы к методам многофакторного анализа (симплекс-метод, дерево принятия решений) чтобы определить лучшее предложение. Это гораздо более рациональный подход по сравнению с тем что применяется сейчас, и кроме того дает Работодателям значимую информацию о том, как улучшить свое предложение.
Вакансия может стать неактуальной если работодатель снимет ее, но работодатели могут забывать это сделать, поэтому можно предусмотреть тайм-аут.
Вакансия также может сниматься по достижению некоторого кол-ва голосов соискателей, которые дозвонились но им сказали, что вакансия уже неактуальна.
Можно сэкономить кучу времени и денег просто не нанимаясь в те компании, в которых “все плохо”. В этом плане соискатели могут помочь друг другу. Возможно и компании тоже будут прислушиваться к такому фидбеку.
Иногда я хочу спланировать маршрут поездки по собеседованиям. Это сервис с картами, которые можно сделать позже.
Полезно распечтывать карты, соответствующие вакансиям.
Вакансии на сайтах размещаются компаниями, которых я назову Работодателями
и
привязываются к ним. Мне, как Соискателю, интересно посмотреть какие вакансии
размещала ранее конкретная компания, какие она размещает теперь, как изменялись
зарплаты - и тому подобная аналитическая информация.
Я также хочу чтобы система проходила по вакансиям и в зависимости от сочетания условий выполняла какие-то действия:
- напоминание мне о собеседованиях, звонках (календарь)
- автоматическое ранжирование вакансий (по перспективам найма, зарплате и.т.п)
Система может анализировать компании с т.з. выставляемых вакансий и формирует профиль компании. По выставляемым вакансиям можно сделать интересные выводы - например, когда у компании внезапно появляются вакансии на одного сеньера и нескольких линейных разработчиков - это напоминает открытие нового отдела/проекта.
Система может классифицировать сохраненные вакансии по формальным признакам, таким как:
- новые вакансии
- измененные
- закрытые (о закрытости вакансии можно судить по ряду критериев)
- особенно интересные
- необычные
В случае изменений или появления новых интересующих пользователя вакансий можно пользователю отправляеть уведомление (через систему очередей сообщений и по email).
Исходя из анализа описания вакансии можно определить требуемую технологию и требуемую степень владения ею.
Еще можно сделать:
Предоставление рекомендаций и отбор вакансий на основе модифицируемых правил и фактах предметной области, таких как “работодатель - компания по разработке ПО” или “ИТ-поддержка не является приоритетом компании”
Предсказание поведения (путей достижения целей) компании (в процессе найма и вне его) на основе моделей и целей.
Выбор вариантов поведения в ответ на предьявляемые требования (цикл распознавание-действие в продукционной системе). Для автоматического построния резюме под вакансию из шаблонов.
Построение концептуальных моделей и преобразования в них - выбор стратегии действий и постановка целей.
Выбор способа представления знаний (правила, фреймы, концептуальные графы)
Выбор стратегии поиска.
Включение терма из набора технологий в заголовке вакансии - присвоение классификатора (тега), который будет виден еще до открытия вакансии.
Правила вывода - сопоставление с профилем Соискателя
Вычисление различий (дифф) требований вакансии и профильных навыков резюме - для подбора или построения оптимального резюме
Интерактивное построение профиля (ответы на вопросы). Необходим видимый прогресс и предварительная классификация предложений
Построение новых правил на основе известных в автоматическом режиме - “домысливание правил”.
Когда вакансия переносится в архив - мы должны отслеживать это на стороннем сайте и
реагировать, устанавливая статус archive
.
Когда мы собираем вакансии, распарсивая их с других сайтов, мы должны отслеживать их состояние на этих сайтах.
Когда Рекрутер ищет вакансии, он пользуется несколькими путями:
- Личные знакомства
- Рекомендации
- Социальные сети
- vkontakte
- Помощь коллег
- Специализированные сайты
Как правило, Рекрутер менее компетентен в предметной области, чем нанимаемый сотрудник, поэтому для него имеет большой вес мнение рекомендателей и коллег соискателя. Вероятно, рекомендательный сервис был бы очень актуален.
Компании-работодатели выбирают одну из моделей найма, в соответствии со своим бюджетом и задачами:
- Всегда (на любую позицию) нанимать (переманивать) лучших
- Нанимать начинающих в подчинение лучшим
- Нанимать начинающих (конвеерная разработка, большая текучка)
- Нанимать тех, кто понравится лидеру отдела
- Нанимать тех, кто лучше соответствует корпоративной культуре
Для каждой из этих моделей характерны свои необходимые сервисы. К примеру, для
модели “нанимать лучших” совершенно необходимо вести и актуализировать базу этих
“лучших”, чтобы вовремя сделать предложение кандидату. О примерах внедрения таких
сервисов мне ничего не известно. Также интересно уточнить у Рекрутеров из
разных
компаний их методы работы.
Для Рекрутера процесс найма выглядит (в общих чертах) так.
- Этап составления вакансий
- Этап опубликования вакансий
- Этап поиска резюме
- По ключевым словам
- По фильтру
- Используя автоподбор
- Этап анализа откликов (неразобранные, подумать, приглашенные, отклоненные)
- Телефонный звонок соискателю (с целью уточнить детали или пригласить)
- Возможно отправка тестового задания
- Получение тестового задания
- Проверка тестового задания
- Скайп-интервью
- Этап собеседования
- Опционально: заполнение анкеты
- Собеседование с Рекрутером (об условиях)
- Тесты (например: на знание языка, ООП, БД, многопоточность)
- Тестовое задание
- Проверка тестового задания
- Собеседование с тех. спецом, (как правило нач. отдела)
Рекрутер анализирует обратную связь о составляемых им вакансиях - у него есть статистическая информация о кол-ве просмотров вакансий и количестве поступивших откликов. Из этих данных можно, например, сделать вывод, что предложенная зарплата неактуальна на рынке.
Также Рекрутер заинтересован в технической поддержке при решении задач типа:
- Мониторинг резюме (сообщения о обновлении резюме, просмотр старой версии)
- Ведение базы кандидатов (часто в экселе)
Рекрутер заинтересован в том, чтобы иметь возможность построить процесс найма под себя.
Составлять вакансию можно многими разными способами, но мы стремимся максимально упростить эту задачу для Рекрутера. Поэтому, разумно было бы сделать мастер, который проведет Рекрутера по примерно таким шагам:
- Название вакансии
- Информация о компании
- Обязанности будущего сотрудника
- Требования к будущему сотруднику
- Условия работы
- Дополнительная информация
На hh.ru предлагается заполнить такие поля:
- Тип вакансии
- Открытая - доступна 1 публикация в любом регионе
- Закрытая
- Название вакансии
- Загрузить шаблон вакансии
- Указать код вакансии
- Специализации
- Указать профобласти
- Вакансия в городе
- Адрес офиса
- Добавить новый адрес
- Показывать кандидатам только станцию метро
- Предполагаемый уровень месячного дохода
- От
- До
- Описание (не менее 200 символов)
- Обязанности
- Требования
- Условия
- Ключевые навыки
- Опыт работы. Здесь у hh все сделано
довольно по идиотски, т.к. выбор производится радиокнопкой, и я
не могу, например, выбрать от 3 лет до бесконечности.
- Нет опыта
- От 1 года до 3 лет
- От 3 до 6 лет
- Более 6 лет
- График работы
- Полный день
- Сменный график
- Гибкий график
- Удаленная работа
- Вахтовый метод
- Тип занятости
- Полная занятость
- Частичная занятость
- Проектная.Временная работа
- Волонтерство
- Стажировка
- Прикрепить вопросы к вакансии
Пока вы не создали ни одного теста
Вопросы и тесты – это инструмент для быстрого отбора резюме, а также
оценки навыков и знаний кандидатов. Создайте собственный вопросник
или воспользуйтесь готовыми из библиотеки
- Создание списка вопросов
- Название
- Описание
- Подробное описание - видно только HR-ам
- Вопросы
- Ответ правильный, если выбраны
- Все правильные ответы
- Хотя бы один правильный ответ
- Свой вариант ответа
- Возможен
- Невозможен
- Ответ правильный, если выбраны
- Создание списка вопросов
- Настройки публикации
- Менеджер вакансии
- Уведомлять об откликах и сообщениях на почту этого менеджера
- Разрешить отклик для соискателей без резюме Мы знаем, что не у всех нужных вам специалистов есть резюме на hh.ru. Но терять этих соискателей не хочется. Поэтому мы нашли решение. Для того, чтобы получать отклики от соискателей без резюме достаточно выбрать эту опцию при публикации вакансии.... Все отклики без резюме будут доступны вам в специальной папке на странице откликов на вакансию.
- Вакансия доступна для соискателей с инвалидностью
- Требовать сопроводительное письмо
- Возможность переписки Как работает переписка с соискателем: Теперь на нашем сайте приглашенные соискатели могут написать вам сообщение, а вы сможете ответить через специальную форму.
- Сохранить вакансию как шаблон
- Менеджер вакансии
Необходимо дать возможность Рекрутеру сохранять и дополнять шаблоны вакансий. Нужно также отслеживать время публикации, истечение оплаченных сроков размещения и тому подобную бизнес-информацию.
Рекрутер должен понимать, какие вакансии в данный момент опубликованы, лежат в архиве, кроме того можно снабдить вакансии тегами, чтобы их было удобнее группировать и выполнять действия над группами вакансий - это может пригодится крупным рекрутинговым агенствам.
К каждой вакансии можно привязать менеджера, который за нее отвечает - это шаг к интеграции с CRM-системой, через API. Соответственно мы можем, например, отслеживать Рекрутеров, у которых нет активных вакансий.
Вакансии можно фильтровать по ключевым словам, регионам и тому подобному.
На странице вакансии есть все то то видит и Соискатель, плюс:
- ссылка на всех, кто откликнулся
- кнопка “разместить повторно”
- Кнопка “удалить вакансию”
- Резюме обновлено - дата, время
- Комментарии
- Добавить комментарий
- Оценка
- Пригласить на встречу
- Отклики
- Не получил ответа
Важный момент - для вакансий, срок публикации которых истек, должна быть явная и заметная возможность опубликовать вакансию снова.
Еще одна недоработка hh - неудобно искать отклики для тех вакансий, срок которых продлевается. А я бы хотел видеть историю по каждому откликнувшемуся.
Надо уметь удалять сниппеты и добавлять их в избранное
Есть множество конкурирующих сайтов для поиска работы, информацию с которых можно аггрегировать.
При поиске работы основной сценарий использования - поиск вакансий
, и практически все
сайты его предоставляют. Однако мне бы хотелось дополнительно иметь дополнительный
функционал:
- заметки по каждой вакансии
- статусы или теги, такие как:
просмотрено
(с датой),отобрано
,не-берут-трубку
,не-актуально
,приглашен-на-интервью
,выслали-тестовое-задание
,отправил-тестовое-задание
,получен-оффер
,вакансия-закрыта
итп.
Работодатель хочет подтверждения навыков соискателя - для этого и тестовые задания. Надо автоматизировать этот момент - если соискатель заявляет, к примеру, знания С++ - он может сделать некоторое тестовое задание один раз и это будет достаточным подтверждением квалификации для многих работодателей
Я бы хотел ранжировать вакансии вручную (по выставленным приоритетам) и автоматически (т.е. скриптом), например, в зависимости от зарплаты или удаленности.
Я бы хотел иметь возможность планировать маршрут, когда еду на собеседование и иметь календарь, чтобы не пропустить встречу.
Я бы хотел иметь версии вакансий, чтобы отслеживать их изменения, например, изменения зарплаты до и после моего интервью - это позволит анализировать рынок и получать больше информации.
Мне также интересно составлять профили компаний и отслеживать как меняется набор сотрудников которых они ищут - это поможет планировать долгосрочную стратегию. Особенно в этом плане интересны лидеры рынка - Яндекс, Гугл и.т.п.
Я бы хотел иметь возможность пообщаться с теми кто работал или работает в интересующей меня компании, иметь подмножество функционала социальных сетей или интеграцию с ними.
Иногда мне приятно работать с уже знакомыми людьми, так что в целом я бы не
отказался создавать на таком сайте что-то типа т.н. рабочих коллективов
, чтобы
наниматься сразу командой. Возможно работодателям такой вариант найма тоже будет
интересен.
В ряде случаев компании меняют свои вакансии, некоторые делают это методом удаления предыдущей и создания новой. Мне как соискателю хотелось бы не обнаруживать уже просмотренную и возможно собеседованную вакансию в новых. Поэтому хотелось бы предусмотреть механизм, который связывает очень похожие вакансии друг с другом.
Иногда вакансии меняются, или в них меняются существенные условия. Например, две недели назад, когда я смотрел вакансию из предыдущей сборки меня не устроила зарплата, а сегодня вакансия стала интереснее. Я хочу отслеживать что вакансия поменялась.
Таким образом при создании вакансии мы должны проверять, может она уже есть в базе и тогда указывать, что эта вакансия включена в несколько сборок.
Несколько вакансий могут быть от одной компании. В этом случае мне бы хотелось отслеживать это в профиле компании, кроме того интересна аналитика по этой компании за определенный период времени.
С социальной точки зрения интересно получать отзывы о компании от ее работников, в том числе и уволенных.
Действия по вакансии: звонки, скайп-интервью, собеседования хорошо бы отслеживать. Это информация вероятно будет интересна и работодателю особенно в плане анализа эффективности работы HR-отдела. В эту таблицу заносим что сделано по каждой вакансии, которая находится в разработке.
Теги вакансий помогают ориентироваться, когда вакансий много.
Важно: Для обеспечения социальных взаимодействий нужно предусмотреть, чтобы вакансию можно было “передать”, т.е. у нее минимум должен быть URI.
Если пользователь просмотрел вакансию, но пока не хочет отправлять отзыв - он может
добавить вакансию в закладки - в этом случае ее статус меняется на favorited
Из favorited
мы снова можем отправить отзыв.
Из favorited
пользователь может вернуть вакансию обратно в interesting
или hidden
.
Из hidden
пользователь может вернуть вакансию в interesting
.
Если по вакансии позвонили, пользователю обычно нужно ее быстро найти. Нужна форма
поиска по вакансиям в статусе responded
- пользователь ищет обычно по названию
фирмы.
После звонка вакансия может быть выкинута или переведена из responded
в статус
“был телефонный звонок” - called
. Выкидывая вакансию пользователь может выбрать
reason - для них можно будет потом сделать отдельную таблицу но пока просто пишем в
поле вакансии. Если в результате телефонного звонка была достигнута договоренность о
собеседовании - пользователь переводит вакансию в состояние “пригласили на
интервью” - wait-interview
и заносит в вакансию данные о том, куда и во сколько
ехать. Если по телефону рекрутер предложил тестовое задание - статус - “ожидание
тестового задания” - wait-test
. Если договорились о интервью по скайпу - “ожидание
скайп-интервью” - wait-skype-interview
.
Получив тестовое задание пользователь переводит вакансию из статуса wait-test
в
“выполнение тестового задания” run-test
, а оттуда либо в test-cancel
либо в
test-sended
. Либо выкидывает.
Пользователи иногда забивают на интервью (случаются накладки) - в этом случае рекрутер часто передоговаривается на другое время. Делать петли в графе значит излишне усложнять его, наверно пусть можно будет просто изменить данные о времени интервью.
После интервью или скайп-интервью от вакансии можно либо отказаться (refuse-employer
,
refuse-applicant
) либо перевести в статус “ожидание результата” - wait-result
. Нужно
включать таймер, по истечении которого напоминать пользователю позвонить рекрутеру и
узнать, как дела.
Иногда после скайп-интервью назначают очное интервью. Также бывает прямо на интервью
предлагают оффер - offer
и соискатель берет время на подумать.
Из “ожидания результата” можно перескочить в “предложен оффер”, “отказ работодателя” -
refuse-employer
или “отказ соискателя” - refuse-аpplicant
.
История статусов нужна, в нее нужно заносить время когда изменяется статус и возможно примечания по изменению. Будет красиво, если в интерфейсе будет отображаться полный граф статусов и текущее положение вакансии в нем.
- Было бы неплохо делать diff-ы между сборами вакансий и отзывов
- Нужен анализ ошибок hh при send-respond (например: вакансия в архиве)
- Нужна привязка к роботу, который по таймеру вынимает данные из hh
- Нужна привязка работы к юзеру
- Поправить неправильное определение emp-name при анализе тизеров
- Подключать несколько hh-аккаунтов к одному профилю пользователя
- Создание и удаление правил
- Предлагать правки статей через гитхаб
Попробуем визуализировать схему БД через graphviz, опираясь на http://graphviz.org/content/datastruct
Простейший пример - реляционные отношения ученика и учителя (здесь мы считаем, что на каждого учителя приходится некоторое кол-во учеников и у каждого ученика есть только один учитель)
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | (primary) | идентификатор | |
name | (or db-null varchar) | |||
teacher-id | integer | ((one-to-many (teacher id))) | связь с преподом |
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | (primary) | идентификатор | |
name | (or db-null varchar) |
Таким образом мы пытаемся построить узел для учителя так, чтобы иметь возможность ссылаться на поля записи (требует загрузки generators.el)
(princ
(concat " \"" (underscore tbl) "\" [\n label = \" [" (underscore tbl) "] |"
(mapconcat #'(lambda (x)
(format " <%s_%s> %s " (underscore tbl) (underscore x) x))
(remove-if-not #'(lambda (x)
(string-match "id" x))
(mapcar #'car flds))
"|")
"\"\n shape = \"record\"\n ];\n"))
Добавляем к этом связь (пока вручную):
"student":student_teacher_id -> "teacher": teacher_id [ id = 1000 ];
И собираем, чтобы отрисовать граф:
file:img/test_graph2.png]]
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | (primary) | идентификатор | |
src-id | integer | идентификатор вакансии в источнике | ||
archive | boolean | признак, что вакансия в архиве | ||
name | varchar | название вакансии | ||
currency | (or db-null varchar) | валюта зарплаты | ||
base-salary | (or db-null integer) | размер компенсации в тизере | ||
salary | (or db-null integer) | размер компенсации | ||
salary-text | (or db-null varchar) | размер компенсации | ||
salary-max | (or db-null integer) | максимальный уровень зарплаты | ||
salary-min | (or db-null integer) | минимальный уровень зарплаты | ||
emp-id | (or db-null integer) | идентификатор работодателя на удаленном ресурсе | ||
emp-name | varchar | имя работодателя на удаленном ресурсе | ||
city | varchar | город | ||
metro | varchar | метро | ||
experience | varchar | требуемый опыт работы | ||
date | varchar | дата опубликования в источнике | ||
respond | varchar | ссылка на отклик | ||
descr | varchar | описание вакансии | ||
notes | (or db-null varchar) | заметки по вакансии | ||
tags | (or db-null varchar) | тeги вакансии | ||
response | (or db-null varchar) | текст отклика на вакансию | ||
emptype | (or db-null varchar) | типа работы (полная занятость / частичная) | ||
workhours | (or db-null varchar) | рабочие часы (полный день / удаленная работа) | ||
skills | (or db-null varchar) | список навыков | ||
datetime | (or db-null varchar) | datetime | ||
date-text | (or db-null varchar) | текстовое представление datetime | ||
responsibility | (or db-null varchar) | обязанности из тизера | ||
requirement | (or db-null varchar) | требования из тизера | ||
addr | (or db-null varchar) | адрес по карте | ||
street-addr | (or db-null varchar) | почтовый адрес |
После загрузки, вакансия получает статус unsort
После сортировки пользователем ваканисия может принять один из статусов: unsort
,
interesting
или uninteresting
Пользователь, работая с этими интересными вакансиями, отслеживает их состояния, выполняя
действия, переводящие вакансию из одного состояния в другое: когда пользователь
отправляет отзыв - вакансия становится responded
.
action | from | to |
---|---|---|
uns-uni | unsort | uninteresting |
uns-int | unsort | interesting |
uns-res | unsort | responded |
uns-bee | unsort | beenviewed |
uns-rej | unsort | reject |
uns-inv | unsort | invite |
uni-int | uninteresting | interesting |
uni-res | uninteresting | responded |
uni-uni | uninteresting | uninteresting |
int-uni | interesting | uninteresting |
int-res | interesting | responded |
int-int | interesting | interesting |
res-bee | responded | beenviewed |
res-uni | responded | uninteresting |
res-rej | responded | reject |
res-inv | responded | invite |
res-res | responded | responded |
bee-uni | beenviewed | uninteresting |
bee-rej | beenviewed | reject |
bee-inv | beenviewed | invite |
bee-tes | beenviewed | testjob |
bee-bee | beenviewed | beenviewed |
tes-inv | testjob | invite |
tes-int | testjob | interview |
tes-uni | testjob | uninteresting |
tes-off | testjob | offer |
tes-tes | testjob | testjob |
rej-res | reject | responded |
rej-uni | reject | uninteresting |
rej-rej | reject | reject |
inv-inv | invite | invite |
inv-uni | invite | uninteresting |
inv-tes | invite | testjob |
inv-int | invite | interview |
int-uni | interview | uninteresting |
int-dis | interview | discard |
int-tes | interview | testjob |
int-int | interview | interview |
dis-uni | discard | uninteresting |
dis-dis | discard | discard |
int-off | interview | offer |
off-uni | offer | uninteresting |
off-off | offer | offer |
off-onj | offer | accept |
acc-acc | accept | accept |
Теперь мы можем полностью описать поведение вакансии как конечный автомат:
(mapcar #'(lambda (x)
(princ (format "%s -> %s [label =\"%s\"];\n"
(second x) (third x) (first x))))
table)
file:img/vacancy-state.png]]
(in-package #:moto)
(defun uns-uni ()
"unsort | uninteresting |")
(defun uns-int ()
"unsort | interesting |")
(defun uns-res ()
"unsort | responded |")
(defun uns-bee ()
"unsort | beenviewed |")
(defun uns-rej ()
"unsort | reject |")
(defun uns-inv ()
"unsort | invite |")
(defun uni-int ()
"uninteresting | interesting |")
(defun uni-res ()
"uninteresting | responded |")
(defun uni-uni ()
"uninteresting | uninteresting |")
(defun int-uni ()
"interesting | uninteresting |")
(defun int-res ()
"interesting | responded |")
(defun int-int ()
"interesting | interesting |")
(defun res-bee ()
"responded | beenviewed |")
(defun res-uni ()
"responded | uninteresting |")
(defun res-rej ()
"responded | reject |")
(defun res-inv ()
"responded | invite |")
(defun res-res ()
"responded | responded |")
(defun bee-uni ()
"beenviewed | uninteresting |")
(defun bee-rej ()
"beenviewed | reject |")
(defun bee-inv ()
"beenviewed | invite |")
(defun bee-tes ()
"beenviewed | testjob |")
(defun bee-bee ()
"beenviewed | beenviewed |")
(defun tes-inv ()
"testjob | invite |")
(defun tes-int ()
"testjob | interview |")
(defun tes-uni ()
"testjob | uninteresting |")
(defun tes-off ()
"testjob | offer |")
(defun tes-tes ()
"testjob | testjob |")
(defun rej-res ()
"reject | responded |")
(defun rej-uni ()
"reject | uninteresting |")
(defun rej-rej ()
"reject | reject |")
(defun inv-inv ()
"invite | invite |")
(defun inv-uni ()
"invite | uninteresting |")
(defun inv-tes ()
"invite | testjob |")
(defun inv-int ()
"invite | interview |")
(defun int-uni ()
"interview | uninteresting |")
(defun int-dis ()
"interview | discard |")
(defun int-tes ()
"interview | testjob |")
(defun int-int ()
"interview | interview |")
(defun dis-uni ()
"discard | uninteresting |")
(defun dis-dis ()
"discard | discard |")
(defun int-off ()
"interview | offer |")
(defun off-uni ()
"offer | uninteresting |")
(defun off-off ()
"offer | offer |")
(defun off-onj ()
"offer | accept |")
(defun acc-acc ()
"accept | accept |")
Иногда у одного соискателя может быть несколько резюме. Опишем структуру данных резюме:
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
hh-id | (or db-null varchar) | идентификатор резюме на hh.ru | ||
title | (or db-null varchar) | заголовок резюме | ||
last-name | (or db-null varchar) | фамилия | ||
first-name | (or db-null varchar) | имя | ||
middle-name | (or db-null varchar) | отчество | ||
birthday | (or db-null varchar) | Дата рождения (по умолчанию: “”) | ||
gender | (or db-null varchar) | пол (по умолчанию: “male”) | ||
area | (or db-null varchar) | город проживания (спб: “2”, москва: “1”) | ||
metro | (or db-null varchar) | метро | ||
relocation | (or db-null varchar) | отношение к переезду (“no_relocation”“relocation_possible”“relocation_desirable”) | ||
relocation-area | (or db-null varchar) | куда переезжать (отправляется несколько полей с одним именем но разными значениями) | ||
business-trip-readiness | (or db-null varchar) | командировки (“never”“ready”“sometimes”) | ||
citizen-ship | (or db-null varchar) | гражданство (Россия: 113) | ||
work-ticket | (or db-null varchar) | разрешение на работу (Россия: 113) | ||
travel-time | (or db-null varchar) | время в пути (“any”“from_hour_to_one_and_half”“less_than_hour”) | ||
cell-phone-country | (or db-null varchar) | 7 | ||
cell-phone-city | (or db-null varchar) | 911 | ||
cell-phone-number | (or db-null varchar) | 2869290 | ||
cell-phone-comment | (or db-null varchar) | |||
home-phone-country | (or db-null varchar) | 7 | ||
home-phone-city | (or db-null varchar) | |||
home-phone-number | (or db-null varchar) | |||
home-phone-comment | (or db-null varchar) | |||
work-phone-country | (or db-null varchar) | 7 | ||
work-phone-city | (or db-null varchar) | |||
work-phone-number | (or db-null varchar) | |||
work-phone-comment | (or db-null varchar) | |||
email-string | (or db-null varchar) | avenger-f%40yandex-ru | ||
preferred-contact | (or db-null varchar) | email/cell-phone/home-phone/work-phone | ||
icq | (or db-null varchar) | icq | ||
skype | (or db-null varchar) | skype | ||
freelance | (or db-null varchar) | freelance | ||
moi_krug | (or db-null varchar) | moi_krug | ||
(or db-null varchar) | ||||
(or db-null varchar) | ||||
livejournal | (or db-null varchar) | livejournal | ||
personal-site | (or db-null varchar) | personal | ||
prof-area | (or db-null varchar) | 1 | ||
specializations | (or db-null varchar) | 82 221 | ||
salary-amount | (or db-null varchar) | 100000 | ||
salary-currency | (or db-null varchar) | RUR | ||
employment | (or db-null varchar) | full | ||
work-schedule | (or db-null varchar) | full_day | ||
education-level-string | (or db-null varchar) | higher | ||
educations | (or db-null varchar) | |||
primary-education-id | (or db-null varchar) | |||
primary-education-name | (or db-null varchar) | |||
primary-education-university-id | (or db-null varchar) | 39864 | ||
primary-education-faculty-id | (or db-null varchar) | |||
primary-education-organization | (or db-null varchar) | |||
primary-education-result | (or db-null varchar) | |||
primary-education-specialty-id | (or db-null varchar) | 224 | ||
primary-education-year | (or db-null varchar) | 2005 | ||
additional-education-id | (or db-null varchar) | |||
additional-education-name | (or db-null varchar) | |||
additional-education-organization | (or db-null varchar) | |||
additional-education-result | (or db-null varchar) | |||
additional-education-year | (or db-null varchar) | |||
certificate-id | (or db-null varchar) | |||
certificate-type | (or db-null varchar) | |||
certificate-selected | (or db-null varchar) | |||
certificate-ownerName | (or db-null varchar) | |||
certificate-transcription-id | (or db-null varchar) | |||
certificate-password | (or db-null varchar) | |||
certificate-title | (or db-null varchar) | |||
certificate-achievementDate | (or db-null varchar) | |||
certificate-url | (or db-null varchar) | |||
attestation-education-id | (or db-null varchar) | |||
attestation-education-name | (or db-null varchar) | |||
attestation-education-organization | (or db-null varchar) | |||
attestation-education-result | (or db-null varchar) | |||
attestation-education-year | (or db-null varchar) | |||
languages | (or db-null varchar) | Владение языками | ||
expiriences | (or db-null varchar) | Опыт работы | ||
skills | (or db-null varchar) | Ключевые навыки | ||
recommendations | (or db-null varchar) | Рекомендации | ||
portfolio | (or db-null varchar) | Портфолио |
Резюме может быть активным или неактивным:
action | from | to |
---|---|---|
rai | active | inactive |
ria | inactive | active |
Теперь мы можем полностью описать поведение резюме как конечный автомат:
(mapcar #'(lambda (x)
(princ (format "%s -> %s [label =\"%s\"];\n"
(second x) (third x) (first x))))
table)
file:img/resume-state.png]]
(in-package #:moto)
(defun rai ()
"active-inactive")
(defun ria ()
"inactive-active")
Создадим резюме, связав их с резюме на hh:
(in-package #:moto)
;; Тест, иллюстрирующий magic-методы
;; При попытке доступа к полю, которого не существует в классе в этот класс добавляется поле. Если доступ был на запись - записывается значение, иначе в поле будет nil.
;; Это поле - член класса, а не объекта. Технически ничто не мешает нам хранить его значение где-то отдельно от самого класса.
(defun direct-slot-defn->initarg (slot-defn)
(list :name (slot-definition-name slot-defn)
:readers (slot-definition-readers slot-defn)
:writers (slot-definition-writers slot-defn)
:initform (slot-definition-initform slot-defn)
:initargs (slot-definition-initargs slot-defn)
:initfunction (slot-definition-initfunction slot-defn)))
(defun add-slot-to-class (class name &key (initform nil) accessors readers writers initargs (initfunction (constantly nil)))
(check-type class symbol)
(let ((new-slots (list (list :name name
:readers (union accessors readers)
:writers (union writers
(mapcar #'(lambda (x)
(list 'setf x))
accessors)
:test #'equal)
:initform initform
:initargs initargs
:initfunction initfunction))))
(dolist (slot-defn (class-direct-slots (find-class class)))
(push (direct-slot-defn->initarg slot-defn) new-slots))
(ensure-class class :direct-slots new-slots)))
(defclass foo ()
((bar :accessor bar :initform "zzzzzz")
(baz :accessor baz :initform "zzzzzz")))
(defmethod slot-missing (class (instance foo) slot-name operation &optional (new-value "defailt value"))
(declare (ignorable class))
(print (list class instance slot-name operation new-value))
;; (err 'zz)
(add-slot-to-class (class-name class) slot-name)
(setf (slot-value instance slot-name) new-value))
(defparameter *foo* (make-instance 'foo))
(setf (slot-value *foo* 'bar) "the-bar")
(setf (slot-value *foo* 't2) "zzz")
(defparameter *foo2* (make-instance 'foo))
(slot-value *foo2* 't5)
(slot-value *foo2* 'bar)
;; Тестовое резюме
(defparameter *test-resume*
(make-resume
:last-name "Глухов"
:first-name "Михаил"
:middle-name "Михайлович"
:birthday "1982-12-15"
:gender "male"
:area "2"
:metro ""
:relocation "relocation_possible"
:relocation-area "1"
:business-trip-readiness "ready"
:citizen-ship "113"
:work-ticket "113"
:travel-time "any"
:cell-phone-country "7"
:cell-phone-city "911"
:cell-phone-number "2869290"
:cell-phone-comment "В любое время"
:home-phone-country "7"
:home-phone-city ""
:home-phone-number ""
:home-phone-comment ""
:work-phone-country "7"
:work-phone-city ""
:work-phone-number ""
:work-phone-comment ""
:email-string "[email protected]"
:preferred-contact "email"
:icq ""
:skype "i.am.rigidus"
:freelance ""
:moi_krug ""
:linkedin ""
:facebook ""
:livejournal "rigidus"
:personal-site "https://rigidus.ru"
:title "Programmer"
:specializations "221"
:prof-area "1"
:salary-amount "160000"
:salary-currency "RUR"
:employment "full"
:work-schedule "full_day"
:education-level-string "higher"
:educations (reduce #'(lambda (a b)
(format nil "~A ~A" a b))
(mapcar #'id
(list
(make-education :education-id "0"
:name "Санкт-Петербургский государственный университет культуры и искусств, Санкт-Петербург"
:university-id "39864"
:faculty-id "0"
:organization "Режиссуры"
:result "Режиссура мультимедиа программ"
:specialty-id "224"
:year "2005")
(make-education :education-id "0"
:name ""
:university-id "0"
:faculty-id "0"
:organization ""
:result ""
:specialty-id "0"
:year "0"))))
:additional-education-id ""
:additional-education-name ""
:additional-education-organization ""
:additional-education-result ""
:additional-education-year ""
:certificate-id ""
:certificate-type ""
:certificate-selected ""
:certificate-ownerName ""
:certificate-transcription-id ""
:certificate-password ""
:certificate-title ""
:certificate-achievementDate ""
:certificate-url ""
:attestation-education-id ""
:attestation-education-name ""
:attestation-education-organization ""
:attestation-education-result ""
:attestation-education-year ""
:languages (reduce #'(lambda (a b)
(format nil "~A ~A" a b))
(mapcar #'id
(list
(make-lang :lang-id "34" :lang-degree "native")
(make-lang :lang-id "57" :lang-degree "can_read")
(make-lang :lang-id "58" :lang-degree "basic")
(make-lang :lang-id "59" :lang-degree "none"))))
:expiriences (reduce #'(lambda (a b)
(format nil "~A ~A" a b))
(mapcar #'id
(list
(make-expirience
:name "Лаборатория Касперского"
:company-id "1057"
:company-area-id "1"
:area-id "1"
:url ""
:industry-id "0"
:industries "540"
:industries ""
:exp-id ""
:job-position "Программист"
:start-date "2000-01-01"
:end-date "2001-01-01"
:description "Работа за деньги")
(make-expirience
:name "Вымпелком"
:company-id "4934"
:company-area-id "1"
:area-id "1"
:url ""
:industry-id "0"
:industries "399"
:exp-id ""
:job-position "Программист"
:start-date "2001-01-01"
:end-date "2005-01-01"
:description "Работа за еду )"))))
:skills (reduce #'(lambda (a b)
(format nil "~A ~A" a b))
(mapcar #'id
(list
(make-skill :name "Разработка архитектуры")
(make-skill :name "Вакуумная чистка лица"))))
:skills-string "В последние годы нахожусь на пенсии )"
:recommendations (reduce #'(lambda (a b)
(format nil "~A ~A" a b))
(mapcar #'id
(list
(make-recommendation
:recommendation-id "0"
:name "Смирнов"
:job-position "Начальник"
:organization "Армия"
:contact-info "9112869290")
(make-recommendation
:recommendation-id "0"
:name "Иванов"
:job-position "Зампотех"
:organization "Армия"
:contact-info "9112878789"))))))
[TODO] - Написать процедуру сопоставления вузов, факультетов и их идентификаторов на hh
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
education-id | (or db-null varchar) | идентификатор обучалки на hh (как правило пустой) | ||
name | varchar | название учебного заведения | ||
university-id | (or db-null varchar) | идентификатор учебного заведения (если оно есть в базе hh) | ||
faculty-id | (or db-null varchar) | идентификатор факультета | ||
organization | varchar | факультет | ||
result | varchar | специальность | ||
specialty-id | (or db-null varchar) | идентификатор специальности | ||
year | (or db-null varchar) | год окончания |
[TODO] - Написать процедуру сопоставления языков и их идентификаторов на hh
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
name | (or db-null varchar) | язык | ||
lang-id | (or db-null varchar) | идентификатор на hh | ||
lang-degree | varchar | уровень владения |
[TODO] - Посмотреть что значат некоторые поля
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
name | varchar | название компании | ||
company-id | (or db-null varchar) | идентификатор компании на hh | ||
company-area-id | (or db-null varchar) | идентикатор города компании? | ||
url | varchar | сайт компании | ||
industry-id | (or db-null varchar) | предположительно отрасль компании (как правило пуста) | ||
industries | varchar | предположительно направления деятельности | ||
exp-id | varchar | не знаю что это такое | ||
job-position | varchar | должность | ||
start-date | varchar | начало работы | ||
end-date | varchar | окончание работы | ||
description | (or db-null varchar) | описание достижений |
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
name | varchar | название навыка |
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
recommendation-id | integer | идентификатор рекомендации на hh | ||
name | varchar | имя рекомендателя | ||
job-position | varchar | позиция рекомендателя | ||
organization | varchar | организация рекоммендателя | ||
contact-info | varchar | контактная информация |
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
descr | varchar | описание файла | ||
file | varchar | файл |
У нас есть два вида правил - для работы с тизерами и для обработки вакансий. Каждое правило закреплено за пользователем, который им владеет и имеет ранг, в соответствии с котором сортируется при применении набора правил.
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
name | varchar | имя | ||
user-id | integer | владелец правила | ||
rank | integer | приоритет правила | ||
ruletype | varchar | :teaser - правило для тизеров, :vacancy - для вакансий | ||
antecedent | varchar | условие срабатывания правила | ||
consequent | varchar | код правила | ||
notes | (or db-null varchar) | заметки к правилу |
Правило может быть активным и неактивным
action | from | to |
---|---|---|
rule-activation | active | inactive |
rule-deactivation | inactive | active |
(in-package #:moto)
(defun rule-activation ()
"| active | inactive |")
(defun rule-deactivation ()
"| inactive | active |")
Для того чтобы иметь возможность работать в нескольких одновременных сессиях
внутри одного потока выполнения мы осуществляем поддержку сессий следующим
образом: Все вызовы получения страниц (hh-get-page
) работают таким образом, как
будто считают себя по умолчанию залогиненными в сессию, для этого им передается
параметр cookie-jar. В случае, если по каким-то причинам это оказалось не так -
сессия восстанавливается вызовом recovery-login
, который получает
авторизационную информацияю из объекта src-account
, который также протягивается
через всю цепочку: factory
-> get-vacancy::closure
-> hh-get-page
->
recovery-login
. Возвращаемые из recovery-login
куки попадают в замыкание и в
дальнейшем используются для работы в сессии.
Таким образом можно внутри одного потока выполения иметь несколько замыканий, каждое из которых работает в своей сессии. Они могут выполнять разнообразные задачи - сбор вакансий, отзывыв, опубликование резюме и.т.п. Для источника вакансий это будет выглядеть как несколько пользователей, работающих с одного адреса.
field name | field type | default | meta | note |
---|---|---|---|---|
id | serial | идентификатор | ||
user_id | integer | идентификатор пользователя, владеющего логином | ||
src_source | varchar | идентификатор источника (“hh” - для headhunter.ru) | ||
src_login | varchar | логин пользователя на источнике | ||
src_password | varchar | пароль пользователя на источнике | ||
src_fio | varchar | ФИО пользователя, чтобы определить что вход в профиль успешен |
Аккаунты могут быть активные и неактивные. С неактивными аккаунтами никаких действий (сбор вакансий, проверка отзывов) не производится.
Если трижды не удалось залогиниться на аккаунте, он переводится в состояние
wrong
после чего требуется ручное устранение ошибки.
action | from | to |
---|---|---|
account-activation | active | inactive |
account-deactivation | inactive | active |
account-login | active | logged |
account-logout | logged | active |
account-wrong | active | wrong |
Мы можем получать вакансии с разных сайтов, поэтому самое первое, что следует определить - это источник вакансий, например, сайт https://hh.ru
Большинство сайтов источников устроено сходным образом - пользователь может
определить критерии поиска
и получить по ним набор вакансий. В зависимости от
интерфейса, это может быть разбитая на страницы выборка или страници с бесконечным
скроллом.
В любом случае, я бы хотел не столько получить весь контейнер с вакансиями,
соответствующими критериям поиска
, сколько иметь возможность извлечь следующую
вакансию, как только она нам понадобится. Поэтому необходимо иметь некторорую
функцию-генератор, которая при вызове возвратит следующую вакансию, или указание на
то, что вакансий больше нет.
Но так как у нас могут быть разные источники, то мы можем конструировать такие
функции-генераторы с помощью фабрики
, передавая этой фабрике параметры, в которых
указано, какой сайт-источник должен использоваться и критерии поиска
;; Тестируем hh
(defun hh-test ()
<<hh_test_contents>>
(dbg "passed: hh-test~%"))
(hh-test)
Соберем шаблоны:
// -*- mode: closure-template-html; fill-column: 140 -*-
{namespace hhtpl}
<<hhtpl_contents>>
Скомпилируем шаблоны при подготовке модуля
(in-package #:moto)
;; Скомпилируем шаблон
(closure-template:compile-template
:common-lisp-backend
(pathname
(concatenate 'string *base-path* "mod/hh/hh-tpl.htm")))
Соберем контроллеры и все функции, которые контроллеры вызывают
(in-package #:moto)
;; Это теперь в vacancy.lisp
;; special syntax for pattern-matching - ON
(named-readtables:in-readtable :fare-quasiquote)
<<tree_match>>
<<run>>
;; Это теперь в response.lisp
;; <<run_response>>
<<hh_fn_contents>>
<<hh_test>>
;; Pattern matching test
;; (dbg "match_1: ~A" (match 1 (1 2)))
;; (dbg "match_2: ~A" (match '(1 2 3 4) (`(1 ,x ,@y) (list x y))))
;; special syntax for pattern-matching - OFF
;; (named-readtables:in-readtable :standard)
[TODO] - Применение макросов отсюда: https://github.com/magnars/dash.el/blob/master/dash.el#L1186 наверное существенно упростит код.
;;; dash.el --- A modern list library for Emacs -*- lexical-binding: t -*-
;; Copyright (C) 2012-2015 Free Software Foundation, Inc.
;; Author: Magnar Sveen <[email protected]>
;; Version: 2.12.1
;; Keywords: lists
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; A modern list api for Emacs.
;;
;; See documentation on https://github.com/magnars/dash.el#functions
;;
;; **Please note** The lexical binding in this file is not utilised at the
;; moment. We will take full advantage of lexical binding in an upcoming 3.0
;; release of Dash. In the meantime, we've added the pragma to avoid a bug that
;; you can read more about in https://github.com/magnars/dash.el/issues/130.
;;
;;; Code:
(defgroup dash ()
"Customize group for dash.el"
:group 'lisp
:prefix "dash-")
(defun dash--enable-fontlock (symbol value)
(when value
(dash-enable-font-lock))
(set-default symbol value))
(defcustom dash-enable-fontlock nil
"If non-nil, enable fontification of dash functions, macros and
special values."
:type 'boolean
:set 'dash--enable-fontlock
:group 'dash)
;; !cons
;; !cdr
;; --each
;; -each
;; --map-when
;; -map-when
(put '-each 'lisp-indent-function 1)
(defmacro --each-while (list pred &rest body)
"Anaphoric form of `-each-while'."
(declare (debug (form form body))
(indent 2))
(let ((l (make-symbol "list"))
(c (make-symbol "continue")))
`(let ((,l ,list)
(,c t)
(it-index 0))
(while (and ,l ,c)
(let ((it (car ,l)))
(if (not ,pred) (setq ,c nil) ,@body))
(setq it-index (1+ it-index))
(!cdr ,l)))))
(defun -each-while (list pred fn)
"Call FN with every item in LIST while (PRED item) is non-nil.
Return nil, used for side-effects only."
(--each-while list (funcall pred it) (funcall fn it)))
(put '-each-while 'lisp-indent-function 2)
(defmacro --dotimes (num &rest body)
"Repeatedly executes BODY (presumably for side-effects) with `it` bound to integers from 0 through NUM-1."
(declare (debug (form body))
(indent 1))
(let ((n (make-symbol "num")))
`(let ((,n ,num)
(it 0))
(while (< it ,n)
,@body
(setq it (1+ it))))))
(defun -dotimes (num fn)
"Repeatedly calls FN (presumably for side-effects) passing in integers from 0 through NUM-1."
(--dotimes num (funcall fn it)))
(put '-dotimes 'lisp-indent-function 1)
(defun -map (fn list)
"Return a new list consisting of the result of applying FN to the items in LIST."
(mapcar fn list))
(defmacro --map (form list)
"Anaphoric form of `-map'."
(declare (debug (form form)))
`(mapcar (lambda (it) ,form) ,list))
(defmacro --reduce-from (form initial-value list)
"Anaphoric form of `-reduce-from'."
(declare (debug (form form form)))
`(let ((acc ,initial-value))
(--each ,list (setq acc ,form))
acc))
(defun -reduce-from (fn initial-value list)
"Return the result of applying FN to INITIAL-VALUE and the
first item in LIST, then applying FN to that result and the 2nd
item, etc. If LIST contains no items, return INITIAL-VALUE and
FN is not called.
In the anaphoric form `--reduce-from', the accumulated value is
exposed as `acc`.
See also: `-reduce', `-reduce-r'"
(--reduce-from (funcall fn acc it) initial-value list))
(defmacro --reduce (form list)
"Anaphoric form of `-reduce'."
(declare (debug (form form)))
(let ((lv (make-symbol "list-value")))
`(let ((,lv ,list))
(if ,lv
(--reduce-from ,form (car ,lv) (cdr ,lv))
(let (acc it) ,form)))))
(defun -reduce (fn list)
"Return the result of applying FN to the first 2 items in LIST,
then applying FN to that result and the 3rd item, etc. If LIST
contains no items, FN must accept no arguments as well, and
reduce return the result of calling FN with no arguments. If
LIST has only 1 item, it is returned and FN is not called.
In the anaphoric form `--reduce', the accumulated value is
exposed as `acc`.
See also: `-reduce-from', `-reduce-r'"
(if list
(-reduce-from fn (car list) (cdr list))
(funcall fn)))
(defun -reduce-r-from (fn initial-value list)
"Replace conses with FN, nil with INITIAL-VALUE and evaluate
the resulting expression. If LIST is empty, INITIAL-VALUE is
returned and FN is not called.
Note: this function works the same as `-reduce-from' but the
operation associates from right instead of from left.
See also: `-reduce-r', `-reduce'"
(if (not list) initial-value
(funcall fn (car list) (-reduce-r-from fn initial-value (cdr list)))))
(defmacro --reduce-r-from (form initial-value list)
"Anaphoric version of `-reduce-r-from'."
(declare (debug (form form form)))
`(-reduce-r-from (lambda (&optional it acc) ,form) ,initial-value ,list))
(defun -reduce-r (fn list)
"Replace conses with FN and evaluate the resulting expression.
The final nil is ignored. If LIST contains no items, FN must
accept no arguments as well, and reduce return the result of
calling FN with no arguments. If LIST has only 1 item, it is
returned and FN is not called.
The first argument of FN is the new item, the second is the
accumulated value.
Note: this function works the same as `-reduce' but the operation
associates from right instead of from left.
See also: `-reduce-r-from', `-reduce'"
(cond
((not list) (funcall fn))
((not (cdr list)) (car list))
(t (funcall fn (car list) (-reduce-r fn (cdr list))))))
(defmacro --reduce-r (form list)
"Anaphoric version of `-reduce-r'."
(declare (debug (form form)))
`(-reduce-r (lambda (&optional it acc) ,form) ,list))
(defmacro --filter (form list)
"Anaphoric form of `-filter'."
(declare (debug (form form)))
(let ((r (make-symbol "result")))
`(let (,r)
(--each ,list (when ,form (!cons it ,r)))
(nreverse ,r))))
(defun -filter (pred list)
"Return a new list of the items in LIST for which PRED returns a non-nil value.
Alias: `-select'"
(--filter (funcall pred it) list))
(defalias '-select '-filter)
(defalias '--select '--filter)
(defmacro --remove (form list)
"Anaphoric form of `-remove'."
(declare (debug (form form)))
`(--filter (not ,form) ,list))
(defun -remove (pred list)
"Return a new list of the items in LIST for which PRED returns nil.
Alias: `-reject'"
(--remove (funcall pred it) list))
(defalias '-reject '-remove)
(defalias '--reject '--remove)
(defun -remove-first (pred list)
"Return a new list with the first item matching PRED removed.
Alias: `-reject-first'
See also: `-remove', `-map-first'"
(let (front)
(while (and list (not (funcall pred (car list))))
(push (car list) front)
(!cdr list))
(if list
(-concat (nreverse front) (cdr list))
(nreverse front))))
(defmacro --remove-first (form list)
"Anaphoric form of `-remove-first'."
(declare (debug (form form)))
`(-remove-first (lambda (it) ,form) ,list))
(defalias '-reject-first '-remove-first)
(defalias '--reject-first '--remove-first)
(defun -remove-last (pred list)
"Return a new list with the last item matching PRED removed.
Alias: `-reject-last'
See also: `-remove', `-map-last'"
(nreverse (-remove-first pred (nreverse list))))
(defmacro --remove-last (form list)
"Anaphoric form of `-remove-last'."
(declare (debug (form form)))
`(-remove-last (lambda (it) ,form) ,list))
(defalias '-reject-last '-remove-last)
(defalias '--reject-last '--remove-last)
(defun -remove-item (item list)
"Remove all occurences of ITEM from LIST.
Comparison is done with `equal'."
(--remove (equal it item) list))
(defmacro --keep (form list)
"Anaphoric form of `-keep'."
(declare (debug (form form)))
(let ((r (make-symbol "result"))
(m (make-symbol "mapped")))
`(let (,r)
(--each ,list (let ((,m ,form)) (when ,m (!cons ,m ,r))))
(nreverse ,r))))
(defun -keep (fn list)
"Return a new list of the non-nil results of applying FN to the items in LIST.
If you want to select the original items satisfying a predicate use `-filter'."
(--keep (funcall fn it) list))
(defun -non-nil (list)
"Return all non-nil elements of LIST."
(-remove 'null list))
(defmacro --map-indexed (form list)
"Anaphoric form of `-map-indexed'."
(declare (debug (form form)))
(let ((r (make-symbol "result")))
`(let (,r)
(--each ,list
(!cons ,form ,r))
(nreverse ,r))))
(defun -map-indexed (fn list)
"Return a new list consisting of the result of (FN index item) for each item in LIST.
In the anaphoric form `--map-indexed', the index is exposed as `it-index`."
(--map-indexed (funcall fn it-index it) list))
(defmacro --map-when (pred rep list)
"Anaphoric form of `-map-when'."
(declare (debug (form form form)))
(let ((r (make-symbol "result")))
`(let (,r)
(--each ,list (!cons (if ,pred ,rep it) ,r))
(nreverse ,r))))
(defun -map-when (pred rep list)
"Return a new list where the elements in LIST that does not match the PRED function
are unchanged, and where the elements in LIST that do match the PRED function are mapped
through the REP function.
Alias: `-replace-where'
See also: `-update-at'"
(--map-when (funcall pred it) (funcall rep it) list))
(defalias '-replace-where '-map-when)
(defalias '--replace-where '--map-when)
(defun -map-first (pred rep list)
"Replace first item in LIST satisfying PRED with result of REP called on this item.
See also: `-map-when', `-replace-first'"
(let (front)
(while (and list (not (funcall pred (car list))))
(push (car list) front)
(!cdr list))
(if list
(-concat (nreverse front) (cons (funcall rep (car list)) (cdr list)))
(nreverse front))))
(defmacro --map-first (pred rep list)
"Anaphoric form of `-map-first'."
`(-map-first (lambda (it) ,pred) (lambda (it) (ignore it) ,rep) ,list))
(defun -map-last (pred rep list)
"Replace first item in LIST satisfying PRED with result of REP called on this item.
See also: `-map-when', `-replace-last'"
(nreverse (-map-first pred rep (nreverse list))))
(defmacro --map-last (pred rep list)
"Anaphoric form of `-map-last'."
`(-map-last (lambda (it) ,pred) (lambda (it) (ignore it) ,rep) ,list))
(defun -replace (old new list)
"Replace all OLD items in LIST with NEW.
Elements are compared using `equal'.
See also: `-replace-at'"
(--map-when (equal it old) new list))
(defun -replace-first (old new list)
"Replace the first occurence of OLD with NEW in LIST.
Elements are compared using `equal'.
See also: `-map-first'"
(--map-first (equal old it) new list))
(defun -replace-last (old new list)
"Replace the last occurence of OLD with NEW in LIST.
Elements are compared using `equal'.
See also: `-map-last'"
(--map-last (equal old it) new list))
(defmacro --mapcat (form list)
"Anaphoric form of `-mapcat'."
(declare (debug (form form)))
`(apply 'append (--map ,form ,list)))
(defun -mapcat (fn list)
"Return the concatenation of the result of mapping FN over LIST.
Thus function FN should return a list."
(--mapcat (funcall fn it) list))
(defun -flatten (l)
"Take a nested list L and return its contents as a single, flat list.
Note that because `nil' represents a list of zero elements (an
empty list), any mention of nil in L will disappear after
flattening. If you need to preserve nils, consider `-flatten-n'
or map them to some unique symbol and then map them back.
Conses of two atoms are considered \"terminals\", that is, they
aren't flattened further.
See also: `-flatten-n'"
(if (and (listp l) (listp (cdr l)))
(-mapcat '-flatten l)
(list l)))
(defmacro --iterate (form init n)
"Anaphoric version of `-iterate'."
(declare (debug (form form form)))
`(-iterate (lambda (it) ,form) ,init ,n))
(defun -flatten-n (num list)
"Flatten NUM levels of a nested LIST.
See also: `-flatten'"
(-last-item (--iterate (--mapcat (-list it) it) list (1+ num))))
(defun -concat (&rest lists)
"Return a new list with the concatenation of the elements in the supplied LISTS."
(apply 'append lists))
(defalias '-copy 'copy-sequence
"Create a shallow copy of LIST.")
(defun -splice (pred fun list)
"Splice lists generated by FUN in place of elements matching PRED in LIST.
FUN takes the element matching PRED as input.
This function can be used as replacement for `,@' in case you
need to splice several lists at marked positions (for example
with keywords).
See also: `-splice-list', `-insert-at'"
(let (r)
(--each list
(if (funcall pred it)
(let ((new (funcall fun it)))
(--each new (!cons it r)))
(!cons it r)))
(nreverse r)))
(defmacro --splice (pred form list)
"Anaphoric form of `-splice'."
`(-splice (lambda (it) ,pred) (lambda (it) ,form) ,list))
(defun -splice-list (pred new-list list)
"Splice NEW-LIST in place of elements matching PRED in LIST.
See also: `-splice', `-insert-at'"
(-splice pred (lambda (_) new-list) list))
(defmacro --splice-list (pred new-list list)
"Anaphoric form of `-splice-list'."
`(-splice-list (lambda (it) ,pred) ,new-list ,list))
(defun -cons* (&rest args)
"Make a new list from the elements of ARGS.
The last 2 members of ARGS are used as the final cons of the
result so if the final member of ARGS is not a list the result is
a dotted list."
(-reduce-r 'cons args))
(defun -snoc (list elem &rest elements)
"Append ELEM to the end of the list.
This is like `cons', but operates on the end of list.
If ELEMENTS is non nil, append these to the list as well."
(-concat list (list elem) elements))
(defmacro --first (form list)
"Anaphoric form of `-first'."
(declare (debug (form form)))
(let ((n (make-symbol "needle")))
`(let (,n)
(--each-while ,list (not ,n)
(when ,form (setq ,n it)))
,n)))
(defun -first (pred list)
"Return the first x in LIST where (PRED x) is non-nil, else nil.
To get the first item in the list no questions asked, use `car'.
Alias: `-find'"
(--first (funcall pred it) list))
(defalias '-find '-first)
(defalias '--find '--first)
(defmacro --some (form list)
"Anaphoric form of `-some'."
(declare (debug (form form)))
(let ((n (make-symbol "needle")))
`(let (,n)
(--each-while ,list (not ,n)
(setq ,n ,form))
,n)))
(defun -some (pred list)
"Return (PRED x) for the first LIST item where (PRED x) is non-nil, else nil.
Alias: `-any'"
(--some (funcall pred it) list))
(defalias '-any '-some)
(defalias '--any '--some)
(defmacro --last (form list)
"Anaphoric form of `-last'."
(declare (debug (form form)))
(let ((n (make-symbol "needle")))
`(let (,n)
(--each ,list
(when ,form (setq ,n it)))
,n)))
(defun -last (pred list)
"Return the last x in LIST where (PRED x) is non-nil, else nil."
(--last (funcall pred it) list))
(defalias '-first-item 'car
"Return the first item of LIST, or nil on an empty list.")
(defun -last-item (list)
"Return the last item of LIST, or nil on an empty list."
(car (last list)))
(defun -butlast (list)
"Return a list of all items in list except for the last."
(let (result)
(while (cdr list)
(!cons (car list) result)
(!cdr list))
(nreverse result)))
(defmacro --count (pred list)
"Anaphoric form of `-count'."
(declare (debug (form form)))
(let ((r (make-symbol "result")))
`(let ((,r 0))
(--each ,list (when ,pred (setq ,r (1+ ,r))))
,r)))
(defun -count (pred list)
"Counts the number of items in LIST where (PRED item) is non-nil."
(--count (funcall pred it) list))
(defun ---truthy? (val)
(not (null val)))
(defmacro --any? (form list)
"Anaphoric form of `-any?'."
(declare (debug (form form)))
`(---truthy? (--first ,form ,list)))
(defun -any? (pred list)
"Return t if (PRED x) is non-nil for any x in LIST, else nil.
Alias: `-any-p', `-some?', `-some-p'"
(--any? (funcall pred it) list))
(defalias '-some? '-any?)
(defalias '--some? '--any?)
(defalias '-any-p '-any?)
(defalias '--any-p '--any?)
(defalias '-some-p '-any?)
(defalias '--some-p '--any?)
(defmacro --all? (form list)
"Anaphoric form of `-all?'."
(declare (debug (form form)))
(let ((a (make-symbol "all")))
`(let ((,a t))
(--each-while ,list ,a (setq ,a ,form))
(---truthy? ,a))))
(defun -all? (pred list)
"Return t if (PRED x) is non-nil for all x in LIST, else nil.
Alias: `-all-p', `-every?', `-every-p'"
(--all? (funcall pred it) list))
(defalias '-every? '-all?)
(defalias '--every? '--all?)
(defalias '-all-p '-all?)
(defalias '--all-p '--all?)
(defalias '-every-p '-all?)
(defalias '--every-p '--all?)
(defmacro --none? (form list)
"Anaphoric form of `-none?'."
(declare (debug (form form)))
`(--all? (not ,form) ,list))
(defun -none? (pred list)
"Return t if (PRED x) is nil for all x in LIST, else nil.
Alias: `-none-p'"
(--none? (funcall pred it) list))
(defalias '-none-p '-none?)
(defalias '--none-p '--none?)
(defmacro --only-some? (form list)
"Anaphoric form of `-only-some?'."
(declare (debug (form form)))
(let ((y (make-symbol "yes"))
(n (make-symbol "no")))
`(let (,y ,n)
(--each-while ,list (not (and ,y ,n))
(if ,form (setq ,y t) (setq ,n t)))
(---truthy? (and ,y ,n)))))
(defun -only-some? (pred list)
"Return `t` if at least one item of LIST matches PRED and at least one item of LIST does not match PRED.
Return `nil` both if all items match the predicate or if none of the items match the predicate.
Alias: `-only-some-p'"
(--only-some? (funcall pred it) list))
(defalias '-only-some-p '-only-some?)
(defalias '--only-some-p '--only-some?)
(defun -slice (list from &optional to step)
"Return copy of LIST, starting from index FROM to index TO.
FROM or TO may be negative. These values are then interpreted
modulo the length of the list.
If STEP is a number, only each STEPth item in the resulting
section is returned. Defaults to 1."
(let ((length (length list))
(new-list nil))
;; to defaults to the end of the list
(setq to (or to length))
(setq step (or step 1))
;; handle negative indices
(when (< from 0)
(setq from (mod from length)))
(when (< to 0)
(setq to (mod to length)))
;; iterate through the list, keeping the elements we want
(--each-while list (< it-index to)
(when (and (>= it-index from)
(= (mod (- from it-index) step) 0))
(push it new-list)))
(nreverse new-list)))
(defun -take (n list)
"Return a new list of the first N items in LIST, or all items if there are fewer than N."
(let (result)
(--dotimes n
(when list
(!cons (car list) result)
(!cdr list)))
(nreverse result)))
(defalias '-drop 'nthcdr "Return the tail of LIST without the first N items.")
(defmacro --take-while (form list)
"Anaphoric form of `-take-while'."
(declare (debug (form form)))
(let ((r (make-symbol "result")))
`(let (,r)
(--each-while ,list ,form (!cons it ,r))
(nreverse ,r))))
(defun -take-while (pred list)
"Return a new list of successive items from LIST while (PRED item) returns a non-nil value."
(--take-while (funcall pred it) list))
(defmacro --drop-while (form list)
"Anaphoric form of `-drop-while'."
(declare (debug (form form)))
(let ((l (make-symbol "list")))
`(let ((,l ,list))
(while (and ,l (let ((it (car ,l))) ,form))
(!cdr ,l))
,l)))
(defun -drop-while (pred list)
"Return the tail of LIST starting from the first item for which (PRED item) returns nil."
(--drop-while (funcall pred it) list))
(defun -split-at (n list)
"Return a list of ((-take N LIST) (-drop N LIST)), in no more than one pass through the list."
(let (result)
(--dotimes n
(when list
(!cons (car list) result)
(!cdr list)))
(list (nreverse result) list)))
(defun -rotate (n list)
"Rotate LIST N places to the right. With N negative, rotate to the left.
The time complexity is O(n)."
(if (> n 0)
(append (last list n) (butlast list n))
(append (-drop (- n) list) (-take (- n) list))))
(defun -insert-at (n x list)
"Return a list with X inserted into LIST at position N.
See also: `-splice', `-splice-list'"
(let ((split-list (-split-at n list)))
(nconc (car split-list) (cons x (cadr split-list)))))
(defun -replace-at (n x list)
"Return a list with element at Nth position in LIST replaced with X.
See also: `-replace'"
(let ((split-list (-split-at n list)))
(nconc (car split-list) (cons x (cdr (cadr split-list))))))
(defun -update-at (n func list)
"Return a list with element at Nth position in LIST replaced with `(func (nth n list))`.
See also: `-map-when'"
(let ((split-list (-split-at n list)))
(nconc (car split-list) (cons (funcall func (car (cadr split-list))) (cdr (cadr split-list))))))
(defmacro --update-at (n form list)
"Anaphoric version of `-update-at'."
(declare (debug (form form form)))
`(-update-at ,n (lambda (it) ,form) ,list))
(defun -remove-at (n list)
"Return a list with element at Nth position in LIST removed.
See also: `-remove-at-indices', `-remove'"
(-remove-at-indices (list n) list))
(defun -remove-at-indices (indices list)
"Return a list whose elements are elements from LIST without
elements selected as `(nth i list)` for all i
from INDICES.
See also: `-remove-at', `-remove'"
(let* ((indices (-sort '< indices))
(diffs (cons (car indices) (-map '1- (-zip-with '- (cdr indices) indices))))
r)
(--each diffs
(let ((split (-split-at it list)))
(!cons (car split) r)
(setq list (cdr (cadr split)))))
(!cons list r)
(apply '-concat (nreverse r))))
(defmacro --split-with (pred list)
"Anaphoric form of `-split-with'."
(declare (debug (form form)))
(let ((l (make-symbol "list"))
(r (make-symbol "result"))
(c (make-symbol "continue")))
`(let ((,l ,list)
(,r nil)
(,c t))
(while (and ,l ,c)
(let ((it (car ,l)))
(if (not ,pred)
(setq ,c nil)
(!cons it ,r)
(!cdr ,l))))
(list (nreverse ,r) ,l))))
(defun -split-with (pred list)
"Return a list of ((-take-while PRED LIST) (-drop-while PRED LIST)), in no more than one pass through the list."
(--split-with (funcall pred it) list))
(defmacro -split-on (item list)
"Split the LIST each time ITEM is found.
Unlike `-partition-by', the ITEM is discarded from the results.
Empty lists are also removed from the result.
Comparison is done by `equal'.
See also `-split-when'"
(declare (debug (form form)))
`(-split-when (lambda (it) (equal it ,item)) ,list))
(defmacro --split-when (form list)
"Anaphoric version of `-split-when'."
(declare (debug (form form)))
`(-split-when (lambda (it) ,form) ,list))
(defun -split-when (fn list)
"Split the LIST on each element where FN returns non-nil.
Unlike `-partition-by', the \"matched\" element is discarded from
the results. Empty lists are also removed from the result.
This function can be thought of as a generalization of
`split-string'."
(let (r s)
(while list
(if (not (funcall fn (car list)))
(push (car list) s)
(when s (push (nreverse s) r))
(setq s nil))
(!cdr list))
(when s (push (nreverse s) r))
(nreverse r)))
(defmacro --separate (form list)
"Anaphoric form of `-separate'."
(declare (debug (form form)))
(let ((y (make-symbol "yes"))
(n (make-symbol "no")))
`(let (,y ,n)
(--each ,list (if ,form (!cons it ,y) (!cons it ,n)))
(list (nreverse ,y) (nreverse ,n)))))
(defun -separate (pred list)
"Return a list of ((-filter PRED LIST) (-remove PRED LIST)), in one pass through the list."
(--separate (funcall pred it) list))
(defun ---partition-all-in-steps-reversed (n step list)
"Private: Used by -partition-all-in-steps and -partition-in-steps."
(when (< step 1)
(error "Step must be a positive number, or you're looking at some juicy infinite loops."))
(let ((result nil))
(while list
(!cons (-take n list) result)
(setq list (-drop step list)))
result))
(defun -partition-all-in-steps (n step list)
"Return a new list with the items in LIST grouped into N-sized sublists at offsets STEP apart.
The last groups may contain less than N items."
(nreverse (---partition-all-in-steps-reversed n step list)))
(defun -partition-in-steps (n step list)
"Return a new list with the items in LIST grouped into N-sized sublists at offsets STEP apart.
If there are not enough items to make the last group N-sized,
those items are discarded."
(let ((result (---partition-all-in-steps-reversed n step list)))
(while (and result (< (length (car result)) n))
(!cdr result))
(nreverse result)))
(defun -partition-all (n list)
"Return a new list with the items in LIST grouped into N-sized sublists.
The last group may contain less than N items."
(-partition-all-in-steps n n list))
(defun -partition (n list)
"Return a new list with the items in LIST grouped into N-sized sublists.
If there are not enough items to make the last group N-sized,
those items are discarded."
(-partition-in-steps n n list))
(defmacro --partition-by (form list)
"Anaphoric form of `-partition-by'."
(declare (debug (form form)))
(let ((r (make-symbol "result"))
(s (make-symbol "sublist"))
(v (make-symbol "value"))
(n (make-symbol "new-value"))
(l (make-symbol "list")))
`(let ((,l ,list))
(when ,l
(let* ((,r nil)
(it (car ,l))
(,s (list it))
(,v ,form)
(,l (cdr ,l)))
(while ,l
(let* ((it (car ,l))
(,n ,form))
(unless (equal ,v ,n)
(!cons (nreverse ,s) ,r)
(setq ,s nil)
(setq ,v ,n))
(!cons it ,s)
(!cdr ,l)))
(!cons (nreverse ,s) ,r)
(nreverse ,r))))))
(defun -partition-by (fn list)
"Apply FN to each item in LIST, splitting it each time FN returns a new value."
(--partition-by (funcall fn it) list))
(defmacro --partition-by-header (form list)
"Anaphoric form of `-partition-by-header'."
(declare (debug (form form)))
(let ((r (make-symbol "result"))
(s (make-symbol "sublist"))
(h (make-symbol "header-value"))
(b (make-symbol "seen-body?"))
(n (make-symbol "new-value"))
(l (make-symbol "list")))
`(let ((,l ,list))
(when ,l
(let* ((,r nil)
(it (car ,l))
(,s (list it))
(,h ,form)
(,b nil)
(,l (cdr ,l)))
(while ,l
(let* ((it (car ,l))
(,n ,form))
(if (equal ,h ,n)
(when ,b
(!cons (nreverse ,s) ,r)
(setq ,s nil)
(setq ,b nil))
(setq ,b t))
(!cons it ,s)
(!cdr ,l)))
(!cons (nreverse ,s) ,r)
(nreverse ,r))))))
(defun -partition-by-header (fn list)
"Apply FN to the first item in LIST. That is the header
value. Apply FN to each item in LIST, splitting it each time FN
returns the header value, but only after seeing at least one
other value (the body)."
(--partition-by-header (funcall fn it) list))
(defmacro --group-by (form list)
"Anaphoric form of `-group-by'."
(declare (debug t))
(let ((n (make-symbol "n"))
(k (make-symbol "k"))
(grp (make-symbol "grp")))
`(nreverse
(-map
(lambda (,n)
(cons (car ,n)
(nreverse (cdr ,n))))
(--reduce-from
(let* ((,k (,@form))
(,grp (assoc ,k acc)))
(if ,grp
(setcdr ,grp (cons it (cdr ,grp)))
(push
(list ,k it)
acc))
acc)
nil ,list)))))
(defun -group-by (fn list)
"Separate LIST into an alist whose keys are FN applied to the
elements of LIST. Keys are compared by `equal'."
(--group-by (funcall fn it) list))
(defun -interpose (sep list)
"Return a new list of all elements in LIST separated by SEP."
(let (result)
(when list
(!cons (car list) result)
(!cdr list))
(while list
(setq result (cons (car list) (cons sep result)))
(!cdr list))
(nreverse result)))
(defun -interleave (&rest lists)
"Return a new list of the first item in each list, then the second etc."
(let (result)
(while (-none? 'null lists)
(--each lists (!cons (car it) result))
(setq lists (-map 'cdr lists)))
(nreverse result)))
(defmacro --zip-with (form list1 list2)
"Anaphoric form of `-zip-with'.
The elements in list1 is bound as `it`, the elements in list2 as `other`."
(declare (debug (form form form)))
(let ((r (make-symbol "result"))
(l1 (make-symbol "list1"))
(l2 (make-symbol "list2")))
`(let ((,r nil)
(,l1 ,list1)
(,l2 ,list2))
(while (and ,l1 ,l2)
(let ((it (car ,l1))
(other (car ,l2)))
(!cons ,form ,r)
(!cdr ,l1)
(!cdr ,l2)))
(nreverse ,r))))
(defun -zip-with (fn list1 list2)
"Zip the two lists LIST1 and LIST2 using a function FN. This
function is applied pairwise taking as first argument element of
LIST1 and as second argument element of LIST2 at corresponding
position.
The anaphoric form `--zip-with' binds the elements from LIST1 as `it`,
and the elements from LIST2 as `other`."
(--zip-with (funcall fn it other) list1 list2))
(defun -zip (&rest lists)
"Zip LISTS together. Group the head of each list, followed by the
second elements of each list, and so on. The lengths of the returned
groupings are equal to the length of the shortest input list.
If two lists are provided as arguments, return the groupings as a list
of cons cells. Otherwise, return the groupings as a list of lists.
Please note! This distinction is being removed in an upcoming 2.0
release of Dash. If you rely on this behavior, use -zip-pair instead."
(let (results)
(while (-none? 'null lists)
(setq results (cons (mapcar 'car lists) results))
(setq lists (mapcar 'cdr lists)))
(setq results (nreverse results))
(if (= (length lists) 2)
;; to support backward compatability, return
;; a cons cell if two lists were provided
(--map (cons (car it) (cadr it)) results)
results)))
(defalias '-zip-pair '-zip)
(defun -zip-fill (fill-value &rest lists)
"Zip LISTS, with FILL-VALUE padded onto the shorter lists. The
lengths of the returned groupings are equal to the length of the
longest input list."
(apply '-zip (apply '-pad (cons fill-value lists))))
(defun -cycle (list)
"Return an infinite copy of LIST that will cycle through the
elements and repeat from the beginning."
(let ((newlist (-map 'identity list)))
(nconc newlist newlist)))
(defun -pad (fill-value &rest lists)
"Appends FILL-VALUE to the end of each list in LISTS such that they
will all have the same length."
(let* ((annotations (-annotate 'length lists))
(n (-max (-map 'car annotations))))
(--map (append (cdr it) (-repeat (- n (car it)) fill-value)) annotations)))
(defun -annotate (fn list)
"Return a list of cons cells where each cell is FN applied to each
element of LIST paired with the unmodified element of LIST."
(-zip (-map fn list) list))
(defmacro --annotate (form list)
"Anaphoric version of `-annotate'."
(declare (debug (form form)))
`(-annotate (lambda (it) ,form) ,list))
(defun dash--table-carry (lists restore-lists &optional re)
"Helper for `-table' and `-table-flat'.
If a list overflows, carry to the right and reset the list."
(while (not (or (car lists)
(equal lists '(nil))))
(setcar lists (car restore-lists))
(pop (cadr lists))
(!cdr lists)
(!cdr restore-lists)
(when re
(push (nreverse (car re)) (cadr re))
(setcar re nil)
(!cdr re))))
(defun -table (fn &rest lists)
"Compute outer product of LISTS using function FN.
The function FN should have the same arity as the number of
supplied lists.
The outer product is computed by applying fn to all possible
combinations created by taking one element from each list in
order. The dimension of the result is (length lists).
See also: `-table-flat'"
(let ((restore-lists (copy-sequence lists))
(last-list (last lists))
(re (make-list (length lists) nil)))
(while (car last-list)
(let ((item (apply fn (-map 'car lists))))
(push item (car re))
(setcar lists (cdar lists)) ;; silence byte compiler
(dash--table-carry lists restore-lists re)))
(nreverse (car (last re)))))
(defun -table-flat (fn &rest lists)
"Compute flat outer product of LISTS using function FN.
The function FN should have the same arity as the number of
supplied lists.
The outer product is computed by applying fn to all possible
combinations created by taking one element from each list in
order. The results are flattened, ignoring the tensor structure
of the result. This is equivalent to calling:
(-flatten-n (1- (length lists)) (-table fn lists))
but the implementation here is much more efficient.
See also: `-flatten-n', `-table'"
(when lists ;Just in case.
(let* ((list1 (pop lists))
(restore-lists (copy-sequence lists))
(last-list (last lists))
re)
(while (car last-list)
(let ((tail (-map #'car lists)))
(dolist (head list1)
(push (apply fn head tail) re)))
(pop (car lists))
(dash--table-carry lists restore-lists))
(nreverse re))))
(defun -partial (fn &rest args)
"Take a function FN and fewer than the normal arguments to FN,
and return a fn that takes a variable number of additional ARGS.
When called, the returned function calls FN with ARGS first and
then additional args."
(apply 'apply-partially fn args))
(defun -elem-index (elem list)
"Return the index of the first element in the given LIST which
is equal to the query element ELEM, or nil if there is no
such element."
(car (-elem-indices elem list)))
(defun -elem-indices (elem list)
"Return the indices of all elements in LIST equal to the query
element ELEM, in ascending order."
(-find-indices (-partial 'equal elem) list))
(defun -find-indices (pred list)
"Return the indices of all elements in LIST satisfying the
predicate PRED, in ascending order."
(apply 'append (--map-indexed (when (funcall pred it) (list it-index)) list)))
(defmacro --find-indices (form list)
"Anaphoric version of `-find-indices'."
(declare (debug (form form)))
`(-find-indices (lambda (it) ,form) ,list))
(defun -find-index (pred list)
"Take a predicate PRED and a LIST and return the index of the
first element in the list satisfying the predicate, or nil if
there is no such element."
(car (-find-indices pred list)))
(defmacro --find-index (form list)
"Anaphoric version of `-find-index'."
(declare (debug (form form)))
`(-find-index (lambda (it) ,form) ,list))
(defun -find-last-index (pred list)
"Take a predicate PRED and a LIST and return the index of the
last element in the list satisfying the predicate, or nil if
there is no such element."
(-last-item (-find-indices pred list)))
(defmacro --find-last-index (form list)
"Anaphoric version of `-find-last-index'."
`(-find-last-index (lambda (it) ,form) ,list))
(defun -select-by-indices (indices list)
"Return a list whose elements are elements from LIST selected
as `(nth i list)` for all i from INDICES."
(let (r)
(--each indices
(!cons (nth it list) r))
(nreverse r)))
;; ->
;; ->>
;; -->
;; -some->
;; -some->>
;; -some-->
(defun -grade-up (comparator list)
"Grade elements of LIST using COMPARATOR relation, yielding a
permutation vector such that applying this permutation to LIST
sorts it in ascending order."
;; ugly hack to "fix" lack of lexical scope
(let ((comp `(lambda (it other) (funcall ',comparator (car it) (car other)))))
(->> (--map-indexed (cons it it-index) list)
(-sort comp)
(-map 'cdr))))
(defun -grade-down (comparator list)
"Grade elements of LIST using COMPARATOR relation, yielding a
permutation vector such that applying this permutation to LIST
sorts it in descending order."
;; ugly hack to "fix" lack of lexical scope
(let ((comp `(lambda (it other) (funcall ',comparator (car other) (car it)))))
(->> (--map-indexed (cons it it-index) list)
(-sort comp)
(-map 'cdr))))
(defvar dash--source-counter 0
"Monotonic counter for generated symbols.")
(defun dash--match-make-source-symbol ()
"Generate a new dash-source symbol.
All returned symbols are guaranteed to be unique."
(prog1 (make-symbol (format "--dash-source-%d--" dash--source-counter))
(setq dash--source-counter (1+ dash--source-counter))))
(defun dash--match-ignore-place-p (symbol)
"Return non-nil if SYMBOL is a symbol and starts with _."
(and (symbolp symbol)
(eq (aref (symbol-name symbol) 0) ?_)))
(defun dash--match-cons-skip-cdr (skip-cdr source)
"Helper function generating idiomatic shifting code."
(cond
((= skip-cdr 0)
`(pop ,source))
(t
`(prog1 ,(dash--match-cons-get-car skip-cdr source)
(setq ,source ,(dash--match-cons-get-cdr (1+ skip-cdr) source))))))
(defun dash--match-cons-get-car (skip-cdr source)
"Helper function generating idiomatic code to get nth car."
(cond
((= skip-cdr 0)
`(car ,source))
((= skip-cdr 1)
`(cadr ,source))
(t
`(nth ,skip-cdr ,source))))
(defun dash--match-cons-get-cdr (skip-cdr source)
"Helper function generating idiomatic code to get nth cdr."
(cond
((= skip-cdr 0)
source)
((= skip-cdr 1)
`(cdr ,source))
(t
`(nthcdr ,skip-cdr ,source))))
(defun dash--match-cons (match-form source)
"Setup a cons matching environment and call the real matcher."
(let ((s (dash--match-make-source-symbol))
(n 0)
(m match-form))
(while (and (consp m)
(dash--match-ignore-place-p (car m)))
(setq n (1+ n)) (!cdr m))
(cond
;; when we only have one pattern in the list, we don't have to
;; create a temporary binding (--dash-source--) for the source
;; and just use the input directly
((and (consp m)
(not (cdr m)))
(dash--match (car m) (dash--match-cons-get-car n source)))
;; handle other special types
((> n 0)
(dash--match m (dash--match-cons-get-cdr n source)))
;; this is the only entry-point for dash--match-cons-1, that's
;; why we can't simply use the above branch, it would produce
;; infinite recursion
(t
(cons (list s source) (dash--match-cons-1 match-form s))))))
(defun dash--match-cons-1 (match-form source &optional props)
"Match MATCH-FORM against SOURCE.
MATCH-FORM is a proper or improper list. Each element of
MATCH-FORM is either a symbol, which gets bound to the respective
value in source or another match form which gets destructured
recursively.
If the cdr of last cons cell in the list is `nil', matching stops
there.
SOURCE is a proper or improper list."
(let ((skip-cdr (or (plist-get props :skip-cdr) 0)))
(cond
((consp match-form)
(cond
((cdr match-form)
(cond
((and (symbolp (car match-form))
(memq (car match-form) '(&keys &plist &alist &hash)))
(dash--match-kv match-form (dash--match-cons-get-cdr skip-cdr source)))
((dash--match-ignore-place-p (car match-form))
(dash--match-cons-1 (cdr match-form) source
(plist-put props :skip-cdr (1+ skip-cdr))))
(t
(-concat (dash--match (car match-form) (dash--match-cons-skip-cdr skip-cdr source))
(dash--match-cons-1 (cdr match-form) source)))))
(t ;; Last matching place, no need for shift
(dash--match (car match-form) (dash--match-cons-get-car skip-cdr source)))))
((eq match-form nil)
nil)
(t ;; Handle improper lists. Last matching place, no need for shift
(dash--match match-form (dash--match-cons-get-cdr skip-cdr source))))))
(defun dash--vector-tail (seq start)
"Return the tail of SEQ starting at START."
(cond
((vectorp seq)
(let* ((re-length (- (length seq) start))
(re (make-vector re-length 0)))
(--dotimes re-length (aset re it (aref seq (+ it start))))
re))
((stringp seq)
(substring seq start))))
(defun dash--match-vector (match-form source)
"Setup a vector matching environment and call the real matcher."
(let ((s (dash--match-make-source-symbol)))
(cond
;; don't bind `s' if we only have one sub-pattern
((= (length match-form) 1)
(dash--match (aref match-form 0) `(aref ,source 0)))
;; if the source is a symbol, we don't need to re-bind it
((symbolp source)
(dash--match-vector-1 match-form source))
;; don't bind `s' if we only have one sub-pattern which is not ignored
((let* ((ignored-places (mapcar 'dash--match-ignore-place-p match-form))
(ignored-places-n (length (-remove 'null ignored-places))))
(when (= ignored-places-n (1- (length match-form)))
(let ((n (-find-index 'null ignored-places)))
(dash--match (aref match-form n) `(aref ,source ,n))))))
(t
(cons (list s source) (dash--match-vector-1 match-form s))))))
(defun dash--match-vector-1 (match-form source)
"Match MATCH-FORM against SOURCE.
MATCH-FORM is a vector. Each element of MATCH-FORM is either a
symbol, which gets bound to the respective value in source or
another match form which gets destructured recursively.
If second-from-last place in MATCH-FORM is the symbol &rest, the
next element of the MATCH-FORM is matched against the tail of
SOURCE, starting at index of the &rest symbol. This is
conceptually the same as the (head . tail) match for improper
lists, where dot plays the role of &rest.
SOURCE is a vector.
If the MATCH-FORM vector is shorter than SOURCE vector, only
the (length MATCH-FORM) places are bound, the rest of the SOURCE
is discarded."
(let ((i 0)
(l (length match-form))
(re))
(while (< i l)
(let ((m (aref match-form i)))
(push (cond
((and (symbolp m)
(eq m '&rest))
(prog1 (dash--match
(aref match-form (1+ i))
`(dash--vector-tail ,source ,i))
(setq i l)))
((and (symbolp m)
;; do not match symbols starting with _
(not (eq (aref (symbol-name m) 0) ?_)))
(list (list m `(aref ,source ,i))))
((not (symbolp m))
(dash--match m `(aref ,source ,i))))
re)
(setq i (1+ i))))
(-flatten-n 1 (nreverse re))))
(defun dash--match-kv (match-form source)
"Setup a kv matching environment and call the real matcher.
kv can be any key-value store, such as plist, alist or hash-table."
(let ((s (dash--match-make-source-symbol)))
(cond
;; don't bind `s' if we only have one sub-pattern (&type key val)
((= (length match-form) 3)
(dash--match-kv-1 (cdr match-form) source (car match-form)))
;; if the source is a symbol, we don't need to re-bind it
((symbolp source)
(dash--match-kv-1 (cdr match-form) source (car match-form)))
(t
(cons (list s source) (dash--match-kv-1 (cdr match-form) s (car match-form)))))))
(defun dash--match-kv-1 (match-form source type)
"Match MATCH-FORM against SOURCE of type TYPE.
MATCH-FORM is a proper list of the form (key1 place1 ... keyN
placeN). Each placeK is either a symbol, which gets bound to the
value of keyK retrieved from the key-value store, or another
match form which gets destructured recursively.
SOURCE is a key-value store of type TYPE, which can be a plist,
an alist or a hash table.
TYPE is a token specifying the type of the key-value store.
Valid values are &plist, &alist and &hash."
(-flatten-n 1 (-map
(lambda (kv)
(let* ((k (car kv))
(v (cadr kv))
(getter (cond
((or (eq type '&plist) (eq type '&keys))
`(plist-get ,source ,k))
((eq type '&alist)
`(cdr (assoc ,k ,source)))
((eq type '&hash)
`(gethash ,k ,source)))))
(cond
((symbolp v)
(list (list v getter)))
(t (dash--match v getter)))))
(-partition 2 match-form))))
(defun dash--match-symbol (match-form source)
"Bind a symbol.
This works just like `let', there is no destructuring."
(list (list match-form source)))
(defun dash--match (match-form source)
"Match MATCH-FORM against SOURCE.
This function tests the MATCH-FORM and dispatches to specific
matchers based on the type of the expression.
Key-value stores are disambiguated by placing a token &plist,
&alist or &hash as a first item in the MATCH-FORM."
(cond
((symbolp match-form)
(dash--match-symbol match-form source))
((consp match-form)
(cond
;; Handle the "x &as" bindings first.
((and (consp (cdr match-form))
(symbolp (car match-form))
(eq '&as (cadr match-form)))
(let ((s (car match-form)))
(cons (list s source)
(dash--match (cddr match-form) s))))
((memq (car match-form) '(&keys &plist &alist &hash))
(dash--match-kv match-form source))
(t (dash--match-cons match-form source))))
((vectorp match-form)
;; We support the &as binding in vectors too
(cond
((and (> (length match-form) 2)
(symbolp (aref match-form 0))
(eq '&as (aref match-form 1)))
(let ((s (aref match-form 0)))
(cons (list s source)
(dash--match (dash--vector-tail match-form 2) s))))
(t (dash--match-vector match-form source))))))
(defmacro -let* (varlist &rest body)
"Bind variables according to VARLIST then eval BODY.
VARLIST is a list of lists of the form (PATTERN SOURCE). Each
PATTERN is matched against the SOURCE structurally. SOURCE is
only evaluated once for each PATTERN.
Each SOURCE can refer to the symbols already bound by this
VARLIST. This is useful if you want to destructure SOURCE
recursively but also want to name the intermediate structures.
See `-let' for the list of all possible patterns."
(declare (debug ((&rest (sexp form)) body))
(indent 1))
(let ((bindings (--mapcat (dash--match (car it) (cadr it)) varlist)))
`(let* ,bindings
,@body)))
(defmacro -let (varlist &rest body)
"Bind variables according to VARLIST then eval BODY.
VARLIST is a list of lists of the form (PATTERN SOURCE). Each
PATTERN is matched against the SOURCE \"structurally\". SOURCE
is only evaluated once for each PATTERN. Each PATTERN is matched
recursively, and can therefore contain sub-patterns which are
matched against corresponding sub-expressions of SOURCE.
All the SOURCEs are evalled before any symbols are
bound (i.e. \"in parallel\").
If VARLIST only contains one (PATTERN SOURCE) element, you can
optionally specify it using a vector and discarding the
outer-most parens. Thus
(-let ((PATTERN SOURCE)) ..)
becomes
(-let [PATTERN SOURCE] ..).
`-let' uses a convention of not binding places (symbols) starting
with _ whenever it's possible. You can use this to skip over
entries you don't care about. However, this is not *always*
possible (as a result of implementation) and these symbols might
get bound to undefined values.
Following is the overview of supported patterns. Remember that
patterns can be matched recursively, so every a, b, aK in the
following can be a matching construct and not necessarily a
symbol/variable.
Symbol:
a - bind the SOURCE to A. This is just like regular `let'.
Conses and lists:
(a) - bind `car' of cons/list to A
(a . b) - bind car of cons to A and `cdr' to B
(a b) - bind car of list to A and `cadr' to B
(a1 a2 a3 ...) - bind 0th car of list to A1, 1st to A2, 2nd to A3 ...
(a1 a2 a3 ... aN . rest) - as above, but bind the Nth cdr to REST.
Vectors:
[a] - bind 0th element of a non-list sequence to A (works with
vectors, strings, bit arrays...)
[a1 a2 a3 ...] - bind 0th element of non-list sequence to A0, 1st to
A1, 2nd to A2, ...
If the PATTERN is shorter than SOURCE, the values at
places not in PATTERN are ignored.
If the PATTERN is longer than SOURCE, an `error' is
thrown.
[a1 a2 a3 ... &rest rest] - as above, but bind the rest of
the sequence to REST. This is
conceptually the same as improper list
matching (a1 a2 ... aN . rest)
Key/value stores:
(&plist key0 a0 ... keyN aN) - bind value mapped by keyK in the
SOURCE plist to aK. If the
value is not found, aK is nil.
(&alist key0 a0 ... keyN aN) - bind value mapped by keyK in the
SOURCE alist to aK. If the
value is not found, aK is nil.
(&hash key0 a0 ... keyN aN) - bind value mapped by keyK in the
SOURCE hash table to aK. If the
value is not found, aK is nil.
Further, special keyword &keys supports \"inline\" matching of
plist-like key-value pairs, similarly to &keys keyword of
`cl-defun'.
(a1 a2 ... aN &keys key1 b1 ... keyN bK)
This binds N values from the list to a1 ... aN, then interprets
the cdr as a plist (see key/value matching above).
You can name the source using the syntax SYMBOL &as PATTERN.
This syntax works with lists (proper or improper), vectors and
all types of maps.
(list &as a b c) (list 1 2 3)
binds A to 1, B to 2, C to 3 and LIST to (1 2 3).
Similarly:
(bounds &as beg . end) (cons 1 2)
binds BEG to 1, END to 2 and BOUNDS to (1 . 2).
(items &as first . rest) (list 1 2 3)
binds FIRST to 1, REST to (2 3) and ITEMS to (1 2 3)
[vect &as _ b c] [1 2 3]
binds B to 2, C to 3 and VECT to [1 2 3] (_ avoids binding as usual).
(plist &as &plist :b b) (list :a 1 :b 2 :c 3)
binds B to 2 and PLIST to (:a 1 :b 2 :c 3). Same for &alist and &hash.
This is especially useful when we want to capture the result of a
computation and destructure at the same time. Consider the
form (function-returning-complex-structure) returning a list of
two vectors with two items each. We want to capture this entire
result and pass it to another computation, but at the same time
we want to get the second item from each vector. We can achieve
it with pattern
(result &as [_ a] [_ b]) (function-returning-complex-structure)
Note: Clojure programmers may know this feature as the \":as
binding\". The difference is that we put the &as at the front
because we need to support improper list binding."
(declare (debug ([&or (&rest (sexp form))
(vector [&rest [sexp form]])]
body))
(indent 1))
(if (vectorp varlist)
`(let* ,(dash--match (aref varlist 0) (aref varlist 1))
,@body)
(let* ((inputs (--map-indexed (list (make-symbol (format "input%d" it-index)) (cadr it)) varlist))
(new-varlist (--map (list (caar it) (cadr it)) (-zip varlist inputs))))
`(let ,inputs
(-let* ,new-varlist ,@body)))))
(defmacro -lambda (match-form &rest body)
"Return a lambda which destructures its input as MATCH-FORM and executes BODY.
Note that you have to enclose the MATCH-FORM in a pair of parens,
such that:
(-lambda (x) body)
(-lambda (x y ...) body)
has the usual semantics of `lambda'. Furthermore, these get
translated into normal lambda, so there is no performance
penalty.
See `-let' for the description of destructuring mechanism."
(declare (doc-string 2) (indent defun)
(debug (&define sexp
[&optional stringp]
[&optional ("interactive" interactive)]
def-body)))
(cond
((not (consp match-form))
(signal 'wrong-type-argument "match-form must be a list"))
;; no destructuring, so just return regular lambda to make things faster
((-all? 'symbolp match-form)
`(lambda ,match-form ,@body))
(t
(let* ((inputs (--map-indexed (list it (make-symbol (format "input%d" it-index))) match-form)))
;; TODO: because inputs to the lambda are evaluated only once,
;; -let* need not to create the extra bindings to ensure that.
;; We should find a way to optimize that. Not critical however.
`(lambda ,(--map (cadr it) inputs)
(-let* ,inputs ,@body))))))
(defmacro -if-let* (vars-vals then &rest else)
"If all VALS evaluate to true, bind them to their corresponding
VARS and do THEN, otherwise do ELSE. VARS-VALS should be a list
of (VAR VAL) pairs.
Note: binding is done according to `-let*'. VALS are evaluated
sequentially, and evaluation stops after the first nil VAL is
encountered."
(declare (debug ((&rest (sexp form)) form body))
(indent 2))
(->> vars-vals
(--mapcat (dash--match (car it) (cadr it)))
(--reduce-r-from
(let ((var (car it))
(val (cadr it)))
`(let ((,var ,val))
(if ,var ,acc ,@else)))
then)))
(defmacro -if-let (var-val then &rest else)
"If VAL evaluates to non-nil, bind it to VAR and do THEN,
otherwise do ELSE. VAR-VAL should be a (VAR VAL) pair.
Note: binding is done according to `-let'."
(declare (debug ((sexp form) form body))
(indent 2))
`(-if-let* (,var-val) ,then ,@else))
(defmacro --if-let (val then &rest else)
"If VAL evaluates to non-nil, bind it to `it' and do THEN,
otherwise do ELSE."
(declare (debug (form form body))
(indent 2))
`(-if-let (it ,val) ,then ,@else))
(defmacro -when-let* (vars-vals &rest body)
"If all VALS evaluate to true, bind them to their corresponding
VARS and execute body. VARS-VALS should be a list of (VAR VAL)
pairs.
Note: binding is done according to `-let*'. VALS are evaluated
sequentially, and evaluation stops after the first nil VAL is
encountered."
(declare (debug ((&rest (sexp form)) body))
(indent 1))
`(-if-let* ,vars-vals (progn ,@body)))
(defmacro -when-let (var-val &rest body)
"If VAL evaluates to non-nil, bind it to VAR and execute body.
VAR-VAL should be a (VAR VAL) pair.
Note: binding is done according to `-let'."
(declare (debug ((sexp form) body))
(indent 1))
`(-if-let ,var-val (progn ,@body)))
(defmacro --when-let (val &rest body)
"If VAL evaluates to non-nil, bind it to `it' and execute
body."
(declare (debug (form body))
(indent 1))
`(--if-let ,val (progn ,@body)))
(defvar -compare-fn nil
"Tests for equality use this function or `equal' if this is nil.
It should only be set using dynamic scope with a let, like:
(let ((-compare-fn #'=)) (-union numbers1 numbers2 numbers3)")
(defun -distinct (list)
"Return a new list with all duplicates removed.
The test for equality is done with `equal',
or with `-compare-fn' if that's non-nil.
Alias: `-uniq'"
(let (result)
(--each list (unless (-contains? result it) (!cons it result)))
(nreverse result)))
(defalias '-uniq '-distinct)
(defun -union (list list2)
"Return a new list containing the elements of LIST1 and elements of LIST2 that are not in LIST1.
The test for equality is done with `equal',
or with `-compare-fn' if that's non-nil."
;; We fall back to iteration implementation if the comparison
;; function isn't one of `eq', `eql' or `equal'.
(let* ((result (reverse list))
;; TODO: get rid of this dynamic variable, pass it as an
;; argument instead.
(-compare-fn (if (bound-and-true-p -compare-fn)
-compare-fn
'equal)))
(if (memq -compare-fn '(eq eql equal))
(let ((ht (make-hash-table :test -compare-fn)))
(--each list (puthash it t ht))
(--each list2 (unless (gethash it ht) (!cons it result))))
(--each list2 (unless (-contains? result it) (!cons it result))))
(nreverse result)))
(defun -intersection (list list2)
"Return a new list containing only the elements that are members of both LIST and LIST2.
The test for equality is done with `equal',
or with `-compare-fn' if that's non-nil."
(--filter (-contains? list2 it) list))
(defun -difference (list list2)
"Return a new list with only the members of LIST that are not in LIST2.
The test for equality is done with `equal',
or with `-compare-fn' if that's non-nil."
(--filter (not (-contains? list2 it)) list))
(defun -contains? (list element)
"Return non-nil if LIST contains ELEMENT.
The test for equality is done with `equal', or with `-compare-fn'
if that's non-nil.
Alias: `-contains-p'"
(not
(null
(cond
((null -compare-fn) (member element list))
((eq -compare-fn 'eq) (memq element list))
((eq -compare-fn 'eql) (memql element list))
(t
(let ((lst list))
(while (and lst
(not (funcall -compare-fn element (car lst))))
(setq lst (cdr lst)))
lst))))))
(defalias '-contains-p '-contains?)
(defun -same-items? (list list2)
"Return true if LIST and LIST2 has the same items.
The order of the elements in the lists does not matter.
Alias: `-same-items-p'"
(let ((length-a (length list))
(length-b (length list2)))
(and
(= length-a length-b)
(= length-a (length (-intersection list list2))))))
(defalias '-same-items-p '-same-items?)
(defun -is-prefix? (prefix list)
"Return non-nil if PREFIX is prefix of LIST.
Alias: `-is-prefix-p'"
(--each-while list (equal (car prefix) it)
(!cdr prefix))
(not prefix))
(defun -is-suffix? (suffix list)
"Return non-nil if SUFFIX is suffix of LIST.
Alias: `-is-suffix-p'"
(-is-prefix? (reverse suffix) (reverse list)))
(defun -is-infix? (infix list)
"Return non-nil if INFIX is infix of LIST.
This operation runs in O(n^2) time
Alias: `-is-infix-p'"
(let (done)
(while (and (not done) list)
(setq done (-is-prefix? infix list))
(!cdr list))
done))
(defalias '-is-prefix-p '-is-prefix?)
(defalias '-is-suffix-p '-is-suffix?)
(defalias '-is-infix-p '-is-infix?)
(defun -sort (comparator list)
"Sort LIST, stably, comparing elements using COMPARATOR.
Return the sorted list. LIST is NOT modified by side effects.
COMPARATOR is called with two elements of LIST, and should return non-nil
if the first element should sort before the second."
(sort (copy-sequence list) comparator))
(defmacro --sort (form list)
"Anaphoric form of `-sort'."
(declare (debug (form form)))
`(-sort (lambda (it other) ,form) ,list))
(defun -list (&rest args)
"Return a list with ARGS.
If first item of ARGS is already a list, simply return ARGS. If
not, return a list with ARGS as elements."
(let ((arg (car args)))
(if (listp arg) arg args)))
(defun -repeat (n x)
"Return a list with X repeated N times.
Return nil if N is less than 1."
(let (ret)
(--dotimes n (!cons x ret))
ret))
(defun -sum (list)
"Return the sum of LIST."
(apply '+ list))
(defun -product (list)
"Return the product of LIST."
(apply '* list))
(defun -max (list)
"Return the largest value from LIST of numbers or markers."
(apply 'max list))
(defun -min (list)
"Return the smallest value from LIST of numbers or markers."
(apply 'min list))
(defun -max-by (comparator list)
"Take a comparison function COMPARATOR and a LIST and return
the greatest element of the list by the comparison function.
See also combinator `-on' which can transform the values before
comparing them."
(--reduce (if (funcall comparator it acc) it acc) list))
(defun -min-by (comparator list)
"Take a comparison function COMPARATOR and a LIST and return
the least element of the list by the comparison function.
See also combinator `-on' which can transform the values before
comparing them."
(--reduce (if (funcall comparator it acc) acc it) list))
(defmacro --max-by (form list)
"Anaphoric version of `-max-by'.
The items for the comparator form are exposed as \"it\" and \"other\"."
(declare (debug (form form)))
`(-max-by (lambda (it other) ,form) ,list))
(defmacro --min-by (form list)
"Anaphoric version of `-min-by'.
The items for the comparator form are exposed as \"it\" and \"other\"."
(declare (debug (form form)))
`(-min-by (lambda (it other) ,form) ,list))
(defun -iterate (fun init n)
"Return a list of iterated applications of FUN to INIT.
This means a list of form:
(init (fun init) (fun (fun init)) ...)
N is the length of the returned list."
(if (= n 0) nil
(let ((r (list init)))
(--dotimes (1- n)
(push (funcall fun (car r)) r))
(nreverse r))))
(defun -fix (fn list)
"Compute the (least) fixpoint of FN with initial input LIST.
FN is called at least once, results are compared with `equal'."
(let ((re (funcall fn list)))
(while (not (equal list re))
(setq list re)
(setq re (funcall fn re)))
re))
(defmacro --fix (form list)
"Anaphoric form of `-fix'."
`(-fix (lambda (it) ,form) ,list))
(defun -unfold (fun seed)
"Build a list from SEED using FUN.
This is \"dual\" operation to `-reduce-r': while -reduce-r
consumes a list to produce a single value, `-unfold' takes a
seed value and builds a (potentially infinite!) list.
FUN should return `nil' to stop the generating process, or a
cons (A . B), where A will be prepended to the result and B is
the new seed."
(let ((last (funcall fun seed)) r)
(while last
(push (car last) r)
(setq last (funcall fun (cdr last))))
(nreverse r)))
(defmacro --unfold (form seed)
"Anaphoric version of `-unfold'."
(declare (debug (form form)))
`(-unfold (lambda (it) ,form) ,seed))
(defun -cons-pair? (con)
"Return non-nil if CON is true cons pair.
That is (A . B) where B is not a list."
(and (listp con)
(not (listp (cdr con)))))
(defun -cons-to-list (con)
"Convert a cons pair to a list with `car' and `cdr' of the pair respectively."
(list (car con) (cdr con)))
(defun -value-to-list (val)
"Convert a value to a list.
If the value is a cons pair, make a list with two elements, `car'
and `cdr' of the pair respectively.
If the value is anything else, wrap it in a list."
(cond
((-cons-pair? val) (-cons-to-list val))
(t (list val))))
(defun -tree-mapreduce-from (fn folder init-value tree)
"Apply FN to each element of TREE, and make a list of the results.
If elements of TREE are lists themselves, apply FN recursively to
elements of these nested lists.
Then reduce the resulting lists using FOLDER and initial value
INIT-VALUE. See `-reduce-r-from'.
This is the same as calling `-tree-reduce-from' after `-tree-map'
but is twice as fast as it only traverse the structure once."
(cond
((not tree) nil)
((-cons-pair? tree) (funcall fn tree))
((listp tree)
(-reduce-r-from folder init-value (mapcar (lambda (x) (-tree-mapreduce-from fn folder init-value x)) tree)))
(t (funcall fn tree))))
(defmacro --tree-mapreduce-from (form folder init-value tree)
"Anaphoric form of `-tree-mapreduce-from'."
(declare (debug (form form form form)))
`(-tree-mapreduce-from (lambda (it) ,form) (lambda (it acc) ,folder) ,init-value ,tree))
(defun -tree-mapreduce (fn folder tree)
"Apply FN to each element of TREE, and make a list of the results.
If elements of TREE are lists themselves, apply FN recursively to
elements of these nested lists.
Then reduce the resulting lists using FOLDER and initial value
INIT-VALUE. See `-reduce-r-from'.
This is the same as calling `-tree-reduce' after `-tree-map'
but is twice as fast as it only traverse the structure once."
(cond
((not tree) nil)
((-cons-pair? tree) (funcall fn tree))
((listp tree)
(-reduce-r folder (mapcar (lambda (x) (-tree-mapreduce fn folder x)) tree)))
(t (funcall fn tree))))
(defmacro --tree-mapreduce (form folder tree)
"Anaphoric form of `-tree-mapreduce'."
(declare (debug (form form form)))
`(-tree-mapreduce (lambda (it) ,form) (lambda (it acc) ,folder) ,tree))
(defun -tree-map (fn tree)
"Apply FN to each element of TREE while preserving the tree structure."
(cond
((not tree) nil)
((-cons-pair? tree) (funcall fn tree))
((listp tree)
(mapcar (lambda (x) (-tree-map fn x)) tree))
(t (funcall fn tree))))
(defmacro --tree-map (form tree)
"Anaphoric form of `-tree-map'."
(declare (debug (form form)))
`(-tree-map (lambda (it) ,form) ,tree))
(defun -tree-reduce-from (fn init-value tree)
"Use FN to reduce elements of list TREE.
If elements of TREE are lists themselves, apply the reduction recursively.
FN is first applied to INIT-VALUE and first element of the list,
then on this result and second element from the list etc.
The initial value is ignored on cons pairs as they always contain
two elements."
(cond
((not tree) nil)
((-cons-pair? tree) tree)
((listp tree)
(-reduce-r-from fn init-value (mapcar (lambda (x) (-tree-reduce-from fn init-value x)) tree)))
(t tree)))
(defmacro --tree-reduce-from (form init-value tree)
"Anaphoric form of `-tree-reduce-from'."
(declare (debug (form form form)))
`(-tree-reduce-from (lambda (it acc) ,form) ,init-value ,tree))
(defun -tree-reduce (fn tree)
"Use FN to reduce elements of list TREE.
If elements of TREE are lists themselves, apply the reduction recursively.
FN is first applied to first element of the list and second
element, then on this result and third element from the list etc.
See `-reduce-r' for how exactly are lists of zero or one element handled."
(cond
((not tree) nil)
((-cons-pair? tree) tree)
((listp tree)
(-reduce-r fn (mapcar (lambda (x) (-tree-reduce fn x)) tree)))
(t tree)))
(defmacro --tree-reduce (form tree)
"Anaphoric form of `-tree-reduce'."
(declare (debug (form form)))
`(-tree-reduce (lambda (it acc) ,form) ,tree))
(defun -tree-map-nodes (pred fun tree)
"Call FUN on each node of TREE that satisfies PRED.
If PRED returns nil, continue descending down this node. If PRED
returns non-nil, apply FUN to this node and do not descend
further."
(if (funcall pred tree)
(funcall fun tree)
(if (and (listp tree)
(not (-cons-pair? tree)))
(-map (lambda (x) (-tree-map-nodes pred fun x)) tree)
tree)))
(defmacro --tree-map-nodes (pred form tree)
"Anaphoric form of `-tree-map-nodes'."
`(-tree-map-nodes (lambda (it) ,pred) (lambda (it) ,form) ,tree))
(defun -tree-seq (branch children tree)
"Return a sequence of the nodes in TREE, in depth-first search order.
BRANCH is a predicate of one argument that returns non-nil if the
passed argument is a branch, that is, a node that can have children.
CHILDREN is a function of one argument that returns the children
of the passed branch node.
Non-branch nodes are simply copied."
(cons tree
(when (funcall branch tree)
(-mapcat (lambda (x) (-tree-seq branch children x))
(funcall children tree)))))
(defmacro --tree-seq (branch children tree)
"Anaphoric form of `-tree-seq'."
`(-tree-seq (lambda (it) ,branch) (lambda (it) ,children) ,tree))
(defun -clone (list)
"Create a deep copy of LIST.
The new list has the same elements and structure but all cons are
replaced with new ones. This is useful when you need to clone a
structure such as plist or alist."
(-tree-map 'identity list))
(defun dash-enable-font-lock ()
"Add syntax highlighting to dash functions, macros and magic values."
(eval-after-load "lisp-mode"
'(progn
(let ((new-keywords '(
"-each"
"--each"
"-each-while"
"--each-while"
"-dotimes"
"--dotimes"
"-map"
"--map"
"-reduce-from"
"--reduce-from"
"-reduce"
"--reduce"
"-reduce-r-from"
"--reduce-r-from"
"-reduce-r"
"--reduce-r"
"-filter"
"--filter"
"-select"
"--select"
"-remove"
"--remove"
"-reject"
"--reject"
"-remove-first"
"--remove-first"
"-reject-first"
"--reject-first"
"-remove-last"
"--remove-last"
"-reject-last"
"--reject-last"
"-remove-item"
"-non-nil"
"-keep"
"--keep"
"-map-indexed"
"--map-indexed"
"-splice"
"--splice"
"-splice-list"
"--splice-list"
"-map-when"
"--map-when"
"-replace-where"
"--replace-where"
"-map-first"
"--map-first"
"-map-last"
"--map-last"
"-replace"
"-replace-first"
"-replace-last"
"-flatten"
"-flatten-n"
"-concat"
"-mapcat"
"--mapcat"
"-copy"
"-cons*"
"-snoc"
"-first"
"--first"
"-find"
"--find"
"-some"
"--some"
"-any"
"--any"
"-last"
"--last"
"-first-item"
"-last-item"
"-butlast"
"-count"
"--count"
"-any?"
"--any?"
"-some?"
"--some?"
"-any-p"
"--any-p"
"-some-p"
"--some-p"
"-all?"
"--all?"
"-every?"
"--every?"
"-all-p"
"--all-p"
"-every-p"
"--every-p"
"-none?"
"--none?"
"-none-p"
"--none-p"
"-only-some?"
"--only-some?"
"-only-some-p"
"--only-some-p"
"-slice"
"-take"
"-drop"
"-take-while"
"--take-while"
"-drop-while"
"--drop-while"
"-split-at"
"-rotate"
"-insert-at"
"-replace-at"
"-update-at"
"--update-at"
"-remove-at"
"-remove-at-indices"
"-split-with"
"--split-with"
"-split-on"
"-split-when"
"--split-when"
"-separate"
"--separate"
"-partition-all-in-steps"
"-partition-in-steps"
"-partition-all"
"-partition"
"-partition-by"
"--partition-by"
"-partition-by-header"
"--partition-by-header"
"-group-by"
"--group-by"
"-interpose"
"-interleave"
"-zip-with"
"--zip-with"
"-zip"
"-zip-fill"
"-cycle"
"-pad"
"-annotate"
"--annotate"
"-table"
"-table-flat"
"-partial"
"-elem-index"
"-elem-indices"
"-find-indices"
"--find-indices"
"-find-index"
"--find-index"
"-find-last-index"
"--find-last-index"
"-select-by-indices"
"-grade-up"
"-grade-down"
"->"
"->>"
"-->"
"-when-let"
"-when-let*"
"--when-let"
"-if-let"
"-if-let*"
"--if-let"
"-let*"
"-let"
"-lambda"
"-distinct"
"-uniq"
"-union"
"-intersection"
"-difference"
"-contains?"
"-contains-p"
"-same-items?"
"-same-items-p"
"-is-prefix-p"
"-is-prefix?"
"-is-suffix-p"
"-is-suffix?"
"-is-infix-p"
"-is-infix?"
"-sort"
"--sort"
"-list"
"-repeat"
"-sum"
"-product"
"-max"
"-min"
"-max-by"
"--max-by"
"-min-by"
"--min-by"
"-iterate"
"--iterate"
"-fix"
"--fix"
"-unfold"
"--unfold"
"-cons-pair?"
"-cons-to-list"
"-value-to-list"
"-tree-mapreduce-from"
"--tree-mapreduce-from"
"-tree-mapreduce"
"--tree-mapreduce"
"-tree-map"
"--tree-map"
"-tree-reduce-from"
"--tree-reduce-from"
"-tree-reduce"
"--tree-reduce"
"-tree-seq"
"--tree-seq"
"-tree-map-nodes"
"--tree-map-nodes"
"-clone"
"-rpartial"
"-juxt"
"-applify"
"-on"
"-flip"
"-const"
"-cut"
"-orfn"
"-andfn"
"-iteratefn"
"-fixfn"
"-prodfn"
))
(special-variables '(
"it"
"it-index"
"acc"
"other"
)))
(font-lock-add-keywords 'emacs-lisp-mode `((,(concat "\\_<" (regexp-opt special-variables 'paren) "\\_>")
1 font-lock-variable-name-face)) 'append)
(font-lock-add-keywords 'emacs-lisp-mode `((,(concat "(\\s-*" (regexp-opt new-keywords 'paren) "\\_>")
1 font-lock-keyword-face)) 'append))
(--each (buffer-list)
(with-current-buffer it
(when (and (eq major-mode 'emacs-lisp-mode)
(boundp 'font-lock-mode)
font-lock-mode)
(font-lock-refresh-defaults)))))))
(provide 'dash)
;;; dash.el ends here
(in-package #:moto)
<<m_util_contents>>
(in-package #:moto)
(defmacro !cons (car cdr)
"Destructive: Set CDR to the cons of CAR and CDR."
`(setq ,cdr (cons ,car ,cdr)))
(defmacro !cdr (list)
"Destructive: Set LIST to the cdr of LIST."
`(setq ,list (cdr ,list)))
(defmacro --each (list &rest body)
"Anaphoric form of `-each'."
;; (declare (debug (form body))
;; (indent 1))
(let ((l (make-symbol "list")))
`(let ((,l ,list)
(it-index 0))
(while ,l
(let ((it (car ,l)))
,@body)
(setq it-index (1+ it-index))
(!cdr ,l)))))
(defun -each (list fn)
"Call FN with every item in LIST. Return nil, used for side-effects only."
(--each list (funcall fn it)))
(defmacro --map-when (pred rep list)
"Anaphoric form of `-map-when'."
;; (declare (debug (form form form)))
(let ((r (make-symbol "result")))
`(let (,r)
(--each ,list (!cons (if ,pred ,rep it) ,r))
(nreverse ,r))))
(defun -map-when (pred rep list)
"Return a new list where the elements in LIST that does not match the PRED function
are unchanged, and where the elements in LIST that do match the PRED function are mapped
through the REP function.
Alias: `-replace-where'
See also: `-update-at'"
(--map-when (funcall pred it) (funcall rep it) list))
(defmacro -> (x &optional form &rest more)
"Thread the expr through the forms. Insert X as the second item
in the first form, making a list of it if it is not a list
already. If there are more forms, insert the first form as the
second item in second form, etc."
(cond
((null form) x)
((null more) (if (listp form)
`(,(car form) ,x ,@(cdr form))
(list form x)))
(:else `(-> (-> ,x ,form) ,@more))))
;; (-> 5 1- ODDP)
;; => (-> (-> 5 1-) ODDP)
;; => (ODDP (-> 5 1-))
;; => (ODDP (1- 5))
;; (sb-cltl2:macroexpand-all '(-> 'first (cons 'second) (cons 'third)))
;; => (CONS (CONS 'FIRST 'SECOND) 'THIRD)
(defmacro ->> (x &optional form &rest more)
"Thread the expr through the forms. Insert X as the last item
in the first form, making a list of it if it is not a list
already. If there are more forms, insert the first form as the
last item in second form, etc."
(cond
((null form) x)
((null more) (if (listp form)
`(,@form ,x)
(list form x)))
(:else `(->> (->> ,x ,form) ,@more))))
;; (sb-cltl2:macroexpand-all '(->> 'first (cons 'second) (cons 'third)))
;; => (CONS 'THIRD (CONS 'SECOND 'FIRST))
(defmacro .> (fn x chain &rest more)
"Chainer for accessors like getf and gethash"
`(,chain ,x ,@(mapcar #'(lambda (x) (list fn x)) more)))
;; (macroexpand-1 '(.> getf y -> :second :third))
;; ;; => (-> Y (GETF :SECOND) (GETF :THIRD))
;; (sb-cltl2:macroexpand-all '(-> Y (GETF :SECOND) (GETF :THIRD)))
;; ;; => (GETF (GETF Y :SECOND) :THIRD)
;; (macroexpand-1 '(.> gethash y ->> :second :third))
;; ;; => (->> Y (GETHASH :SECOND) (GETHASH :THIRD))
;; (sb-cltl2:macroexpand-all '(->> Y (GETHASH :SECOND) (GETHASH :THIRD)))
;; ;; => (GETHASH :THIRD (GETHASH :SECOND Y))
(defmacro --> (x form &rest more)
"Thread the expr through the forms. Insert X at the position
signified by the token `it' in the first form. If there are more
forms, insert the first form at the position signified by `it' in
in second form, etc."
(if (null more)
(if (listp form)
(--map-when (eq it 'it) x form)
(list form x))
`(--> (--> ,x ,form) ,@more)))
;; (sb-cltl2:macroexpand-all
;; '(--> "test" (list* it '(:a)) reverse (getf it :a)))
;; => (GETF (REVERSE (LIST* "test" '(:A))) :A)
(defmacro -some-> (x &optional form &rest more)
"When expr is non-nil, thread it through the first form (via `->'),
and when that result is non-nil, through the next form, etc."
(if (null form) x
(let ((result (make-symbol "result")))
`(-some-> (-when-let (,result ,x)
(-> ,result ,form))
,@more))))
(defmacro -some->> (x &optional form &rest more)
"When expr is non-nil, thread it through the first form (via `->>'),
and when that result is non-nil, through the next form, etc."
(if (null form) x
(let ((result (make-symbol "result")))
`(-some->> (-when-let (,result ,x)
(->> ,result ,form))
,@more))))
(defmacro -some--> (x &optional form &rest more)
"When expr in non-nil, thread it through the first form (via `-->'),
and when that result is non-nil, through the next form, etc."
(if (null form) x
(let ((result (make-symbol "result")))
`(-some--> (-when-let (,result ,x)
(--> ,result ,form))
,@more))))
(in-package #:moto)
<<f_util_contents>>
(in-package #:moto)
(defun maptree-if (predicate transformer tree)
(multiple-value-bind (t-tree control)
(if (funcall predicate tree)
(funcall transformer tree)
(values tree #'mapcar))
(if (and (consp t-tree)
control)
(funcall control
#'(lambda (x)
(maptree-if predicate transformer x))
t-tree)
t-tree)))
(in-package #:moto)
(defmacro define (form* form)
(etypecase form*
(symbol (etypecase form
;; alias for function or macro
(symbol `(defmacro ,form* (&rest args)
`(,',form ,@args)))
;; alias for lambda
(cons `(defun ,form* (&rest args)
(apply ,form args)))))
(cons ;; scheme-like function definition
` (defun ,(first form*) ,(rest form*)
,form))))
Тут typecase используется до генерации кода - в зависимости от того символы или списки связываются друг с другом генерируются различные определения. Можно определять псевдонимы для функций и маросов, псевдоним будет макросом:
(in-package #:moto)
(define head car)
(define tail cdr)
(define \\ lambda)
(define $ funcall)
Можно определить функцию f2, которая является псевдонимом для лямбды, возвращённой формой (f1 a1):
(in-package #:moto)
(define f2 (f1 a1))
(f2 a2) ~ (apply (f1 a1) a2)
Простое определение функций в Scheme-стиле, более соответствующее представлению о редукции форм:
(in-package #:moto)
(define (f1 a1) (f2 a2))
(f1 a1) ~ (defun f1 (a1) (f2 a2))
Также, чтобы определять функции миксующую аргументы с другой функцией, можно ввести такой макрос:
(in-package #:moto)
(defmacro define* (form* form)
`(defun ,(first form*) ,(rest form*)
(,(first form) ,@(rest form) ,@(rest form*))))
Пример:
(in-package #:moto)
(define* (f1 a1) (f2 a2))
(f1 a1) ~ (f2 a2 a1)
Либо использовать карринг.
далее f, g, … обозначают функции, a, b, … - их аргументы.
(in-package #:moto)
(define (self object) object)
(define (flip f) (\\ (a b) ($ f b a)))
(define (curry f a) (\\ (b) ($ f a b)))
(define (curry* f g) (\\ (a b) ($ f g a b)))
(define (compose f g) (\\ (a) ($ f ($ g a))))
(in-package #:moto)
(define (foldl f a list)
(typecase list
(null a)
(cons (foldl f ($ f a (head list)) (tail list)))))
(define (foldr f a list)
(typecase list
(null a)
(cons ($ f (head list) (foldr f a (tail list))))))
(define (unfold f i p)
(if ($ p i)
(cons i '())
(cons i (unfold f ($ f i) p))))
(define fold foldl)
(define my-reduce fold)
(in-package #:moto)
;; map & filter
(define (my-map f list) (foldr (\\ (x y) (cons ($ f x) y)) '() list))
(define (filter p list) (foldr (\\ (x y) (if ($ p x) (cons x y) y)) '() list))
(in-package #:moto)
;; functions for lists
(define (my-list &rest objs) objs)
(define (my-length list) (fold (\\ (x y) (1+ x)) 0 list))
(define (my-reverse list) (fold (flip 'cons) '() list))
(define (my-append list &rest lists) (fold (flip (curry* 'foldr 'cons)) list lists))
(in-package #:moto)
;; functions for numbers
(define zero? (curry '= 0))
(define positive? (curry '< 0))
(define negative? (curry '> 0))
(define (odd? number) (= (mod number 2) 1))
(define (even? number) (= (mod number 2) 0))
(define (my-max a &rest numbers) (fold (\\ (y z) (if (> y z) y z)) a numbers))
(define (my-min a &rest numbers) (fold (\\ (y z) (if (< y z) y z)) a numbers))
(define (summa &rest numbers) (fold '+ 0 numbers))
(define (product &rest numbers) (fold '* 1 numbers))
(in-package #:moto)
;; functions for booleans
(define (my-and &rest list) (fold 'and t list))
(define (my-or &rest list) (fold 'or nil list))
(define (any? p &rest list) (apply 'my-or (my-map p list)))
(define (every? p &rest list) (apply 'my-and (my-map p list)))
(in-package #:moto)
;; member & assoc
(flet ((helper (p op)
(\\ (a next) (if (and (not a) ($ p ($ op next))) next a))))
(define (my-member object list &key (test 'equal))
(fold (helper (curry test object) 'self) nil list))
(define (my-assoc object alist &key (test 'equal))
(fold (helper (curry test object) 'car) nil alist)))
Теперь, собственно, код для деревьев. Нужно заметить, что учитывая весь код для “абстракций”, получается существенно меньше, чем при реализации “в лоб” (как у Грэхама, например).
(in-package #:moto)
;; for (1 . (2 . 3)) trees
(define (my-append a b)
(append (if (atom a) (list a) a)
(if (atom b) (list b) b)))
(define (fold-tree f g tree)
(typecase tree
(atom ($ f tree))
(cons ($ g (fold-tree f g (head tree))
(fold-tree f g (tail tree))))))
(define* (summa/tree tree) (fold-tree 'self '+))
(define* (depth/tree tree) (fold-tree 'one 'max+1))
(define* (flatten tree) (fold-tree 'self 'my-append))
(in-package #:moto)
;; (defun my-range (n)
;; (let ((i 0))
;; #'(lambda ()
;; (if (< i n) (incf i) nil))))
;; (let ((f (my-range 3)))
;; (list
;; (funcall f)
;; (funcall f)
;; (funcall f)
;; (funcall f)
;; (funcall f)
;; ))
;; (range 3)
;; (defmacro do-closure ((i clos) &body body)
;; (let ((c (gensym)))
;; `(let ((,c ,clos))
;; (loop for ,i = (funcall ,c)
;; while ,i do ,@body))))
;; (do-closure (i (my-range 100)) (print i))
(declaim (inline zip))
(defun zip (&rest args)
"
Zips the elements of @arg{args}.
Example:
@lisp
> (zip '(2 3 4) '(a b c) '(j h c s))
=> ((2 A J) (3 B H) (4 C C))
@end lisp
"
(apply #'map 'list #'list args))
(defun symstuff (l)
"From the Common Lisp Cookbook - https://cl-cookbook.sourceforge.net/macros.html
Helper function to (build-symbol)"
`(concatenate 'string
,@(for (x :in l)
(cond ((stringp x)
`',x)
((atom x)
`',(format nil "~a" x))
((eq (car x) ':<)
`(format nil "~a" ,(cadr x)))
((eq (car x) ':++)
`(format nil "~a" (incf ,(cadr x))))
(t
`(format nil "~a" ,x))))))
(defmacro build-symbol (&rest l)
"From the Common Lisp Cookbook - https://cl-cookbook.sourceforge.net/macros.html"
(let ((p (find-if (lambda (x) (and (consp x) (eq (car x) ':package)))
l)))
(cond (p
(setq l (remove p l))))
(let ((pkg (cond ((eq (cadr p) 'nil)
nil)
(t `(find-package ',(cadr p))))))
(cond (p
(cond (pkg
`(values (intern ,(symstuff l) ,pkg)))
(t
`(make-symbol ,(symstuff l)))))
(t
`(values (intern ,(symstuff l))))))))
(defun remove-nth (n seq)
"Remove nth element from sequence"
(remove-if (constantly t) seq :start n :count 1))
(defun make-hash (&rest keyvals)
"Create a hash table given keys and values"
(plist-hash-table keyvals))
(defmacro make-hash* (&rest keyvals)
"Make a hash table given key/value pairs, allowing use of prior key/val pairs in late r definitions"
(loop while keyvals
for k = (intern (symbol-name (pop keyvals)))
for v = (pop keyvals)
collect `(,k ,v) into letargs
collect (make-keyword k) into objargs
collect k into objargs
finally (return
`(let* (,@letargs)
(make-hash ,@objargs)))))
(defun maphash2 (fn ht)
"Returns a hash-table with the results of the function of key & value as values"
(let ((ht-out (make-hash-table
:test (hash-table-test ht)
:size (hash-table-size ht)
:rehash-size (hash-table-rehash-size ht)
:rehash-threshold (hash-table-rehash-threshold ht))))
(maphash #'(lambda (k v)
(setf (gethash k ht-out) (funcall fn k v)))
ht)
ht-out))
(defun maphash-values2 (fn ht)
"Returns a hash-table with the results of the function of value as values"
(let ((ht-out (make-hash-table)))
(maphash #'(lambda (k v) (setf (gethash k ht-out) (funcall fn v))) ht)
ht-out))
(defmacro swap (pl1 pl2)
"Macro to swap two places"
(let ((temp1-name (gensym)) ; don't clobber existing names
(temp2-name (gensym)))
`(let ((,temp1-name ,pl1)
(,temp2-name ,pl2))
(setf ,pl1 ,temp2-name)
(setf ,pl2 ,temp1-name))))
(defun print-hash-key-or-val (kv stream)
(format stream (typecase kv
(keyword " :~a")
(string " \"~a\"")
(symbol " '~a")
(list " '~a")
(t " ~a")) kv))
(defun printhash (h &optional (stream t))
"Pretty print a hash table as :KEY VAL on separate lines"
(format stream "#<HASH-TABLE~{~a~a~^~&~}>"
(loop for k being the hash-keys in h using (hash-value v)
collect (print-hash-key-or-val k nil)
collect (print-hash-key-or-val v nil))))
(defmacro lethash (keys h &body body)
"Let form binding hash table entries to let variables names"
(let ((ht (gensym)))
`(let ((,ht ,h))
(let ,(loop for key in keys
collect `(,key (gethash ,(make-keyword key) ,ht)))
,@body))))
(defmacro with-keys (keys h &body body)
"Make keys of hash table available to body for use & changable via setf"
(let ((ht (gensym)))
(loop for key in keys
for newbody = (subst `(gethash ,(make-keyword key) ,ht) key body)
then (subst `(gethash ,(make-keyword key) ,ht) key newbody)
finally (return `(let ((,ht ,h))
,@newbody)))))
(defun linear-interpolation (ys xs x)
"Linear interpolation: calculate y(x) at x given table of ys and xs. Also returns ind ex of lookup table interval. Works from first x to less than last x."
(let* ((i (position x xs :test #'>= :from-end t))
(x0 (elt xs i))
(x1 (elt xs (1+ i)))
(y0 (elt ys i))
(y1 (elt ys (1+ i))))
(+ y0 (* (- y1 y0) (- x x0) (/ (- x1 x0))))))
(defun maptree (f tree)
"Map a function on the leaves of a tree"
(cond
((null tree) nil)
((atom tree) (funcall f tree))
(t (cons (maptree f (car tree))
(maptree f (cdr tree))))))
(defun maptreenode (f tree)
(cond
((null tree) nil)
((atom tree) tree)
(t (progn
(funcall f tree)
(cons (maptreenode f (car tree))
(maptreenode f (cdr tree)))))))
(defun flat-list-p (param)
(mapcar #'(lambda (x) (unless (atom x) (return-from flat-list-p nil)))
param)
t)
(defmethod diff ((l list))
"Return list of the 1st differences of given list: l(1)-l(0),...,l(n)-l(n-1)"
(loop for i below (1- (length l))
for li in l
collect (- (elt l (1+ i)) li)))
(defmethod diff ((v vector))
"Return vector of the 1st differences of given vector: v(1)-v(0),...,v(n)-v(n-1)"
(let* ((n (length v))
(v2 (make-array (1- n))))
(dotimes (i (1- n))
(setf (aref v2 i) (- (aref v (1+ i)) (aref v i))))
v2))
(defun slot-ref (obj slots)
"Reference nested objects by a list of successive slot names. For example, (slot-ref o 'foo 'bar 'baz) should return (slot-value (slot-value (slot-value o 'foo) 'bar) 'baz) "
(cond
((atom slots) (slot-value obj slots))
((null (cdr slots)) (slot-value obj (car slots)))
(t (slot-ref (slot-value obj (first slots)) (rest slots)))))
(defun slot-ref-set (obj slots val)
"Set nested object slot reference to new value"
(cond
((atom slots) (setf (slot-value obj slots) val))
((null (cdr slots)) (setf (slot-value obj (car slots)) val))
(t (slot-ref-set (slot-value obj (first slots)) (rest slots) val))))
(defsetf slot-ref slot-ref-set)
(defmacro bind-nested-slots (forms obj &body body)
"For each form of (VAR SLOT1 SLOT2 ...) bind VAR to (NESTED-SLOT OBJ SLOT1 SLOT2 ...) "
`(let ,(loop for form in forms
collect `(,(first form) (slot-ref ,obj ',(rest form))))
,@body))
(defmacro defpfun (name args pargs &body body)
"Define pandoric function given name, arguments, pandoric arguments,
& body forms."
`(setf (symbol-function ',name)
(plambda ,args ,pargs
,@body)))
(in-package #:moto)
(defparameter *user-agent* "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0")
(defparameter *cookies*
(list "portal_tid=1291969547067-10909"
"__utma=189530924.115785001.1291969547.1297497611.1297512149.377"
"__utmc=3521885"))
(setf *drakma-default-external-format* :utf-8)
(defun get-headers (referer)
`(
("Accept" . "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
("Accept-Language" . "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3")
("Accept-Charset" . "utf-8")
("Referer" . ,referer)
;; ("Cookie" . ,(format nil "~{~a; ~}" *cookies*))
("Cookie" . "ad20c=2; ad17c=2; __utma=48706362.2093251633.1396569814.1413985658.1413990550.145; __utmz=48706362.1413926450.142.18.utmcsr=vk.com|utmccn=(referral)|utmcmd=referral|utmcct=/im; email=avenger-f%40yandex.ru; password=30e3465569cc7433b34d42baeadff18f; PHPSESSID=ms1rrsgjqvm3lhdl5af1aekvv0; __utmc=48706362; __utmb=48706362.5.10.1413990550")
))
(defmacro web (to ot)
(let ((x-to (append '(format nil) to))
(x-ot (append '(format nil) ot)))
`(let ((r (sb-ext:octets-to-string
(drakma:http-request ,x-to
:user-agent *user-agent*
:additional-headers (get-headers ,x-ot)
:redirect 10
:force-binary t)
:external-format :utf-8)))
r)))
(defmacro fnd (var pattern)
`(multiple-value-bind (all matches)
(ppcre:scan-to-strings ,pattern ,var)
(let ((str (format nil "~a" matches)))
(subseq str 2 (- (length str) 1)))))
(defun merge-plists (&rest plists)
"Merge all the given plists into a new plist. The new plist has all
the keys from each plist, with values of keys in later lists
overriding the values of the same keys in earlier plists.
No particular order of key/value pairs is guaranteed.
E.g.:
> (merge-plists '(:a 1 :b 2) '(:a 3 :c 4) '(:d 5))
(:D 5 :C 4 :A 3 :B 2)"
(let ((result (copy-list (first plists))))
(dolist (plist (rest plists))
(do* ((prop (first plist) (first plist))
(value (second plist) (second plist))
(oldpl plist plist)
(plist plist (cddr plist)))
((not oldpl))
(setf (getf result prop) value)))
result))
;; eval-always
(defmacro eval-always (&body body)
"Wrap <_:arg body /> in <_:fun eval-when /> with all keys \(compile, load and execute) mentioned"
`(eval-when (:compile-toplevel :load-toplevel :execute)
,@body))
;; #` syntax
;; (eval-always
;; (defun |#`-reader| (stream char arg)
;; "Literal syntax for zero/one/two argument lambdas.
;; Use @ as the function's argument, % as the second.
;; Examples:
;; CL-USER> #`(+ 2 @)
;; \(lambda (&optional x y)
;; (+ 2 x))
;; CL-USER> #`((1+ @) (print @))
;; \(lambda (&optional x y)
;; (1+ x)
;; (print x))
;; CL-USER> #`(+ 1 2)
;; \(lambda (&optional x y)
;; (+ 1 2))
;; CL-USER> #`(+ @ %)
;; \(lambda (&optional x y)
;; (+ x y))
;; "
;; (declare (ignore char arg))
;; (let ((sexp (read stream t nil t))
;; (x (gensym "X"))
;; (y (gensym "Y")))
;; `(lambda (&optional ,x ,y)
;; (declare (ignorable ,x)
;; (ignorable ,y))
;; ,@(subst y '%
;; (subst x '@
;; (if (listp (car sexp))
;; sexp
;; (list sexp)))))))
;; ;; set #`
;; (set-dispatch-macro-character #\# #\` #'|#`-reader|))
;; anaphoric
(eval-always
(defmacro if-it (test then &optional else)
"Like IF. IT is bound to TEST."
`(let ((it ,test))
(if it ,then ,else))))
(eval-always
(defmacro when-it (test &body body)
"Like WHEN. IT is bound to TEST."
`(let ((it ,test))
(when it
,@body))))
(eval-always
(defmacro and-it (&rest args)
"Like AND. IT is bound to the value of the previous AND form."
(cond ((null args) t)
((null (cdr args)) (car args))
(t `(when-it ,(car args) (and-it ,@(cdr args)))))))
(eval-always
(defmacro dowhile-it (test &body body)
"Like DOWHILE. IT is bound to TEST."
`(do ((it ,test ,test))
((not it))
,@body)))
;; (eval-always
;; (defmacro cond-it (&body body)
;; "Like COND. IT is bound to the passed COND test."
;; `(let (it)
;; (cond
;; ,@(mapcar #``((setf it ,(car @)) ,(cadr @))
;; ;; uses the fact, that SETF returns the value set
;; body)))))
;; maybe
(defmacro maybecall (val &rest funs)
`(and-it ,val
,@(mapcar (lambda (fun)
`(funcall ,fun it))
funs)))
(defmacro maybe (form)
"Return a value, returned by a <_:arg form /> or nil, if <_:class error /> is signalled"
`(restart-case
(handler-bind ((error #'(lambda (c)
(declare (ignore condition))
(invoke-restart 'skip))))
,form)
(skip () nil)))
(in-package #:moto)
;; clear db
;; (drop '("profile" "vacancy"))
Соберем все сущности и автоматы
(in-package #:moto)
<<gen_automat("vacancy", "вакансии", vacancy_flds, vacancy_state)>>
<<gen_automat("resume", "резюме", resume_flds, vacancy_state)>>
<<gen_automat("rule", "правила", rule_flds, rule_state)>>
<<gen_automat("srcaccount", "аккаунта", srcaccount_flds, srcaccount_state)>>
;; Вспомогательные сущности резюме
<<gen_entity("education", "основного образования", education_flds)>>
<<gen_entity("lang", "языка", lang_flds)>>
<<gen_entity("expirience", "опыта работы", expirience_flds)>>
<<gen_entity("skill", "ключевых навыков", skill_flds)>>
<<gen_entity("recommendation", "рекомендации", recommendation_flds)>>
<<gen_entity("portfolio", "портфолио", portfolio_flds)>>
Здесь придется повторить тот код, который собирает сущности и автоматы, несмотря на то, что он есть в doc.org. Это может вызвать проблемы при обновлении в двух местах, но я пока не нашел адекватного решения этой проблемы
(ql:quickload "restas")
(ql:quickload "postmodern")
(ql:quickload "anaphora")
(ql:quickload "cl-who")
(ql:quickload "parenscript")
(ql:quickload "optima")
(ql:quickload "fare-quasiquote-extras")
(ql:quickload "fare-quasiquote-optima")
(ql:quickload "bit-smasher")
(load "~/repo/moto/src/package.lisp")
(load "~/repo/moto/src/mod/hh/m-util.lisp")
(in-package :moto)
(use-package :bit-smasher)
(defparameter *msg* "слава роботам")
(defparameter *key* "нашсуперключ.")
(defconstant +size-of-char+ 16)
(defun splitter (size param &optional acc)
"Разбивает входную последовательность =param=
на подпоследовательности размером =size=.
Если остается хвост, размером меньше size
- сигнализирует ошибку"
(cond ((>= (length param) size)
(splitter size (subseq param size)
(append acc
(list (subseq param 0 size)))))
((= 0 (length param)) acc)
(t (error param))))
;; (splitter 2 "010203040506") ;; => ("01" "02" "03" "04" "05" "06")
(defun bit-list->integer (bits)
(reduce #'(lambda (first-bit second-bit)
(+ (ash first-bit 1) second-bit))
bits))
(defun integer->bit-list (int &optional acc)
"Преобразует входное число в список битов"
(cond ((> int 0) (multiple-value-bind (i r)
(truncate int 2)
(integer->bit-list i (push r acc))))
((null acc) (push 0 acc))
(t acc)))
;; (integer->bit-list 8) ;; => (1 0 0 0)
(defun string->bit-list (string)
(->> string
(map 'list #'identity)
(mapcar #'char-code)
(mapcar #'integer->bit-list)
(mapcar #'(lambda (x)
(append (loop :repeat (- +size-of-char+ (length x)) :collect 0)
x)))
(reduce #'append)
))
;; (string->bit-list "hi") ;; => (0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 1)
(defun bit-list->string (string)
(coerce
(->> string
(string->bit-list)
(splitter +size-of-char+)
(mapcar #'bit-list->integer)
(mapcar #'code-char))
'string))
(bit-list->string '(0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 1))
(defparameter *hex-chars* "0123456789ABCDEF")
(defparameter *hex-to-char*
(loop
:for char :across *hex-chars*
:for idx :to 16
:collect (let ((x (integer->bit-list idx)))
(cons (append (loop :repeat (- 4 (length x)) :collect 0)
x)
char))))
(defparameter *char-to-hex*
(loop :for (idx . char) :in *hex-to-char* :collect `(,char . ,idx)))
(defun hex-to-char-lookup (lst)
(cdr (assoc lst *hex-to-char* :test #'equal)))
(defun char-to-hex-lookup (lst)
(cdr (assoc lst *char-to-hex* :test #'equal)))
(defparameter *crypt* (coerce
(mapcar #'hex-to-char-lookup
(splitter 4 (mapcar #'(lambda (a b)
(logxor a b))
(string->bit-list *msg*)
(string->bit-list *key*))))
'string))
(defparameter *decrypt* (coerce
(->> (map 'list #'identity *crypt*)
(mapcar #'char-to-hex-lookup)
(reduce #'append)
(mapcar #'(lambda (a b)
(logxor a b))
(string->bit-list *key*))
(splitter +size-of-char+)
(mapcar #'bit-list->integer)
(mapcar #'code-char))
'string))
(assert (equal *msg* *decrypt*))
(ql:quickload "ltk")
(use-package :ltk)
(defparameter *msg* "слава роботам")
(defun my-nbtest ()
(with-ltk ()
(let* ((nb (make-instance 'notebook :width 600 :height 400))
(t1 (make-instance 'text :master nb :width 40 :height 10))
(t2 (make-instance 'text :master nb :width 40 :height 10))
(b1 (make-instance 'button :master nb :text "Encrypt"
:command (lambda ()
(setf (text t2)
(coerce
(mapcar #'hex-to-char-lookup
(splitter 4 (mapcar #'(lambda (a b)
(logxor a b))
(string->bit-list *msg*)
(string->bit-list *key*))))
'string)
;; (format nil "the text is:~a~%" (text t1))
))))
(b2 (make-instance 'button :master nb :text "Decrypt"
:command (lambda ()
(format t "the index is:~%" )
(finish-output))))
)
(pack nb :fill :both :expand t)
(pack t1 :fill :both :expand t)
(pack t2 :fill :both :expand t)
(pack b1 :side :left)
(pack b2 :side :right)
(append-text t1 *msg*)
)))
(my-nbtest)
;; (defparameter *nb* (make-instance 'notebook :width 600 :height 400))
;; (defparameter *z* (make-instance 'text :master *nb* :width 40 :height 10))
;; ttk::notebook .nb -width 600 -height 400
;; text .nb.t1 -width 40 -height 10
;; text .nb.t2 -width 40 -height 10
;; button .nb.b1 -text "Encrupt" -command {encrypt [.nb.t1 get 1.0 end] $key}
;; button .nb.b2 -text "Decrypt" -command {decrypt}
;; pack .nb -fill both -expand true
;; pack .nb.t1 -fill both -expand true
;; pack .nb.t2 -fill both -expand true
;; pack .nb.b1 -side left
;; pack .nb.b2 -side right
;; .nb.t1 insert 1.0 "qwertyuiopцуа"
puts "zz"