Skip to content

Advent of Code 2022: Day 1 to 5

Solutions in C# for day 1 through 5 of the Advent of Code 2022 challenge

Andrew Moore
Andrew MooreDec 10, 2022

Every year, as the holiday season approaches, I look forward to participating in the Advent of Code challenge. For those who may not be familiar, Advent of Code is an annual programming challenge that provides a new puzzle to solve each day from December 1st to December 25th. It’s a fun and engaging way to get into the holiday spirit, and I love the challenge and competition that comes with it. It really gets the brain going.

My language of choice for the past few years has been C#. It’s my way of staying well versed with the .NET ecosystem while working for an employer that is deep into the Java one. I use a custom execution harness based on the excellent AoCHelper library by Eduardo Cáceres, that separates input file parsing from solution solving for timing purposes, and adds a --validate CLI flag that allows you to run your solutions through the example inputs to validate your solutions. I plan on releasing this execution harness once I clean up the code a bit.

In the solutions, you will see references to the following StringExtensions. They simplify my task of parsing inputs by giving me convenient shortcuts to common type conversions or token splitting tasks.

public static class StringExtensions
{
  public static int ToIntInvariant(this string str)
  {
    return int.Parse(str, NumberStyles.Integer, CultureInfo.InvariantCulture);
  }

  public static long ToLongInvariant(this string str)
  {
    return long.Parse(str, NumberStyles.Integer, CultureInfo.InvariantCulture);
  }

  public static ushort ToUShortInvariant(this string str)
  {
    return ushort.Parse(str, NumberStyles.Integer, CultureInfo.InvariantCulture);
  }

