Очередная надуманная задачка по ненормальному программированию на JavaScript. В этот раз по случаю грядущего Нового 2019 года. Надеюсь, будет так же интересно решать, как мне было интересно придумывать. Любопытных прошу под кат. Всем шампанского и всех с наступающим!
Предыдущие задачи:
Формулировка
За уходящий год Дед Мороз собрал порядочный список имен нормальных разработчиков и теперь планирует написать программу для поздравления. Формат такой:happy new year, ${username}!
. Но вот незадача: клавиатура сбоит и не позволяет ввести многие символы латиницы. Исследовав дефект, эльфы сделали интересное наблюдение, что из того, что еще работает, можно сложитьSnowing day
. Источник вывода можно выбрать на свое усмотрение.
Итак, на входе — некоторый массив не пустых строк (имя не может быть пустым). Требуется написать программу, используя из латиницы только символы: S
, n
, o
, w
, i
, g
, d
, a
, y
(всего 9 символов, один из которых в верхнем регистре). Программа должна обойти переданный массив и вывести для каждого имени фразу happy new year, ${username}!
, используя какой-либо источник вывода: alert, console.log или что придет на ум. Ну и хорошо бы не загрязнять глобальный контекст.
Привычное решение
Если ничего не придумывать, то все очень просто:
function happy(users) {
for (let i = 0; i !== users.length; i += 1) {
console.log(`happy new year, ${users[i]}!`);
}
}
или лучше так:
function happy(users) {
users.forEach(user => console.log(`happy new year, ${user}!`));
}
Используем с нашим массивом, пусть это будет users:
let users = ['John', 'Jack', 'James'];
happy(users);
// happy new year, John!
// happy new year, Jack!
// happy new year, James!
Но тут надумано: использовать в реализации из латиницы только разрешенные символы. Попробуйте сначала справиться самостоятельно, а потом присоединяйтесь к рассуждениям.
Занимательное решение
Нетерпеливые могут посмотреть представленное ниже решение в JSFiddle прямо сейчас.
Для решения задачи нужно избавиться от лишней латиницы в следующем:
- Ключевое слово function в декларации функции.
- Ключевое слово let (или var) для объявления переменных.
- Ключевые слова при организации цикла для итерации по переданному массиву.
- Формирование текста сообщения.
- Вызов некоторой функции для вывода результата.
С первой проблемой нам легко помогут стрелочные функции:
(arr => el.forEach(/* ... */))(users);
Не будем сейчас обращать внимание на имена переменных, так как их мы легко переименуем в самом конце.
Используем "стрелки" с IIFE везде, где понадобится функция или сразу ее результат. Кроме того, функции позволяют избавиться и от директив let и var двумя способами:
(param => /* ... */)(value);
((param = value) => /* ... */)();
В обоих вариантах мы объявляем переменную в параметрах функции. Только в первом случае мы передаем значение при вызове функции, а во втором — используем параметр функции по умолчанию.
Действительно проблемы начинаются на третьем пункте. Нам не хватает символов ни для классических циклов for, do, while, ни для вариантов обхода через for..in и for..of, ни для методов массива forEach, map, filter (где можно передать колбэк). Но мы можем реализовать свою функцию итерации по массиву:
function iterate(arr, consume) {
function iter(i) {
if (arr[i]) {
consume(arr[i]);
iter(++i);
}
}
iter(0);
}
Мы рекурсивно обходим элементы, пока проверка текущего в условии не отвалится. Почему мы можем полагаться здесь на логическое преобразование? потому что элемент нашего массива — это не пустая строка (только она оборачивается false), а при выходе за массив через инкремент индекса, мы получим undefined (приводится к false).
Перепишем функцию с помощью "стрелочных" выражений:
let iterate = (arr, consume) => (
(iter = i => {
if (arr[i]) {
consume(arr[i]);
iter(++i);
}
}) => iter(0)
)();
Но мы не можем использовать if statement, так как у нас нет символа f
. Чтобы наша функция удовлетворяла условию, от него надо избавиться:
let iterate = (arr, consume) => (
(iter = i => arr[i] ? (consume(arr[i]), iter(++i)) : 0) => iter(0)
)();
В этом нам помог тернарный оператор и возможность объединить два выражения в одно через comma operator. Воспользуемся этой функцией далее при компоновке решения.
Проблема четвертая связана с тем, что нам в любом случае требуется получить строку с отсутствующими символами. Очевидно, что мы будем использовать числа для представления символов. Вариантов тут несколько:
- Функция String.fromCharCode, которая ожидает на вход целые number и возвращает строку, созданную из указанной последовательности Юникода.
- Escape-последовательность
\uhhhh
позволяет вывести любой символ Юникода по указанному шестнадцатиричному коду. - Формат
&#dddd;
для html-символов позволяет вывести в документ страницы символ по указанному десятичному коду. - Функция toString объекта-прототипа Number имеет дополнительный параметр radix — основание системы счисления.
- Возможно есть что-то еще...
Вы можете самостоятельно покопать в сторону первых трех вариантов, а сейчас рассмотрим вероятно самый простой для этой задачи: Number.prototype.toString. Максимальное значение параметра radix — 36 (10 цифр + 26 символов латиницы в нижнем регистре):
let symb = sn => (sn + 9).toString(36);
Таким образом мы можем получить любой символ латиницы по номеру в алфавите, начиная с 1. Единственное ограничение в том, что все символы в нижнем регистре. Да, нам этого достаточно для выводимого текста в сообщении, но некоторые методы и функции (тот же forEach) мы уже сложить не сможем.
Но рано радоваться, сначала надо избавиться от toString в записи функции. Первое — это обратимся к методу следующим образом:
let symb = sn => (sn + 9)['toString'](36);
Если присмотреться, то для строки toString
нам не хватает всего двух символов: t
и r
: все остальные есть в слове Snowing
. Получить их довольно просто, так как их порядок уже намекает на true
. Используя неявные приведения типов, получить эту строку и нужные нам символы можно так:
!0+''; // 'true'
(!0+'')[0]; // 't'
(!0+'')[1]; // 'r'
Добиваем функцию получения любой буквы латиницы:
let symb = sn => (sn + 9)[(!0+'')[0] + 'oS' + (!0+'')[0] + (!0+'')[1] + 'ing'](36);
Чтобы получать слова по массиву порядковых номеров букв с помощью symb, воспользуемся стандартной функцией Array.prototype.reduce:
[1,2,3].reduce((res, sn) => res += symb(sn), ''); // 'abc'
Да, это нас не устраивает. Но в нашем решении мы можем сделать что-то похожее с помощью функции iterate:
let word = chars => (res => (iterate(chars, ch => res += symb(ch)), res))('');
word([1,2,3]); // 'abc'
Внимательные отметят, что функцию iterate мы разрабатывали для массива строк, а используем тут с числами. Именно поэтому начальный индекс нашего алфавита — 1, а не 0. Иначе импровизированный цикл завершался бы, встретив 0 (буква a
).
Для простоты сопоставления символов их порядковым номерам можно получить словарь:
[...Array(26).keys()].reduce((map, i) => (map[symb(i + 1)] = i + 1, map), {});
// {a: 1, b: 2, c: 3, d: 4, e: 5, …}
Но разумнее поступить еще проще и написать функцию обратного преобразования слов целиком:
let reword = str => str.split('').map(s => parseInt(s, 36) - 9);
reword('happy'); // [8,1,16,16,25]
reword('new'); // [14,5,23]
reword('year'); // [25,5,1,18]
Завершаем функцией формирования самого сообщения:
let message = name => word([8,1,16,16,25]) + ' ' + word([14,5,23]) + ' ' + word([25,5,1,18]) + ', ' + name + '!';
Осталось совсем немного — разобраться с выводом в пятой проблеме. На ум приходят console, alert, confirm, prompt, innerHTML, document.write. Но ни к одному из перечисленных вариантов не подобраться напрямую.
Еще у нас появилась возможность получать любое слово с помощью функции word. Это значит, что мы можем вызывать многие функции от объектов, обращаясь к ним через квадратные скобки, как это было с toString.
Учитывая, что мы используем стрелочные функции, контекст this остается глобальным (и пробрасывать его не надо). В любом месте мы можем обратиться ко многим его функциям через строку:
this[word([1,12,5,18,20])]('hello'); // alert('hello');
this[word([3,15,14,19,15,12,5])][word([12,15,7])]('hello'); // console.log('hello');
Но для "начертания" this
нам опять таки не хватает символов. Мы можем заменить его на Window.self, но с ним еще хуже в плане доступного алфавита. Однако, стоит обратить внимание на сам объект window, "начертание" которого нас вполне устраивает, хоть, козалось бы, и порядком длиннее!
К слову, в первой версии задачи ключевой фразой было только слово Snowing
, и window
было не сложить (из-за отсутствия символа d
). Доступ к контексту основывался на одном из трюков jsfuck:
(_ => 0)['constructor']('return this')()['alert']('hello');
Или так же можно получить доступ к чему-либо в глобальном контексте напрямую:
(_ => 0)['constructor']('return alert')()('hello');
Как видно, в примерах вся латиница — в строках. Здесь мы создаем функцию из строки, а доступ к Function (конструктору) получаем от впустую созданной стрелочной функции. Но это как-то уже перебор! Может кто-то знает еще способы получить доступ к контексту в наших условиях?
Наконец, собираем все вместе! Тело нашей "main"-функции будет вызывать iterate для переданного массива, а консьюмером будет вывод результата уже встроенной функции формирования сообщения. Для текста сообщения и команд используется одна функция word, которой тоже необходим iterate, и мы определим ее следом в default parameters. Вот так:
(users => (
((
// firstly we need the iterating function
iterate = (array, consume) =>
((iter = i => array[i] ? (consume(array[i]), iter(++i)) : 0) => iter(0))(),
// then we determine the word-creating function
word = chars => (res =>
(iterate(chars, ch => res +=
(ch + 9)[(!0+'')[0] + 'oS' + (!0+'')[0] + (!0+'')[1] + 'ing'](36)
), res)
)('')
) => iterate(users, name =>
// using console.log in window for printing out
window[word([3,15,14,19,15,12,5])][word([12,15,7])](
word([8,1,16,16,25]) + ' ' + word([14,5,23]) + ' ' + word([25,5,1,18]) + ', ' + name + '!'
)
))()
))(users);
Переименовываем переменные, используя разрешенный алфавит:
(_10 => (
((
_123 = (ann, snow) =>
((_12 = i => ann[i] ? (snow(ann[i]), _12(++i)) : 0) => _12(0))(),
wo = ann => (w =>
(_123(ann, an => w +=
(an + 9)[(!0+'')[0] + 'oS' + (!0+'')[0] + (!0+'')[1] + 'ing'](36)
), w)
)('')
) => _123(_10, _1 =>
window[wo([3,15,14,19,15,12,5])][wo([12,15,7])](
wo([8,1,16,16,25]) + ' ' + wo([14,5,23]) + ' ' + wo([25,5,1,18]) + ', ' + _1 + '!'
)
))()
))(users);
Переменная | Описание |
---|---|
_123 {function} |
Функция iterate для итерации по элементам массива. |
_12 {function} |
Локальная функция iter, которую рекурсивно вызывает iterate. |
snow {function} |
Функция consume как колбэк для iterate. |
ann {Array<Any>} |
Параметр-массив. |
an {Any} |
Параметр-элемент массива. |
wo {function} |
Функция word для формирования слов. |
w {string} |
Локальная переменная для аккумуляции строки в word. |
_10 {Array<string>} |
Исходный массив пользователей. |
_1 {string} |
Пользователь из исходного массива, его имя. |
Вот и все. Пишите свои идеи и мысли по этому поводу, так как здесь много вариантов сделать что-то иначе или совсем не так.
Заключение
Интересно, что придумать слово или фразу для условия задачи оказалось настоящим испытанием. Хотелось, чтобы она была и короткой, и не сильно подсказывала, и подходящей для более-менее лаконичного решения.
Вдохновением для этой задачи послужили функциональные возможности JavaScript и известная многим изотерика 6 символов. Как и ранее рассмотренные задачи, эта может иметь и несколько вариаций на тему, и не единственное решение. Достаточно лишь придумать простую формулировку и ключевую фразу. До встречи в новом году!