Skip to content

Commit 8a2ad25

Browse files
committed
Add Day 4
1 parent 01a798f commit 8a2ad25

File tree

5 files changed

+451
-1
lines changed

5 files changed

+451
-1
lines changed

day-4/README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Day 4: Mull It Over
2+
3+
<br>
4+
5+
## Part 1
6+
7+
"Looks like the Chief's not here. Next!" One of The Historians pulls out a device and pushes the only button on it. After a brief flash, you
8+
recognize the interior of the [Ceres monitoring station](https://adventofcode.com/2019/day/10)!
9+
10+
As the search for the Chief continues, a small Elf who lives on the station tugs on your shirt; she'd like to know if you could help her
11+
with her **word search** (your puzzle input). She only has to find one word: `XMAS`.
12+
13+
This word search allows words to be horizontal, vertical, diagonal, written backwards, or even overlapping other words. It's a little
14+
unusual, though, as you don't merely need to find one instance of `XMAS` - you need to find **all of them**. Here are a few ways `XMAS`
15+
might appear, where irrelevant characters have been replaced with `.`:
16+
17+
```txt
18+
..X...
19+
.SAMX.
20+
.A..A.
21+
XMAS.S
22+
.X....
23+
```
24+
25+
The actual word search will be full of letters instead. For example:
26+
27+
```txt
28+
MMMSXXMASM
29+
MSAMXMSMSA
30+
AMXSXMAAMM
31+
MSAMASMSMX
32+
XMASAMXAMM
33+
XXAMMXXAMA
34+
SMSMSASXSS
35+
SAXAMASAAA
36+
MAMMMXMMMM
37+
MXMXAXMASX
38+
```
39+
40+
In this word search, `XMAS` occurs a total of `18` times; here's the same word search again, but where letters not involved in any `XMAS`
41+
have been replaced with `.`:
42+
43+
```txt
44+
....XXMAS.
45+
.SAMXMS...
46+
...S..A...
47+
..A.A.MS.X
48+
XMASAMX.MM
49+
X.....XA.A
50+
S.S.S.S.SS
51+
.A.A.A.A.A
52+
..M.M.M.MM
53+
.X.X.XMASX
54+
```
55+
56+
Take a look at the little Elf's word search. **How many times does `XMAS` appear?**
57+
58+
<br>
59+
60+
## Part 2
61+
62+
The Elf looks quizzically at you. Did you misunderstand the assignment?
63+
64+
Looking for the instructions, you flip over the word search to find that this isn't actually an `XMAS` puzzle; it's an `X-MAS` puzzle in
65+
which you're supposed to find two `MAS` in the shape of an `X`. One way to achieve that is like this:
66+
67+
```txt
68+
M.S
69+
.A.
70+
M.S
71+
```
72+
73+
Irrelevant characters have again been replaced with `.` in the above diagram. Within the `X`, each `MAS` can be written forwards or
74+
backwards.
75+
76+
Here's the same example from before, but this time all of the `X-MAS`es have been kept instead:
77+
78+
```txt
79+
.M.S......
80+
..A..MSMS.
81+
.M.S.MAA..
82+
..A.ASMSM.
83+
.M.S.M....
84+
..........
85+
S.S.S.S.S.
86+
.A.A.A.A..
87+
M.M.M.M.M.
88+
..........
89+
```
90+
91+
In this example, an `X-MAS` appears `9` times.
92+
93+
Flip the word search from the instructions back over to the word search side and try again. **How many times does an `X-MAS` appear?**

day-4/day-4.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it } from 'node:test';
2+
import assert from 'node:assert';
3+
import path from 'node:path';
4+
import { countXMASAppearances, countXShapedMASAppearances } from './day-4';
5+
6+
describe('Day 4: Ceres Search', () => {
7+
const wordSearchFilePath = path.join(__dirname, 'word-search.txt');
8+
9+
it('Part 1: should count XMAS appearances', async () => {
10+
const expectedNumberOfXMASAppearances = 2462; // Verified for this dataset
11+
12+
const numberOfXMASAppearances = await countXMASAppearances(wordSearchFilePath);
13+
14+
assert.strictEqual(numberOfXMASAppearances, expectedNumberOfXMASAppearances);
15+
});
16+
17+
it('Part 2: should count X-shaped MAS appearances', async () => {
18+
const expectedNumberOfXShapedMASAppearances = 1877; // Verified for this dataset
19+
20+
const numberOfXShapedMASAppearances = await countXShapedMASAppearances(wordSearchFilePath);
21+
22+
assert.strictEqual(numberOfXShapedMASAppearances, expectedNumberOfXShapedMASAppearances);
23+
});
24+
});

