Полный список актуальных статей:

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

Как и в предыдущих статьях, я собираюсь поместить входные данные в файл data.txt, импортировать его и создать конвейер функций с помощью Ramda. Давайте также разделим ввод на отдельные строки:

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  console.log
)(data);

Следующий шаг немного сложен: нам нужно разделить каждую строку пополам, чтобы получить две строки одинаковой длины:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

R.pipe(
  R.split("\n"),
  R.map(
    R.pipe(
      R.juxt([
        R.pipe(R.prop("length"), flippedDivide(2)),
        R.identity
      ]),
      R.apply(R.splitAt),
      R.map(R.split("")),
      R.apply(R.intersection)
    )
  ),
  console.log
)(data);

Давайте взглянем на вложенный вызов pipe — поскольку для разделения строки пополам нам понадобятся две вещи (сама строка и ее длина), мы будем использовать juxt (о котором я уже говорил в «предыдущем статья"). Первая функция, переданная juxt, извлекает длину строки и делит ее на два; второй передает строку дальше. Порядок функций, переданных в juxt, не случаен — это то, что нам нужно передать в splitAt, чтобы разбить строку по заданному индексу (что составляет половину длины, которую мы вычислили ранее).

Затем мы можем продолжить разбивать строки на отдельные буквы (с помощью вызова split("")), а затем мы передаем оба массива букв в intersection, который возвращает массив со всеми элементами, присутствующими в обоих входных массивах. Что нам и нужно было сделать!

Далее, поскольку теперь у нас есть массив массивов букв, нам нужно сделать одно небольшое преобразование:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

R.pipe(
  R.split("\n"),
  R.map(
    R.pipe(
      R.juxt([
        R.pipe(R.prop("length"), flippedDivide(2)),
        R.identity
      ]),
      R.apply(R.splitAt),
      R.map(R.split("")),
      R.apply(R.intersection)
    )
  ),
  R.unnest,
  console.log
)(data);

unnest в Ramda просто удаляет вложенные массивы, так что теперь у нас есть массив букв, для которого мы можем вычислить значение приоритета каждой буквы:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

R.pipe(
  R.split("\n"),
  R.map(
    R.pipe(
      R.juxt([
        R.pipe(R.prop("length"), flippedDivide(2)),
        R.identity
      ]),
      R.apply(R.splitAt),
      R.map(R.split("")),
      R.apply(R.intersection)
    )
  ),
  R.unnest,
  R.map(
    R.pipe(
      letterToCharCode,
      R.cond([
        [flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
        [flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
      ])
    )
  ),
  console.log
)(data);

Ну, это много нового кода. Давайте посмотрим, что здесь произошло. В задании указано, что:

  • Типы элементов нижнего регистра с a по z имеют приоритеты от 1 до 26.
  • Типы элементов с A по Z в верхнем регистре имеют приоритеты от 27 до 52.

Учитывая, что сначала нам нужно перевести букву в числовое значение. Давайте возьмем за основу код таблицы ASCII. Мы создаем следующую пользовательскую функцию:

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

Функция invoker позволяет вызывать метод объекта для любой цели (конечно, вызов ее для объекта, не имеющего этого метода, приведет к ошибке). Все, что нам нужно сделать, это указать арность метода и его имя. Поскольку мы работаем только с буквами (или, точнее, со строками длиной 1), нас интересует только код символа с индексом 0. Следовательно, мы можем использовать функцию Ramda call, чтобы уже передать первый аргумент 0 в метод charCodeAt — теперь нам просто нужно вызвать метод результата с рассматриваемой строкой.

Теперь мы можем передать значение буквы в вызов cond (я также описываю call в предыдущей статье). Давайте взглянем:

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

// ...

R.cond([
  [flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
  [flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
])

Это много новых констант и функций, но это очень просто. Поскольку значение приоритета для каждой буквы, определенной в задаче, не равно символьному коду ASCII, и, кроме того, в отличие от таблицы ASCII, в задаче прописные буквы имеют более высокое значение, чем строчные, нам необходимо обеспечить механизм перевода для каждого из наборов. Для этого нам нужно знать смещения (значение приоритета ниже символьного кода ASCII на 38 и 96 для прописных и строчных букв соответственно) и диапазоны символьного кода для каждого из наборов (чтобы иметь возможность определить, какой набор, с которым мы имеем дело). Нам также нужны перевернутые версии функций includes и subtract.

Мы передаем каждую букву в вызов cond, и если UPPERCASE_RANGE включает букву, мы subtract UPPERCASE_OFFSET получаем значение приоритета, определенное в задаче. Теперь у нас есть массив всех значений! Осталось сделать только одно (что уже не должно быть сюрпризом):

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

R.pipe(
  R.split("\n"),
  R.map(
    R.pipe(
      R.juxt([
        R.pipe(R.prop("length"), flippedDivide(2)),
        R.identity
      ]),
      R.apply(R.splitAt),
      R.map(R.split("")),
      R.apply(R.intersection)
    )
  ),
  R.unnest,
  R.map(
    R.pipe(
      letterToCharCode,
      R.cond([
        [flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
        [flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
      ])
    )
  ),
  R.sum,
  console.log
)(data);

И вуаля! Это позволяет нам найти ответ на 3-й день появления кода в 2022 году!

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