Advent of Code 2025 in Charly
Day 6
This article is part of my series on implementing each Advent of Code 2025 challenge in my own programming language Charly.
Part 1
The first part of today’s puzzle asked us to solve the math homework for the friendly cephalopods I encountered in the garbage disposal unit. Their homework came in a file that looked like this:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
Each column contained a list of input numbers and an operator to apply to those numbers. The above worksheet contains four such problems:
123*45*6=33210328+64+98=49051*387*215=424345564+23+314=401
To verify your work, you had to provide the sum of all the individual problems’ solutions.
My solution to part 1 of the puzzle:
const input_path = ARGV[1]
const lines = readfile(input_path)
.lines()
.map(->(line, i, lines) {
let line = line
.split(" ")
.filterEmpty()
if i < lines.length - 1 {
line = line.map(->(l) l.to_number())
}
line
})
const (...data, operators) = lines
const problems = operators
.map(->(op, index) {
const operands = data.map(->(row) row[index])
(op, operands)
})
const results = problems.map(->(problem) {
const (operand, inputs) = problem
const result = operand == "+" ? inputs.sum() : inputs.product()
result
})
const totalSum = results.sum()
print("totalSum = {totalSum}")Part 2
The second part of today’s puzzle was a bit more complicated. As a quick reminder, here’s the example worksheet again:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
The cephalopods have a different way of writing down their math problems than we humans do. I’m not going to attempt to explain this myself, so instead I’ll just reference the problem description:
Cephalopod MathCephalopod math is written right-to-left in columns. Each number is given in its own column, with the most significant digit at the top and the least significant digit at the bottom. (Problems are still separated with a column consisting only of spaces, and the symbol at the bottom of the problem is still the operator to use.)
This changed the problem significantly. The worksheet now consisted of these problems:
356*24*1=85448+248+369=625175*581*32=32536004+431+623=1058
I now had to consume all input lines right-to-left and
construct the input numbers myself. Additionally, in the example
worksheet provided, all the data columns had a width of exactly
3. This wasn’t the case in the actual worksheet I had to
process. Each column could be anywhere from 2 to
4 characters wide.
I solved this by repeatedly popping off characters from the end of all input lines. If all characters were whitespace, that meant I had reached the end of a problem and could move on to the next one.
The rest of the puzzle remained unchanged: solve each problem by
performing either a + or * operation, and then
sum up the results to get the final solution.
My solution to part 2 of the puzzle:
const input_path = ARGV[1]
let lines = readfile(input_path).lines()
const maxLineLength = lines.map(->(l) l.length).findMax()
lines = lines.map(->(line) {
while line.length < maxLineLength {
line = "{line} "
}
line
})
const (...inputLines, operandLine) = lines
const (inputs, operands) = (
inputLines.map(->(line) line.chars()),
operandLine.split(" ").filterEmpty().reverse()
)
let readerOffset = 0
const problems = operands
.map(->(operand) {
let inputData = []
loop {
if inputs.first().empty() {
break
}
const charsRead = inputs.map(->(chars) {
chars.pop()
})
if charsRead.all(->(c) c == " ") {
break
}
const number = charsRead
.filter(->(c) c != " ")
.join("")
.to_number()
inputData.push(number)
}
(operand, inputData)
})
const solutions = problems.map(->(problem) {
const (operand, inputs) = problem
const result = operand == "+" ? inputs.sum() : inputs.product()
result
})
const totalSum = solutions.sum()
print("totalSum = {totalSum}")Changes to the stdlib / VM
Standard library methods I added:
List::all: Checks whether the provided callback returnstruefor all entries in the listList::product: Returns the product of all numbers in the listList::reverse: Returns a copy of the list in reverse orderList::filterEmpty: Filters out all elements withlength == 0
Methods I had to modify:
Tuple::map: Now returns aListinstead of aTuple
You can find the individual commits below:
8abec0bAdd List::all279726eAdd List::productd967d7aAdd List::reverse4cba80bAdd List::filterEmpty9eb9e54Modify Tuple::map to return a List, not Tuple
Late-night addendum
I was really inspired by the solution that Lukas Lebovitz came up with, so I decided to port it to Charly. Since his code relies heavily on Kotlin standard library functions, I first had to implement those in the Charly standard library as well. My final version looks like this:
const input_path = ARGV[1]
// ensure all lines have the same length, as the IDE will remove trailing spaces
const lines = readfile(input_path).lines().apply(->(lines) {
const maxLength = lines.findMaxBy(->(it) it.length).length
lines.map(->(line) line.padRight(maxLength, " "))
}).map(->(line) line.chars())
// originally implemented in Kotlin by @lukaslebo
let totalSum = 0
const currentProblemOperands = []
lines.first().indices().reverse().each(->(i) {
const column = lines.map(->(line) line[i])
const digits = column.dropLast(1)
const operand = column.last()
if (digits.all(->(d) d == " ")) {
i -= 1
return
}
currentProblemOperands.push(digits.join("").to_number())
switch operand {
case "+" totalSum += currentProblemOperands.sum()
case "*" totalSum += currentProblemOperands.product()
default return
}
currentProblemOperands.clear()
})
print("totalSum = {totalSum}")I added the following methods to the standard library:
Value::also: Runs the provided callback and returnsselfValue::apply: Runs the provided callback and returns the callback’s resultNumber::downTo: Downwards counting variant ofNumber::upToNumber::collectDownTo: Downwards counting variant ofNumber::collectUpToString::padLeft: Pads the beginning of a string to reach a desired minimum lengthString::padRight: Pads the end of a string to reach a desired minimum lengthList::clear: Clears the contents of a listList::indices: Returns a list of all valid indicesList::takeFirst: Returns a copy of the list with the firstNelementsList::takeLast: Returns a copy of the list with the lastNelementsList::dropFirst: Returns a copy of the list without the firstNelementsList::dropLast: Returns a copy of the list without the lastNelements
I modified the following method:
List::join: Added an optional mapping callback to preprocess the list contents before joining
You can find the individual commits below:
aab7c9cAdd Value::also3a13da5Add Value::apply1c49921Add Number::downTof61f990Add Number::collectDownTo7f76963Add String::padLeft and String::padRightfa1c6d1Add List::clear0fb468dAdd List::indicese89aa67Add map callback to List::joine52e0b5Add List::dropFirst and List::dropLast579df89Add List::takeFirst and List::takeLast
Links
Copyright © 2024 - 2025 Leonard Schütz | Attributions This post was written by a human and reviewed and proof-read by LLMs