day-4/day-4.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import fs from 'node:fs/promises';
2+
3+
/**
4+
* Read file
5+
*/
6+
const readFile = async (filePath: string): Promise<string> => {
7+
const fileContents = await fs.readFile(filePath, {
8+
encoding: 'utf8',
9+
});
10+
const normalizedFileContents = fileContents.trim().split(/\r?\n/).join('\n');
11+
return normalizedFileContents;
12+
};
13+
14+
/**
15+
* Parse word search as grid
16+
*/
17+
const parseWordSearchAsGrid = (wordSearchFileContents: string): Array<Array<string>> => {
18+
// Parse word search string into grid
19+
// Note: Sadly, we can only parse out a YX grid immediately
20+
const wordSearchYXGrid = wordSearchFileContents.split('\n').map((wordSearchLine) => {
21+
return wordSearchLine.split('');
22+
});
23+
24+
// Transform YX grid into XY grid (mostly for it to be easier and more understandable to use)
25+
const wordSearchXYGrid: Array<Array<string>> = [];
26+
for (let y = 0; y < wordSearchYXGrid.length; y++) {
27+
for (let x = 0; x < wordSearchYXGrid[y].length; x++) {
28+
(wordSearchXYGrid[x] ??= [])[y] = wordSearchYXGrid[y][x];
29+
}
30+
}
31+
32+
// Done
33+
return wordSearchXYGrid;
34+
};
35+
36+
/**
37+
* Part 1: Count XMAS appearances
38+
*/
39+
export const countXMASAppearances = async (wordSearchFilePath: string) => {
40+
// Get data
41+
const wordSearchFileContents = await readFile(wordSearchFilePath);
42+
const wordSearchGrid = parseWordSearchAsGrid(wordSearchFileContents);
43+
44+
// Setup search parameters
45+
const searchTerm = 'XMAS';
46+
// Setup all search directions (also covers reverse search terms)
47+
// Note: Coordinate system starts with 0-0 at top left, search directions run clockwise and start at the top
48+
const searchOffsets: Array<[x: number, y: number]> = [
49+
[0, -1], // top
50+
[1, -1], // top-right
51+
[1, 0], // right
52+
[1, 1], // bottom-right
53+
[0, 1], // bottom
54+
[-1, 1], // bottom-left
55+
[-1, 0], // left
56+
[-1, -1], // top-left
57+
];
58+
59+
// Look at each coordinate ...
60+
const searchTermMatches: Array<Array<[x: number, y: number]>> = [];
61+
for (let x = 0; x < wordSearchGrid.length; x++) {
62+
for (let y = 0; y < wordSearchGrid[x].length; y++) {
63+
// Search into each direction ...
64+
for (let searchOffsetIndex = 0; searchOffsetIndex < searchOffsets.length; searchOffsetIndex++) {
65+
// Compare each search term character ...
66+
const searchTermMatch: Array<[x: number, y: number]> = [];
67+
for (let searchTermIndex = 0; searchTermIndex < searchTerm.length; searchTermIndex++) {
68+
// Find current coordinate
69+
const currentCoordinate: [x: number, y: number] =
70+
searchTermIndex === 0
71+
? // Start with original coordinate for first character
72+
[x, y]
73+
: // Continue looking at the next coordinate based on previous match and search offset
74+
[
75+
searchTermMatch[searchTermMatch.length - 1][0] + searchOffsets[searchOffsetIndex][0],
76+
searchTermMatch[searchTermMatch.length - 1][1] + searchOffsets[searchOffsetIndex][1],
77+
];
78+
79+
// Get character at coordinate
80+
// Note: Possibly undefined when going "off grid"
81+
const currentCharacter: string | undefined = wordSearchGrid[currentCoordinate[0]]?.[currentCoordinate[1]];
82+
83+
// Check whether the character matches the search term
84+
if (currentCharacter === searchTerm[searchTermIndex]) {
85+
searchTermMatch.push(currentCoordinate);
86+
} else {
87+
break; // Early exit
88+
}
89+
}
90+
91+
// Accept match if all characters have been found
92+
if (searchTermMatch.length === searchTerm.length) {
93+
searchTermMatches.push(searchTermMatch);
94+
}
95+
}
96+
}
97+
}
98+
const numberOfSearchTermMatches = searchTermMatches.length;
99+
100+
// !DEBUG: Print each match
101+
// for (let matchIndex = 0; matchIndex < searchTermMatches.length; matchIndex++) {
102+
// let searchTermResult = '';
103+
// for (let coordinateIndex = 0; coordinateIndex < searchTermMatches[matchIndex].length; coordinateIndex++) {
104+
// searchTermResult +=
105+
// wordSearchGrid[searchTermMatches[matchIndex][coordinateIndex][0]][searchTermMatches[matchIndex][coordinateIndex][1]];
106+
// }
107+
// console.log(searchTermResult);
108+
// }
109+
110+
// Done
111+
return numberOfSearchTermMatches;
112+
};
113+
114+
/**
115+
* Part 1: Count X-shaped MAS appearances
116+
*/
117+
export const countXShapedMASAppearances = async (wordSearchFilePath: string) => {
118+
// Get data
119+
const wordSearchFileContents = await readFile(wordSearchFilePath);
120+
const wordSearchGrid = parseWordSearchAsGrid(wordSearchFileContents);
121+
122+
// Setup search parameters
123+
const searchTerm = 'MAS';
124+
// Setup X-shaped search directions (also covers reverse search terms)
125+
// Note: Coordinate system starts with 0-0 at top left, search directions run clockwise and start at the top-right
126+
const searchOffsets: Array<[x: number, y: number]> = [
127+
[1, -1], // top-right
128+
[1, 1], // bottom-right
129+
[-1, 1], // bottom-left
130+
[-1, -1], // top-left
131+
];
132+
133+
// Look at each coordinate ...
134+
const searchTermMatches: Array<Array<[x: number, y: number]>> = [];
135+
for (let x = 0; x < wordSearchGrid.length; x++) {
136+
for (let y = 0; y < wordSearchGrid[x].length; y++) {
137+
// Search into each direction ...
138+
const searchTermMatchesForSearchOffsets: Array<Array<[x: number, y: number]>> = [];
139+
for (let searchOffsetIndex = 0; searchOffsetIndex < searchOffsets.length; searchOffsetIndex++) {
140+
// Compare each search term character ...
141+
const searchTermMatch: Array<[x: number, y: number]> = [];
142+
for (let searchTermIndex = 0; searchTermIndex < searchTerm.length; searchTermIndex++) {
143+
// Find current coordinate
144+
const currentCoordinate: [x: number, y: number] =
145+
searchTermIndex === 0
146+
? // Start with search offset coordinate for first character
147+
[x + searchOffsets[searchOffsetIndex][0], y + searchOffsets[searchOffsetIndex][1]]
148+
: // Continue looking at the next coordinate based on previous match and reverse search offset
149+
[
150+
searchTermMatch[searchTermMatch.length - 1][0] - searchOffsets[searchOffsetIndex][0],
151+
searchTermMatch[searchTermMatch.length - 1][1] - searchOffsets[searchOffsetIndex][1],
152+
];
153+
154+
// Get character at coordinate
155+
// Note: Possibly undefined when going "off grid"
156+
const currentCharacter: string | undefined = wordSearchGrid[currentCoordinate[0]]?.[currentCoordinate[1]];
157+
158+
// Check whether the character matches the search term
159+
if (currentCharacter === searchTerm[searchTermIndex]) {
160+
searchTermMatch.push(currentCoordinate);
161+
} else {
162+
break; // Early exit
163+
}
164+
}
165+
166+
// Accept match if all characters have been found
167+
if (searchTermMatch.length === searchTerm.length) {
168+
searchTermMatchesForSearchOffsets.push(searchTermMatch);
169+
}
170+
}
171+
172+
// Accept match if two matches along the diagonals (X-shape) have been found
173+
if (searchTermMatchesForSearchOffsets.length === 2) {
174+
searchTermMatches.push(searchTermMatchesForSearchOffsets.flat(1));
175+
}
176+
}
177+
}
178+
const numberOfSearchTermMatches = searchTermMatches.length;
179+
180+
// !DEBUG: Print each match
181+
// for (let matchIndex = 0; matchIndex < searchTermMatches.length; matchIndex++) {
182+
// let searchTermResult = '';
183+
// for (let coordinateIndex = 0; coordinateIndex < searchTermMatches[matchIndex].length; coordinateIndex++) {
184+
// searchTermResult +=
185+
// wordSearchGrid[searchTermMatches[matchIndex][coordinateIndex][0]][searchTermMatches[matchIndex][coordinateIndex][1]];
186+
// }
187+
// console.log(searchTermResult);
188+
// }
189+
190+
// Done
191+
return numberOfSearchTermMatches;
192+
};

0 commit comments

Comments
 (0)