Skip to content

Commit

Permalink
Day 5 solution (#8)
Browse files Browse the repository at this point in the history
* Added day 5 part 1 bare solution

* Added part 2 base code and necessary changes

* Added first draft of MultiRange handy for part 2

* More extensions of MultiRange class

* Push forward solution held back by dysfunctional MultiRange.Remove

* Fix tests after main branch merge

* Added state of part 2 solution

* Use newest Common package with MultiRange instead of implementing own

* Add Day 5 to solved solution folder

* Format
  • Loading branch information
mMosiur authored Jan 10, 2024
1 parent 141fcea commit dbc09eb
Show file tree
Hide file tree
Showing 22 changed files with 1,063 additions and 0 deletions.
7 changes: 7 additions & 0 deletions AdventOfCode2023.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day03", "Day03 - Gear Ratio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day04", "Day04 - Scratchcards\Day04.csproj", "{C9EC74D2-0555-49D2-89FC-47623D7C6AC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day05", "Day05 - If You Give A Seed A Fertilizer\Day05.csproj", "{6D04B3FB-811D-4B21-A844-DD731503C867}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -54,11 +56,16 @@ Global
{C9EC74D2-0555-49D2-89FC-47623D7C6AC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9EC74D2-0555-49D2-89FC-47623D7C6AC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9EC74D2-0555-49D2-89FC-47623D7C6AC1}.Release|Any CPU.Build.0 = Release|Any CPU
{6D04B3FB-811D-4B21-A844-DD731503C867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D04B3FB-811D-4B21-A844-DD731503C867}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D04B3FB-811D-4B21-A844-DD731503C867}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D04B3FB-811D-4B21-A844-DD731503C867}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CE8086B8-A3E4-40E0-859F-607F95D25514} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26}
{53C7A524-9A5C-4CEB-BC34-C8EFD43560D8} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26}
{4E66F73F-3457-4E4E-AFBB-61FA5210064B} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26}
{C9EC74D2-0555-49D2-89FC-47623D7C6AC1} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26}
{6D04B3FB-811D-4B21-A844-DD731503C867} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26}
EndGlobalSection
EndGlobal
18 changes: 18 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch (my input)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net8.0/Day05.dll",
"args": [
"input.txt"
],
"cwd": "${workspaceFolder}",
"console": "internalConsole",
"stopAtEntry": false
}
]
}
21 changes: 21 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"group": {
"kind": "build",
"isDefault": true
},
"args": [
"build",
"${workspaceFolder}/Day05.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
22 changes: 22 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/Day05.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AdventOfCode.Year2023.Day05</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="mMosiur.AdventOfCode.Abstractions" Version="4.4.0" />
<PackageReference Include="mMosiur.AdventOfCode.Common" Version="0.1.2" />
</ItemGroup>

<ItemGroup>
<None Update="*.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/Day05Solver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using AdventOfCode.Abstractions;
using AdventOfCode.Year2023.Day05.Puzzle;

namespace AdventOfCode.Year2023.Day05;

public sealed class Day05Solver : DaySolver
{
public override int Year => 2023;
public override int Day => 5;
public override string Title => "If You Give A Seed A Fertilizer";

private readonly InputReaderWithCaching _inputReader;
private readonly SeedNumberCategoryCalculator _calculator;

public Day05Solver(Day05SolverOptions options) : base(options)
{
_inputReader = new(Input);
_calculator = new(NumberCategory.Seed, NumberCategory.Location);
}

public Day05Solver(Action<Day05SolverOptions> configure)
: this(DaySolverOptions.FromConfigureAction(configure))
{
}

public Day05Solver() : this(new Day05SolverOptions())
{
}

public override string SolvePart1()
{
var almanac = _inputReader.ReadInputSingleSeeds();
var locationNumbers = _calculator.CalculateTargetCategoryNumbers(almanac);
uint minLocationNumber = locationNumbers.Min();
return minLocationNumber.ToString();
}

public override string SolvePart2()
{
var almanac = _inputReader.ReadInputSeedRanges();
var locationNumbers = _calculator.CalculateTargetCategoryNumbers(almanac);
uint minLocationNumber = locationNumbers.Select(r => r.Start).Min();
return minLocationNumber.ToString();
}
}
7 changes: 7 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/Day05SolverOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using AdventOfCode.Abstractions;

namespace AdventOfCode.Year2023.Day05;

public sealed class Day05SolverOptions : DaySolverOptions
{
}
2 changes: 2 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using Range = AdventOfCode.Common.Numerics.Interval<uint>;
global using MultiRange = AdventOfCode.Common.Numerics.MultiInterval<uint>;
45 changes: 45 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Diagnostics;
using AdventOfCode;
using AdventOfCode.Year2023.Day05;

try
{
string? filepath = args.Length switch
{
0 => null,
1 => args[0],
_ => throw new CommandLineException(
$"Program was called with too many arguments. Proper usage: \"dotnet run [<input filepath>]\"."
)
};

Day05Solver solver = new(options =>
{
options.InputFilepath = filepath ?? options.InputFilepath;
});

Console.WriteLine($"--- Day {solver.Day}: {solver.Title} ---");

Console.Write("Part one: ");
string part1 = solver.SolvePart1();
Console.WriteLine(part1);

Console.Write("Part two: ");
string part2 = solver.SolvePart2();
Console.WriteLine(part2);
}
catch (AdventOfCodeException e)
{
string errorPrefix = e switch
{
CommandLineException => "Command line error",
InputException => "Input error",
DaySolverException => "Day solver error",
_ => throw new UnreachableException($"Unknown exception type \"{e.GetType()}\".")
};

Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"{errorPrefix}: {e.Message}");
Console.ResetColor();
Environment.Exit(1);
}
17 changes: 17 additions & 0 deletions Day05 - If You Give A Seed A Fertilizer/Puzzle/Almanac.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace AdventOfCode.Year2023.Day05.Puzzle;

