
Очередная надуманная задачка по ненормальному программированию на 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 символов. Как и ранее рассмотренные задачи, эта может иметь и несколько вариаций на тему, и не единственное решение. Достаточно лишь придумать простую формулировку и ключевую фразу. До встречи в новом году!
