From b4fb2f0643e25c695206b57cfd61e50187353f25 Mon Sep 17 00:00:00 2001 From: Kirill Vorotov Date: Tue, 12 Dec 2023 22:41:20 +0000 Subject: [PATCH] Day 2-1 --- .../AdventOfCode/AdventOfCode.csproj | 3 + src/AdventOfCode/AdventOfCode/Day2.cs | 376 ++++++++++++++++++ src/AdventOfCode/ConsoleApp/Program.cs | 8 +- 3 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 src/AdventOfCode/AdventOfCode/Day2.cs 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