Part 1
Read in a list of banks of batteries, one bank per line, where each battery has a joltage defined by a single digit from 1-9. For each bank, the maximum joltage is that obtained by switching on two of the batteries.
Solution
As usual, first of all we need to design some data structures. We need a data structure for a battery which contains the joltage:
type Battery struct {
joltage int
}We also need a battery bank:
type BatteryBank struct {
batteries []Battery
}Next we need a function to parse the input of a single battery bank and convert it into the data structure. Although the input is known in this case and we could get away without error handling, we will check the input against a regular expression to ensure it only contains the digits 1-9, and at least one digit.
func getBatteryBank(input string) (BatteryBank, error) {
batteryBank := BatteryBank{}
input = strings.TrimSpace(input)
bankRegex := regexp.MustCompile(`^[1-9]+$`)
if bankRegex.MatchString(input) {
batteryDigits := strings.Split(input, "")
for bd := range batteryDigits {
batteryDigit, _ := strconv.Atoi(batteryDigits[bd])
batteryBank.batteries = append(batteryBank.batteries, Battery{
joltage: batteryDigit,
})
}
return batteryBank, nil
}
return batteryBank, errors.New("invalid battery bank")
}We also need to read in all the battery banks to create a slice. This is easy as we split the input by newlines and then call the getBatteryBank function.
func getBatteryBanks(input string) []BatteryBank {
batteryBanks := []BatteryBank{}
lines := strings.Split(input, "\n")
for lineIndex := range lines {
batteryBank, err := getBatteryBank(lines[lineIndex])
if err == nil {
batteryBanks = append(batteryBanks, batteryBank)
}
}
return batteryBanks
}Now that we have all the battery banks, we need to find our their maximum joltage from turning on two batteries. To do this, we first need to sort the batteries by descending order of joltage, i.e. largest to smallest. There are a few things to bear in mind here:
- We can’t sort batteries directly, because structs can only be compared for equality (i.e. x == y but not x < y).
slices.Sortonly works withintand sorts in ascending order.- Slices are generally sorted in place, i.e. they modify the original slice rather than returning the current slice.
The way we can work this is by:
- Map the slice of batteries to a slice of joltages.
- Sort the joltages in ascending order.
- Reverse the joltages to be in descending order.
- Get the first two joltages from the slice.
Although the maximum joltage involves concatenating two digits, we don’t need to convert them to strings. Instead we can multiply the largest digit by 10 and then add the second digit, e.g. maximumJoltage = (9*10) + 8.
func maximumJoltage(batteryBank BatteryBank) int {
// Only calculate the maximum joltage if we have at least 2 batteries
if len(batteryBank.batteries) >= 2 {
// Extract the joltages as a slice
joltages := lo.Map(batteryBank.batteries, func(battery Battery, index int) int {
return battery.joltage
})
// Sort in ascending order
slices.Sort(joltages)
// Reverse to get descending order
slices.Reverse(joltages)
// Maximum voltage is concatenation of first two joltages, which
// we can calculate by multiplying the first one by 10
return (joltages[0] * 10) + joltages[1]
}
return 0
}I got to this point feeling rather pleased with myself, then realised you can’t rearrange batteries! Once again, the test suite saved the day.
So the actual problem is: given a slice of joltages, what is the biggest number that can be made from two joltages? A simple method would be to calculate all the possible combinations of two joltages, and then find the largest (or only keep the current combination if it is larger than the current maximum). We can achieve that with a nested loop, where the inner loop starts 1 element further than the outer loop, and the outer loop stops one element before the end (because we always want a second element).
func maximumJoltage(batteryBank BatteryBank) int {
maxJoltage := 0
// Only calculate the maximum joltage if we have at least 2 batteries
if len(batteryBank.batteries) >= 2 {
for i := 0; i < len(batteryBank.batteries)-1; i++ {
for j := i + 1; j < len(batteryBank.batteries); j++ {
currentJoltage := (batteryBank.batteries[i].joltage * 10) + batteryBank.batteries[j].joltage
if currentJoltage > maxJoltage {
maxJoltage = currentJoltage
}
}
}
}
return maxJoltage
}The above function passes the tests, let’s hope it is efficient enough when used on the real input!
Finally, we need to find the sum of the maximum joltages for each battery bank, for which we’ll use SumBy:
func main() {
inputBytes, _ := os.ReadFile("../2025-03-input.txt")
inputString := string(inputBytes)
batteryBanks := getBatteryBanks(inputString)
totalOutputJoltage := lo.SumBy(batteryBanks, func(batteryBank BatteryBank) int {
return maximumJoltage(batteryBank)
})
fmt.Println(totalOutputJoltage)
}It ran in 0.12s, so that’s efficient enough. An interesting thought is whether we can skip any joltage combinations because we know they will be smaller than the current maximum joltage, but given how cheap calculating the current joltage is (one multiplication, one addition and one comparison), I think any test required to skip a combination would probably be less efficient than just trying the combination.
Part 2
Overall thoughts
Other than my mistake in not reading the specification correctly (caught by my test suite), this was an easy puzzle to solve. I worked out how to extract data from within a slice of structs (using lo.Map) and then process it, which I think will be useful in other puzzles.