DEV Community

Cover image for Solved: Anyone doing Advent of Code in powershell?
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Anyone doing Advent of Code in powershell?

🚀 Executive Summary

TL;DR: Tackling Advent of Code in PowerShell presents challenges like verbose input parsing and performance bottlenecks for complex algorithms. Solutions involve leveraging idiomatic PowerShell’s pipeline and .NET integration, boosting speed with C# interop for critical sections, and orchestrating external, highly optimized utilities.

🎯 Key Takeaways

  • Idiomatic PowerShell, utilizing its object-oriented pipeline, cmdlets, and direct .NET calls, is the most natural and often sufficient approach for many Advent of Code problems, prioritizing readability.
  • C# interop via Add-Type is a powerful technique to overcome PowerShell’s performance limitations for computationally intensive AoC sections, allowing C# classes to run at native speed within a PowerShell script.
  • PowerShell excels at orchestrating external, highly optimized tools (e.g., grep, custom compiled binaries) for specific sub-problems, acting as the ‘glue’ to prepare inputs, execute, and process outputs.

Tackling Advent of Code with PowerShell presents unique challenges, from intricate input parsing to optimizing performance for complex algorithms. This post explores effective strategies for IT professionals to solve AoC puzzles using idiomatic PowerShell, C# interop for speed, and orchestration of external utilities.

The Quirks of Advent of Code in PowerShell: Symptoms

