Фронтенд, алгоритмы и опоссум Фридрих. Разбираем задачи конкурса Яндекса

Этим постом мы завершаем серию публикаций о конкурсах Яндекс.Блиц в 2018 году. Надеемся, что вам довелось поучаствовать или хотя бы посмотреть, какие приближенные к продакшену задачи мы предлагаем. Последний контест был посвящен применению алгоритмов во фронтенде. Сегодня мы публикуем подробные разборы 12 задач: первые 6 из них предлагались в квалификационном раунде, а задачи 7–12 — в финале. Мы постарались объяснить, как формировались условия, на какие навыки мы обращали внимание. Спасибо всем участникам за проявленный интерес!

qslceahk05pazepzdxsboshaqrg.jpeg

Задача 1


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

Рассмотрим один из вариантов: Почтамт.

Условие


На планете Джопан-14–53 местные жители хотят отправлять друг другу письма, но роботы-почтовые голуби, которые должны их доставлять, путаются в адресах. Вам предстоит научить их разбирать письма и проверять их на валидность.

Структура базовой части адреса состоит из региона, муниципалитета, имени и фамилии адресата. Муниципалитет может делиться на округа и почтовые отделения. Все части разделены знаком запятой ,.

Названия регионов, муниципалитетов и округов — слова, первая буква в каждом слове — большая, остальные — маленькие. Возможны названия из двух слов, разделенных пробелом или знаком минуса. В каждом слове от трех до восьми букв А-Я.

У жителей на руках по 6 пальцев, в быту двенадцатеричная система. Цифры 0–9 используются как есть, а 10 и 11 обозначаются знаками ~ и .

Номер почтового отделения в составе имеет либо 3 цифры подряд, либо 4, разделенные на 2 группы по 2 цифры знаком минуса. Примеры: 300, 44-99.

Иногда жители отправляют письма в муниципалитет до востребования. В этом случае адреса нет: только муниципалитет и имя адресата.

Смешно, но людей на планете зовут всего шестью именами и девятью фамилиями. Имена: Сёб, Рёб, Моб, Сян, Рян, Ман. Фамилии: Сё, Рё, Мо, Ся, Ря, Ма, Сю, Рю, Му. Жители на этой планете совсем не фантазёры.

Разбор


Структура базовой части адреса состоит из региона, муниципалитета, имени и фамилии адресата. Муниципалитет может делиться на округа и почтовые отделения. Все части разделены знаком запятой ,.


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

Названия регионов, муниципалитетов и округов — слова, первая буква в каждом слове — большая, остальные — маленькие. Возможны названия из двух слов, разделенных пробелом или знаком минуса. В каждом слове от трех до восьми букв А-Я.


Из этого абзаца понятно, что словам соответствует группа ([А-ЯЁ][а-яё]{2,7}). А названиям, соответственно,  ([А-ЯЁ][а-яё]{2,7}(?:[- ][А-ЯЁ][а-яё]{2,7})).

У жителей на руках по 6 пальцев, в быту двенадцатеричная система. Цифры 0–9 используются как есть, а 10 и 11 обозначаются знаками ~ и .


Здесь мы узнаём, что для цифр недостаточно использовать \d — нужно использовать [0-9~≈].

Номер почтового отделения в составе имеет либо 3 цифры подряд, либо 4, разделенные на 2 группы по 2 цифры знаком минуса. Примеры: 300, 44-99.


Таким образом, номерам почтовых отделений сответствует группа ([0-9~≈]{3}|[0-9~≈]{2}-[0-9~≈]{2}).

Иногда жители отправляют письма в муниципалитет до востребования. В этом случае адреса нет: только муниципалитет и имя адресата.


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

Смешно, но людей на планете зовут всего шестью именами и девятью фамилиями. Имена: Сёб, Рёб, Моб, Сян, Рян, Ман. Фамилии: Сё, Рё, Мо, Ся, Ря, Ма, Сю, Рю, Му. Жители на этой планете совсем не фантазёры.


Это последняя часть условия. Здесь нужна была еще одна простая группа (Сё|Рё|Мо|Ся|Ря|Ма|Сю|Рю|Му) (Сёб|Рёб|Моб|Сян|Рян|Ман) или её более короткий аналог ([СР][ёяю]|М[оау]) ([CР]ёб|[СР]ян|Моб|Ман).

Для простоты сохраняем группы в переменные и используем их в итоговом регулярном выражении.

