- Intro
- Structure
- Install
- Running extended Small Lang, the last solution
- Run Individual Tests
- Run All Tests
- Day 1: Filter gift strings containing '#'
- Day 2: Make q total gifts from n object orders
- Day 3: Draw square gift perimeter
- Day 4: Decipher pin from cyphered tokens
- Day 5: Countdown
- Day 6: Matching gloves
- Day 7: Draw a custom tree
- Day 8: Find first non-repeating letter
- Day 9: Move robot
- Day 13: Assembly board
- Day 25: Small Lang
| Day | Difficulty | Solved | Time Complexity | Space Complexity |
|---|---|---|---|---|
| Day 1: Filter gift strings containing '#' | Easy | ✅ | O(L) | O(n) |
| Day 2: Make q total gifts from n object orders | Easy | ✅ | O(n + q) | O(n + q) |
| Day 3: Draw square gift perimeter | Easy | ✅ | O(n^2) | O(n^2) |
| Day 4: Decipher pin from cyphered tokens | Medium | ✅ | O(L) | O(L) |
| Day 5: Countdown | Easy | ✅ | O(L) | O(1) |
| Day 6: Matching gloves | Easy | ✅ | O(n) | O(k) |
| Day 7: Draw a custom tree | Medium | ✅ | O(h^2) | O(h^2) |
| Day 8: Find first non-repeating letter | Easy | ✅ | O(n) | O(n) |
| Day 9: Move robot | Hard | ✅ | O(L + k) | O(L + k) |
| Day 10: Depth | Easy | ✅ | ||
| Day 11: Unwatched gifts | Easy | ✅ | ||
| Day 12: Elf battle | Medium | ✅ | ||
| Day 13 Assembly board | Medium | ✅ | ||
| Day 14: Gift path | Easy | ✅ | ||
| Day 15: Draw table | Medium | ✅ | ||
| Day 16 Packing gifts | Easy | ✅ | ||
| Day 17: Consecutive lights | Easy | ✅ | ||
| Day 18: Consecutive lights II | Medium | ✅ | ||
| Day 19: Connecting flights | Easy | ✅ | ||
| Day 20: Vertical warehouse | Easy | ✅ | ||
| Day 21: Cleaning robot | Medium | ✅ | ||
| Day 22: Maze | Hard | ✅ | O(n) | O(n) |
| Day 23: Shortest distance | Medium | ✅ | ||
| Day 24: Mirror Trees | Medium | ✅ | ||
| Day 25: Small Lang | Medium | ✅ | O(n + T) | O(n) |
Space complexity in this document means Auxiliary space, i.e. space reserved for the algorithm itself including the output, but without the input space itself.
I solved AdventJS 2025 coding challenge using TypeScript and Node.js for both runtime and tests.
Each day-XX-... directory contains both:
solution.tssolution.test.ts
The tests are the biggest benefit you can take from this repo (apart from technical discussion) because they contain some of the 'official' input cases which I used them to validate my solution before submitting, and in case the online runner found edge cases my solution didn't handle, I added those case to the test suite and fixed the solution.
You will need:
git clone https://github.com/new-AF/AdventJS-2025-challenge-solutions
cd AdventJS-2025-challenge-solutions
pnpm installpnpm day-25-small-language-extended
# outputs
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++!+++++++++++++++++++++++++++++!+++++++!!+++!-------------------------------------------------------------------------------!+++++++++++++++++++++++++++++++++++++++++++++++++++++++!++++++++++++++++++++++++!+++!------!--------!-------------------------------------------------------------------!
# Hello World!
# pnpm day-xx-folder-...
pnpm day-8-non-repeating-letterpnpm testReturn a filtered out array of strings that don't contain character '#'
{
input: ["car", "doll#arm", "ball", "#train"],
expectedOutput: ["car", "ball"],
}Pretty straightforward:
- we'll use
.filterarray method - provide a predicate function that will run on each string,
- use the built-in
.includesmethod to ensure the string does not contain'#'
O(L) where L is the total length of all strings provided. This is because in the worst-case we have to process every character of each string before concluding it does not contain the offending character.
In other words the work is proportional to the total length, if the total character count doubled, or tripled, so would at least the amount of work done.
O(n) because in the worst case, if no string contains the offending character, we're returning a entire new copy of the array.
Given an array of instruction objects, produce quantity copies for each string in toy value, and return the flattened array. Ignore objects with quantity <= 0
{
input: [
{ toy: "car", quantity: 3 },
{ toy: "doll", quantity: 1 },
{ toy: "ball", quantity: 2 },
],
expectedOutput: ["car", "car", "car", "doll", "ball", "ball"],
}Pretty straightforward too:
- we'll
.filterout objects withquantity <= 0 .mapover each object, allocate a new nested array of lengthquantity.fillit withtoyvalue strings
- call
.flaton the outer array - return the flatten array.
O(n + q) where n is the count of all objects, and q is the sum of all quantities. This is broken as O(n) for total objects work, in addition to O(q) for all elements produced.
The work is proportional to both the objects count and how many strings to produce.
O(n + q) This is broken as O(n) for the temporary array holding references to the objects filtered, in addition O(q) for all elements produced.
Given a size length of a square, and a symbol, draw the square outer perimeter. Print inner cells as empty (space).
{
size: 4,
symbol: "*",
expectedOutput: `
****
* *
* *
****`,
}- Iterate
sizetimes, - depending on the index, if it's the first or last line:
- draw either the top full
symbolline, or - draw the mostly hollow middle line, with only 2
symbolon the outer perimeter.
- draw either the top full
.jointhe lines with\nand print the output.
O(n^2) because on each iteration, we do O(n) work, creating the n length line.
O(n^2) because we store n^2 cell elements of the square output.
Given a string of tokens (substrings), process them and output the digit for each token.
Each token resembles this format n_Ops1_Ops2_... (without the underscores) where
nis the initial digit to modifyOpsare instructions like+to increment,-to decrement,<to return digit from previous token.
{
input: "[1++][2-][3+][<]",
expectedOutput: "3144"
}- tokenize the string (with regular expressions) as
["1++", "2-", "3+", "<"] - keep a global
previousDigit - iterate over each token,
- apply
decipher(...)to obtain the digit .jointhe outputs
O(L) where L is the entire length of the input string, because we have to process each character and apply the deciphering logic.
O(L) because I store all the tokens, and other intermediate arrays.
The optimal solution (streaming parser) would be:
- O(1) if the output size is constrained, as in this case to 4 fixed size slots,
- O(n) if the input is unbounded, where n where n is the tokens count.
Return the seconds difference between two date-time strings. The caveat is the strings are in a non-standard format so need processing.
const takeoff = "2025*12*25@00|00|00 NP";
{
input: "2025*12*24@23|59|30 NP",
takeoff,
expectedOutput: 30
}- apply a fixed number of
.replaceAlloperations to convert the strings to standard format- e.g.
2025*12*24@23|59|30 NPto2025-12-24T23:59:30Z
- e.g.
- obtain
Date()objects - subtract the dates, to obtain the milliseconds.
- divide the result by 1000 and
Math.floorit.
O(L) because replaceAll checks each character to convert all the non-standard symbols.
O(1) because we only need fixed space for the Date objects, and calculations.
Count the match pairs of glove objects that are identical in both color and hand direction. Preserve the order, as soon as a new pair is found list it, multiple pairs of the same color are allowed.
{
input: [
{ hand: "L", color: "red" },
{ hand: "R", color: "red" },
{ hand: "R", color: "green" },
{ hand: "L", color: "blue" },
{ hand: "L", color: "green" },
],
expectedOutput: ["red", "green"],
}- use a hash map
Mapas quasi tally .forEachover the input array- on the fist encounter of
color, set it as key, and value as{left: 0, right:0}initially - later increment the left and right piece
- as soon as either the left or right count is
>=1we found a new pair, push it, and decrement counts accordingly
O(n) where n is the number of objects, because we have to process each.
O(k) or O(n) where k is the number of distinct colors, because they are the keys of the hash map. In the worst case k = n because the whole input would be objects of unique colors but only one piece.
Draw a centered Christmas tree of height and with custom ornament that repeat every frequency
{
height: 5,
ornament: "o",
frequency: 2,
expectedOutput: `
*
o*o
*o*o*
o*o*o*o
*o*o*o*o*
#
`,
}- Iterate
heighttimes - generate the
2 * index + 1inner array.fillit with* - iterate over all cells and mark those that are
% frequency - center the tree by padding it as
Math.floor((maxWidth - array.length) / 2)
O(h^2) where h is height. This is because the tree has h^2 + 1 elements, and this comes from math, the sum of the sequence that represents rows lengths 1, 3, 5, ..., 2 * height - 1 is h^2 In addition to the base element #
O(h^2) because we store at least the entire tree elements count, in addition to intermediary processing arrays.
https://adventjs.dev/challenges/2025/8
Return the first non-repeating letter (regardless of casing) in a string. If all letters are repeated, the function should return an empty string. e.g.
"Gift"should return"G""sS"should return"""reindeeR"should return"i"
Rest of test cases in day-8-non-repeating-letter/solution.test.ts
-
Use a dictionary (JS Object
{}) -
Iterate over the string and mark if the lowercase letter occurred before.
We have to run the entire length of the string, because the first non-repeating letter could be at the very end (e.g.
"aaaaaaaaaaaab") -
Do another pass, and break out of the function at the first letter that is marked as having no duplicates by referencing the dictionary.
Assuming the dictionary key insertion and retrieval is O(1) then:
findUniqueToyruns in O(n) or linear time, because:
- Dictionary construction is O(n) because we iterate over the entire string, and do n queries and insertions.
- O(n) for the final pass, doing n dictionary retrievals.
Overall space complexity is O(n) because:
-
Dictionary size is O(1) or constant time due to fixed size of alphabet.
-
O(n) for extra array allocation
const array = Array.from(toy);We could get rid of the extra allocation, and bring overall space complexity to O(1) but we would have to use a regular
forloop instead of theforEacharray method (because strings in JS don't have a nativeforEach)To me that's an acceptable tradeoff because I gain extra code readability:
forEachexplicitly states that we run the entire length of the string, and there's no early exit as would be possible with a traditionalforloop.
Instead of the ambiguous empty string ("") on failure, the function should always return an object:
{
success: boolean;
(optional) value: string;
}This will explicitly tell if the string had any non-repeating letters. If all the letters are repeated success would be false, and we wouldn't return value
Move the robot on the 2D board (nested array), using a separate input string as control. The board has the following element types:
@is our robot*is something to pick, return'success'immediately.#is obstacle , return'crash'immediately, or when robot goes outside board..is an empty spot, exhaust input and return'fail'if robot ends here.
.....
.*#.*
.@...
.....
| Input | Expected Return |
|---|---|
'D' (Down) |
'fail' because robot ended on empty spot ('.'), having exhausted all input |
'U' (Up) |
'success' immediately, because the robot picked up picked up something ('*') |
'RU' (Right, Up) |
'crash' immediately, because the robot hit an obstacle ('#') |
-
Convert board from one giant string to a 2D nested array (
string[][])Both Time and space complexity are O(L) where L is the length of the original string, or equivalenty O(n) where n is the count of cells on the board. The work done is proprtional to the number of cells on the board.
-
Find initial robot location, linear sweep. Time complexity is O(n) or equivalently O(L)
-
.mapthe ambiguous control characters ('U') into readable TypeScript enums (BoardMove.Up)Both Time and space complexity are O(k) where k is the length of the control string.
-
Follow control moves, and apply result logic.
Time complexity is O(k) where k is the length of the control string. There is no loop detection because that would violate the problem description, so k is unbounded by n the number of cells.
O(n + k) or equivalently O(L + k) where n is the number of cells on the board, and L is the length of the original board represented as a string, k is the control string length.
The work is proportional to both the board size and control input string, and grows independently with respect to both.
O(n + k) or equivalently O(L + k) The is broken as:
-
O(n) to convert the original string board to a 2D nested array.
-
O(k) to convert the control string to a 1D array and
.mapthe characters to enumns.
Had we traded off readability by not implementing those 2 steps and hence increased the likelihood of bugs in order to save on memory, then the Space complexity would be O(1)
Link: https://adventjs.dev/challenges/2025/13
type Factory = string[];
type Result = "completed" | "broken" | "loop";
export const runFactory = (factory: Factory): Result => {
// ...
};{
factory: [
">v.",
"^.."
],
expectedOutput: "completed"
}Move a gift using instructions (string[]) on a 2D assembly board, where each string is a series of instructions:
'>'move gift one step right on same row.'<'move gift one step left on same row.'v'move gift one row down.'^'move gift one row up.'.'exit pod.
Return these outcomes, early if possible:
'completed', if gift hits an exit spot (.)'loop', if gift returns to a previously visited position.'broken'if gift ends outside of assembly board.
day-13-assembly-board/solution.ts
-
Use a single while loop: get current the row, column position of the gift.
-
Detect loops: use a
Setto check if we visited a previous position (row, column), and return'loop'if need be. -
Call the handler function for that instruction (
'>'), it returns either:'completed'outcome, in which case return early or,- new position.
-
Check if new position is out of board bounds, and return
'broken' -
Continue and process next instruction.
/*
Day 13: Assembly board
Move a gift using instructions (`string[]`) on a **2D assembly board**, where each `string` is a series of instructions:
- `'>'` move gift one step right on same row.
- `'<'` move gift one step left on same row.
- `'v'` move gift one row down.
- `'^'` move gift one row up.
- `'.'` exit pod.
Return these outcomes, early if possible:
- `'completed'`, if gift hits an exit spot (`.`)
- `'loop'`, if gift returns to a previously visited position.
- `'broken'` if gift ends outside of assembly board.
*/
type Factory = string[];
type Result = "completed" | "broken" | "loop";
export const runFactory = (factory: Factory): Result => {
type Instruction = ">" | "<" | "^" | "v" | ".";
// assembly line outcome
enum Outcome {
Broken = "broken",
Completed = "completed",
Loop = "loop",
}
type Position = { row: number; column: number };
type Transition = { outcome: Outcome } | Position;
// visited locations key
const makeKey = ({ row, column }: Position) => `${row}, ${column}`;
// is next location out of bounds
const isOutOfBounds = ({ width, height, row, column }) =>
row < 0 || row >= height || column < 0 || column >= width;
// returns either next coordinates, or assembly line outcome
const instructionHandlers: Record<
Instruction,
(pos: Position) => Transition
> = {
".": ({ row, column }) => {
return { outcome: Outcome.Completed };
},
">": ({ row, column }) => {
// advance column
return { row, column: column + 1 };
},
"<": ({ row, column }) => {
// move back
return { row, column: column - 1 };
},
v: ({ row, column }) => {
// move down
return { row: row + 1, column };
},
"^": ({ row, column }) => {
// move up
return { row: row - 1, column };
},
};
// assembly line state
const state: {
row: number;
column: number;
visitedPositions: Set<string>;
} = {
row: 0,
column: 0,
visitedPositions: new Set(),
};
const boardInfo = {
width: factory.length > 0 ? factory[0].length : 0,
height: factory.length,
};
while (state.row < boardInfo.height) {
const line = factory[state.row];
const instruction = line[state.column];
// current position
const currentPosition: Position = {
row: state.row,
column: state.column,
};
// 1) detect loops (if we visited this previously)
const key = makeKey(currentPosition);
if (state.visitedPositions.has(key)) {
return Outcome.Loop;
}
state.visitedPositions.add(key);
// returns either next coordinates, or assembly line outcome
const result = instructionHandlers[instruction](currentPosition);
// 2) break early if success
if ("outcome" in result) {
return result.outcome;
}
// 3) are new coordinates out of bounds
const nextPosition = result;
if (
isOutOfBounds({
...nextPosition,
width: boardInfo.width,
height: boardInfo.height,
})
) {
return Outcome.Broken;
}
state.row = nextPosition.row;
state.column = nextPosition.column;
}
// if factory is empty
return Outcome.Broken;
};Is linear O(n) with respect to n = count of instructions. If instructions doubled so would run time.
Is also linear linear O(n) because of the Set<Position> we use to detect loops. At most we'd store n positions, i.e. row, column of each instruction result.
We would have store entire input length (4) before we can tell this is a loop.
{
// classic loop
factory: `
>v
^<
`,
expectedOutput: "loop",
}day-13-assembly-board/solution.test.ts
runFactory takes in string[], where each string contains instructions for that specific row, but this format although easy for the algorithm to digest, makes it hard to visualize the 2D board.
So I made tests input a single multi-line string, that gets converted (split) into string[] before being fed to runFactory.
Example:
{
// classic loop
factory: `
>v
^<
`,
expectedOutput: "loop",
}Gets converted to below:
{
// classic loop
factory: [
">v",
"^<"
],
expectedOutput: "loop",
}Build a single counter machine, which consumes an input string (encoded program), and returns the value of the single counter. Each character in the input string is an instruction as following:
">"is effectively the keywordcontinuein programming languages, it does nothing but but advance to the next character/instruction. -"+": Increments the single counter,"+": Increments the single counter value, and moves onto the next instruction,"-": Decrement the single counter value, and moves onto the next instruction,"[": Loop Start, marks the beginning of loop, you can think of it as a simplifiedwhilewhich operates as following:- If the single counter is
0(effectivelyfalse) it jumps to the end of the loop ("]") and onto the instruction after. - If the single counter is not zero, it enters the loops and executes the instructions contained there.
- For example
"--[++]enters the loops when the counter is-2and loops incrementing until it's0. - Be careful though, the input program can continue infinite loops, as in the case of real world programs, but this is not your problem anymore.
- For example
- If the single counter is
"]": Loop End, at this point, you need to check the single counter value-- If it is
0, you end looping and move onto the next instruction. - If it not zero, you jump back to
"[", continue looping, and so forth.
- If it is
"{": Condition Check Start, effectively anifstatement:- If the single counter is
0this meansfalseand you jump to the end of the condition"}" - If it is not zero, you enter the code block and execute the instructions contained there.
- For example:
"+{---}"returns-2because you entered when the value is1and ran the instructions contained there.
- For example:
- If the single counter is
"}": Condition Check End, you don't do anything here and move onto the next instruction.There is no jumping back, or looping inherent into the condition itself, it executes 0 or 1 times.
For example:
[
{ input: "+++", expectedOutput: 3 },
{ input: "+--", expectedOutput: -1 },
{ input: ">+++[-]", expectedOutput: 0 },
{ input: ">>>+{++}", expectedOutput: 3 },
{ input: "+{[-]+}+", expectedOutput: 2 },
{ input: "{+}{+}{+}", expectedOutput: 0 },
{ input: "------[+]++", expectedOutput: 2 },
{ input: "-[++{-}]+{++++}", expectedOutput: 5 },
{ input: "-[+{+}]+", expectedOutput: 1 },
{ input: "[{}]", expectedOutput: 0 },
];While similar to BrainF---, Small Lang is different because it is not Turing-complete, while BF is.
Turing completeness meaning you can program arbitrary functions like the Fibonacci sequence, and for that feature you need at least 2 counters, but we only have 1. But this doesn't mean we can't do some useful things like we'll show later.
The solution consists of 2 passes:
- Building the Jump Table this enables looping and conditional checks.
- The main
whilerunning the program through the State Functions.
-
We model our machine as a simple state object that contains the current reading head index, and the single value counter (that we return later.):
// our machine state type State = { tapeIndex: number; value: number; }; let currentState: State = { tapeIndex: 0, value: 0, };
tapeIndexalone determines how the machine operates. -
For each of the token types, we define a corresponding function that takes 1) the current state of the machine, 2) the Jump Table, returns the new state, containing the new read index and the single value counter:
// produce our next state type StateFunction = (state: State, globalJumpTable: JumpTable) => State; // handling looping, check if value is 0, jump to end of loop, otherwise advance (currentState: State, passedJumpTable: JumpTable): State => { const { value, tapeIndex } = currentState; // jump to end of loop if (value === 0) { // malformed input if (!passedJumpTable.has(tapeIndex)) { raiseMalformedInput(); } const loopEnd = passedJumpTable.get(tapeIndex); const nextIndex = loopEnd.index; return { value, tapeIndex: nextIndex }; } // else enter the loop, move to next instruction return { value, tapeIndex: tapeIndex + 1 }; };
-
We run a simple
whileas long as there are tokens available, feed then into the state function, and update the state:// run the program the program, as long as there are instructions while (currentState.tapeIndex < code.length) { const { type, _ } = allTokens[currentState.tapeIndex]; const transform = tokenTypeToStateFunction[type]; const nextState = transform(currentState, globalJumpTable); currentState = nextState; } // return final value return currentState.value;
-
We use a Stack to handle arbitrarily nesting of loops and conditions, e.g.:
{ input: "+{[-]+}+", expectedOutput: 2 }
-
We iterate over the now tokens array
Tokens[]and as soon we encounter the opening token: eitherTokenType.Loop_StartorTokenType.If_Startwe push them onto the stack. TheTokenalso contains theindexso this will be handy later. -
We continue iterating, we continue iterating and as soon we find the closing token:
TokenType.Loop_EndorTokenType.If_Endwe pop the last token:-
To avoid processing malformed inputs like
"{]", We inspect the popped token to ensure it mirrors the current token. -
If the tokens mirror each other, we insert them into the table. e.g.:
// in case we need to skip over the loop; jump[start] = end globalJumpTable.set(loopStart.index, token); // in case we need to loop back; jump[end] = start globalJumpTable.set(index, loopStart);
Both Time and Spce complexity for building the Jump Table is O(n/2) or O(n) because the most insertions we do is n/2 because in the worst case of well formed input e.g.
{{}}we'll processing half the tokens array. -
We have shown the main loop before, it doesn't have any indexing manipulation logic, it relies one the state function to do that, and it runs as long as the tapeIndex is within bounds, which means it can go forward or backward, and also run indefinitely if the program is encodes an infinite loop:
// run the program the program, as long as there are instructions
while (currentState.tapeIndex < code.length) {
const { type, _ } = allTokens[currentState.tapeIndex];
const transform = tokenTypeToStateFunction[type];
const nextState = transform(currentState, globalJumpTable);
currentState = nextState;
}
// return final value
return currentState.value;The time complexity is O(T) where T is the total number of instructions executed encoded within the program itself, not the number of tokens of the static program.
O(n + T) where n is the number of tokens of the static program (size of the program), while T is the number of instructions encoded in the program itself.
O(T) describes the work done by our machine which directly reflects the behavior of the encoded program itself, If the program encodes a linear algorithm, our machine will run in linear time, if the encoded algorithm is exponential our machine will take exponential time to complete, If the program encodes an infinite loop, our machine will never halt.
In short our solution O(n + T) reflects 1) the work done by building the machine O(n) including constructing the Jump Table, and 2) O(T) the cost of running the machine which is entirely determined by the growth behavior of the encoded program itself. Our solution grows independently with respect to both variables.
O(n) where n is the number of tokens in the static program, or size of the program, and this accounts for:
- O(n) the size of the array used to convert the the string characters to
Tokens - O(n) the size of the Jump Table
- O(n) the size of the stack used to construct the Jump Table
Because how we structured the solution, as series of State Functions it's easy to add a printing functionality.
We will designate the character "!" to print the single counter as ASCII character.
The State Function is the first one to have a side-effect as it will print the ASCII character to the console, and advance the character.
pnpm day-25-small-language-extended
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++!+++++++++++++++++++++++++++++!+++++++!!+++!-------------------------------------------------------------------------------!+++++++++++++++++++++++++++++++++++++++++++++++++++++++!++++++++++++++++++++++++!+++!------!--------!-------------------------------------------------------------------!
Hello World!
This is broken as the joined string of the individual characters
[
// H=72
"++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++!",
// +29; e=101
"+++++++++++++++++++++++++++++!",
// +7; l=108
"+++++++!",
// l=108 again
"!",
// +3; o=111
"+++!",
// -79; space=32
"-------------------------------------------------------------------------------!",
// +55; W=87
"+++++++++++++++++++++++++++++++++++++++++++++++++++++++!",
// +24; o=111
"++++++++++++++++++++++++!",
// +3; r=114
"+++!",
// -6; l=108
"------!",
// -8; d=100
"--------!",
// -67; !=33
"-------------------------------------------------------------------!",
];



