diff --git a/05/IYGASAF.Tests/AlmanacTests.cs b/05/IYGASAF.Tests/AlmanacTests.cs new file mode 100644 index 0000000..9d6b07c --- /dev/null +++ b/05/IYGASAF.Tests/AlmanacTests.cs @@ -0,0 +1,140 @@ +namespace IYGASAF.Tests; + +public class AlmanacTests +{ + private readonly Almanac _sut = TestData.TestAlmanac; + + [Theory] + [MemberData(nameof(TestData.TestAlmanacData), MemberType = typeof(TestData))] + public void Parse_WhenGivenValidAlmanac_ItShouldReturnExpectedAlmanac(string[] almanac, Almanac expected) + { + var result = Almanac.Parse(almanac); + result.Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(79, 82)] + [InlineData(14, 43)] + [InlineData(55, 86)] + [InlineData(13, 35)] + public void GetSeedLocation_WhenGivenValidSeed_ItShouldReturnExpectedLocation(int seed, int expected) + { + var result = _sut.GetSeedLocation(seed); + result.Should().Be(expected); + } + + [Fact] + public void GetLowestSeedLocation_WhenCalled_ItShouldReturnExpectedLocation() + { + var result = _sut.GetLowestSeedLocation(); + result.Should().Be(35); + } + + [Fact] + public void GetLowestSeedLocation_WhenCalledWithPuzzleInput_ItShouldReturnExpectedLocation() + { + var almanac = File.ReadAllLines("INPUT.txt"); + var sut = Almanac.Parse(almanac); + var result = sut.GetLowestSeedLocation(); + result.Should().Be(806029445); + } + + public static class TestData + { + public static readonly Almanac TestAlmanac = + new( + [ + 79, + 14, + 55, + 13, + ], + [ + new( + [ + new(2, 98, 50), + new(48, 50, 52), + ] + ), + new( + [ + new(37, 15, 0), + new(2, 52, 37), + new(15, 0, 39), + ] + ), + new( + [ + new(8, 53, 49), + new(42, 11, 0), + new(7, 0, 42), + new(4, 7, 57), + ] + ), + new( + [ + new(7, 18, 88), + new(70, 25, 18), + ] + ), + new( + [ + new(23, 77, 45), + new(19, 45, 81), + new(13, 64, 68), + ] + ), + new( + [ + new(1, 69, 0), + new(69, 0, 1), + ] + ), + new( + [ + new(37, 56, 60), + new(4, 93, 56), + ] + ), + ] + ); + + public static readonly IEnumerable TestAlmanacData = + new List + { + new object[] + { + new string[] + { + "seeds: 79 14 55 13", + "seed-to-soil map:", + "50 98 2", + "52 50 48", + "soil-to-fertilizer map:", + "0 15 37", + "37 52 2", + "39 0 15", + "fertilizer-to-water map:", + "49 53 8", + "0 11 42", + "42 0 7", + "57 7 4", + "water-to-light map:", + "88 18 7", + "18 25 70", + "light-to-temperature map:", + "45 77 23", + "81 45 19", + "68 64 13", + "temperature-to-humidity map:", + "0 69 1", + "1 0 69", + "humidity-to-location map:", + "60 56 37", + "56 93 4", + }, + TestAlmanac, + }, + }; + } +} \ No newline at end of file diff --git a/05/IYGASAF.Tests/MapTests.cs b/05/IYGASAF.Tests/MapTests.cs new file mode 100644 index 0000000..7b02f32 --- /dev/null +++ b/05/IYGASAF.Tests/MapTests.cs @@ -0,0 +1,275 @@ +namespace IYGASAF.Tests; + +public class MapTests +{ + [Theory] + [MemberData(nameof(TestData.ParseMapData), MemberType = typeof(TestData))] + public void Parse_WhenGivenValidMap_ItShouldReturnExpectedMap(string[] ranges, Map expected) + { + var result = Map.Parse(ranges); + result.Should().BeEquivalentTo(expected); + } + + [Theory] + [MemberData(nameof(TestData.ConvertData), MemberType = typeof(TestData))] + public void ConvertSourceToDestination_WhenGivenValidSourceValue_ItShouldReturnExpectedDestinationValue(Map map, int sourceValue, int expected) + { + var result = map.ConvertSourceToDestination(sourceValue); + result.Should().Be(expected); + } + + public static class TestData + { + private static readonly Map TestSeedToSoilMap = new( + [ + new(2, 98, 50), + new(48, 50, 52), + ] + ); + + private static readonly Map TestSoilToFertilizerMap = new( + [ + new(37, 15, 0), + new(2, 52, 37), + new(15, 0, 39), + ] + ); + + private static readonly Map TestFertilizerToWaterMap = new( + [ + new(8, 53, 49), + new(42, 11, 0), + new(7, 0, 42), + new(4, 7, 57), + ] + ); + + private static readonly Map TestWaterToLightMap = new( + [ + new(7, 18, 88), + new(70, 25, 18), + ] + ); + + private static readonly Map TestLightToTemperatureMap = new( + [ + new(23, 77, 45), + new(19, 45, 81), + new(13, 64, 68), + ] + ); + + private static readonly Map TestTemperatureToHumidityMap = new( + [ + new(1, 69, 0), + new(69, 0, 1), + ] + ); + + private static readonly Map TestHumidityToLocationMap = new( + [ + new(37, 56, 60), + new(4, 93, 56), + ] + ); + + public static IEnumerable ConvertData => + new List + { + new object[] + { + TestSeedToSoilMap, + 79, + 81, + }, + new object[] + { + TestSeedToSoilMap, + 14, + 14, + }, + new object[] + { + TestSeedToSoilMap, + 55, + 57, + }, + new object[] + { + TestSeedToSoilMap, + 13, + 13, + }, + new object[] + { + TestSoilToFertilizerMap, + 81, + 81, + }, + new object[] + { + TestSoilToFertilizerMap, + 14, + 53, + }, + new object[] + { + TestSoilToFertilizerMap, + 57, + 57, + }, + new object[] + { + TestSoilToFertilizerMap, + 13, + 52, + }, + new object[] + { + TestFertilizerToWaterMap, + 81, + 81, + }, + new object[] + { + TestFertilizerToWaterMap, + 53, + 49, + }, + new object[] + { + TestFertilizerToWaterMap, + 57, + 53, + }, + new object[] + { + TestFertilizerToWaterMap, + 52, + 41, + }, + new object[] + { + TestWaterToLightMap, + 81, + 74, + }, + new object[] + { + TestWaterToLightMap, + 49, + 42, + }, + new object[] + { + TestWaterToLightMap, + 53, + 46, + }, + new object[] + { + TestWaterToLightMap, + 41, + 34, + }, + new object[] + { + TestLightToTemperatureMap, + 74, + 78, + }, + new object[] + { + TestLightToTemperatureMap, + 42, + 42, + }, + new object[] + { + TestLightToTemperatureMap, + 46, + 82, + }, + new object[] + { + TestLightToTemperatureMap, + 34, + 34, + }, + new object[] + { + TestTemperatureToHumidityMap, + 78, + 78, + }, + new object[] + { + TestTemperatureToHumidityMap, + 42, + 43, + }, + new object[] + { + TestTemperatureToHumidityMap, + 82, + 82, + }, + new object[] + { + TestTemperatureToHumidityMap, + 34, + 35, + }, + new object[] + { + TestHumidityToLocationMap, + 78, + 82, + }, + new object[] + { + TestHumidityToLocationMap, + 43, + 43, + }, + new object[] + { + TestHumidityToLocationMap, + 82, + 86, + }, + new object[] + { + TestHumidityToLocationMap, + 35, + 35, + }, + }; + + public static IEnumerable ParseMapData => + new List + { + new object[] + { + new string[] { "50 98 2", "52 50 48" }, + new Map( + [ + new(2, 98, 50), + new(48, 50, 52) + ] + ), + }, + new object[] + { + new string[] {"0 15 37", "37 52 2", "39 0 15"}, + new Map( + [ + new(37, 15, 0), + new(2, 52, 37), + new(15, 0, 39), + ] + ), + } + }; + } +} \ No newline at end of file diff --git a/05/IYGASAF.Tests/RangeTests.cs b/05/IYGASAF.Tests/RangeTests.cs new file mode 100644 index 0000000..385bacc --- /dev/null +++ b/05/IYGASAF.Tests/RangeTests.cs @@ -0,0 +1,23 @@ +namespace IYGASAF.Tests; + +public class RangeTests +{ + [Theory] + [MemberData(nameof(TestData.ParseRangeData), MemberType = typeof(TestData))] + public void Parse_WhenGivenValidRange_ItShouldReturnExpectedRange(string range, Range expected) + { + var result = Range.Parse(range); + result.Should().BeEquivalentTo(expected); + } + + + public static class TestData + { + public static IEnumerable ParseRangeData => + new List + { + new object[] { "50 98 2", new Range(2, 98, 50) }, + new object[] { "52 50 48", new Range(48, 50, 52) }, + }; + } +} \ No newline at end of file diff --git a/05/IYGASAF.Tests/UnitTest1.cs b/05/IYGASAF.Tests/UnitTest1.cs deleted file mode 100644 index 6c2a1c6..0000000 --- a/05/IYGASAF.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace IYGASAF.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/05/IYGASAF/Program.cs b/05/IYGASAF/Program.cs index ebf228a..f121d98 100644 --- a/05/IYGASAF/Program.cs +++ b/05/IYGASAF/Program.cs @@ -4,6 +4,158 @@ public class Program { public static async Task Main(string[] args) { - return await Task.FromResult(0); + if (args.Length is 0) + { + Console.WriteLine("Please provide a path to the input file."); + return -1; + } + + if (File.Exists(args[0]) is false) + { + Console.WriteLine("The provided file does not exist."); + return -2; + } + + + var input = await File.ReadAllLinesAsync(args[0]); + var result = Almanac.Parse(input).GetLowestSeedLocation(); + + Console.WriteLine($"The lowest seed location is {result}."); + + return (int)result; + } +} + +public class Almanac( + List seeds, + List maps +) +{ + public List Seeds { get; init; } = seeds; + public List Maps { get; init; } = maps; + + public long GetSeedLocation(long seed) => Maps.Aggregate( + seed, + (currentSeed, map) => map.ConvertSourceToDestination(currentSeed) + ); + + public long GetLowestSeedLocation() => Seeds + .Select(GetSeedLocation) + .Min(); + + public static Almanac Parse(string[] almanac) + { + var seedsString = almanac[0].Split(':')[1]; + var seeds = seedsString + .Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) + .Select(long.Parse) + .ToList(); + + var maps = new List(); + var ranges = new List(); + + for (int i = 1; i < almanac.Length; i++) + { + var currentLine = almanac[i]; + + if (string.IsNullOrWhiteSpace(currentLine)) + { + continue; + } + + if (currentLine.Contains(':')) + { + if (ranges.Count is not 0) + { + maps.Add(Map.Parse([.. ranges])); + ranges.Clear(); + } + continue; + } + + ranges.Add(currentLine); + + if (i == almanac.Length - 1) + { + maps.Add(Map.Parse([.. ranges])); + } + } + + return new Almanac( + seeds, + maps + ); + } +} + +public class Map( + List ranges +) +{ + public List Ranges { get; init; } = ranges; + + public static Map Parse(string[] ranges) => + new( + ranges.Select(Range.Parse).ToList() + ); + + public Range? GetSourceRange(long sourceValue) => + Ranges + .Where(range => range.SourceStart <= sourceValue && range.SourceEnd >= sourceValue) + .FirstOrDefault(); + + public long ConvertSourceToDestination(long valueToConvert) + { + var sourceRange = GetSourceRange(valueToConvert); + + if (sourceRange is null) + { + return valueToConvert; + } + + var sourceToDestinationOffset = Math.Abs(sourceRange.SourceStart - sourceRange.DestinationStart); + + return sourceRange.DestinationStart <= sourceRange.SourceStart + ? valueToConvert - sourceToDestinationOffset + : valueToConvert + sourceToDestinationOffset; + } +} + +public class Range( + long rangeLength, + long sourceStart, + long destinationStart +) +{ + public long RangeLength { get; init; } = rangeLength; + public long SourceStart { get; init; } = sourceStart; + public long SourceEnd => SourceStart + RangeLength - 1; + public long DestinationStart { get; init; } = destinationStart; + public long DestinationEnd => DestinationStart + RangeLength - 1; + + public static Range Parse(string range) + { + var parts = range.Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + + if (parts.Length is not 3 || parts.Any(part => long.TryParse(part, out _) is false)) + { + throw new ArgumentException("Range must be in the format of 'DestinationRangeStart:long SourceRangeStart:long RangeLength:long'"); + } + + var rangeLength = long.Parse(parts[2]); + var sourceStart = long.Parse(parts[1]); + var destinationStart = long.Parse(parts[0]); + + return new Range( + rangeLength, + sourceStart, + destinationStart + ); } } \ No newline at end of file