DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
We're back for another day of Advent of Code using C#.
Notes:
/*
Part 1:
* Grid of paper @, . nothing
* Find all locations of paper where it can be retrieved (fewer than 4 papers in 8 adjancent squares)
SubProblems:
* A: Hold grid
* 1: Loop over lines, building 2d grid
* Grab line - @ is paper, . is nothing
* Put in the thing
* B: Figuring out if paper is reachable
* 1: Helper that takes in grid, location, and counts
* Counts adjacent squares to see if okay
*/
I wanted to try and do an approach without for loops - more similar to smth I would build in F#. I've been pretty pleasantly surprised with how easy it is to chain things, especially with some helper methods for Pipe and Tap. (These didn't end up in the final solution but they are VERY useful for adding debug lines in chains).
public static class PipeExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn value, Func<TIn, TOut> func) => func(value);
public static T Tap<T>(this T value, Action<T> action)
{
action(value);
return value;
}
}
While it is a bit wordier than other languages might be, I quite like the chainability and I find the lack of meaningful whitespace to help a lot as I can reformat many of the chains to be more readable to me.
One thing that is a bit annoying is the opening { on a new line. I feel that adds a lot of loc for little readability and prob could be removed.
public static int Day4Part1(string[] lines)
{
var grid = BuildGrid(lines);
return YieldGridSpaces(grid)
.Count(gridSpaceCoord =>
gridSpaceCoord.GridSpace.Match(
paper => isPaperReachable(grid, gridSpaceCoord.Coord),
empty => false
));
}
public static GridSpace[][] BuildGrid(string[] lines)
{
return lines
.Select(l =>
l.Select<char, GridSpace>(CharToGridSpace).ToArray()
).ToArray();
}
public static IEnumerable<GridSpaceCoord> YieldGridSpaces(GridSpace[][] grid)
{
return grid.SelectMany((row, y) =>
row.Select((gridSpace, x) =>
new GridSpaceCoord(
GridSpace: gridSpace,
Coord: new Coordinate(X: x, Y: y)
))
);
}
public static GridSpace CharToGridSpace(char c)
{
switch(c)
{
case '@': return new PaperSpace();
case '.': return new EmptySpace();
default: throw new InvalidDataException($"Got unexpected character: {c}");
}
}
public static bool isValidCoordinate(GridSpace[][] grid, Coordinate coord)
{
return !(coord.Y < 0 || coord.Y >= grid.Length || coord.X < 0 || coord.X >= grid[0].Length);
}
public static GridSpace GetGridSpaceAtCoordinate(GridSpace[][] grid, Coordinate coord)
{
return grid[coord.Y][coord.X];
}
public static Coordinate[] GetSurroundingCoordinates(Coordinate coord)
{
return [
coord with { Y = coord.Y - 1}, // bottom
coord with { Y = coord.Y - 1, X = coord.X - 1},
coord with { X = coord.X - 1}, // left
coord with { Y = coord.Y + 1, X = coord.X - 1},
coord with { Y = coord.Y + 1}, // top
coord with { Y = coord.Y + 1, X = coord.X + 1},
coord with { X = coord.X + 1}, // right
coord with { Y = coord.Y - 1, X = coord.X + 1},
];
}
public static bool isPaperReachable(GridSpace[][] grid, Coordinate coord)
{
if(!isValidCoordinate(grid, coord))
{
throw new InvalidDataException($"Got unreachable coordinate! {coord}");
}
return GetSurroundingCoordinates(coord)
.Select(surroundingCoord =>
isValidCoordinate(grid, surroundingCoord)
&& GetGridSpaceAtCoordinate(grid, surroundingCoord).Match(paper => true, empty => false))
.Count(isPaper => isPaper) < 4;
}
Notes:
Part 2:
* Same as part 1 except now a paper can be removed from the grid
Solutions:
* A: Run algorithm over grid, take note of the ones that can be removed
* Remove them at each juncture
* Run again til no paper removed
I realized that a recursive solution would probably make the most sense because I'm avoiding loops and want to pass new state on to the next calculation. Interestingly, I've been finding the recursive logic easier to reason ab these days than when I used F# heavily and F# is generally better geared towards those solutions.
Just goes to show that you can build similar things / logic constructs in many languages.
One area I really don't like ab C# right now is its lack of native unions. OneOf is decent but the type system just does not narrow as ergonomically as you'd want it to - I basically have an As cast in there to get this working as I want which is a runtime error waiting to happen.
public static int Day4Part2(string[] lines)
{
var grid = BuildGrid(lines);
return Day4Part2Recurse(grid);
}
private static int Day4Part2Recurse(GridSpace[][] grid)
{
var paperSpacesFound = YieldGridSpaces(grid)
.Select(gridSpaceCoord =>
gridSpaceCoord.GridSpace.Match<Option<Coordinate>>(
paper => isPaperReachable(grid, gridSpaceCoord.Coord)
? new Some<Coordinate>(gridSpaceCoord.Coord)
: new None(),
empty => new None()
))
.Where(opt => opt.IsT0)
.Select(opt => opt.AsT0.Value)
.ToArray();
if(paperSpacesFound.Length == 0)
return 0;
return paperSpacesFound.Length
+ (DeletePaper(grid, paperSpacesFound)
.Pipe(Day4Part2Recurse));
}
public static GridSpace[][] DeletePaper(GridSpace[][] grid, Coordinate[] coordsToDelete)
{
var coordsToDeleteIdSet = new HashSet<string>(coordsToDelete.Select(c => $"{c.X}_{c.Y}").ToList());
return grid.Select((row, y) =>
row.Select((space, x) => coordsToDeleteIdSet.Contains($"{x}_{y}")
? new EmptySpace()
: space)
.ToArray()
).ToArray();
}
Overall I've been pretty pleased with C#'s ergonomics. If we just got native unions that the type system could narrow and exhaustively pattern match, I think this lang would be an A+.
HAMINIONs Members get access to the full source code (Github Repo) and that of dozens of other example projects.
If you liked this post you might also like: