Advent of Code 2024 - Day 2

Day 2 is here, and it’s time to level up! :computer: The second challenge has just dropped, and with it comes new opportunities to showcase the power of Erlang. :rocket:

As always, the key is to break the problem down, embrace functional programming, and let the BEAM work its magic. Whether you’re refining your recursion skills, experimenting with list comprehensions, or diving deeper into pattern matching, there’s something here for everyone.

:bulb: Remember:

  • Test early, test often.
  • Don’t hesitate to share your “aha!” moments or ask questions.
  • Erlang thrives on collaboration - let’s learn and grow together.

:sparkles: Motivation for today:
The best solutions come from persistence and creativity. Take it one step at a time, and don’t forget to enjoy the process!

Share your thoughts, approaches, and solutions below. Let’s keep the momentum going and make Day 2 another success story for the Erlang community!

Happy coding, and may your processes always terminate! :santa:

3 Likes

Here is my solution:

2 Likes

Here’s my solution:

-module(day2).

-compile(export_all).

main(File) ->
    {ok, RawData} = file:read_file(File),
    Data =
        [[binary_to_integer(X) || X <- binary:split(L, <<" ">>, [global, trim])]
         || L <- binary:split(RawData, <<"\n">>, [global, trim])],
    io:format("part 1: ~p~n", [solve1(Data)]),
    io:format("part 2: ~p~n", [solve2(Data)]).

solve1(Data) ->
    length([X || X <- Data, is_safe(X)]).

is_safe([H1, H2 | _] = Line) ->
    is_safe(Line, H1 < H2).

is_safe([_], _) ->
    true;
is_safe([H1, H2 | T], true = Asc) when H2 - H1 >= 1, H2 - H1 =< 3 ->
    is_safe([H2 | T], Asc);
is_safe([H1, H2 | T], false = Desc) when H1 - H2 >= 1, H1 - H2 =< 3 ->
    is_safe([H2 | T], Desc);
is_safe(_, _) ->
    false.

solve2(Data) ->
    length([X || X <- Data, lists:any(fun is_safe/1, [X | remove_one(X)])]).

remove_one(List) ->
    remove_one(List, []).

remove_one([H | T], Tail) ->
    [lists:reverse(Tail) ++ T | remove_one(T, [H | Tail])];
remove_one([], _) ->
    [].

4 Likes

Hey folks! Here is my ugly solution :smile:

-module(day_2).

-export([task_1/0, task_2/0]).

task_1() ->
    File = "input",
    {ok, RawData} = file:read_file(File),
    Raw = binary:split(RawData, <<"\n">>, [global, trim]),
    Data = [[binary_to_integer(X) || X <- binary:split(R, <<" ">>, [global, trim]), X /= <<>>] || R <- Raw],
    lists:foldl(fun sum/2, 0, Data).

sum(List, Sum) ->
    case maybe_safe(List, none) of
        descrise ->
            Sum + 1;
        increase ->
            Sum + 1;
        _ ->
            Sum
    end.

maybe_safe([], Acc) ->
    Acc;
maybe_safe([H, H | _T], _Acc) ->
    not_safe;
maybe_safe([X, Y | T], none) when X - Y > 0, abs(X - Y) < 4 ->
    maybe_safe([Y | T], descrise);
maybe_safe([X, Y | T], descrise) when X - Y > 0, abs(X - Y) < 4 ->
    maybe_safe([Y | T], descrise);
maybe_safe([X, Y | T], increase) when X - Y > 0, abs(X - Y) < 4 ->
    maybe_safe([Y | T], not_safe);
maybe_safe([X, Y | T], descrise) when abs(X - Y) < 4 ->
    maybe_safe([Y | T], not_safe);
maybe_safe([X, Y | T], none) when abs(X - Y) < 4 ->
    maybe_safe([Y | T], increase);
maybe_safe([X, Y | T], increase) when abs(X - Y) < 4 ->
    maybe_safe([Y | T], increase);
maybe_safe([_, _Y | _T], _Acc) ->
    not_safe;
maybe_safe([_ | T], Acc) ->
    maybe_safe(T, Acc).

task_2() ->
    File = "input",
    {ok, RawData} = file:read_file(File),
    Raw = binary:split(RawData, <<"\n">>, [global, trim]),
    Data = [[binary_to_integer(X) || X <- binary:split(R, <<" ">>, [global, trim]), X /= <<>>] || R <- Raw],
    lists:foldl(fun sum2/2, 0, Data).

sum2(List, Sum) ->
    case maybe_safe2(List, none) of
        descrise ->
            Sum + 1;
        increase ->
            Sum + 1;
        _ ->
            Sum
    end.

maybe_safe2([], Data) ->
    Data;
maybe_safe2([H, H | T], Data) ->
    maybe_safe2([H | T], Data);