const w = '[А-ЯЁ][а-яё]{2,7}'; // word
const d = '[0-9~≈]';           // digit

const name = `(?:${w}(?:[- ]${w})?)`;
const number = `(?:${d}{3}|${d}{2}-${d}{2})`;
const person = '(?:[СР][ёяю]|М[оау]) (?:Сёб|Рёб|Моб|Сян|Рян|Ман)';

// регион, муниципалитет, (округ, почтовое отделение, )?имя фамилия
const re = new RegExp(`^(${name}),\\s*(${name}),\\s*(?:(${name}),\\s*(${number}),\\s*)?(${person})$`);

module.exports = function(str) {
    // Если пришло совсем не то
    if (typeof str !== 'string') return null;

    const res = str.match(re);

    // Если пришло что-то не то
    if (!res) return null;

    // Иначе все хорошо
    // Только отрезаем первый элемент, в котором полное совпадение
    return res.slice(1);
};


Задача 2. Код, в котором есть ошибка


Условие


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

Вам предоставляется HTML-страница с поломанными стилями и макет в формате PNG (с ожидаемым результатом). Необходимо исправить ошибки, чтобы результат при просмотре в Chrome стал совпадать с оригинальным скриншотом. Исправленную страницу отправьте как решение задачи.

HTML-страница с поломанными стилями. Макет:

on3kpuxrurmtfvpllwopkxd7j7e.png

Разбор


В этом задании мы постарались сымитировать ситуацию, с которой регулярно сталкиваются разработчики Яндекса: в огромной кодовой базе найти те самые несколько простых строк кода, исправление которых приводит к нужному результату. То есть сложность состояла не в том, чтобы написать код, а в том, чтобы понять, где именно его написать (или удалить).

Мы взяли реальный код поисковой выдачи и внесли буквально несколько правок, которые развалили верстку. У участников соревнования было меньше часа, чтобы разобраться примерно с 250 килобайтами верстки и привести код к состоянию, соответствующему макету.

Понятно, что ограничение времени на соревнование не позволяло прочитать весь код, поэтому участникам следовало воспользоваться инструментами для разработчиков в браузере.

Для исправления одного из четырех вариантов задания было достаточно следующих изменений:

diff --git a/blitz.html b/blitz.html
index 36b9af8..1e30545 100644
--- a/blitz.html
+++ b/blitz.html
@@ -531,10 +531,6 @@
 iframe[src$='ext-analytics.html'] {
   height: auto;
 }
-.search2__button .suggest2-form__button :nth-child(1){
-    background: #ff0 !important;
-}
-
 /* ../../blocks-desktop/input/__control/input__control.styl end */
 /* ../../node_modules/islands/common.blocks/input/__clear/input__clear.css begin */
 /* Позиционируется относительно input__box.
@@ -744,10 +740,6 @@
 iframe[src$='ext-analytics.html'] {
     background-clip: padding-box;
 }
 .input_theme_websearch .input__clear {
     background-image: url("/static/web4/node_modules/islands/common.blocks/input/_theme/input_theme_websearch.assets/clear.svg");
     background-size: 16px;
@@ -857,6 +849,7 @@
 iframe[src$='ext-analytics.html'] {
   background-color: #f2cf46;
 }
 .websearch-button__text:before {
+  position: absolute;
   top: -6px;
   right: -9px;
   width: 0;
@@ -866,8 +859,6 @@
 iframe[src$='ext-analytics.html'] {
   border-style: solid;
   border-color: rgba(255,219,76,0);
   border-left-color: inherit;
-  position: relative;
-  z-index: -1000;
 }
 /* ../../blocks-deskpad/websearch-button/websearch-button.styl end */
