Advent of Code 2025 - Day Five

Advent Calendar.

Overview

Oops... killed the streak.

Welcome back to my weekly (current streak: 2) PowerShell post.

Continuing with our Advent of Code fun we're back at it with big numbers again. This time with big ranges. If you're still working with [int] for this puzzle, well then the only thing I have to say to you is:

You gotta pump those numbers up. Those are rookie numbers.

My solutions here: https://github.com/theznerd/AdventOfCode/tree/main/2025/05

This Inventory System is Dumb.

Okay - this seems pretty straightforward. We've got a bunch of ranges, and separately we've got a bunch of ingredients. If an ingredient ID is within any of the ranges, then the ingredient is fresh and we can keep it. The ranges are inclusive (top and bottom of the range are included), and some of the ranges may or may not overlap. I like to quickly see what I'm up against for part two of the puzzle, so if there is a way to get there quickly (even if not optimally) then I'm all for that route.

"We can optimize the code later"
     - every developer creating tech debt

The numbers in our puzzle input include things well outside of the range for [int] - so we know we're going to want to cast them to [long] or something similar that has a big space. We'll create an array or arrays of all of our ranges like this:

1foreach($freshID in $freshIDs -split "`r`n")
2{
3    $rangeStart, $rangeEnd = $freshID -split "-"
4    $freshIDRanges += ,@([long]$rangeStart, [long]$rangeEnd)
5}

I can already hear you ask - why is there a comma in front of that array on line 4? It's the Unary Comma Operator and it is going to prevent the array @([long]$rangeStart, [long]$rangeEnd) from unrolling so that our $freshIDRanges array is truly an array of arrays and not just a flat array of [long]s. By the way, the Language Specification documentation that I linked above here is a good way to find out things you might not know about how PowerShell works. It's a lot of reading, and frankly I don't think it's necessary for everyone to read in full (I've not had the pleasure either), but whenever you see someone do something interesting in a script, there's a good chance it's documented somewhere in this specification.

Okay great. So now we have our array of ranges. Now we can loop through all our ingredients and test whether they are within the boundary of the range. Since there are a lot of ranges, and some overlap, it makes sense for us to dump out of our loop as soon as we find a match. We'll do this using a labeled continue inside of our loop.

 1:ingredient foreach($ingredient in $availableIngredients -split "`r`n")
 2{
 3    foreach($range in $freshIDRanges)
 4    {
 5        if([long]$ingredient -ge $range[0] -and [long]$ingredient -le $range[1])
 6        {
 7            $freshIngredients++
 8            continue ingredient
 9        }
10    }
11}

Why a labeled loop here instead of just continue? Well, we're two loops deep at this point in our code, right? If we were just to continue, we'd continue on the next range in the $freshIDRanges which is the behavior we're trying to avoid here. By continuing at ingredient, we stop evaluating the ranges and continue on to our next ingredient. It saves quite a bit of time since we don't need to continue testing ranges if we already found a match.

So, the problem is solved and the elves now know what ingredients they need to throw out. However, there's always a catch.

We'd Really Hate To Bother You

You can sort of predict these second parts when you spend a couple years doing Advent of Code puzzles. Of course the elves are polite and don't wish to keep asking you if an ingredient is spoiled or fresh, so they've asked you to give them a list of ALL of the ingredients that are fresh so they can check. That's gonna be one hell of a list.

Luckily, the puzzle masters did not make us generate such a list, and instead wisely asked us - exactly how many ids would be in this list if you were to generate it? That is much easier (and requires much less memory).

For those of you who know me, you know that I like classes in PowerShell. Especially when it comes to sorting (because it's realtively easy to implement the IComparable interface). The plan here is to create a list of Range objects (that we create), sort them, merge them until we can't merge no more, and then for the remaining ranges get the total of the values in the range (rangeEnd - rangeStart + 1 [to account for the inclusiveness]).

The class to implement IComparable looks like this:

 1class Range : IComparable {
 2    [long]$Start
 3    [long]$End
 4    Range([long]$start, [long]$end){
 5        $this.Start = $start
 6        $this.End = $end
 7    }
 8
 9    [int]CompareTo($other) {
10        if ($this.Start -lt $other.Start) { return -1 }
11        if ($this.Start -gt $other.Start) { return 1 }
12        return 0
13    }
14}

Now why are we implementing IComparable? Well, if we sort our ranges by the start range, we can reduce the number of ranges we need to check for overlap (previous ranges in the search start earlier, so if there were an overlap it already would have been merged).

Next we create the list of ranges and then sort it:

1$freshIDRanges = [Collections.Generic.List[Range]]::new()
2foreach($freshID in $freshIDs -split "`r`n")
3{
4    $rangeStart, $rangeEnd = $freshID -split "-"
5    $freshIDRanges.Add([Range]::new([long]$rangeStart, [long]$rangeEnd))
6}
7$freshIDRanges.Sort()

Notice that we don't have to do something like $freshIDRanges = $freshIDRanges.Sort(). When calling the Sort() method on the range, we are "destructively" sorting the list (we lose the previous arrangement of items). After we've got our sorted list, we do the fun of merging overlapping ranges:

 1do{
 2    $changesMade = $false
 3    for($i = 0; $i -lt $freshIDRanges.Count - 1; $i++) # iterate through all the ranges
 4    {
 5        for($j = $i + 1; $j -lt $freshIDRanges.Count; $j++) # check all subsequent ranges (we sorted by start already, so can assume earlier ranges start earlier)
 6        {
 7            if($freshIDRanges[$j].Start -le $freshIDRanges[$i].End)
 8            {
 9                # Ranges overlap, merge them
10                $freshIDRanges[$i].End = [math]::Max($freshIDRanges[$i].End, $freshIDRanges[$j].End)
11                $freshIDRanges.RemoveAt($j)
12                $changesMade = $true
13            }
14        }
15    }
16} while($changesMade -eq $true) # repeat until all the ranges are merged

As we find a range that overlaps, we merge them and continue our search. There are certainly more efficient algorithms to do this, but this works and is relatively easy to read and understand. One thing to note here is that for each loop iteration $i -lt $freshIDRanges.Count is re-evaluated. That means that it is okay for me to make changes to the list in the middle of the loop because on the next iteration, we'll make sure that we're within the bounds of that loop before executing the block. If I had used foreach or something similar, PowerShell would have started complaining that I was making changes to something that it was using for iteration.

With that I think it's time to close it out for the day. If there are any other topics that you would like me to talk over, please let me know in the comments (I'm going to run out of Advent of Code things at some point). Also if you've got any questions about what I did, feel free to dump into the comments. Otherwise, as always, Happy Scripting!