Community Blog
Get the latest updates on the Splunk Community, including member experiences, product education, events, and more!

[Puzzles] Solve, Learn, Repeat: Advent of Code - Day 7

ITWhisperer
SplunkTrust
SplunkTrust

Advent of Code

In order to participate in these challenges, you will need to register with the Advent of Code site, so that you can retrieve your own datasets for the puzzles. When you get an answer, you will need to submit it to the Advent of Code site to determine whether the answer is correct. I have already completed the 2025 set using python, so I will know when my SPL generates the correct result.

Day 7

Each day's puzzle is split into two parts; part one is usually easier than part two, and you cannot normally reach part two until you have successfully completed part one. Day 7 is about counting how many times a "tachyon beam" will be "split" as it passes through a "tachyon manifold". Please visit the website for full details of the puzzle.

This article contains spoilers!

In fact, the whole article is a spoiler as it contains solutions to the puzzle. If you are trying to solve the puzzle yourself and just want some pointers to get you started, stop reading when you have enough, and return if you get stuck again, or just want to compare your solution to mine!

Part One

As with all the Advent of Code puzzles, the description of the problem is aided by some example input; in this case, the input is grid showing the "manifold", with the start point for the "tachyon beam", and the location of the "splitters". The aim of the puzzle is to determine, for your own dataset, how many times the "beam" is "split" as it passes through the "manifold".

Initialising your data

The first thing to do is initialise your data. One way to do this is to save it to a csv file and use inputlookup to load it. Alternative, you could just use makeresults (as I have done here), and set a field to the data:

| makeresults
| eval _raw=".......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
..............."

The next step is to break the data up into separate lines in a multi-value field,

| rex max_match=0 "(?<line>\S+)"

Interpreting the data

Each line represents a layer or row in the "manifold"; so create a set of fields in order to iterate over the lines.

| appendpipe
    [
    | eval list=mvrange(1,mvcount(line))
    | mvexpand list
    | eval list="l_".list
    | eval name="day_7"
    | chart values(eval(0)) as zero by name list useother=f limit=0
    | fields - name
    ]
| where isnotnull(line)

Note that the mvrange starts at 1 since we do not need to process the first line. However, we do need to preserve it as the previous line ready for when we start iterating over the lines.

| eval previous=mvindex(line, 0)

Next, we need a multi-value field so that we can iterate over each position in the line.

| eval width=len(previous)
| eval list=mvrange(0, width)

Zero the total and initialise line index to 1 i.e. the second line.

| eval total=0
| eval line_index=1

Iterate over the lines (starting with the second line)

| foreach l_*
    [
    | eval current_line=mvindex(line, line_index)

Iterate over the positions in the line

    | foreach mode=multivalue list 
        [
        eval y=<<ITER>> + 1,

Note the use of the <<ITER>> variable, which is an index into the multivalue field being iterated over. This index is zero-based but we are going to be doing substring indexing which is one-based, hence the plus one.

For each position in the line, if it is a "." ("empty space") and the same position in the previous line is "S" ("start") or "|" (a "beam"), rebuild the line replacing the "." ("empty space")with "|" (a "beam").

        new_line=if(substr(current_line, y, 1)="." and (substr(previous, y, 1)="S" or substr(previous, y, 1)="|"), if(y = 1, "", substr(current_line, 1, y - 1) )."|".if(y < width, substr(current_line, y + 1), ""),

Otherwise, if the position on the current line is "^" (a "splitter") and the same position in the previous line is "|" (a "beam"), rebuild the line replacing "empty spaces" to the left and right with "|" (a "beam").

if(substr(current_line, y, 1)="^" and substr(previous, y, 1)="|", if(y > 1, if(substr(current_line, y - 1, 1)=".", if(y = 2, "", substr(current_line, 1, y - 2) )."|^".if(y < width, if(substr(current_line, y + 1, 1)=".", "|".if(y < width - 1, substr(current_line, y + 2), ""), substr(current_line, y + 1) ), ""), substr(current_line, 1, y).if(y < width, if(substr(current_line, y + 1, 1)=".", "|".if(y < width - 1, substr(current_line, y + 2), ""), substr(current_line, y + 1) ), "") ), "^".if(substr(current_line, y + 1, 1)=".", "|".substr(current_line, y + 2), substr(current_line, y + 1) ) ), current_line) ),

Note some adjustments have to be made when rebuilding the new line depending on how close to the edges the current position (y) is.

Increment the count if the "beam" has been "split".

        total=if(substr(current_line, y, 1)="^" and substr(previous, y, 1)="|", total + 1, total),

Update the current line so that further processing is done correctly.

        current_line=new_line
        ]

Preserve the current line as the new previous line

    | eval previous=current_line
    | eval line_index=line_index + 1
    ]

The total field now has the number of times the "beam" was split.

| table total

Part Two

The second part of the puzzle requires that we determine, for your own dataset, the total number of paths ("timelines") a single "tachyon beam" could take through the "manifold".

Solving Part Two

Starting in the same manner as Part One, extract each line from the input, and create iterators for each line and each position in the line.

| rex max_match=0 "(?<line>\S+)"
| appendpipe
    [
    | eval list=mvrange(0,mvcount(line))
    | mvexpand list
    | eval list="l_".list
    | eval name="day_7"
    | chart values(eval(0)) as zero by name list useother=f limit=0
    | fields - name
    ]