@@ -1349,6 +1340,7 @@
 iframe[src$='ext-analytics.html'] {
   font-size: 14px;
   line-height: 40px;
   position: relative;
+  display: inline-block;
   height: auto;
   padding: 0;
   vertical-align: middle;

Задача 3. Картинка с заданной вариативностью


Условие


5zmgw8gjtcs5o-01l9ic8atnusy.png

Дизайнер разработал логотип. Его потребуется использовать в самых разных условиях. Чтобы это было максимально удобно, сверстайте его с помощью одного HTML-элемента на чистом CSS.

Использовать картинки (даже через data: uri) нельзя.

Разбор


Так как задание состояло в том, чтобы пользоваться только одним div, то всё, чем мы распологаем, — это сам div и псевдоэлементы :: before и :: after.

Картинка на макете, к счастью, состоит как раз из трех областей разного цвета. Делаем для каждого доступного элемента свой фон, правильно позиционируем и скругляем уголки. На серой области макета есть тень — делаем ее с помощью градиента.

div {
    background: #0C0C0C;
    border-radius: 10px;
    position: relative;
}       

div:before {
    border-radius: 9px 9px 0 0;
    position: absolute;
    width: 100%;
    height: 50%;
    background: #F8E34B;
    content: '';
}

div:after {
    content: '';
    background: linear-gradient(178deg, #C8C8C8 0px , transparent 7px), #EEEDEF;
    position: absolute;
    width: 50%;
    height: 50%;
    bottom: 0;
    border-radius: 0 0 0 9px;
}


Задача 4


Условие


Петя работает старшим верстальщиком в газете «Московский фронтендер». Для верстки газеты Петя использует стек веб-технологий. Самая сложная задача, с которой столкнулся Петя, — это верстка заголовков в газетных статьях: колонки газеты в каждом выпуске имеют разную ширину, а заголовки — разный шрифт и разное количество символов.

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

Помогите Пете: реализуйте функцию calcFontSize, которая позволит вписать переданный текст в контейнер таким образом, чтобы он влезал в контейнер целиком и имел максимально возможный размер. Если же это не удается сделать, то решение должно возвращать null. Максимальная длина строки на вход — 100 символов. Входная строка не может быть пустой. Ваше решение должно содержать код функции целиком и не должно использовать внешние зависимости, чтобы Петя мог скопировать ее в свой проект и вызывать у себя в коде.

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

Разбор


Первое, что нужно сделать, — это научиться определять, вписывается ли текст в контейнер с учетом того, что текст в контейнере может занимать несколько строк. Самый простой способ сделать это — воспользоваться функцией element.getBoundingClientRect (), которая позволяет получить размеры элемента.

Можно отрисовать текст отдельным span-элементом внутри контейнера, и проверить, превышает ли ширина или высота этого элемента размеры контейнера. Если да, значит, текст не помещается в контейнер.

Далее следует проверить граничные условия. Если текст не влезает в контейнер даже с минимальным размером шрифта — значит, его нельзя вписать. Если текст влезает с максимально разрешенным размером — правильным ответом будет max. В иных случаях искомый размер шрифта находится где-то в промежутке [min, max].

Для поиска решения нужно перебрать весь этот промежуток и найти такое значение font-size, при котором текст помещается в контейнер, но если увеличить его на 1 –—перестанет помещаться.

Можно сделать это простым циклом for по диапазону [min, max], но при этом решение будет делать очень много проверок и перерисовок страницы — как минимум по одной для каждого проверяемого значения в диапазоне. Алгоритмическая сложность такого решения будет линейной.

Чтобы минимизировать число проверок и получить решение, работающее за O (log n), нужно воспользоваться алгоритмом бинарного поиска. Идея алгоритма состоит в том, что на каждой итерации поиска диапазон значений делится на две половины, и поиск рекурсивно продолжается в той половине, в которой находится решение. Поиск закончится, когда диапазон схлопнется до одного значения. Подробнее о алгоритме бинарного поиска можно прочитать в статье в Википедии.

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

Чтобы получить полный балл за задачу, также нужно было аккуратно проверить разные граничные условия (совпадающие min и max, пустая строка текста на входе) и предусмотреть несколько условий окружения, в котором запускается код (например, фиксированный с ! important font-size в CSS страницы).

Задача 5. Трудности общения


В каждом из вариантов квалификации была задача, где в качестве входных данных предлагалась HTML-страничка с каким-то интерфейсом. У заданий была разная легенда, но все они сводились к тому, что надо было извлечь какие-то данные со страницы и, используя эти данные, как-то повзаимодействовать с интерфейсом.

Разберем решение одной из задач этой серии, которая называлась «Трудности общения».

Условие


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

К счастью, телефон Адольфа поддерживает JavaScript. Пожалуйста, напишите программу, которая набирает телефоны друзей Адольфа, кликая по клавиатуре. Все нужные номера записаны в блокноте. Несчастный конь будет вам очень благодарен!

Разбор


Вот как выглядит страничка, которая предлагалась в качестве входных данных:

tzb-ebphwda4nx25n2dlforpplo.png

Первая часть решения — извлекаем данные
Каждая из цифр номера телефона задается картинкой через background-image. При этом во время проверки цифры подставляются динамически. Если открыть отладчик в браузере и посмотреть на DOM-дерево страницы, то вы заметите, что на каждом DOM-элементе используются понятные классы:


В данном случае достаточно было просто создать словарь, извлечь CSS-классы и преобразовать их в строку с номером по словарю, исключив разделители (CSS-класс separator). Например, так:

const digits = document.querySelectorAll('.game .target .symbol:not(.separator)');
const dict = {
    'one': 1,
    'two': 2,
    'three': 3,
    'four': 4,
    'five': 5,
    'six': 6,
    'seven': 7,
    'eight': 8,
    'nine': 9,
    'zero': 0,
};
const phoneNumber = Array.from(digits).reduce((res, elem) => {
    for (const className of elem.classList) {
        if (className in dict) {
            res.push(dict[className]);
            break;
        }
    }
    return res;
}, []);


В результате получим такой массив:  [9, 8, 5, 4, 1, 2, 8, 0, 9, 0].

Вторая часть решения — повзаимодействовать с интерфейсом
На этом этапе у нас уже есть массив со всеми цифрами номера, и надо понять, какие кнопки на клавиатуре какой цифре соответствуют. Если мы посмотрим на HTML-код, то увидим, что никаких специальных классов-подсказок, обозначающих цифру, на клавиатуре нет:


Можно было бы просто вручную создать еще один словарь по индексу, но есть способ проще. Если посмотреть внимательно на стили в веб-инспекторе, то можно заметить, что цифра на кнопке задается через CSS-свойство content для псевдоэлемента : before. Например, для клавиши »3» стили выглядят так:

.key:nth-child(3):before {
    content: '3';
}


Чтобы получить значение этого свойства, воспользуемся методом window.getComputedStyle:

const keys = Array.from(document.querySelectorAll('.game .key')).reduce((res, elem) => {
    const key = window
        // Получаем CSSStyleDeclaration для псевдо-элемента
        .getComputedStyle(elem, ':before')
        // Получаем значение свойства
        .getPropertyValue('content')
        // Значение будет вместе с кавычками, поэтому не забываем убрать их
        .replace(/"/g, '');
    res[key] = elem;
    return res;
}, {});


В результате мы получим объект, где в качестве ключей будут выступать тексты на кнопках (цифра или «call»), а в качестве значений — DOM-узлы.

Остается только взять значения из первого массива (phoneNumber), пройтись по ним и прокликать соответствующие кнопки, используя наш словарь keys:

phoneNumber.push('call');

const call = () => {
    const event = new Event('click');
    const next = phoneNumber.shift();    
    keys[next].dispatchEvent(event);
    
    if (phoneNumber.length) {
        setTimeout(call, 100);
    }
}

call();


Обратите внимание: прежде чем сделать набор номера, мы добавляем в конец массива значение call. Этого требуют условия задачи, так как если набрать номер и не нажать «вызов», то решение не пройдет проверки.

Другая особенность — асинхронное нажатие на кнопки клавиатуры. Если временной интервал между нажатиями на клавиши при наборе номера меньше 50 мс, то такое решение тоже не пройдет проверки. Эта особенность не была описана в условиях к задаче явно, и мы ожидали, что участник выяснит это, прочитав исходный код страницы. Кстати, в исходном коде участников ждал небольшой сюрприз. ;)

Задача 6


Условие


Фёдор Ракушкин администрирует систему управления задач в своей компании. Сервер, на котором размещается система, вышел из строя (а бекапов Фёдор не делал).

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

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

Markdown-документ должен иметь следующий формат:

## Задачи
        
- %Название задачи 1%, делает %username1%, наблюдают: %username2%
- %Название задачи 2%, делает %username1%, наблюдают: %username2%, %username3%
- %Название задачи 3%, делает %username1% // у задачи нет наблюдателей
- %Название задачи 4%, наблюдают: %username1%, %username2%  // у задачи нет исполнителя
- %Название задачи 5% // у задачи нет исполнителя и наблюдателей
- %Название задачи 6%, наблюдают: %username1%
  - %Название подзадачи 1%, делает %username1% // подзадача
  - %Название подзадачи 2%
    - %Название подзадачи 3%, наблюдают: %username1%

## Пользователи
- %username1%
  * %Название задачи 1% // задачи, которые делает пользователь
  * %Название задачи 2%
  * %Название задачи 3%
  * %Название подзадачи 1%
- %username2%
  * %Название задачи 3%


Список задач, список исполнителей в задаче, список наблюдателей в задаче, список пользователей и список задач, которые делает пользователь, должны быть отсортированы лексикографически.

Описание структур в кэше


Пользователи хранятся в кэше в виде структуры типа `User`:

type User = {
    login: string;
    tasks: Task[];
    spectating: Task[];
};


Задачи хранятся в кэше в виде структуры типа `Task`:

type Task = {
    title: string;
    assignee: User;
    spectators: User[];
    subtasks: Task[];
    parent: Task | null;
};


Шаблон решения


Ваше решение должно содержать CommonJS-модуль, экспортирующий функцию, соответствующую следующей сигнатуре:

/**
 * @param {User|Task} data - последний отредактированный объект в кэше,
 * из которого нужно восстановить все возможные данные (User или Task)
 * @return {string}
 */
module.exports = function (data) {
    // ваш код
    return '…';
}


Примеры

// Пользователи в памяти
const User1 = { type: 'user', login: 'fedor', tasks: [], spectating: [] };
const User2 = { type: 'user', login: 'arkady', tasks: [], spectating: [] };
                                                                        
// Задачи в памяти
const Task1 = { type: 'task', title: 'Do something', assignee: null, spectators: [], subtasks: [], parent: null };
const Task2 = { type: 'task', title: 'Something else', assignee: null, spectators: [], subtasks: [], parent: null };
const Task3 = { type: 'task', title: 'Sub task', assignee: null, spectators: [], subtasks: [], parent: null };

// И связи между ними:
// Первую задачу делает первый пользователь
Task1.assignee = User1;
User1.tasks.push(Task1);
// ...а наблюдает за ней — второй
Task1.spectators.push(User2);
User2.spectating.push(Task1);

// Вторую задачу пока никто не делает,
// но первый пользователь за ней наблюдает
Task2.spectators.push(User1);
User1.spectating.push(Task2);

// Третья задача является подзадачей второй
Task3.parent = Task2;
Task2.subtasks.push(Task3);

// Известно, что последняя измененная структура — задача 3
const lastEdited = Task3;


Если вывести ссылку на `lastEdited` — получается такая структура:

{ type: 'task',
  title: 'Sub task',
  assignee: null,
  spectators: [],
  subtasks: [],
  parent:       
   { type: 'task',
     title: 'Something else',
     assignee: null,
     spectators:
      [ { type: 'user',
          login: 'fedor',
          tasks:
           [ { type: 'task',
               title: 'Do something',
               assignee: [Circular],
               spectators:
                [ { type: 'user',
                    login: 'arkady',
                    tasks: [],
                    spectating: [ [Circular] ] } ],
               subtasks: [],
               parent: null } ],
          spectating: [ [Circular] ] } ],
     subtasks: [ [Circular] ],
     parent: null } }


На выходе должен получиться текст в формате Markdown со всеми найденными задачами и пользователями, отсортированными в алфавитном порядке:

## Задачи

- Do something, делает fedor, наблюдают: arkady
- Something else, наблюдают: fedor
  - Sub task

## Пользователи

- arkady
- fedor
  * Do something


Разбор


Решение задачи состояло в том, чтобы восстановить данные графа по ссылке и затем сериализовать его в требуемом формате.

Это можно сделать, например, через обход графа в ширину и в глубину. Напишем код для обхода и соберем все данные, которые нужно будет вывести:

/**
 * Абстрактный обходчик, подходящий для всех вариантов
 * @param {{ref: object, visited: ?boolean}} ctx
 * @param {object} handlers — обработчики для каждого типа
 */
function traverse(ctx, handlers) {
    // Не падаем в случае, если ctx.ref пустой, — например, в случае вызова с task.parent
    if (!ctx.ref) {
        return;
    }           

    // Предотвращаем обход узлов, сохраняя все посещенные узлы, используя контекст и набор уже посещенных
    const visited = ctx.visited || new Set();
    if (visited.has(ctx.ref)) {
        return;
    }
    visited.add(ctx.ref);

    // Вызываем обработчик для исходного типа узла
    handlers[ctx.ref.type](ctx.ref, goDeeper);

    // Промежуточная функция, позволяющая рекурсивно пойти глубже в обработчиках
    function goDeeper(subrefs) {
        // Запускаем для каждого подузла нужный обработчик
        for (const subref of [].concat(subrefs)) {
            traverse({ visited, ref: subref }, handlers);
        }
    }
}

// Коллекции для пользователей и задач
const users = [];
const tasks = [];

// ref в начале — ссылка на переданный узел
traverse({ ref }, {
    user(user, deeper) {
        users.push(user);
        deeper(user.tasks); // to task.assignee
        deeper(user.spectating); // to task.spectators
    },
    task(task, deeper) {
        tasks.push(task);
        deeper(task.assignee); // to user.tasks
        deeper(task.spectators); // to user.spectating
        deeper(task.parent); // to task.subtasks
        deeper(task.subtasks); // to task.parent
    }
);


В итоге получилась коллекция пользователей и задач. Далее, согласно условию задачи, нужно было отсортировать коллекции…

users.sort((u1, u2) => u1.login < u2.login ? -1 : (u1.login > u2.login ? 1 : 0));
tasks.sort((t1, t2) => t1.title < t2.title ? -1 : (t1.title > t2.title ? 1 : 0));


… после чего — вывести их в нужном формате:

// форматирует строку задачи
const taskLine = t => `${
    t.title
}${     
    t.assignee ? `, делает ${t.assignee.login}` : ''
}${
    t.spectators.length ? `, наблюдают: ${t.spectators.map(u => u.login).join(', ')}` : ''
}`;

function renderTasks (parent = null, indent = 0) {
    return tasks
        .filter(t => t.parent === parent) 
        .map(t => [
            '\n',
            ' '.repeat(indent), // отбивка
            '- ', taskLine(t), // вывод названия задачи
            t.subtasks.length ? printTasks(t, indent + 2) : '' // подзадачи рекурсивно
        ].join(''))
        .join('');
}

function renderUsers () {
    return ${users.map(u => `\n- ${u.login}${
        u.tasks.map(t => `\n  * ${t.title}`).join('')
    }`).join('')}
}

const result =  `
## Задачи
${renderTasks()}
## Пользователи
${renderUsers()}
`.trim();


Задача 7. Здесь могла быть ваша реклама


Условие


Представьте, что вы работаете в Яндексе и верстаете баннеры для Яндекс.Директа. Вы получили задание доработать шаблон баннера:

3kiftxvr-zioqome_lll3jtsirg.png

Недавно разработчики бэкенда изменили формат данных, которые приходят с сервера. Максимальный размер заголовка увеличился на несколько символов, и теперь длинные заголовки не помещаются в отведенное для них место между левой границей баннера и кнопкой «Перейти».

gdkkcqwr9zs3qa2ta2_oyz1e9o0.png

Дизайнер решил перенести кнопку в правый нижний угол баннера.

zbyxgz3jdls7g5iu-yro6jxp7xy.png

Исправьте шаблон баннера, чтобы он соответствовал новому макету. Нужно, чтобы текст обтекал кнопку слева и сверху. Текст баннера подставляется в шаблон динамически и может быть любым. Баннер всегда имеет одинаковый размер. В шаблоне можно использовать только HTML и CSS. Использовать JavaScript и картинки нельзя.

Примечательно, что это одна из реальных задач, которая когда-то возникла при верстке рекламных баннеров. Если изучить сайты РСЯ, можно встретить похожий баннер и подсмотреть там решение.

Разбор


Для решения задачи нужно написать всего несколько строчек HTML/CSS, но придумать, что написать, не так-то просто.

Единственное средство CSS, влияющее на обтекание блоков текстом, — это float, но его недостаточно для решения задачи. Свойство float влияет на элементы, которые идут после текущего, а в задании нужно сделать обтекание текстом сверху.

Первый вариант решения — добавить над кнопкой распорку нулевой ширины, которая сдвинет кнопку вниз. (Пример на jsfiddle.net.)

Еще один вариант — сдвинуть весь контент родителя вниз при помощи свойства padding-top, а после этого вложенный блок с текстом сдвинуть на такое же расстояние вверх при помощи свойства margin-top (задать отрицательное значение). В этом случае не понадобится дополнительный DOM-элемент (распорка). (Пример.)

Задача 8. Сверстать макет


Условие


Ефросинья работает HTML-верстальщиком. Дизайнер прислал ей очередной макет, который нужно сверстать. Ефросинья еще не закончила предыдущую задачу, а верстка нового макета нужна очень срочно. Помогите Ефросинье сделать работу вовремя.

Верстка должна в точности соответствовать макету по этой ссылке (pixel perfect). В верстке нельзя использовать изображения.

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

На макете не элементы интерфейса, как могло показаться из условия, а рисунок из линий. Линий довольно много, и их пересечения трудно одновременно удержать в голове. Мы старались, чтобы макет выглядел абсурдно: на нем изображена какая-то неведомая абракадабра, но проставлены размеры, как это делают дизайнеры.

Кажется, нам удалось достичь цели. У всех, кому мы показывали задачу, макет вызывал удивление.

Разбор


Так как по условию задачи в верстке нельзя было использовать картинки, единственный способ ее решить — сверстать фигуру блочными элементами. Средства для рисования — CSS-свойства border (цвет и толщина),  background (фоновый цвет или градиенты с резкими границами) и box-shadow (тени с резкими границами и отступами).

Из-за большого количества линий на макете вариант решения «в лоб» (сверстать каждую линию блочным элементом с фоном и абсолютным позиционированием) занял бы много времени. Мы рассчитывали, что участники выделят и сверстают повторяющиеся фрагменты макета, а потом скомбинируют их для получения финального изображения.

Пример решения — считать, что макет состоит из квадратных фрагментов 70 на 70 пикселей. Фрагменты могут быть трех видов: угловой, боковой и пустой. Написав CSS-классы для каждого из этих фрагментов, а также CSS-классы для их поворота, можно легко составить из них нужное изображение.

hwfhko2djj5sbammehzss2qkkhm.png

Еще один вариант решения — собрать изображение из больших квадратов 210 на 210 пикселей, перекрывая места их пересечения маленькими полосатыми квадратами 70 на 70 пикселей.

qyqpelprwqkmw_ka5io82h_vk2e.png

Задача 9. Злоключения Адольфа


Условие


Конь Адольф — владелец телефона с пользовательским интерфейсом, сделанным на веб-технологиях. Адольф научился набирать номера на телефоне с помощью программы на JavaScript, которая нажимает кнопки интерфейса в нужном порядке. Для набора номера ему больше не нужно мучиться, пытаясь попасть в кнопки копытами.

Но случилась беда: от частых разговоров телефон перегрелся и сгорел.

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

Постоянно переписывать программу набора Адольфу не хотелось, ограничивать себя в беседах — тоже. Помочь бедному Адольфу вызвался его друг — опоссум Фридрих. Он рассказал Адольфу, что производитель телефонов поддерживает JavaScript API и обещает сохранение обратной совместимости. Чтобы упростить набор номеров, Фридрих написал веб-сервер, управляющий телефоном, и добавил функцию быстрого набора.
Быстрый набор позволяет хранить в телефоне до 10 номеров и звонить, отправляя на телефон HTTP-запрос с цифрой нужного номера.

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

Помогите коню Адольфу убрать ошибки из кода веб-сервера.

Примечания:
— API поставляется npm-пакетом @yandex-blitz/phone.
— Документация к API.
— Код веб-сервера, написанный Фридрихом:  task.js.
— Исправлять и тестировать код веб-сервера удобно в runkit-блокноте.

В качестве решения предоставьте файл с кодом веб-сервера, в котором исправлены ошибки.

Разбор


Первые два красных теста исправляются довольно тривиально — возвращаем потерянный в обработчике GET-запросов return и пишем обработку ошибок для вызова connect.


С последним тестом чуть сложнее: ошибка происходит из-за асинхронности. Если два запроса на запись приходят одновременно, в обработчиках каждого из них считается текущее состояние хранилища, а потом хранилище будет дважды перезаписано. В итоге в нем останется информация только о последнем записанном номере.


Чтобы этого избежать, можно упорядочить обработку запросов: не исполнять их сразу, а отправлять в очередь и исполнять по одному. Заведем очередь и функцию ее обработки:

const writeQueue = [];    
        
const processQueue = () => {
    if (writeQueue.length) {
        const fn = writeQueue.shift();

        fn().then(() => {
            processQueue();
        });
    }
}


Чтобы ограничить количество исполняемых одновременно записей, заведем флаг «происходит ли сейчас обработка элемента».

const writeQueue = [];
let isWriteInProgress = false;
        
const processQueue = () => {
    if (isWriteInProgress) {
        return;
    }

    if (writeQueue.length) {
        isWriteInProgress = true;

        const fn = writeQueue.shift();

        fn().then(() => {
            isWriteInProgress = false;
            processQueue();
        });
    }
}


Наконец, в обработчике POST-запросов на запись номеров в хранилище будем добавлять элементы в очередь. На случай, если очередь была пустой, будем запускать обработку очереди.

app.post("/speeddial/:digit/:phonenumber", (req, res) => {
    writeQueue.push(makeWriteJob(phone, req, res));
    processQueue();
});


Полный код решения:

const express = require('express');
const { BEEP_CODES } = require('@yandex-blitz/phone');
                                
const writeQueue = [];
let isWriteInProgress = false;

const processQueue = () => {
    if (isWriteInProgress) {
        return;
    }

    if (writeQueue.length) {
        isWriteInProgress = true;

        const fn = writeQueue.shift();

        fn().then(() => {
            isWriteInProgress = false;
            processQueue();
        });
    }
}

const makeWriteJob = (phone, req, res) => {
    return () => {
        return phone.getData()
            .then(value => {
                const speeddialDict = JSON.parse(value);

                speeddialDict[req.params.digit] = Number(req.params.phonenumber);

                return phone
                    .setData(JSON.stringify(speeddialDict))
                    .then(() => phone.beep(BEEP_CODES.SUCCESS))
                    .then(() => {
                        res.sendStatus(200);
                    })
            })
            .catch(e => {
                phone.beep(BEEP_CODES.ERROR).then(() => {
                    res.sendStatus(500);
                })
            })
    }
};

const createApp = ({ phone }) => {
    const app = express();

    // звонит по номеру, записанному в «быстром наборе» под цифрой digit
    app.get("/speeddial/:digit", (req, res) => {
        phone.getData().then(value => {
            const speeddialDict = JSON.parse(value);

            return phone.connect()
                .then(async () => {
                    await phone.dial(speeddialDict[req.params.digit]);

                    res.sendStatus(200);
                }, async() => {
                    await phone.beep(BEEP_CODES.FATAL);

                    res.sendStatus(500);
                });
        }).catch(async (e) => {
            await phone.beep(BEEP_CODES.ERROR);

            res.sendStatus(500);
        });
    });

    // записывает в «быстрый набор» под цифру digit номер phonenumber
    app.post("/speeddial/:digit/:phonenumber", (req, res) => {
        writeQueue.push(makeWriteJob(phone, req, res));
        processQueue();
    });

    return app;
};

exports.createApp = createApp;


Задача 10. Лабиринт


Условие


Системный администратор Евгений очень любит свою работу. Но еще больше он любит старые восьмибитные игры, поэтому в свободное время решил сделать игру «Лабиринт».

Правила игры простые:
— Лабиринт состоит из стенок, пустых полей и выхода.
— В лабиринте есть шарик, который может кататься по пустым полям, пока не упрется в стенку.
— Перемещением шарика можно управлять при помощи наклона поля в одну из четырех сторон, при этом шарик будет катиться, пока может.
— Чтобы наклонить поле, надо использовать кнопки управления: ← ↓ ↑ →.
— Когда шарик попадает на клетку выхода — игра закончена, с этого момента он не должен реагировать на кнопки управления.

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

Помогите Евгению дописать игру.

Все карты в наших тестах имеют прямоугольную форму и ограничены по периметру стенками.

Решение должно представлять из себя один HTML-файл (все нужные скрипты и стили должны содержаться внутри).

После того как игра будет проинциализирована, надо вызывать глобальную функцию window.onMazeReady (). Только после этого будет запущено автоматическое тестирование вашего решения. Если вызов функции не произойдет в течение 2 минут, задание считается невыполненным.

Контейнер с игровым полем должен иметь CSS-класс map.

Кнопки управления должны реагировать на событие click и иметь следующие CSS-классы:
— влево — control_direction_left,
— вниз — control_direction_down,
— вверх — control_direction_up,
— вправо — control_direction_right.

CSS-стили для градиента на шарике:

background: radial-gradient(circle at 5px 5px, #eee, #000);


Поле наклоняется в каждую из сторон на 25 градусов с центральной перспективой, удаленной на 500 пикселей. Например:

ofho_49u3jdimgdt1befkrkxcxs.png

ugvy7fgr1bh7cqbpwlzfmsyofeu.png

2zu1nroaxbrg_zcft6lb3hp6rzg.png

6nprmdy2_skg53_cesb3c4rftvi.png

Карта лабиринта доступна в поле window.map типа String. Следующие символы являются специальными:
# — стенка
. — пустое место
o — шарик
x — выход

Каждая строка, содержащая хотя бы один специальный символ, является линией лабиринта, а каждый символ — полем лабиринта. Любые

© Habrahabr.ru