internal abstract class Almanac<TSeedNumbersType>
{
private readonly Dictionary<NumberCategory, NumberMap> _numberMapsBySourceCategory;

public abstract TSeedNumbersType SeedNumbers { get; }
public IReadOnlyCollection<NumberMap> NumberMaps { get; }

protected Almanac(IReadOnlyCollection<NumberMap> numberMaps)
{
NumberMaps = numberMaps;
_numberMapsBySourceCategory = NumberMaps.ToDictionary(nm => nm.SourceCategory);
}

public NumberMap this[NumberCategory sourceCategory] => _numberMapsBySourceCategory[sourceCategory];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AdventOfCode.Year2023.Day05.Puzzle;

internal sealed class AlmanacSeedRanges : Almanac<MultiRange>
{
public override MultiRange SeedNumbers { get; }

public AlmanacSeedRanges(MultiRange seedNumbers, IReadOnlyCollection<NumberMap> numberMaps)
: base(numberMaps)
{
SeedNumbers = seedNumbers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AdventOfCode.Year2023.Day05.Puzzle;

internal sealed class AlmanacSingleSeeds : Almanac<IReadOnlyCollection<uint>>
{
public override IReadOnlyCollection<uint> SeedNumbers { get; }

public AlmanacSingleSeeds(IReadOnlyCollection<uint> seedNumbers, IReadOnlyCollection<NumberMap> numberMaps)
: base(numberMaps)
{
SeedNumbers = seedNumbers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using AdventOfCode.Common.SpanExtensions;

namespace AdventOfCode.Year2023.Day05.Puzzle;

internal sealed class InputReaderWithCaching
{
private static readonly string[] NewLines = { "\r\n", "\r", "\n" };
private static readonly string[] DoubleNewLines = NewLines.Select(nl => $"{nl}{nl}").ToArray();
private static readonly Regex SeedNumbersRegex = new(@"^seeds: ([\d ]+)$", RegexOptions.Compiled);
private static readonly Regex NumberMapHeaderRegex = new(@"^(?<SourceNumberCategory>\w+)-to-(?<DestinationNumberCategory>\w+) map:$", RegexOptions.Compiled);

private readonly string _input;

private IReadOnlyList<uint>? _almanacSeedNumbers;
private IReadOnlyList<NumberMap>? _almanacNumberMaps;
private AlmanacSingleSeeds? _almanacSingleSeeds;
private AlmanacSeedRanges? _almanacSeedRanges;

public InputReaderWithCaching(string input)
{
_input = input;
}

private static IReadOnlyList<uint> ReadSeedNumbers(string input)
{
var match = SeedNumbersRegex.Match(input);
if (!match.Success)
{
throw new InputException($"""Seed numbers line was in an unrecognized format ("{input}").""");
}

var seedsSpan = match.Groups[1].ValueSpan;
List<uint> seedNumbers = new(seedsSpan.Count(' ') + 1);
foreach (var seedNumberSpan in seedsSpan.Split(' '))
{
if (!uint.TryParse(seedNumberSpan, out uint seedNumber))
{
throw new InputException($"""Could not parse a number in a seed number line ("{seedNumberSpan}").""");
}

seedNumbers.Add(seedNumber);
}

return seedNumbers;
}

private static (NumberCategory SourceCategory, NumberCategory DestinationCategory) ReadNumberMapHeader(string header)
{
var match = NumberMapHeaderRegex.Match(header);
if (!match.Success)
{
throw new InputException($"""Number map header was in an unrecognized format ("{header}").""");
}

return (
SourceCategory: Enum.Parse<NumberCategory>(match.Groups["SourceNumberCategory"].ValueSpan, ignoreCase: true),
DestinationCategory: Enum.Parse<NumberCategory>(match.Groups["DestinationNumberCategory"].ValueSpan, ignoreCase: true)
);
}

private static NumberMapLine ReadNumberMapLine(ReadOnlySpan<char> lineSpan)
{
if (!lineSpan.TrySplitInThree(
separator: ' ',
out var firstNumberSpan,
out var secondNumberSpan,
out var thirdNumberSpan,
allowMultipleSeparators: true))
{
throw new InputException($"""Could not parse number map line from input line "{lineSpan}".""");
}

return new NumberMapLine(
destinationRangeStart: uint.Parse(firstNumberSpan),
sourceRangeStart: uint.Parse(secondNumberSpan),
rangeLength: uint.Parse(thirdNumberSpan));
}

private static NumberMap ReadNumberMap(string section)
{
var sectionSpan = section.AsSpan();
var lineSpanIterator = sectionSpan.EnumerateLines();
if (!lineSpanIterator.MoveNext()) throw new InputException($"Could not read number map header from input section \"{section}\".");
(NumberCategory sourceCategory, NumberCategory destinationCategory) = ReadNumberMapHeader(lineSpanIterator.Current.ToString());
var numberMapLines = new List<NumberMapLine>(sectionSpan.Count('\n'));
while (lineSpanIterator.MoveNext())
{
var numberMapLine = ReadNumberMapLine(lineSpanIterator.Current);
numberMapLines.Add(numberMapLine);
}

return new NumberMap(sourceCategory, destinationCategory, numberMapLines);
}

[MemberNotNull(nameof(_almanacSeedNumbers))]
[MemberNotNull(nameof(_almanacNumberMaps))]
private void InternalReadInput()
{
if (_almanacSeedNumbers is not null && _almanacNumberMaps is not null)
{
return;
}

string[] result = _input.Split(DoubleNewLines, StringSplitOptions.TrimEntries);
string seedNumbersRow = result[0];
_almanacSeedNumbers = ReadSeedNumbers(seedNumbersRow);

var numberMaps = new List<NumberMap>(result.Length - 1);
for (int i = 1; i < result.Length; i++)
{
string mapSectionLine = result[i];
var mapSection = ReadNumberMap(mapSectionLine);
numberMaps.Add(mapSection);
}

_almanacNumberMaps = numberMaps;
}

private AlmanacSingleSeeds BuildAlmanacSingleSeeds()
{
InternalReadInput();
return new(_almanacSeedNumbers, _almanacNumberMaps);
}

private AlmanacSeedRanges BuildAlmanacSeedRanges()
{
InternalReadInput();
if (_almanacSeedNumbers.Count % 2 != 0)
{
throw new InputException("Seed number count must be even to interpret them as ranges.");
}


var seedNumberRanges = new MultiRange(_almanacSeedNumbers.Count / 2);
for (int i = 0; i < _almanacSeedNumbers.Count; i += 2)
{
uint rangeStart = _almanacSeedNumbers[i];
uint rangeLength = _almanacSeedNumbers[i + 1];
var range = new Range(rangeStart, rangeStart + rangeLength - 1);
seedNumberRanges.Add(range);
}

return new(seedNumberRanges, _almanacNumberMaps);
}

[MemberNotNull(nameof(_almanacSingleSeeds))]
public AlmanacSingleSeeds ReadInputSingleSeeds()
{
return _almanacSingleSeeds ??= BuildAlmanacSingleSeeds();
}

[MemberNotNull(nameof(_almanacSeedRanges))]
public AlmanacSeedRanges ReadInputSeedRanges()
{
return _almanacSeedRanges ??= BuildAlmanacSeedRanges();
}
}
Loading

0 comments on commit dbc09eb

Please sign in to comment.