Advent of Code (AoC) is a fantastic way to sharpen your programming skills, and attempting it in PowerShell can highlight both the language’s strengths and its inherent limitations for specific types of problems. When using PowerShell for AoC, you might encounter symptoms like:

  • Verbose Input Parsing: AoC puzzles often involve highly structured or complex text inputs (e.g., grids, nested lists, specific patterns). While PowerShell’s string manipulation and regex capabilities are powerful, parsing these inputs can sometimes lead to lengthy or less elegant code compared to other languages.
  • Performance Bottlenecks: For problems involving large datasets, deep recursion, or computationally intensive algorithms (like pathfinding on large grids or complex simulations), PowerShell’s interpreted nature and object overhead can result in significantly slower execution times compared to compiled languages. You might notice scripts taking minutes instead of seconds, especially for Part 2 of a daily puzzle.
  • Implementing Advanced Data Structures: While PowerShell can represent most data structures (arrays, hashtables, generic lists), building highly optimized or specialized structures (e.g., priority queues, custom graph implementations, immutable collections) purely in PowerShell can be less straightforward or less performant than using a language with strong native support or extensive library ecosystems.
  • Lack of Built-in Algorithmic Primitives: Unlike languages with rich standard libraries for algorithms (like Python’s collections or C#’s LINQ for specific scenarios), PowerShell sometimes requires more manual implementation of common algorithmic patterns, which can increase development time and potential for errors.

Solution 1: Idiomatic PowerShell – Embracing the Pipeline

The most natural and often sufficient approach for many AoC problems is to lean into PowerShell’s strengths: its object-oriented pipeline, powerful cmdlets, and seamless .NET integration. This approach prioritizes readability and leveraging the tool’s core design principles.

Concept

Use PowerShell as it’s intended. This means chaining cmdlets, utilizing the pipeline for data flow, employing native string manipulation and regex, and making direct calls to .NET types when built-in cmdlets aren’t enough. For most day-to-day scripting tasks and many AoC problems, this is the most productive path.

Real Examples and Commands

Let’s consider a common AoC task: reading input, parsing numbers, and performing calculations.

Example: Summing Numbers from a Delimited Input File

Suppose your input file input.txt contains:

10,20,30
15,25,35
Enter fullscreen mode Exit fullscreen mode

You want to sum all numbers.

# Read each line, split by comma, convert to integer, and sum
$totalSum = Get-Content -Path 'input.txt' | 
    ForEach-Object { $_ -split ',' } |
    ForEach-Object { [int]$_ } |
    Measure-Object -Sum | Select-Object -ExpandProperty Sum

Write-Host "Total Sum: $totalSum"
Enter fullscreen mode Exit fullscreen mode

Example: Working with a Grid/Matrix

Many puzzles involve grids. Representing them as an array of arrays or an array of character arrays is common.

# Assume input.txt contains a grid like:
# #.##
# .#.#
# ##.#

$grid = Get-Content -Path 'grid.txt' | ForEach-Object { $_.ToCharArray() }

# Accessing an element (e.g., row 1, column 2)
Write-Host "Element at (1,2): $($grid[1][2])"

# Iterating through the grid
for ($row = 0; $row -lt $grid.Length; $row++) {
    for ($col = 0; $col -lt $grid[$row].Length; $col++) {
        Write-Host "($row,$col) = $($grid[$row][$col])"
    }
}
Enter fullscreen mode Exit fullscreen mode

Using .NET Collections for Performance/Flexibility

When you need mutable lists or other specific collections, directly using .NET types is crucial.

# Creating a mutable generic list
$myList = New-Object System.Collections.Generic.List[int]
$myList.Add(10)
$myList.Add(20)
$myList.Add(30)

Write-Host "List count: $($myList.Count)"
$myList.Remove(20)
Write-Host "List after removal: $($myList -join ', ')"

# Using a Dictionary (HashTable in PowerShell)
$myDictionary = @{}
$myDictionary["apple"] = 1
$myDictionary["banana"] = 2
Write-Host "Value for apple: $($myDictionary["apple"])"
Enter fullscreen mode Exit fullscreen mode

Solution 2: Performance Boost with C# Interop

When “idiomatic PowerShell” hits a performance wall for computationally intensive parts of an AoC problem, leveraging C# interop is a powerful technique. You can define and compile C# classes directly within your PowerShell session.

Concept

This approach involves writing C# code for the performance-critical sections (e.g., complex algorithms, large loops, recursive functions) and embedding it within your PowerShell script using the Add-Type cmdlet. PowerShell then instantiates and calls methods on these C# objects as if they were native PowerShell objects. This combines PowerShell’s excellent orchestration capabilities with C#’s raw execution speed.

Real Examples and Commands

Let’s imagine an AoC problem requires a highly optimized search or a custom data structure like a priority queue. Implementing this in pure PowerShell might be slow. Here’s how you could use C# for a simple, fast calculation:

Example: Fast Fibonacci Sequence Calculation in C#

We’ll define a C# class with a static method to calculate the Nth Fibonacci number quickly.

$cSharpCode = @'
using System;

public static class FibonacciCalculator
{
    public static long GetNthFibonacci(int n)
    {
        if (n <= 1) return n;

        long a = 0;
        long b = 1;
        for (int i = 2; i <= n; i++)
        {
            long temp = b;
            b = a + b;
            a = temp;
        }
        return b;
    }
}
'@

# Add the C# type to the current PowerShell session
Add-Type -TypeDefinition $cSharpCode -Language CSharp

# Now you can call the static method
$fib10 = [FibonacciCalculator]::GetNthFibonacci(10)
Write-Host "10th Fibonacci number: $fib10" # Expected: 55

$fib50 = [FibonacciCalculator]::GetNthFibonacci(50)
Write-Host "50th Fibonacci number: $fib50" # Expected: 12586269025
Enter fullscreen mode Exit fullscreen mode

Example: A Simple Point Struct for Grid Problems

Defining lightweight structs in C# can be more efficient than custom PowerShell objects for high-frequency operations.

$cSharpCode = @'
using System;

public struct Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString()
    {
        return $"({X},{Y})";
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Point))
        {
            return false;
        }

        var point = (Point)obj;
        return X == point.X &&
               Y == point.Y;
    }

    public override int GetHashCode()
    {
        var hashCode = 1502939027;
        hashCode = hashCode * -1521134295 + X.GetHashCode();
        hashCode = hashCode * -1521134295 + Y.GetHashCode();
        return hashCode;
    }

    public static bool operator ==(Point left, Point right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(Point left, Point right)
    {
        return !(left == right);
    }
}
'@

Add-Type -TypeDefinition $cSharpCode -Language CSharp

# Create and use Point structs
$p1 = New-Object Point(10, 20)
$p2 = New-Object Point(10, 20)
$p3 = New-Object Point(5, 5)

Write-Host "Point 1: $p1"
Write-Host "Point 2: $p2"
Write-Host "Point 3: $p3"

if ($p1 -eq $p2) {
    Write-Host "P1 and P2 are equal."
}
if ($p1 -ne $p3) {
    Write-Host "P1 and P3 are not equal."
}
Enter fullscreen mode Exit fullscreen mode

Solution 3: Orchestrating External Tools and Utilities

