diff --git a/src/AdventOfCode/AdventOfCode/AdventOfCode.csproj b/src/AdventOfCode/AdventOfCode/AdventOfCode.csproj
index 9e20eb1..ad15b4e 100644
--- a/src/AdventOfCode/AdventOfCode/AdventOfCode.csproj
+++ b/src/AdventOfCode/AdventOfCode/AdventOfCode.csproj
@@ -10,6 +10,9 @@
Always
+
+ Always
+
diff --git a/src/AdventOfCode/AdventOfCode/Day2.cs b/src/AdventOfCode/AdventOfCode/Day2.cs
new file mode 100644
index 0000000..5e50019
--- /dev/null
+++ b/src/AdventOfCode/AdventOfCode/Day2.cs
@@ -0,0 +1,376 @@
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace AdventOfCode;
+
+public static class Day2
+{
+ public enum TokenType
+ {
+ Undefined,
+ Invalid,
+ Space,
+ Comma,
+ Colon,
+ Semicolon,
+ Identifier,
+ DecimalLiteral,
+ IntegerLiteral,
+ }
+
+ public readonly struct Token(TokenType type, string value)
+ {
+ public TokenType Type { get; } = type;
+ public string Value { get; } = value;
+ }
+
+ public readonly struct GameSettings(int reds, int greens, int blues)
+ {
+ public int Reds { get; } = reds;
+ public int Greens { get; } = greens;
+ public int Blues { get; } = blues;
+ }
+
+ public struct GameSet
+ {
+ public int Red { get; set; }
+ public int Green { get; set; }
+ public int Blue { get; set; }
+ }
+
+ public struct GameData
+ {
+ public Dictionary Games { get; init; }
+ }
+
+ public class Result
+ {
+ public int Sum;
+ }
+
+ private static readonly Regex containsSpaceOrPunctuatorRegex = new("[ ,:;]");
+
+ private static readonly Regex spacesRegex = new("^ *$");
+ private static readonly Regex commaRegex = new("^,$");
+ private static readonly Regex colonRegex = new("^:$");
+ private static readonly Regex semicolonRegex = new("^;$");
+ private static readonly Regex integerRegex = new("^[0-9]+$");
+ private static readonly Regex decimalRegex = new("^[0-9]+(\\.[0-9]+)?$");
+ private static readonly Regex identifierRegex = new("^[a-zA-Z][a-zA-Z0-9]*$");
+
+ private static readonly List<(Regex regex, TokenType tokenType)> matchers =
+ [
+ (spacesRegex, TokenType.Space),
+ (commaRegex, TokenType.Comma),
+ (colonRegex, TokenType.Colon),
+ (semicolonRegex, TokenType.Semicolon),
+ (integerRegex, TokenType.IntegerLiteral),
+ (decimalRegex, TokenType.DecimalLiteral),
+ (identifierRegex, TokenType.Identifier),
+ ];
+
+ public static async Task ExecuteAsync(string filePath, GameSettings gameSettings)
+ {
+ if (!File.Exists(filePath))
+ {
+ return 0;
+ }
+
+ await using var fs = new FileStream(filePath, FileMode.Open);
+ return await ExecuteAsync(fs, gameSettings);
+ }
+
+ public static async Task ExecuteAsync(Stream s, GameSettings gameSettings)
+ {
+ Result result = new();
+ var pipe = new Pipe();
+ var writing = FillPipeAsync(s, pipe.Writer);
+ var reading = ReadPipeAsync(pipe.Reader, gameSettings, result);
+
+ await Task.WhenAll(writing, reading);
+
+ return result.Sum;
+ }
+
+ private static async Task FillPipeAsync(Stream fs, PipeWriter writer)
+ {
+ const int minBufferSize = 512;
+
+ while (true)
+ {
+ var memory = writer.GetMemory(minBufferSize);
+ try
+ {
+ var bytesRead = await fs.ReadAsync(memory, CancellationToken.None);
+ if (bytesRead == 0)
+ {
+ break;
+ }
+ writer.Advance(bytesRead);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ break;
+ }
+
+ var result = await writer.FlushAsync();
+ if (result.IsCompleted)
+ {
+ break;
+ }
+ }
+
+ await writer.CompleteAsync();
+ }
+
+ private static async Task ReadPipeAsync(PipeReader reader, GameSettings gameSettings, Result result)
+ {
+ var sum = 0;
+ result.Sum = 0;
+ while (true)
+ {
+ var readResult = await reader.ReadAsync();
+ var buffer = readResult.Buffer;
+
+ while (TryReadLine(ref buffer, out var line))
+ {
+ sum += ProcessLine(line, gameSettings);
+ }
+
+ reader.AdvanceTo(buffer.Start, buffer.End);
+
+ if (readResult.IsCompleted)
+ {
+ break;
+ }
+ }
+
+ await reader.CompleteAsync();
+ result.Sum = sum;
+ return sum;
+ }
+
+ private static bool TryReadLine(ref ReadOnlySequence buffer, out ReadOnlySequence line)
+ {
+ var position = buffer.PositionOf((byte)'\n');
+
+ if (position == null)
+ {
+ line = default;
+ return false;
+ }
+
+ line = buffer.Slice(0, position.Value);
+ buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
+ return true;
+ }
+
+ private static int ProcessLine(ReadOnlySequence line, GameSettings gameSettings)
+ {
+ var str = Encoding.UTF8.GetString(line);
+ if (string.IsNullOrWhiteSpace(str))
+ {
+ return 0;
+ }
+ var game = ParseString(str);
+ var sum = 0;
+ foreach (var (gameId, gameSets) in game.Games)
+ {
+ var possibleGame = true;
+
+ foreach (var gameSet in gameSets)
+ {
+ if (gameSet.Red <= gameSettings.Reds && gameSet.Blue <= gameSettings.Blues && gameSet.Green <= gameSettings.Greens)
+ {
+ continue;
+ }
+
+ possibleGame = false;
+ break;
+ }
+
+ if (possibleGame)
+ {
+ sum += gameId;
+ }
+ }
+ return sum;
+ }
+
+ public static GameData ParseString(string str)
+ {
+ var strSpan = str.AsSpan();
+ var tokens = TokenizeString(strSpan);
+ var gameData = new GameData()
+ {
+ Games = new Dictionary()
+ };
+
+ var headerSeparator = 0;
+ foreach (var token in tokens)
+ {
+ if (token.Type == TokenType.Colon)
+ {
+ break;
+ }
+ headerSeparator++;
+ }
+
+ if (headerSeparator != 2)
+ {
+ throw new Exception("Header contains more than 2 tokens");
+ }
+
+ if (tokens[0].Type != TokenType.Identifier ||
+ tokens[1].Type != TokenType.IntegerLiteral)
+ {
+ throw new Exception($"Header has wrong tokens: {tokens[0].Type} {tokens[1].Type}");
+ }
+
+ if (!int.TryParse(tokens[1].Value, out var gameId))
+ {
+ throw new Exception($"Unable to parse game id: {tokens[1].Type} {tokens[1].Value}");
+ }
+
+ List dataList = [];
+ string? identifier = default;
+ int? value = default;
+ GameSet data = default;
+
+ for (var i = headerSeparator; i < tokens.Length; i++)
+ {
+ var endOfTokens = i == tokens.Length - 1;
+ var addToken = tokens[i].Type == TokenType.Comma ||
+ tokens[i].Type == TokenType.Semicolon ||
+ endOfTokens;
+
+ switch (tokens[i].Type)
+ {
+ case TokenType.Identifier:
+ {
+ identifier = tokens[i].Value;
+ break;
+ }
+ case TokenType.IntegerLiteral:
+ {
+ value = int.Parse(tokens[i].Value);
+ break;
+ }
+ }
+
+ if (addToken)
+ {
+ FillDataStruct(identifier, value, ref data);
+ value = default;
+ identifier = default;
+ }
+
+ if (tokens[i].Type == TokenType.Semicolon || endOfTokens)
+ {
+ dataList.Add(data);
+ data = default;
+ }
+ }
+
+ gameData.Games.Add(gameId, dataList.ToArray());
+ return gameData;
+ }
+
+ private static void FillDataStruct(string? identifier, int? value, ref GameSet data)
+ {
+ if (identifier is null)
+ {
+ return;
+ }
+
+ switch (identifier)
+ {
+ case "red":
+ {
+ data.Red = value ?? 0;
+ break;
+ }
+ case "green":
+ {
+ data.Green = value ?? 0;
+ break;
+ }
+ case "blue":
+ {
+ data.Blue = value ?? 0;
+ break;
+ }
+ }
+ }
+
+ public static Token[] TokenizeString(ReadOnlySpan strSpan)
+ {
+ List result = [];
+
+ var tail = 0;
+ for (var head = 0; head < strSpan.Length; head++)
+ {
+ var prevTestString = strSpan[tail .. head];
+ var testString = strSpan[tail .. (head + 1)];
+ var ch = strSpan[head];
+
+ var endOfLine = head == strSpan.Length - 1;
+ var spaceOrPunctuator = containsSpaceOrPunctuatorRegex.IsMatch(testString);
+ var prevTestStringContainsOnlySpaces = !prevTestString.IsEmpty && spacesRegex.IsMatch(prevTestString);
+ var separatorType = ch switch
+ {
+ ',' when spaceOrPunctuator => TokenType.Comma,
+ ':' when spaceOrPunctuator => TokenType.Colon,
+ ';' when spaceOrPunctuator => TokenType.Semicolon,
+ ' ' when spaceOrPunctuator => TokenType.Space,
+ _ => TokenType.Undefined,
+ };
+
+ var foundToken = (prevTestStringContainsOnlySpaces && separatorType != TokenType.Space) ||
+ (!prevTestStringContainsOnlySpaces && separatorType != TokenType.Undefined) ||
+ endOfLine;
+
+ if (!foundToken)
+ {
+ continue;
+ }
+
+ var punctuator = separatorType != TokenType.Undefined && separatorType != TokenType.Space;
+ var isSeparator = punctuator || (separatorType == TokenType.Space && !prevTestStringContainsOnlySpaces);
+
+ var targetSpan = endOfLine && !isSeparator ? testString : prevTestString;
+
+ var foundMatch = false;
+ foreach (var (regex, tokenType) in matchers)
+ {
+ if (!regex.IsMatch(targetSpan))
+ {
+ continue;
+ }
+
+ if (tokenType != TokenType.Space)
+ {
+ result.Add(new Token(tokenType, new string(targetSpan)));
+ }
+ foundMatch = true;
+ break;
+ }
+
+ if (!foundMatch)
+ {
+ result.Add(new Token(TokenType.Invalid, new string(targetSpan)));
+ }
+ tail = head;
+
+ if (endOfLine && isSeparator && separatorType != TokenType.Space)
+ {
+ result.Add(new Token(separatorType, $"{ch}"));
+ }
+ }
+
+ return result.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/src/AdventOfCode/ConsoleApp/Program.cs b/src/AdventOfCode/ConsoleApp/Program.cs
index 7e92e1d..c62c073 100644
--- a/src/AdventOfCode/ConsoleApp/Program.cs
+++ b/src/AdventOfCode/ConsoleApp/Program.cs
@@ -1,10 +1,6 @@
// See https://aka.ms/new-console-template for more information
-using System.Diagnostics;
using AdventOfCode;
-var sw = Stopwatch.StartNew();
-var day1Result = await Day1.ExecuteAsync("day1.txt");
-Console.WriteLine($"Advent of code: Day 1 result: {day1Result}");
-sw.Stop();
-Console.WriteLine($"Advent of code: Day 1 time: {sw.Elapsed.TotalMilliseconds} ms.");
\ No newline at end of file
+var day2Result = await Day2.ExecuteAsync("day2.txt", new Day2.GameSettings(12, 13, 14));
+Console.WriteLine($"Advent of code: Day 2 result: {day2Result}");
\ No newline at end of file