Всем привет)
Сегодня я покажу одну из задач, предложенных на Yandex Cup "20. Она совсем несложная, но на мой взгляд интересная. Этап пробный, поэтому я думаю нет ничего страшного в том, что я расскажу как решил ее. Но если вы хотите решить ее сами, закройте статью прямо сейчас.
Для ее просмотра надо зарегистрироваться на контест, а копировать ее сюда я не хочу. Если вкратце, суть задачи в следующем:
Имеется какой-то JS-объект отладочной информации и мы должны отобразить ее на экранчике 300х96 пикселей в виде баркода. Что то типа такого:
Вот структура:
%lang(js)% { /** * Идентификатор — строка [A-Za-z0-9], len=10 */ id: string; /** * Код ошибки — целое число от 0 до 999 */ code: number; /** * Сообщение об ошибке — строка [A-Za-z0-9 ], 0 <= len <= 34 */ message: string; }
Структура преобразуется в строку (массив байтов) путем конкатенации компонент (плюс, добавляем незначащие нули и пробелы где это нужно).
Далее мы считаем контрольную сумму всех элементов путем сложения по модулю 2 всех элементов массива. Если мы все сделали правильно, мы получим массив из 48 элементов, где каждый элемент принадлежит диапазону .
Для начала рассмотрим вот такие функции:
%lang(js)% const fixCode = (code) => { if (code < 10) return `00${code}`; if (code < 100) return `0${code}`; return String(code); } const fixMessage = (message) => { let newMessage = message; while (newMessage.length < 34) newMessage += ' '; return newMessage; }
Они нужны для того, чтобы наше итоговое сообщение всегда было длиной 48 символов.
Нам нужно как то переводить строку в массив байт и считать контрольную сумму. Для этого мы будем использовать вот эти две функции
%lang(js)% /* * Мы превращаем строку в массив строк длиной=1 (грубо говоря, массив символов) * затем проходим по каждому элементу получившегося массива с целью * узнать код символа, находящегося в позиции 0 */ const toByteArray = (message) => message.split('').map(x => x.charCodeAt(0)); /* * Тут мы делаем побитовое исключающее ИЛИ для подсчета контрольной суммы */ const getChecksum = (byteArray) => byteArray.reduce((acc, b) => acc ^= b, 0);
Для рендера кода я решил использовать HTML5 Canvas, потому что двигать div`ы мне не очень то и хотелось. К тому же, решение должно быть предоставлено в виде только JS кода, поэтому все стили пришлось бы писать там же.
%lang(js)% const prepareBarcodeDOM = (barcodeElement) => { // создаем новый canvas элемент const canvas = document.createElement('canvas'); // устанавливаем ему ширину и высоту с помощью атрибутов canvas.setAttribute('width', '300px'); canvas.setAttribute('height', '96px'); /* * на всякий случай устанавливаем ширину и высоту div контейнера * не знаю точно как происходит проверка решения, но div растягивается * на всю ширину (т.к. это блочный элемент) и, возможно, робот, * проверяющий решение посчитал бы, что я сгенерировал баркод, * ширина которого явно больше 300 пикс. */ barcodeElement.style.width = '300px'; barcodeElement.style.height = '96px'; // присоединяем canvas к DOM в качестве ребенка barcodeElement barcodeElement.append(canvas); // получаем 2d контекст canvas const context = canvas.getContext('2d'); // будем рисовать только черным context.fillStyle = 'rgb(0, 0, 0)'; // вернем контекст (он нам еще пригодится) return context; }
Контекст нам пригодится для рисования, вот кстати функция, которая отвечает за это:
%lang(js)% const draw = (ctx, buffer) => { /* * по условию задачи нас просят нарисовать "зебру" из 3 черных и 2 белых линий * c двух сторон баркода. * При этом ширина черной линии - 4 пикс., белой - 5 пикс. */ [0, 9, 18, 296, 287, 278].forEach(x => ctx.fillRect(x, 0, 4, 96)); /* * Потом мы просто бежим по всему экрану, но не по отдельным пикселям, а по * блокам 8х8 */ for (let i = 0; i < 12; i += 1) { for (let j = 0; j < 32; j += 1) { /* * Эта страшная запись проверяет каждый бит (j / 8 + i * 4)-го * элемента нашего массива (обращаю внимание, что j / 8 - * операция целочисленного деления * * (7 - (j % 8)) - эта штука считает сдвиг, при этом * порядок проверки от седьмого бита к нулевому, а не наоборот */ if ((buffer[Math.floor(j / 8) + i * 4] >> (7 - (j % 8))) & 1) { /* * Рисуем квадратик 8х8, если в конкретном бите конкретного байта * входных данные едицица, иначе ничего не делаем * 22 - это смещение вправо из за 5 полосок (сумма их ширин) */ ctx.fillRect(j * 8 + 22, i * 8, 8, 8); } } } }
Выглядит сложночитаемо, не правда ли? Довольно забавная ситуация на самом деле. Дело в том, что я создавал константы для этого, но программа не прошла по лимиту памяти, лол. Потом я убрал константы и некоторые промежуточные присвоения и смог сдать решение.
Ну и, собственно, штука, которая все это объединяет:
%lang(js)% const renderBarcode = ({ id, code, message }, el) => { const buffer = toByteArray(`${id}${fixCode(code)}${fixMessage(message)}`); buffer.push(getChecksum(buffer)); draw(prepareBarcodeDOM(el), buffer); }
Входные данные поступают в виде вышеописанного объекта (и мы сразу достаем эти поля с помощью деструктуризации). Затем мы конкатенируем (склеиваем воедино) строки, предварительно дополнив их (code незначащими нулями, а message пробелами). Сейчас в нашем массиве должно быть 47 элементов и мы добавляем еще один - контрольную сумму. Ну и в конце концов вызываем draw().
Тестируем:
%lang(js)% { "id": "VladIvanov", "code": 999, "message": "Thank you for reading!" }
Как-то так:
Спасибо за внимание!