> ## Content Index
> Fetch the complete content index at: https://til.codes/llms.txt
> Use this file to discover other available public pages before exploring further.

# Advent of Code 2024: Day 4 - Pattern Matching in Multiple Dimensions
- URL: https://til.codes/advent-of-code-2024-day-4-pattern-matching-in-multiple-dimensions/
- Published: 2024-12-04T02:47:00.000Z
- Updated: 2024-12-07T02:47:52.000Z
- Author: Manu Ajith
- Tags: elixir, rust, haskell, go, aoc

The elves handed me what looked like a simple word search puzzle today. Find "XMAS", they said. Easy enough - until I discovered I needed to find it in every possible direction, including diagonals and backwards. Then part 2 hit me with X-shaped patterns, and suddenly I was deep in geometric territory. Let me show you how I solved this with my four languages.

## The Challenge: Beyond Simple Word Search

The first part asked me to find all occurrences of "XMAS" in any direction - horizontal, vertical, diagonal, and even backwards. But part 2 threw a curveball: find X-shaped patterns where each diagonal contains "MAS" (forwards or backwards). This shift from linear pattern matching to geometric pattern recognition opened up fascinating implementation choices.

## Four Languages, Four Approaches

### Rust: Type Safety Meets Performance

In Rust, I started by modeling the directions and positions with zero-cost abstractions:

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Position {
    x: i32,
    y: i32,
}

const DIRECTIONS: [Direction; 8] = [
    Direction::new(0, 1),   // right
    Direction::new(1, 0),   // down
    Direction::new(1, 1),   // down-right
    Direction::new(1, -1),  // down-left
    // ... other directions
];

```

The beauty of Rust's implementation lies in its pattern matching for part 2:

```rust
fn check_xmas_pattern(&self, pos: Position) -> bool {
    match (
        self.get_diagonal(pos, UP_RIGHT),
        self.get_diagonal(pos, DOWN_RIGHT),
    ) {
        (Some(up), Some(down)) => 
            Self::is_valid_pattern(up) && Self::is_valid_pattern(down),
        _ => false,
    }
}

```

### Elixir: Pattern Matching Paradise

Elixir's pattern matching made the directional checks particularly elegant:

```elixir
@directions [
  {0, 1},    # right
  {1, 0},    # down
  {1, 1},    # down-right
  {1, -1},   # down-left
  {0, -1},   # left
  {-1, 0},   # up
  {-1, 1},   # up-right
  {-1, -1}   # up-left
]

defp valid_x_pattern?(diag1, diag2) do
  (is_mas?(diag1) and is_mas?(diag2)) or
    (is_mas?(diag1) and is_sam?(diag2)) or
    (is_sam?(diag1) and is_mas?(diag2)) or
    (is_sam?(diag1) and is_sam?(diag2))
end

```

### Go: Pragmatic Grid Operations

Go's approach shines in its straightforward handling of grid operations:

```go
func (g *Grid) getDiagonal(i, j int, d Direction) []byte {
    result := make([]byte, 3)
    for step := -1; step <= 1; step++ {
        x, y := i+d.dx*step, j+d.dy*step
        result[step+1] = g.data[x][y]
    }
    return result
}

```

### Haskell: Pure Pattern Recognition

Haskell's solution leverages its type system for clear pattern matching:

```haskell
getDiagonal :: Grid -> Position -> Direction -> Maybe String
getDiagonal grid (x, y) (dx, dy) =
    let positions = [(x + dx * i, y + dy * i) | i <- [-1..1]]
     in if all (inBounds grid) positions
            then Just [grid !! px !! py | (px, py) <- positions]
            else Nothing

```

## Performance Optimization Through Geometry

The geometric nature of the patterns led to some interesting optimizations. In part 2, I realized I could skip the grid edges entirely since an X-pattern needs space in all directions:

```rust
fn solve_part2(&self) -> usize {
    (1..self.height.saturating_sub(1))
        .flat_map(|x| {
            (1..self.width.saturating_sub(1))
                .map(move |y| Position::new(x as i32, y as i32))
        })
        .filter(|&pos| self.get_char(pos) == Some('A'))
        .filter(|&pos| self.check_xmas_pattern(pos))
        .count()
}

```

## Lessons in Pattern Recognition

This challenge taught me valuable lessons about pattern recognition in grids. The transition from part 1's linear search to part 2's geometric patterns showed how different abstractions can make complex pattern matching more manageable.

Rust's type system helped prevent coordinate confusion, Elixir's pattern matching made the logic crystal clear, Go's straightforward approach kept the code readable, and Haskell's pure functions made pattern composition natural.

What I love about this problem is how it demonstrates that even seemingly simple pattern matching can reveal elegant geometric properties. Whether through type-safe coordinates, pattern matching, or list comprehensions, each language offered its own insights into handling multi-dimensional patterns.