Published: Sat 06 December 2025
By Ofer Yehuda
In Programming .
tags: aoc
import fs from "node:fs" ;
const input = fs . readFileSync ( "input.txt" , "utf-8" );
const sample = `\
123 328 51 64
45 64 387 23
6 98 215 314
* + * + ` ;
Part 1
In the first part of today, the input consists of 3 rows of space separated numbers and a row of space separated operators. We need to apply the operator on all the numbers in the column, so the challenge is to have everything aligned correctly. This begs for regexes.
const rows = sample . split ( "\n" );
const num_pat = /\d+/g ;
const op_pat = /[*+]/g ;
rows [ 0 ]. match ( num_pat ). map ( n => parseInt ( n ))
[ 123 , 328 , 51 , 64 ]
[ "*" , "+" , "*" , "+" ]
function process ( input : string ) {
const rows = input . split ( "\n" );
const num_pat = /\d+/g ;
const op_pat = /[*+]/g ;
return [ rows [ 0 ]. match ( num_pat ). map ( n => parseInt ( n )),
rows [ 1 ]. match ( num_pat ). map ( n => parseInt ( n )),
rows [ 2 ]. match ( num_pat ). map ( n => parseInt ( n )),
rows [ 3 ]. match ( op_pat )];
}
const psample = process ( sample );
psample
[
[ 123 , 328 , 51 , 64 ],
[ 45 , 64 , 387 , 23 ],
[ 6 , 98 , 215 , 314 ],
[ "*" , "+" , "*" , "+" ]
]
const mat = psample ;
const [ h , w ] = [ mat . length , mat [ 0 ]. length ];
const tmat = [];
for ( let c = 0 ; c < w ; c ++ ) {
const nrow = [];
for ( let r = 0 ; r < h ; r ++ ) {
nrow . push ( mat [ r ][ c ]);
}
tmat . push ( nrow );
}
tmat
[
[ 123 , 45 , 6 , "*" ],
[ 328 , 64 , 98 , "+" ],
[ 51 , 387 , 215 , "*" ],
[ 64 , 23 , 314 , "+" ]
]
Let's write a reusable transpose function
function transpose < T > ( matrix : T [][]) : T [][] {
const [ rows , cols ] = [ matrix . length , matrix [ 0 ]. length ];
const transposed : T [][] = [];
for ( let c = 0 ; c < cols ; c ++ ) {
const column : T [] = [];
for ( let r = 0 ; r < rows ; r ++ ) {
column . push ( matrix [ r ][ c ]);
}
transposed . push ( column );
}
return transposed ;
}
const tsample = transpose ( psample )
Let's create an obj that maps the operator string to lambdas. First, let me verify the reducer callback signature.
console . log ( tsample [ 0 ], tsample [ 0 ]. slice ( 0 , - 1 ). reduce (( r , x ) => r * x ))
[ 123 , 45 , 6 , "*" ] 33210
const ops : Record < string , ( acc : number , val : number ) => number > = {
"*" : ( acc , val ) => acc * val ,
"+" : ( acc , val ) => acc + val
}
ops
tsample [ 0 ]. slice ( 0 , - 1 ). reduce ( ops [ tsample [ 0 ][ tsample [ 0 ]. length - 1 ]])
33210
Okay, this works now, let's make it clearer
for ( const row of tsample ) {
const nums = row . slice ( 0 , - 1 );
const op = row [ row . length - 1 ];
console . log ( row , nums . reduce ( ops [ op ]));
}
looks good. Let's wrap it up
function part1 ( input : string ) : number {
const processed = process ( input );
const transposed = transpose ( processed );
let result = 0 ;
for ( const row of transposed ) {
const nums = row . slice ( 0 , - 1 );
const op = row [ row . length - 1 ];
result += nums . reduce ( ops [ op ]);
}
return result ;
}
part1 ( sample );
[
[
527 , 781 , 232 , 95 , 3 , 75 , 59 , 66 , 43 , 5 , 68 , 877 ,
31 , 4 , 54 , 914 , 15 , 22 , 2 , 29 , 4 , 17 , 5 , 9176 ,
8 , 885 , 97 , 88 , 42 , 6 , 64 , 161 , 1675 , 36 , 363 , 738 ,
71 , 224 , 453 , 72 , 256 , 594 , 914 , 97 , 668 , 95 , 723 , 84 ,
738 , 437 , 38 , 1 , 893 , 416 , 5 , 21 , 745 , 18 , 4566 , 32 ,
627 , 16 , 5 , 45 , 8 , 88 , 4256 , 52 , 9 , 66 , 317 , 691 ,
224 , 375 , 17 , 25 , 3 , 3529 , 42 , 77 , 86 , 9 , 2 , 865 ,
793 , 87 , 469 , 86 , 452 , 85 , 51 , 44 , 11 , 43 , 31 , 22 ,
15 , 7 , 42 , 5 ,
... 900 more items
],
[
471 , 289 , 691 , 56 , 2 , 16 , 6 , 47 , 35 , 3 , 53 , 959 ,
91 , 8 , 771 , 841 , 49 , 81 , 644 , 459 , 99 , 7874 , 7 , 7994 ,
29 , 993 , 41 , 234 , 34 , 78 , 142 , 146 , 5353 , 46 , 746 , 939 ,
54 , 596 , 36 , 11 , 551 , 471 , 29 , 12 , 477 , 23 , 415 , 29 ,
573 , 545 , 16 , 8 , 286 , 429 , 3 , 49 , 792 , 38 , 3581 , 68 ,
557 , 694 , 99 , 23 , 2 , 462 , 3287 , 54 , 82 , 66 , 585 , 867 ,
992 , 168 , 49 , 9 , 39 , 6394 , 572 , 738 , 47 , 63 , 79 , 11 ,
994 , 787 , 928 , 61 , 133 , 17 , 277 , 64 , 748 , 75 , 63 , 63 ,
28 , 2 , 44 , 12 ,
... 900 more items
],
[
527 , 84 , 5159 , 46 , 96 , 74 , 9 , 31 , 1 , 54 , 93 , 2525 ,
19 , 87 , 555 , 328 , 96 , 13 , 698 , 3168 , 535 , 2246 , 58 , 5345 ,
63 , 297 , 23 , 235 , 54 , 239 , 445 , 262 , 3591 , 98 , 7913 , 674 ,
26 , 649 , 9 , 12 , 82 , 329 , 18 , 45 , 178 , 3 , 947 , 65 ,
715 , 68 , 47 , 57 , 547 , 938 , 1 , 84 , 653 , 97 , 9138 , 37 ,
341 , 918 , 14 , 4329 , 375 , 847 , 1118 , 33 , 99 , 38 , 773 , 498 ,
7 , 11 , 48 , 2 , 65 , 246 , 117 , 358 , 36 , 14 , 49 , 26 ,
46 , 638 , 724 , 67 , 382 , 16 , 9749 , 954 , 725 , 86 , 17 , 51 ,
47 , 38 , 18 , 42 ,
... 900 more items
],
null
]
Stack trace:
TypeError: Cannot read properties of null (reading '0')
at transpose (<anonymous>:10:23)
at part1 (<anonymous>:3:14)
at <anonymous>:1:22
Okay, looking at the input there's actually more than 3 rows of numbers, let's fix the processing function
[...[ 1 , 2 , 3 ], 4 ] // checking if this works
[ 1 , 2 , 3 , 4 ]
function process ( input : string ) {
const rows = input . split ( "\n" );
const num_pat = /\d+/g ;
const op_pat = /[*+]/g ;
return [... rows . slice ( 0 , - 1 ). map ( r => r . match ( num_pat ). map ( n => parseInt ( n ))),
rows [ rows . length - 1 ]. match ( op_pat )];
}
[
[
527 , 781 , 232 , 95 , 3 , 75 , 59 , 66 , 43 , 5 , 68 , 877 ,
31 , 4 , 54 , 914 , 15 , 22 , 2 , 29 , 4 , 17 , 5 , 9176 ,
8 , 885 , 97 , 88 , 42 , 6 , 64 , 161 , 1675 , 36 , 363 , 738 ,
71 , 224 , 453 , 72 , 256 , 594 , 914 , 97 , 668 , 95 , 723 , 84 ,
738 , 437 , 38 , 1 , 893 , 416 , 5 , 21 , 745 , 18 , 4566 , 32 ,
627 , 16 , 5 , 45 , 8 , 88 , 4256 , 52 , 9 , 66 , 317 , 691 ,
224 , 375 , 17 , 25 , 3 , 3529 , 42 , 77 , 86 , 9 , 2 , 865 ,
793 , 87 , 469 , 86 , 452 , 85 , 51 , 44 , 11 , 43 , 31 , 22 ,
15 , 7 , 42 , 5 ,
... 900 more items
],
[
471 , 289 , 691 , 56 , 2 , 16 , 6 , 47 , 35 , 3 , 53 , 959 ,
91 , 8 , 771 , 841 , 49 , 81 , 644 , 459 , 99 , 7874 , 7 , 7994 ,
29 , 993 , 41 , 234 , 34 , 78 , 142 , 146 , 5353 , 46 , 746 , 939 ,
54 , 596 , 36 , 11 , 551 , 471 , 29 , 12 , 477 , 23 , 415 , 29 ,
573 , 545 , 16 , 8 , 286 , 429 , 3 , 49 , 792 , 38 , 3581 , 68 ,
557 , 694 , 99 , 23 , 2 , 462 , 3287 , 54 , 82 , 66 , 585 , 867 ,
992 , 168 , 49 , 9 , 39 , 6394 , 572 , 738 , 47 , 63 , 79 , 11 ,
994 , 787 , 928 , 61 , 133 , 17 , 277 , 64 , 748 , 75 , 63 , 63 ,
28 , 2 , 44 , 12 ,
... 900 more items
],
[
527 , 84 , 5159 , 46 , 96 , 74 , 9 , 31 , 1 , 54 , 93 , 2525 ,
19 , 87 , 555 , 328 , 96 , 13 , 698 , 3168 , 535 , 2246 , 58 , 5345 ,
63 , 297 , 23 , 235 , 54 , 239 , 445 , 262 , 3591 , 98 , 7913 , 674 ,
26 , 649 , 9 , 12 , 82 , 329 , 18 , 45 , 178 , 3 , 947 , 65 ,
715 , 68 , 47 , 57 , 547 , 938 , 1 , 84 , 653 , 97 , 9138 , 37 ,
341 , 918 , 14 , 4329 , 375 , 847 , 1118 , 33 , 99 , 38 , 773 , 498 ,
7 , 11 , 48 , 2 , 65 , 246 , 117 , 358 , 36 , 14 , 49 , 26 ,
46 , 638 , 724 , 67 , 382 , 16 , 9749 , 954 , 725 , 86 , 17 , 51 ,
47 , 38 , 18 , 42 ,
... 900 more items
],
[
96 , 7 , 9932 , 96 , 25 , 77 , 3 , 6 , 1 , 198 , 33 , 1758 ,
64 , 99 , 928 , 1738 , 8 , 73 , 373 , 4115 , 531 , 6283 , 86 , 71 ,
66 , 442 , 58 , 525 , 3 , 328 , 8563 , 461 , 662 , 6 , 9546 , 295 ,
33 , 662 , 2 , 32 , 95 , 5 , 25 , 96 , 58 , 3 , 921 , 72 ,
783 , 92 , 11 , 71 , 1 , 81 , 51 , 41 , 28 , 8 , 9666 , 5 ,
78 , 743 , 11 , 2264 , 995 , 244 , 74 , 27 , 32 , 64 , 62 , 42 ,
3 , 83 , 52 , 1 , 86 , 119 , 234 , 981 , 9 , 85 , 55 , 8 ,
27 , 622 , 225 , 4 , 98 , 77 , 4974 , 7215 , 491 , 37 , 24 , 75 ,
5 , 383 , 5 , 81 ,
... 900 more items
],
[
"*" , "*" , "+" , "*" , "*" , "+" , "*" , "*" , "*" , "*" , "+" , "+" ,
"+" , "*" , "*" , "+" , "*" , "+" , "*" , "+" , "+" , "+" , "+" , "+" ,
"*" , "+" , "+" , "+" , "+" , "*" , "+" , "*" , "+" , "+" , "+" , "+" ,
"+" , "*" , "+" , "+" , "+" , "+" , "*" , "+" , "*" , "*" , "*" , "*" ,
"+" , "+" , "*" , "+" , "+" , "*" , "*" , "+" , "*" , "*" , "+" , "+" ,
"*" , "+" , "*" , "+" , "*" , "*" , "+" , "+" , "*" , "*" , "*" , "*" ,
"+" , "*" , "+" , "*" , "+" , "+" , "*" , "*" , "+" , "*" , "+" , "+" ,
"*" , "*" , "*" , "*" , "+" , "*" , "+" , "+" , "*" , "*" , "+" , "*" ,
"*" , "+" , "*" , "+" ,
... 900 more items
]
]
5335495999141
correct!
Part 2
Now we need to parse the input in a different way, instead of parsing row by row we need to parse column by column.
input . split ( "\n" ). map ( r => r . length )
[ 3708 , 3708 , 3708 , 3708 , 3708 ]
So the input is rectangular/matrix. We can try to use the transpose function, since it will be easier to parse the numbers by rows like in the first part.
const processedSample = transpose ( sample . split ( "\n" ));
processedSample . slice ( 0 , 8 );
Each row now has a number, the last column has the operator. A batdh of numbers is now seperated by a blank line. It would be nice if we could batch the lines by this separator then figure out from the last column the op and drop it and parse the numbers. Let's start with trying to "collect" the rows
const sep = " " ;
console . log ( psample [ 3 ], psample [ 3 ]. join ( '' ) === sep )
[ " " , " " , " " , " " ] true
for ( const r of psample ) {
console . log ( r , r . join ( '' ) === sep )
}
[ "1" , " " , " " , "*" ] false
[ "2" , "4" , " " , " " ] false
[ "3" , "5" , "6" , " " ] false
[ " " , " " , " " , " " ] true
[ "3" , "6" , "9" , "+" ] false
[ "2" , "4" , "8" , " " ] false
[ "8" , " " , " " , " " ] false
[ " " , " " , " " , " " ] true
[ " " , "3" , "2" , "*" ] false
[ "5" , "8" , "1" , " " ] false
[ "1" , "7" , "5" , " " ] false
[ " " , " " , " " , " " ] true
[ "6" , "2" , "3" , "+" ] false
[ "4" , "3" , "1" , " " ] false
[ " " , " " , "4" , " " ] false
Seems to work. Now let's see how we can right the "chunker" function using reduce.
[ 1 , 2 , 3 , 0 , 1 , 2 , 3 , 4 , 0 , 5 ]. reduce (( r , x ) => {
x === 0 ? r . push ([]) : r [ r . length - 1 ]. push ( x );
return r ;
}, [[]])
[ [ 1 , 2 , 3 ], [ 1 , 2 , 3 , 4 ], [ 5 ] ]
Nice, js can be quite succinct with functional style. And now in our case
function chunker ( rows : string [][]) : string [][][] {
return rows . reduce (( chunks , row ) => {
row . join ( '' ). trim () === "" ? chunks . push ([]) : chunks [ chunks . length - 1 ]. push ( row );
return chunks ;
}, [[]] as string [][][])
}
const chunks = chunker ( processedSample );
chunks ;
Finally, let's see how we process a chunk
const chunk = chunks [ 0 ];
chunk
[
[ "1" , " " , " " , "*" ],
[ "2" , "4" , " " , " " ],
[ "3" , "5" , "6" , " " ]
]
const tchunk = transpose ( chunk )
tchunk
[
[ "1" , "2" , "3" ],
[ " " , "4" , "5" ],
[ " " , " " , "6" ],
[ "*" , " " , " " ]
]
const op = tchunk [ tchunk . length - 1 ]. join ( '' ). trim ()
op
"*"
cool, and the numbers
parseInt ( chunk [ 0 ]. slice ( 0 , - 1 ). join ( '' ). trim ())
1
chunk . map ( r => parseInt ( r . slice ( 0 , - 1 ). join ( '' ). trim ()))
[ 1 , 24 , 356 ]
Cool, the rest is like the last part, let's merge to one function
function process_chunk ( chunk : string [][]) : number {
const transposedChunk = transpose ( chunk );
const op = transposedChunk [ transposedChunk . length - 1 ]. join ( '' ). trim ();
const nums = chunk . map ( row => parseInt ( row . slice ( 0 , - 1 ). join ( '' ). trim ()));
return nums . reduce ( ops [ op ]);
}
Let's verify on sample. According to the problem page, we should have
1058 + 3253600 + 625 + 8544 = 3263827.
chunks . map ( c => process_chunk ( c ))
[ 8544 , 625 , 3253600 , 1058 ]
Looks good. Let's finish this
[ 1 , 2 , 3 ]. reduce (( acc , x ) => acc + x )
6
function part2 ( input : string ) : number {
const transposedInput = transpose ( input . split ( '\n' ));
const chunks = chunker ( transposedInput );
return chunks . map ( chunk => process_chunk ( chunk )). reduce (( acc , val ) => acc + val );
}
part2 ( sample )
10142723156431
Correct!