Advent of Code, in Erlang: Day 5

Published Thursday, December 16, 2021 by Bryan

There's probably a good way to solve Advent of Code 2021, Day 5 without actually rendering the lines, as shown in the puzzle description, but rendering the lines provided such a good debugging aid that I couldn't see bothering with other methods.

parse_line(String) ->
    [Start, End] = string:split(String, <<" -> ">>),
    {parse_point(Start), parse_point(End)}.

parse_point(String) ->
    [X, Y] = string:split(String, <<",">>),
    {binary_to_integer(X), binary_to_integer(Y)}.

map_lines(Lines) ->
    {MaxX, MaxY} = lists:foldl(fun({{X1,Y1},{X2,Y2}}, {MX, MY}) ->
                                       {lists:max([X1, X2, MX]),
                                        lists:max([Y1, Y2, MY])}
                               end,
                               {0, 0},
                               Lines),
    Map = lists:duplicate(MaxY+1, lists:duplicate(MaxX+1, 0)),
    lists:foldl(fun map_line/2, Map, Lines).

I first parsed each line into a start,end pair of X,Y pairs. Folding over them, I found the maximum X and Y coordinates in the bunch, and used that information to create an empty field large enough to draw everything. Plotting each line onto that field was all that was left.

map_line({{X1, Y}, {X2, Y}}, Map) ->
    %% horizontal
    {Above, [Row|Below]} = lists:split(Y, Map),
    [MinX, MaxX] = lists:sort([X1, X2]),
    {NewRow, _} = lists:mapfoldl(
                    fun(V, X) ->
                            {case (X >= MinX andalso X =< MaxX) of
                                 true -> V+1;
                                 false -> V
                             end,
                             X+1}
                    end, 0, Row),
    Above ++ [NewRow|Below];
map_line({{X, Y1}, {X, Y2}}, Map) ->
    %% vertical
    [MinY, MaxY] = lists:sort([Y1, Y2]),
    element(1,
            lists:mapfoldl(
              fun(Row, Y) ->
                      case (Y >= MinY andalso Y =< MaxY) of
                          true ->
                              {Left, [V|Right]} = lists:split(X, Row),
                              NewRow = Left ++ [V+1|Right];
                          false ->
                              NewRow = Row
                      end,
                      {NewRow, Y+1}
              end,
              0,
              Map));

Part 1 wanted information about only horizontal and vertical lines. Pattern matching in function headers makes it easy to keep the rendering code for those two separate. It states their definition exactly the same way the puzzle description does: when Y is the same in the start and end coordinate, that's a horizontal line, and when X is the same, that's vertical. The actual plotting code could probably be nicer. As is, it iterates over all of the pixels in the given row or column, and increments the values in those that are between the line's endpoints. Bulk-skipping to the start, and after the end might have been nicer, but this met the requirements.

map_line({{X1, Y1}, {X2, Y2}}, Map) ->
    %% diagonal
    [{StartX, StartY}, {EndX, EndY}] = lists:keysort(2, [{X1, Y1}, {X2, Y2}]),
    XDir = case EndX > StartX of
               true -> 1;
               false -> -1
           end,
    element(1,
            lists:mapfoldl(
              fun(Row, Y) ->
                      case (Y >= StartY andalso Y =< EndY) of
                          true ->
                              X = StartX + XDir * (Y - StartY),
                              {Left, [V|Right]} = lists:split(X, Row),
                              NewRow = Left ++ [V+1|Right];
                          false ->
                              NewRow = Row
                      end,
                      {NewRow, Y+1}
              end,
              0,
              Map)).

Part 2 asked us to also render the diagonal lines. I kept the pattern of visiting each row. This is the reason for all the sorting and comparison at the top: I needed to make sure I had the highest endpoint to start with, since I would see the rows in that order. Within a row, I did skip to the exact pixel to increment.

count_overlaps(Map) ->
    lists:sum([ length([ N || N <- Row, N > 1 ]) || Row <- Map ]).

To count the number of line crossing, as the puzzle asked for, I iterated the rows of the field, and then filtered each row for pixels that I had incremented more than once. There is no real value in keeping the actual pixel value (N) value around, instead of mapping to 1 or some other placeholder. But it was there, and when I wrote this for Part 1, I wondered if I would need to score multiple overlaps differently.

Lines = [ puzzle05:parse_line(S) || S <- string:split(Example, <<"\n">>, all) ].
Map = puzzle05:map_lines(Lines).
12 = puzzle05:count_overlaps(Map).

Did you write a version that compared the line definitions directly, instead of plotting and analyzing the plot? Send me a link on Twitter (@hobbyist) - I'd love to see it. Want to try making this code cleaner? Grab it from my advent-of-code github repo. And if you really want to see Erlang pattern matching shine, come back later for my Day 16 writeup!