Sometimes, the most efficient solution for a specific sub-problem within an AoC puzzle isn’t to write it in PowerShell or C#, but to leverage an existing, highly optimized external tool. PowerShell excels at orchestrating these tools, passing data between them and integrating their output.

Concept

This approach involves calling external executables (e.g., grep, awk via WSL, custom compiled binaries, or even specialized data processing tools) from within your PowerShell script. PowerShell acts as the glue, preparing inputs for the external tool, executing it, and then processing its output.

Real Examples and Commands

Consider scenarios where pattern matching, complex text manipulation, or checksum calculations are best handled by highly optimized, external command-line utilities.

Example: Using WSL grep for Advanced Pattern Matching

If you’re on Windows and need the power of grep for complex regex matching across your input file, you can invoke WSL utilities.

# Assume input.txt has lines you want to grep
# "This is a test line."
# "Another line with the word test."
# "No match here."

# Find all lines containing "test"
$matchedLines = wsl.exe grep "test" "input.txt"

Write-Host "Lines containing 'test':"
$matchedLines | ForEach-Object { Write-Host $_ }
Enter fullscreen mode Exit fullscreen mode

Example: Invoking a Custom Compiled Executable

If you’ve written a particularly tricky algorithm in C++, Rust, or Go, and compiled it to an executable, PowerShell can easily run it.

Assume you have an executable named pathfinder.exe that takes an input file path and returns the shortest path length.

# Create a dummy input file for the external tool
Set-Content -Path 'puzzle_map.txt' -Value @'
###########
#S........#
###.......#
#.........#
#.......E.#
###########
'@

# Invoke the external pathfinder.exe with the input file
# The `&` operator is used to run external commands.
$pathResult = & ".\pathfinder.exe" "puzzle_map.txt"

Write-Host "Pathfinder result: $pathResult"

# If the external tool outputs structured data (e.g., JSON), you can parse it
# $jsonResult = & ".\myparser.exe" "data.json" | ConvertFrom-Json
# $value = $jsonResult.SomeProperty
Enter fullscreen mode Exit fullscreen mode

Example: Piping Data to/from External Tools

PowerShell’s pipeline can interact with external processes, passing data to their standard input and capturing their standard output.

# Simulate an external tool that counts characters (e.g., `wc -c` on Linux)
# For Windows, we might use a simple Python script or a custom .exe

# Example: Using a simple Python script (counter.py)
# import sys
# print(sum(len(line) for line in sys.stdin))

$dataToSend = @(
    "Line 1"
    "Another Line"
    "Last One"
)

# Pipe data to the Python script and capture its output
# Make sure python is in your PATH or specify the full path to python.exe
$charCount = $dataToSend | python.exe - ".\counter.py"

Write-Host "Total characters (excluding newlines): $charCount"
Enter fullscreen mode Exit fullscreen mode

Comparison Table: Choosing Your Approach

Selecting the right approach depends on the specific AoC puzzle, your comfort level, and the performance requirements.

Feature / Approach Pure PowerShell Idiomatic C# Interop (Add-Type) External Tools Orchestration
Readability & Maintainability High (for PowerShell users), aligns with scripting paradigms. Medium-Low (mix of PowerShell and C# syntax, more setup overhead). Medium (PowerShell orchestrates, external tool logic opaque).
Performance Medium-Low (interpreted, object overhead can be slow for tight loops/large data). High (compiled C# code runs at native speed for specific sections). Very High (leverages highly optimized, compiled executables).
Development Effort Low to Medium (familiar language, rapid prototyping). Medium to High (requires C# knowledge, debugging can be split). Medium (requires understanding external tool APIs, input/output).
Complexity Handled Good for most text processing, data manipulation, simpler algorithms. Excellent for complex algorithms, custom data structures, performance-critical logic. Excellent for specific sub-problems where an existing tool is a perfect fit.
Suitability for AoC Primary choice for majority of puzzles, especially Part 1. Ideal for Part 2 of puzzles where Part 1 solution is too slow. Niche for specific problems (e.g., heavy regex, specific graph algorithms if a tool exists).
Debugging Experience Excellent within PowerShell ISE/VS Code. Mixed (PowerShell debugger for script, C# logic often opaque within PowerShell). Mixed (PowerShell debugger for orchestration, external tool debugging separate).

Darian Vance

👉 Read the original article on TechResolve.blog

Top comments (0)