  public static string[] SplitTokens(this string str)
  {
    return str.Split(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] SplitTokens(this string str, char splitChar)
  {
    return str.Split(splitChar, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] SplitTokens(this string str, char splitChar, int count)
  {
    return str.Split(splitChar, count, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] SplitTokens(this string str, string splitString)
  {
    return str.Split(splitString, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] SplitTokens(this string str, string splitString, int count)
  {
    return str.Split(splitString, count, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] Split(this string str, int count)
  {
    return str.Split(count, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
  }

  public static string[] Split(this string str, StringSplitOptions options)
  {
    return str.Split(default(char[]), options);
  }

  public static string[] Split(this string str, int count, StringSplitOptions options)
  {
    return str.Split(default(char[]), count, options);
  }
}

That said, let’s go through the solutions for the first five days of this challenge.

Day 1: Calorie Counting

Day 1’s input consisted of groups of integers separated by a blank line. I chose to represent that in a jagged array of integers (int[][]) as each group’s size is indeterminate and can be different from each other.

Part 1 consisted of calculating the sum of all values for each group, and returning the largest sum.

In part 2, you also needed to calculate the sum of all values for each group, but this time we were returning the sum total of the 3 groups who had the largest sum.

public class Day01 : BaseDay<int[][]>
{
  protected override int[][] ParseInputFile(IEnumerable<string> lines)
  {
    var inventoryGroups = new List<int[]>();
    var currentInventory = new List<int>();

    // Iterate through each line
    foreach (var line in lines)
    {
      // If the line is empty, this indicates the start of a new group
      if (string.IsNullOrEmpty(line))
      {
        // flush the current group
        inventoryGroups.Add(currentInventory.ToArray());
        currentInventory.Clear();
        continue;
      }

      // Add the line's value to the current group.
      currentInventory.Add(line.ToIntInvariant());
    }

    // If we have any remaining values, those form the last group
    if (currentInventory.Count > 0)
    {
      inventoryGroups.Add(currentInventory.ToArray());
    }

    return inventoryGroups.ToArray();
  }

  protected override string SolvePart1()
  {
    var topGroupSum = InputData
      .Select(group => group.Sum()) // Calculate each group's sum
      .MaxBy(groupSum => groupSum); // Get the greatest value

    return topGroupSum.ToString();
  }

  protected override string SolvePart2()
  {
    var top3CaloriesCount = InputData
      .Select(group => group.Sum()) // Calculate each group's sum
      .OrderByDescending(groupSum => groupSum) // Order by largest
      .Take(3) // Take the top 3
      .Sum(); // Get the total sum of the sums

    return top3CaloriesCount.ToString();
  }

  public override string ExpectedValidationResultPart1 => "24000";
  public override string ExpectedValidationResultPart2 => "45000";
}

Day 2: Rock Paper Scissors

Day 2’s input consisted of multiple lines, each with two letters separated by a space.

The first letter, which could be either A, B, or C represented the move your opponent did, and always stood for Rock, Paper, and Scissors respectively.

The second letter, which could either be X, Y, or Z, represented the move you did, but what that move was changed between part 1 and part 2.

The first thing I did was to define a few types to represent the input (record struct RawPlay) and the various moves in Rock Paper Scissors (enum Move).

In part 1, X meant you played Rock, Y meant you played Paper, and Z meant you played Scissors. This straightforward mapping is handled by the ResolvePlayPart1() method.

In part 2, the meaning of X, Y, and Z changed. X now meant that you were supposed to lose the exchange, Y meant that you needed to draw, and Z meant that you were supposed to win this exchange. This mapping is handled by the ResolvePlayPart2() method.

Finally, each part required you to provide your score at the end of the match. Your score was calculated based on your move; and on whether you won, lost, or caused a draw in each bout. The score for each bout is calculated according to the problem’s rules in ResolvedPlay.Score.

public class Day02 : BaseDay<Day02.RawPlay[]>
{
  protected override RawPlay[] ParseInputFile(IEnumerable<string> lines)
  {
    var plays = new List<RawPlay>();

    foreach (var line in lines)
    {
      var moves = line.SplitTokens(' ');

      plays.Add(new RawPlay(moves[0][0], moves[1][0]));
    }

    return plays.ToArray();
  }

  protected override string SolvePart1()
  {
    var totalScore = InputData.Select(ResolvePlayPart1).Sum(play => play.Score);

    return $"{totalScore}";
  }

  private ResolvedPlay ResolvePlayPart1(RawPlay play)
  {
    var theirMove = play.TheirMove;

    var yourMove = play.YourCode switch
    {
      'X' => Move.Rock,
      'Y' => Move.Paper,
      'Z' => Move.Scissors,
      _ => throw new Exception($"Invalid move {play.YourCode}")
    };

    return new ResolvedPlay(theirMove, yourMove);
  }

  protected override string SolvePart2()
  {
    var totalScore = InputData.Select(ResolvePlayPart2).Sum(play => play.Score);

    return $"{totalScore}";
  }

  private ResolvedPlay ResolvePlayPart2(RawPlay play)
  {
    var theirMove = play.TheirMove;
    Move yourMove;

    switch (play.YourCode)
    {
      case 'X':
        yourMove = theirMove switch
        {
          Move.Rock => Move.Scissors,
          Move.Paper => Move.Rock,
          Move.Scissors => Move.Paper,
          _ => throw new Exception($"Invalid move {theirMove}")
        };

        break;
      case 'Y':
        yourMove = theirMove;
        break;
      case 'Z':
        yourMove = theirMove switch
        {
          Move.Rock => Move.Paper,
          Move.Paper => Move.Scissors,
          Move.Scissors => Move.Rock,
          _ => throw new Exception($"Invalid move {theirMove}")
        };

        break;
      default:
        throw new Exception($"Invalid move {play.YourCode}");
    }

    return new ResolvedPlay(theirMove, yourMove);
  }

  public override string ExpectedValidationResultPart1 => "15";
  public override string ExpectedValidationResultPart2 => "12";

  public enum Move
  {
    Rock,
    Paper,
    Scissors
  }

  public readonly record struct RawPlay(char TheirCode, char YourCode)
  {
    public Move TheirMove
    {
      get
      {
        return TheirCode switch
        {
          'A' => Move.Rock,
          'B' => Move.Paper,
          'C' => Move.Scissors,
          _ => throw new Exception($"Invalid move {TheirCode}")
        };
      }
    }
  }

  private readonly record struct ResolvedPlay(Move TheirMove, Move YourMove)
  {
    public int Score
    {
      get
      {
        var score = 0;

        // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
        switch (YourMove)
        {
          case Move.Rock:
            score += 1;

            // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
            switch (TheirMove)
            {
              case Move.Rock:
                score += 3;
                break;

              case Move.Scissors:
                score += 6;
                break;
            }

            break;

          case Move.Paper:
            score += 2;

            // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
            switch (TheirMove)
            {
              case Move.Paper:
                score += 3;
                break;

              case Move.Rock:
                score += 6;
                break;
            }

            break;

          case Move.Scissors:
            score += 3;

            // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
            switch (TheirMove)
            {
              case Move.Scissors:
                score += 3;
                break;

              case Move.Paper:
                score += 6;
                break;
            }

            break;
        }

        return score;
      }
    }
  }
}

Day 3: Rucksack Reorganization

Fair warning: I may have done a lot of LINQ abusing in this one 😅.

Day 3’s input consisted of a series of lines with alphabetic characters, each character representing a unique item, and each line representing the content of a rucksack. Each line had to be split in two equal parts to separate both half of the rucksack (important for part 1).

Both parts of the problem required you to calculate an item’s priority based on two simple rules:

  • Lowercase item types a through z have priorities 1 through 26.
  • Uppercase item types A through Z have priorities 27 through 52.

In my solution, this is handled by the GetItemPriority() method. Since I’m parsing the input as a char[], I can take advantage of C# mathematical operators on that type to get the priority as an int.

Part 1 consisted of, for each rucksack, finding the single common item between both halves. As I didn’t know at the time if this was also required for part 2, I implemented the logic to find the common item as a property: Rucksack.CommonItem. To find the puzzle answer for part 1, you needed to calculate the item priority for the common item of each sack, and return the sum of all item priorities.

For part 2, you needed to group rucksacks in groups of 3, and then find the single item that appears 3 times within that group. My solution, located in the SolvePart2() method, involves a lot of LINQ to group and select the proper item. It’s not the most readable, but it works perfectly!

public class Day03 : BaseDay<Day03.Rucksack[]>
{
  protected override Rucksack[] ParseInputFile(IEnumerable<string> lines)
  {
    var sacks = new List<Rucksack>();

    foreach (var line in lines)
    {
      var halfLineLength = line.Length / 2;
      var items = line.ToCharArray();

      sacks.Add(new Rucksack(items[..halfLineLength], items[halfLineLength..]));
    }

    return sacks.ToArray();
  }

  protected override string SolvePart1()
  {
    var sumOfCommonItemPriorities = InputData
      .Select(s => s.CommonItem) // Find the common item
      .Sum(GetItemPriority); // Calculate its priority

    return $"{sumOfCommonItemPriorities}";
  }

  protected override string SolvePart2()
  {
    var sumOfBadgeItemPrioritiesPerGroup = InputData
      .Select((sack, index) => new { Rucksack = sack, Index = index })
      .GroupBy(p => p.Index / 3) // Group each sack in groups of 3
      .Select(g => g
        .SelectMany(p => p.Rucksack.AllItems) // Select All Items in the group
        .GroupBy(c => c) // Group by item
        .Single(cg => cg.Count() == 3) // Select the single item which appears thrice
        .Key
      )
      .Sum(GetItemPriority); // Calculate its priority

    return $"{sumOfBadgeItemPrioritiesPerGroup}";
  }

  public override string ExpectedValidationResultPart1 => "157";
  public override string ExpectedValidationResultPart2 => "70";

  private int GetItemPriority(char item)
  {
    return item switch
    {
      >= 'A' and <= 'Z' => item - 'A' + 27,
      >= 'a' and <= 'z' => item - 'a' + 1,
      _ => throw new ArgumentOutOfRangeException(nameof(item))
    };
  }

  public readonly record struct Rucksack(char[] LeftCompartment, char[] RightCompartment)
  {
    public char CommonItem
    {
      get
      {
        var rightComp = RightCompartment;
        return LeftCompartment.Distinct()
          .Single(i => rightComp.Contains(i));
      }
    }

    public char[] AllItems =>
      LeftCompartment
        .Union(RightCompartment)
        .ToArray();
  }
}

Day 4: Camp Cleanup

For Day 4, I managed to anticipate part 2 before I solved part 1. It’s always a great day when that happens!

The puzzle’s input consisted of a series of lines, each containing a pair of ranges. I parsed the input lines in two data structures: record struct Range and record struct ElfPair.

Part 1’s task was to count the number of pairs where one of the ranges was fully contained within the other. Part 2’s task was to count the number of pairs who had any type of intersection.

My intersection logic, defined in ElfPair.IntersectionType, works as follows:

  • If the UpperLimit of either range is smaller than its counterpart’s LowerLimit, then we are sure that there is no intersection between both ranges. In that case, NoIntersection is returned.

  • If the LowerLimit of range A is greater or equal than the LowerLimit of range B, and the UpperLimit of range A is lower or equal than the UpperLimit of range B, then range A is contained within range B. In that case, Contained is returned. This is used for part 1.

  • In all other cases, we have a partial intersection. PartialIntersection is returned.

public class Day04 : BaseDay<Day04.ElfPair[]>
{
  protected override ElfPair[] ParseInputFile(IEnumerable<string> lines)
  {
    var elfPairs = new List<ElfPair>();

    foreach (var line in lines)
    {
      var rawPairs = line.SplitTokens(',');

      // Assertions
      Guard.HasSizeEqualTo(rawPairs, 2);

      var ranges = new Range[2];

      for (var i = 0; i < 2; i++)
      {
        var pair = rawPairs[i];

        var rangeComponents = pair.SplitTokens('-')
          .Select(c => c.ToIntInvariant())
          .ToArray();

        // Assertions
        Guard.HasSizeEqualTo(rangeComponents, 2);
        Guard.IsLessThanOrEqualTo(rangeComponents[0], rangeComponents[1]);

        ranges[i] = new Range(rangeComponents[0], rangeComponents[1]);
      }

      elfPairs.Add(new ElfPair(ranges[0], ranges[1]));
    }

    return elfPairs.ToArray();
  }

  protected override string SolvePart1()
  {
    return InputData
      .Count(ep => ep.IntersectionType == IntersectionType.Contained)
      .ToString();
  }

  protected override string SolvePart2()
  {
    return InputData
      .Count(ep => ep.IntersectionType != IntersectionType.NoIntersection)
      .ToString();
  }

  public override string ExpectedValidationResultPart1 => "2";
  public override string ExpectedValidationResultPart2 => "4";

  public enum IntersectionType
  {
    NoIntersection,
    PartialIntersection,
    Contained
  }

  public readonly record struct Range(int LowerLimit, int UpperLimit);

  public readonly record struct ElfPair(Range LeftRange, Range RightRange)
  {
    public IntersectionType IntersectionType
    {
      get
      {
        if (
          LeftRange.UpperLimit < RightRange.LowerLimit ||
          RightRange.UpperLimit < LeftRange.LowerLimit
        )
        {
          return IntersectionType.NoIntersection;
        }

        if (LeftRange.LowerLimit >= RightRange.LowerLimit &&
            LeftRange.UpperLimit <= RightRange.UpperLimit)
        {
          return IntersectionType.Contained;
        }

        if (RightRange.LowerLimit >= LeftRange.LowerLimit &&
            RightRange.UpperLimit <= LeftRange.UpperLimit)
        {
          return IntersectionType.Contained;
        }

        return IntersectionType.PartialIntersection;
      }
    }
  }
}

Day 5: Supply Stacks

While Day 5’s puzzle was relatively straightforward, its difficulty was in parsing its input file. Its input looked more like a diagram than an input:

    [D]
[N] [C]
[Z] [M] [P]
 1   2   3

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2

My strategy for parsing the input file (seen in ParseInputFile()) was to first find the empty line separating the move commands from the box diagram. I then looked at the last number of the previous line to detect the number of box columns we had to deal with.

From there, I could parse each line from bottom to top of the diagram. I chose to represent each column as a Stack<char>. This, it turns out, would come to complicate things later during part 2.

Once we had our input parsed, part 1 was relatively straightforward. We simply had to play through each move instruction, Pop()ing and Push()ing boxes as required.

The puzzle’s answer for both part was to return the top box in each stack. ExtractStateToString() contains my logic to do that.

For part 2, this is where my choice of using a Stack<char> fell apart. It wasn’t that big of a deal, however, I just had to make sure to Push() boxes into the appropriate stack in reverse order that I picked them up.

public class Day05 : BaseDay<Day05.PuzzleInput>
{
  private readonly Regex _instructionParsingRegex = new(
    @"^move (\d+) from (\d+) to (\d+)$",
    RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline
  );

  protected override PuzzleInput ParseInputFile(IEnumerable<string> lines)
  {
    var arrayOfLines = lines.ToArray();

    var numOfLines = arrayOfLines.Length;
    var lastBoxStackLineIndex = 0;
    var firstInstructionLineIndex = 0;
    var numberOfBoxStacks = 0;

    // Find the number of box stacks
    for (var i = 0; i < numOfLines; i++)
    {
      if (!string.IsNullOrEmpty(arrayOfLines[i]))
      {
        continue;
      }

      numberOfBoxStacks = arrayOfLines[i - 1]
        .SplitTokens(' ')
        .Last()
        .ToIntInvariant();

      firstInstructionLineIndex = i + 1;
      lastBoxStackLineIndex = i - 2;
      break;
    }

    // Initialize Box Stacks
    var boxStacks = new Stack<char>[numberOfBoxStacks];
    for (var i = 0; i < numberOfBoxStacks; i++)
    {
      boxStacks[i] = new Stack<char>();
    }

    for (var i = lastBoxStackLineIndex; i >= 0; i--)
    {
      var line = arrayOfLines[i];

      for (var x = 0; x < numberOfBoxStacks; x++)
      {
        var characterPosition = x * 4 + 1;

        if (line.Length <= characterPosition)
          break;

        if (line[characterPosition] == ' ')
          continue;

        boxStacks[x].Push(line[characterPosition]);
      }
    }

    // Reading Instructions
    var instructions = new List<Instruction>();
    for (var i = firstInstructionLineIndex; i < numOfLines; i++)
    {
      var line = arrayOfLines[i];
      var match = _instructionParsingRegex.Match(line);

      instructions.Add(
        new Instruction(
          match.Groups[1].Value.ToIntInvariant(),
          match.Groups[2].Value.ToIntInvariant(),
          match.Groups[3].Value.ToIntInvariant()
        )
      );
    }

    return new PuzzleInput(boxStacks, instructions.ToArray());
  }

  protected override string SolvePart1()
  {
    var state = InputData.GetClonedState();

    foreach (var instruction in InputData.Instructions)
    {
      for (var count = 0; count < instruction.HowMany; count++)
      {
        var boxToMove = state[instruction.From - 1].Pop();
        state[instruction.To - 1].Push(boxToMove);
      }
    }

    return ExtractStateToString(state);
  }

  protected override string SolvePart2()
  {
    var state = InputData.GetClonedState();

    foreach (var instruction in InputData.Instructions)
    {
      var boxesToMove = new char[instruction.HowMany];

      for (var count = 0; count < instruction.HowMany; count++)
      {
        boxesToMove[count] = state[instruction.From - 1].Pop();
      }

      for (var count = instruction.HowMany - 1; count >= 0; count--)
      {
        state[instruction.To - 1].Push(boxesToMove[count]);
      }
    }

    return ExtractStateToString(state);
  }

  private static string ExtractStateToString(Stack<char>[] state)
  {
    var puzzleResultBuilder = new StringBuilder();
    foreach (var stack in state)
    {
      puzzleResultBuilder.Append(stack.TryPeek(out var item) ? item : ' ');
    }

    return puzzleResultBuilder.ToString();
  }

  public override string ExpectedValidationResultPart1 => "CMZ";
  public override string ExpectedValidationResultPart2 => "MCD";

  public readonly record struct PuzzleInput(Stack<char>[] InitialState, Instruction[] Instructions)
  {
    public Stack<char>[] GetClonedState()
    {
      var boxStacks = new Stack<char>[InitialState.Length];
      for (var i = 0; i < InitialState.Length; i++)
      {
        boxStacks[i] = new Stack<char>(InitialState[i].Reverse());
      }

      return boxStacks;
    }
  }

  public readonly record struct Instruction(int HowMany, int From, int To);
}