This commit is contained in:
Kirill Vorotov 2023-12-12 22:41:20 +00:00
parent 1c6c068e44
commit b4fb2f0643
3 changed files with 381 additions and 6 deletions

View File

@ -10,6 +10,9 @@
<None Update="day1.txt"> <None Update="day1.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="day2.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -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<int, GameSet[]> 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<int> 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<int> 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<int> 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<byte> buffer, out ReadOnlySequence<byte> 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<byte> 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<int, GameSet[]>()
};
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<GameSet> 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<char> strSpan)
{
List<Token> 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();
}
}

View File

@ -1,10 +1,6 @@
// See https://aka.ms/new-console-template for more information // See https://aka.ms/new-console-template for more information
using System.Diagnostics;
using AdventOfCode; using AdventOfCode;
var sw = Stopwatch.StartNew(); var day2Result = await Day2.ExecuteAsync("day2.txt", new Day2.GameSettings(12, 13, 14));
var day1Result = await Day1.ExecuteAsync("day1.txt"); Console.WriteLine($"Advent of code: Day 2 result: {day2Result}");
Console.WriteLine($"Advent of code: Day 1 result: {day1Result}");
sw.Stop();
Console.WriteLine($"Advent of code: Day 1 time: {sw.Elapsed.TotalMilliseconds} ms.");