| where isnotnull(line)
| eval width=len(mvindex(line,0))
| eval list=mvrange(0, width)

Running totals

This time we need to keep a running total of the number of paths passing through each position (which we will total at the end). So, initialise a comma-separated line of zeroes.

| foreach mode=multivalue list 
    [
    | eval zeroed_timeline=mvappend(zeroed_timeline,"0")
    ]
| eval zeroed_timeline=mvjoin(zeroed_timeline,",")

We could have used a multi-value field here, but this sometimes can lead to instabilities in Splunk when passing the multi-value field through multiple iterations, and handling strings can also be more performant.

Iterate over the lines, starting at the top (of the "manifold")

| eval line_index=0
| foreach l_*
    [
    | eval current_line=mvindex(line, line_index)

Initialise the totals for the current line and start iterating over the positions in the line

    | eval new_timeline=zeroed_timeline
    | foreach mode=multivalue list 
        [
        | eval y=<<ITER>>,

Create a multi-value field representing the current totals to the left of the current position (the totals to the right will be picked up a little later)

        new_totals=split(new_timeline, ","),
        lhs=if(y > 0, mvindex(new_totals, 0, y - 1), null()),

Note that we are splitting the string of totals, but this is done as close to when the multi-value version is used as possible.

Calculate the running total for the current position, treating the first iteration as a special case

        current_position=if(line_index = 0, if(substr(current_line, y + 1, 1) = "S", 1, 0),

For subsequent iterations, if the position in the line is an "empty space", we need to add the current total to the previous total for this position

        current_position=if(line_index = 0, if(substr(current_line, y + 1, 1) = "S", 1, 0), if(substr(current_line, y + 1, 1) = ".", mvindex(new_totals, y) + mvindex(split(current_timeline, ","), y), mvindex(new_totals, y))),

 For subsequent iterations, we also need to consider if there is a "splitter" to the right, and add the paths that hit that "splitter"

        current_position=if(line_index > 0 and y < width - 1 and substr(current_line, y + 2, 1) = "^", current_position + tonumber(mvindex(split(current_timeline, ","), y + 1)), current_position),

Similarly, if there is a "splitter" to the left

        current_position=if(line_index > 0 and y > 0 and substr(current_line, y, 1) = "^", current_position + tonumber(mvindex(split(current_timeline, ","), y - 1)), current_position),

This can be compacted into a single line

        current_position=if(line_index = 0, if(substr(current_line, y + 1, 1) = "S", 1, 0), if(substr(current_line, y + 1, 1) = ".", tonumber(mvindex(new_totals, y)) + tonumber(mvindex(current_timeline, y)) + if(y < width - 1 and substr(current_line, y + 2, 1) = "^", tonumber(mvindex(current_timeline, y + 1)), 0) + if(y > 0 and substr(current_line, y, 1) = "^", tonumber(mvindex(current_timeline, y - 1)), 0), mvindex(new_totals, y))),

Now create a multi-value field for the totals to the right of the current position

        rhs=if(y < width - 1, mvindex(new_totals, y + 1, -1), null()),

Rebuild the complete list of running totals, ready to be split on the next position iteration

        new_timeline=mvjoin(mvappend(lhs, printf("%d", current_position), rhs), ",")
        ]

 When all the position iterations have completed, we can reset the the current timeline running totals ready for the next line iteration

    | eval current_timeline=split(new_timeline, ",")
    | eval line_index=line_index + 1
    ]

Finally, we can sum all the running totals for the positions, to get the grand total of "paths" through the "manifold"

| stats sum(current_timeline) as total

Summary

There are many ways to solve this puzzle, and, in the end, I settled on slightly different approaches for Part One and Part Two. The key issue to crack for Part Two was the stability of Splunk when dealing with multiple iterations of multi-value fields. I dealt with this by reducing the number of events (keeping the puzzle in a single event) and reducing multi-value fields to strings (and converting back to multi-value fields as late as possible). I do not normally compare my solution with Gabriel's (or others), but I think on this occasion it is worthy of comment. Gabriel's solution (outlined here) reduced the problem back to the basics and determined the essential elements of the puzzle i.e. the location of the "splitters", whereas, my solution attempted to get around the limitation of Splunk by adapting the memory and field structures to fit within the constraints of my environment. Personally, I think Gabriel's solution is much cooler than mine (and is more in keeping with the spirit of Advent of Code), but I feel my approach (which was more like brute-forcing my original Python solution) might be more adaptable to other (real-life) usecases. What do you think?

Have questions or thoughts? Comment on this article or in Slack #puzzles channel. Whichever you prefer.

If you’re not subscribed, you’re probably missing something good. Fix that! 

Contributors
Get Updates on the Splunk Community!

Build the Future of Agentic AI: Join the Splunk Agentic Ops Hackathon

AI is changing how teams investigate incidents, detect threats, automate workflows, and build intelligent ...

[Puzzles] Solve, Learn, Repeat: Character substitutions with Regular Expressions

This challenge was first posted on Slack #puzzles channelFor BORE at .conf23, we had a puzzle question which ...

Splunk Community Badges!

  Hey everyone! Ready to earn some serious bragging rights in the community? Along with our existing badges ...