maybe_safe2([X, Y | T], none)     when X - Y > 0, abs(X - Y) < 4 ->
    maybe_safe2([Y | T], descrise);
maybe_safe2([X, Y | T], descrise) when X - Y > 0, abs(X - Y) < 4 ->
    maybe_safe2([Y | T], descrise);
maybe_safe2([X, Y | T], increase) when X - Y > 0, abs(X - Y) < 4 ->
    case maybe_safe2([X | T], none) of
        not_safe->
           not_safe;
        Safe ->
            maybe_safe2([Y | T], Safe)
    end;
maybe_safe2([X, Y | T], none)     when abs(X - Y) < 4 ->
    maybe_safe2([Y | T], increase);
maybe_safe2([X, Y | T], increase) when abs(X - Y) < 4 ->
    maybe_safe2([Y | T], increase);
maybe_safe2([X, Y | T], descrise) when abs(X - Y) < 4 ->
    case maybe_safe2([X | T], none) of
        not_safe->
           not_safe;
        _ -> 
            case maybe_safe2([Y | T], none) of
                not_safe ->
                    not_safe;
                Safe ->
                    maybe_safe2(T, Safe)
            end
    end;
maybe_safe2([_, _ | _], _) ->
    not_safe;
maybe_safe2([_ | T], Data) ->
    maybe_safe2(T, Data).

I can’t say I thoroughly enjoyed the process :smile:, but it is what it is, and we’ve got the result we needed.

3 Likes

Here’s my sloppy solution. I again wish for a function in the lists module that can remove the Xth element of a list :sweat_smile:

-module(day02).
-export([task/1]).

%% Check if a list is safe according to task 1's rules
safe([E1,E2|T]) ->
    case E1 < E2 andalso E1 + 4 > E2 of
        true -> safe([E2|T]);
        false -> false
    end;
safe([_E1]) ->
    true.

%% Check if a list is safe according to task 2's rules
safe2([E1,E2|T], []) ->
    case E1 < E2 andalso E1 + 4 > E2 of
        true -> safe2([E2|T], [E1]);
        false -> safe([E1|T]) orelse safe([E2|T])
    end;
safe2([E1,E2|T1], [H|_T2]=L2) ->
    case E1 < E2 andalso E1 + 4 > E2 of
        true -> safe2([E2|T1], [E1|L2]);
        false -> safe([E2|T1]) orelse safe([H,E2|T1])
    end;
safe2([_E1,_E2], _) ->
    true;
safe2([_E1], _) ->
    true.

%% Reverse decreasing lists
to_inc([E1, E2|_T] = L, 1) ->
    case E1 < E2 of
        true -> safe(L);
        false -> safe(lists:reverse(L))
    end;
to_inc([E1, E2|_T] = L, 2) ->
    case E1 < E2 of
        true -> safe2(L,[]);
        false -> safe2(lists:reverse(L),[])
    end.

%% Main function for task 1 and 2. Call task(1) and task(2) respectively.
task(Number) ->
    FileName = "input02.txt",
    case file:read_file(FileName) of
        {ok, Content} ->
            Lines = binary:split(Content, <<"\n">>, [global]),
            count_safe(Lines, 0, Number);
        {error, Reason} ->
            {error, Reason}
    end.

%% Helper function
count_safe([<<>>], Acc, _Task) ->
    Acc;
count_safe([Line|Lines], Acc0, Task) ->
    L0 = binary:split(Line, <<" ">>, [global, trim]),
    L1 = [binary_to_integer(N) || N <- L0],
    Acc1 = case to_inc(L1,Task) of
        true -> Acc0 + 1;
        false -> Acc0
    end,
    count_safe(Lines, Acc1, Task).
4 Likes

Nice solution! :christmas_tree: I noticed your Day 1 solution on GitHub - great work there too! Don’t forget to share it in the forum thread: Advent of Code 2024 - Day 1. Looking forward to seeing more of your approaches! :metal:

2 Likes

I admitted defeat on an elegant solution for part 2 and came here seeking enlightenment. Disappointed there’s nothing simple and clean.

Part 1, I was tolerably happy with:

p1(InputData) ->
    length([Row || Row <- InputData, safe(util:str_to_ints(Row))]).

safe(L) ->
    safe(L, 0) =:= length(L) - 1.

safe([_H], A) -> abs(A);
safe([H | T], A) when A >= 0 andalso H < hd(T) andalso H >= hd(T) - 3 -> safe(T, A + 1);
safe([H | T], A) when A =< 0 andalso H > hd(T) andalso H =< hd(T) + 3 -> safe(T, A - 1);
safe(_, _) -> 0.

(See my Day 1 post for utility & unit test functions).

Note that the incrementing/decrementing guards (A >= 0 and A =< 0 respectively) are actually superfluous, but would make it more efficient with longer input rows.

1 Like

Part 2 required some ugliness to cover an edge case:

p2(InputData) ->
    length([Row || Row <- InputData, safe2(util:str_to_ints(Row))]).

safe2([_ | T] = L) ->
    % Also consider dampening the first element.
    A = max(safe(L, 0), safe(T, 0)),
    length(L) - A =< 2.

safe([_], A) -> abs(A);
safe([H | T], A) when A >= 0 andalso H < hd(T) andalso H >= hd(T) - 3 -> safe(T, A + 1);
safe([H | T], A) when A =< 0 andalso H > hd(T) andalso H =< hd(T) + 3 -> safe(T, A - 1);
safe([H | [_ | T]], A) -> safe([H | T], A).
1 Like

Did day 2 too late at night, came back with fresh eyes:

#!/usr/bin/env escript
%%! +A0 -sname day1
%% -*- coding: utf-8 -*-

-mode('compile').

-export([main/1]).

%% API

main([]) ->
    {ok, Input} = file:read_file("puzzle.txt"),
    %% {ok, Input} = file:read_file("sample.txt"),
    Reports = [[binary_to_integer(Level)
                >> Level <- binary:split(Report, <<" ">>, ['global'])
               ]
               >> Report <- binary:split(Input, <<"\n">>, ['global', 'trim'])
              ],
    {SafeReports, _UnsafeReports} = lists:partition(fun is_safe_report/1, Reports),

    {DampSafe, _UnsafeDamp} = lists:partition(fun is_damp_report/1, Reports),

    io:format("safe: ~p~n", [length(SafeReports)]),
    io:format("damp: ~p~n", [length(DampSafe)]).

is_safe_report([L1, L2 | _]=Levels) ->
    is_safe_report(Levels, L1 < L2).

is_safe_report([], _Increasing) -> 'true';
is_safe_report([_], _Increasing) -> 'true';

%% increasing levels
is_safe_report([L1, L2 | Rest], 'true')
  when L1 < L2
       andalso L2-L1 < 4
       andalso L2-L1 > 0
       ->
    is_safe_report([L2 | Rest], 'true');

%% decreasing levels
is_safe_report([L1, L2 | Rest], 'false')
  when L1 > L2
       andalso L1-L2 < 4
       andalso L1-L2 > 0
       ->
    is_safe_report([L2 | Rest], 'false');
is_safe_report(_Rest, _Increasing) ->
    'false'.

%% too low: 381, 364, 367, 385, 375, 376
is_damp_report([L1, L2 | Rest] = Levels) ->
    is_damp_report(Levels, L1 < L2, [], 'false')
        orelse is_damp_report([L2 | Rest], L2 < hd(Rest), [], 'true').

is_damp_report([_], _, _, _) -> 'true';

%% asc and valid
is_damp_report([L1, L2 | Rest], 'true', ValidLevels, Restarted)
  when L1 < L2,
       L2-L1 > 0,
       L2-L1 < 4 ->
    is_damp_report([L2 | Rest], 'true', [L1 | ValidLevels], Restarted);

%% desc and valid
is_damp_report([L1, L2 | Rest], 'false', ValidLevels, Restarted)
  when L1 > L2,
       L1-L2 > 0,
       L1-L2 < 4 ->
    is_damp_report([L2 | Rest], 'false', [L1 | ValidLevels], Restarted);

%% invalid but not restarted
is_damp_report([L1, L2 | Rest], _IsAsc, ValidLevels, 'false') ->
    restart([L2 | Rest], ValidLevels)
        orelse restart([L1 | Rest], ValidLevels);
%% invalid and already restarted, unsafe
is_damp_report(_Levels, _, _, 'true') ->
    'false'.

restart(Levels, ValidLevels) ->
    [N1, N2 | _] = NewLevels = lists:reverse(ValidLevels) ++ Levels,
    is_damp_report(NewLevels, N1 < N2, [], 'true').
1 Like

Part 1 looks great - safe/2 is a clever way to capture the logic, and I appreciate the thought you put into making it efficient for longer input rows. As for Part 2, you’re not alone in the struggle for elegance - it’s a tough one! Sometimes functionality wins over simplicity, and that’s okay. Great work overall! :rocket::christmas_tree:

Nicely done handling that edge case! The addition to safe2 makes it robust while keeping the core logic intact. Sometimes a little ‘ugliness’ is necessary to get the job done - great work adapting to the challenge! :rocket::christmas_tree:

Impressive work coming back with fresh eyes and refining the solution! :computer: The detailed guards and clear logic make this approach robust and easy to follow. Handling the restart logic for invalid cases shows great attention to edge cases. :rocket::christmas_tree:

I wasn’t happy with evaluating it twice for every row, “just in case”. The following is more verbose, but does less unnecessary work:

safe2([_ | T] = L) ->
    % Also consider dampening the first element.
    Len = length(L),
    case Len - safe(L, 0) =< 2 of
        true -> true;
        false -> Len - safe(T, 0) =:= 2
    end.
1 Like