Занимательный JavaScript: Снежный день

Image

Очередная надуманная задачка по ненормальному программированию на 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 прямо сейчас.

Для решения задачи нужно избавиться от лишней латиницы в следующем:


  1. Ключевое слово function в декларации функции.
  2. Ключевое слово let (или var) для объявления переменных.
  3. Ключевые слова при организации цикла для итерации по переданному массиву.
  4. Формирование текста сообщения.
  5. Вызов некоторой функции для вывода результата.

С первой проблемой нам легко помогут стрелочные функции:

(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} Параметр-массив.
an {Any} Параметр-элемент массива.
wo {function} Функция word для формирования слов.
w {string} Локальная переменная для аккумуляции строки в word.
_10 {Array} Исходный массив пользователей.
_1 {string} Пользователь из исходного массива, его имя.

Вот и все. Пишите свои идеи и мысли по этому поводу, так как здесь много вариантов сделать что-то иначе или совсем не так.


Заключение

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

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

© Habrahabr.ru