Hung-Yi's LogoHung-Yi’s Journal

Advent of Code 2022: Day 3

A TypeScript solution for the Advent of Code 2022, Day 3 puzzle: playing with sets and intersections.

Today’s puzzle deals heavily with sets and intersections. We’re asked to find a character that appears somewhere in one or more strings, then combine results by mapping the repeated characters with a simple scoring system.

JavaScript gives you a Set object to help with this, but lacks an intersection function, so we’ll have to write our own.

The puzzle input looks something like this.

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw

First we split the data into individual rucksacks (one per line).

const rucksacks: string[] = puzzleInput.split('\n');

Now define an intersection function to find the overlapping characters between two or more strings.

function intersection(...xs: string[]): string[] {
  // Create sets of characters for each string.
  // Note: dedupes all characters within each string.
  const sets = xs
    .map(x => new Set<string>(x.split('')));

  // Then reduce over the sets, using filter()
  // to accumulate the overlapping values
  return sets.slice(1).reduce(
    (overlap, set) => overlap.filter(x => set.has(x)),
    Array.from(sets[0].values())
  );
}

For our final puzzle answers, we also need a lookup table of characters (a-z and A-Z) that maps them to a priority number. We can build it succinctly with clever usage of map() and its index parameter.

const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const priorityMap = new Map([
  // lowercase letters 1 through 26
  ...alphabet
    .split('')
    .map((char, i) => [char, i+1] as const),
  // uppercase letters 27 through 52
  ...alphabet
    .toUpperCase()
    .split('')
    .map((char, i) => [char, i+27] as const)
]);

The stage is now set 🙃 so we can finally get to the actual data crunching.

For part 1, split each rucksack into left and right equal-sized compartments, then use intersection to figure out the mistakes where the same characters are in both compartments. Then we can calculate the total priority of the mistakes by using the priorityMap lookup table and reduce for summation.

const part1 = rucksacks
  .flatMap(rucksack => {
    const middle = rucksack.length / 2;
    const left = rucksack.slice(0, middle);
    const right = rucksack.slice(middle);
    return intersection(left, right)
  })
  .map(mistake => priorityMap.get(mistake))
  .reduce((sum, p) => sum + p, 0);

For part 2, we instead need to find the intersection between groups of 3 rucksacks, which is the badge for each team of 3 elves. We can use the same priorityMap from part 1 to sum up the priorities of the badges for the answer.

const part2 = rucksacks
  // Group into bunches of 3 rucksacks by reducing,
  // creating a new group whenever index mod 3 is 0
  .reduce(
    (acc, curr, i) => {
      if (i % 3 === 0) { acc.push([]); }
      const prev = acc.length - 1;
      acc[prev].push(curr);
      return acc;
    },
    [] as string[][]
  )
  // Find the badge (i.e. intersection) of each triplet,
  // then look up the priorities and total them up
  .flatMap(triplet => intersection(...triplet))
  .map(badge => priorityMap.get(badge))
  .reduce((sum, p) => sum + p, 0);

Final Solution

const rucksacks: string[] = puzzleInput.split('\n');

function intersection(...xs: string[]): string[] {
  // Create sets of characters for each string.
  // Note: dedupes all characters within each string.
  const sets = xs
    .map(x => new Set<string>(x.split('')));

  // Then reduce over the sets, using filter()
  // to accumulate the overlapping values
  return sets.slice(1).reduce(
    (overlap, set) => overlap.filter(x => set.has(x)),
    Array.from(sets[0].values())
  );
}

const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const priorityMap = new Map([
  // lowercase letters 1 through 26
  ...alphabet
    .split('')
    .map((char, i) => [char, i+1] as const),
  // uppercase letters 27 through 52
  ...alphabet
    .toUpperCase()
    .split('')
    .map((char, i) => [char, i+27] as const)
]);

const part1 = rucksacks
  .flatMap(rucksack => {
    const middle = rucksack.length / 2;
    const left = rucksack.slice(0, middle);
    const right = rucksack.slice(middle);
    return intersection(left, right)
  })
  .map(mistake => priorityMap.get(mistake))
  .reduce((sum, p) => sum + p, 0);

const part2 = rucksacks
  // Group into bunches of 3 rucksacks by reducing,
  // creating a new group whenever index mod 3 is 0
  .reduce(
    (acc, curr, i) => {
      if (i % 3 === 0) { acc.push([]); }
      const prev = acc.length - 1;
      acc[prev].push(curr);
      return acc;
    },
    [] as string[][]
  )
  // Find the badge (i.e. intersection) of each triplet,
  // then look up the priorities and total them up
  .flatMap(triplet => intersection(...triplet))
  .map(badge => priorityMap.get(badge))
  .reduce((sum, p) => sum + p, 0);

console.log("Part 1:", part1);
console.log("Part 2:", part2);
Part 1: 7908
Part 2: 2838