Advent of Code 2024 - Day 3

Hey all, Ukrainian Erlanger is here :metal:! Let’s tackle Day 3 together! :computer: Today’s challenge promises to test our functional programming skills even further. It’s time to dive into Erlang, craft some elegant solutions, and learn from one another. :rocket:

As always, whether you’re flying through the puzzles or debugging that tricky edge case, this is your space to share progress, insights, and solutions.

:bulb: Pro Tips for Day 3:

  • Leverage pattern matching to simplify your logic.
  • Explore the lists module - it’s packed with utilities to make life easier.
  • Recursion is your friend, but don’t forget to tail-optimize where needed!

:sparkles: Motivation for today:
Complex problems become simple when broken into smaller parts. Let’s tackle this step by step and solve it together!

Drop your thoughts, ideas, or even your frustrations below. Let’s Beam together and make Day 3 another success for the Erlang community!

Happy coding, and may your solutions always compile! :santa:

1 Like

I used regular expressions in my Elixir solution, but it took me a while to get it right. For my Erlang solution, I decided to use the bit syntax to do the parsing:

4 Likes

My solution, felt like using regex this time instead writing a simple parser :slight_smile: :

-module(day3).

-compile(export_all).

main(File) ->
    {ok, RawData} = file:read_file(File),
    io:format("part 1: ~p~n", [solve1(RawData)]),
    io:format("part 2: ~p~n", [solve2(RawData)]).

solve1(Data) ->
    {match, Nums} =
        re:run(Data,
               ".*mul\\((\\d{1,3}),(\\d{1,3})\\).*",
               [dotall, global, ungreedy, {capture, all_but_first, binary}]),
    lists:sum([binary_to_integer(N1) * binary_to_integer(N2) || [N1, N2] <- Nums]).

solve2(Data) ->
    {match, Capture} =
        re:run(Data,
               ".*(?:(don't|do)|(?:mul\\((\\d{1,3}),(\\d{1,3})\\))).*",
               [dotall, global, ungreedy, {capture, all_but_first, binary}]),
    process(Capture, true).

process([[_, N1, N2] | R], Do) when Do ->
    binary_to_integer(N1) * binary_to_integer(N2) + process(R, Do);
process([[<<"don't">>] | R], _) ->
    process(R, false);
process([[<<"do">>] | R], _) ->
    process(R, true);
process([_ | R], Do) ->
    process(R, Do);
process([], _) ->
    0.

4 Likes

Impressive work! Using the bit syntax for parsing is such a powerful approach in Erlang - kudos for leveraging it. :muscle: Also, great to see the variety in your solutions between Elixir and Erlang. Keep up the awesome work, and thanks for sharing! :rocket::christmas_tree:

3 Likes

Great solution! Regex is definitely a handy choice here, and I love how cleanly you’ve integrated it with the parsing logic. Using re:run like this showcases Erlang’s versatility. Nice work on both parts - keep it up! :rocket::christmas_tree:

2 Likes

I’ve faced similar tasks during a few job interviews where I had to write code on the fly. Honestly, I failed both times! :sweat_smile: Maybe that’s why I don’t particularly enjoy these types of tasks. However, I believe it’s essential for every developer to have these skills in their toolkit. That’s why I always revisit and implement such tasks after the interview, even if I didn’t succeed during it. It’s all part of learning and improving! :muscle:

So, here’s my approach for Day 3. I opted for binary pattern matching to parse the input and handle the calculations efficiently. It’s a great opportunity to practice and refine these techniques, even if the process can be a bit tricky at first.

-module(day_3).

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

% xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))
task_1() ->
    File = "input",
    {ok, RawData} = file:read_file(File),
    sum(RawData, 0).

sum(<<>>, Sum) ->
    Sum;
sum(<<"mul(", Bin:1/binary, Rest/binary>>, Sum) ->
    case find_num_till_coma(<<Bin/binary, Rest/binary>>, <<>>) of
        {<<>>, _} ->
            sum(Rest, Sum);
        {NumLeft, Rest2} ->
            case find_num_till_close(Rest2, <<>>) of
                {<<>>, _} ->
                    sum(Rest, Sum);
                {NumRight, Rest3} ->
                    sum(Rest3, binary_to_integer(NumLeft) * binary_to_integer(NumRight) + Sum)
            end
    end;
sum(<<_, Rest/binary>>, Sum) ->
    sum(Rest, Sum).

find_num_till_coma(<<Bin:1/binary, Rest/binary>>, Acc)  when
    Bin == <<"0">>; Bin == <<"1">>; Bin == <<"2">>; Bin == <<"3">>; Bin == <<"4">>;
    Bin == <<"5">>; Bin == <<"6">>; Bin == <<"7">>; Bin == <<"8">>; Bin == <<"9">> ->
        find_num_till_coma(Rest, <<Acc/binary, Bin/binary>>);
find_num_till_coma(<<",", Rest/binary>>, Acc) ->
    {Acc, Rest};
find_num_till_coma(_X, _A) ->
    {<<>>, <<>>}.

find_num_till_close(<<Bin:1/binary, Rest/binary>>, Acc)  when
    Bin == <<"0">>; Bin == <<"1">>; Bin == <<"2">>; Bin == <<"3">>; Bin == <<"4">>;
    Bin == <<"5">>; Bin == <<"6">>; Bin == <<"7">>; Bin == <<"8">>; Bin == <<"9">> ->
        find_num_till_close(Rest, <<Acc/binary, Bin/binary>>);
find_num_till_close(<<")", Rest/binary>>, Acc) ->
    {Acc, Rest};
find_num_till_close(_, _) ->
    {<<>>, <<>>}.

% xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))
task_2() ->
    File = "input2",
    {ok, RawData} = file:read_file(File),
    sum2(RawData, 0, do).

sum2(<<>>, Sum, _) ->
    Sum;
sum2(<<"don't", Rest/binary>>, Sum, _) ->
    sum2(Rest, Sum, dont);
sum2(<<"do", Rest/binary>>, Sum, dont) ->
    sum2(Rest, Sum, do);
sum2(<<"mul(", Bin:1/binary, Rest/binary>>, Sum, do = Act) ->
    case find_num_till_coma(<<Bin/binary, Rest/binary>>, <<>>) of
        {<<>>, _} ->
            sum2(Rest, Sum, Act);
        {NumLeft, Rest2} ->
            case find_num_till_close(Rest2, <<>>) of
                {<<>>, _} ->
                    sum2(Rest, Sum, Act);
                {NumRight, Rest3} ->
                    sum2(Rest3, binary_to_integer(NumLeft) * binary_to_integer(NumRight) + Sum, Act)
            end
    end;
sum2(<<_, Rest/binary>>, Sum, Act) ->
    sum2(Rest, Sum, Act).
3 Likes

Today’s challenge is a nice intro into the re module for me. After finally figuring out the patterns, I spent quite some time in options, especially capture. Even now, I still have the feeling that I fluked the challenge and haven’t gotten every option right.

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

%% Find all mul(X, Y) and sum the multiplication.
match(L) ->
    Pattern = <<".*mul\\((\\d{1,3}),(\\d{1,3})\\).*">>,
    case re:run(L, Pattern, [global,ungreedy,{capture, [1,2], list}]) of
        {match, Matches} ->
            lists:sum([list_to_integer(X) * list_to_integer(Y) || [X, Y] <- Matches]);
        nomatch ->
            0
    end.

match2(L, do, Acc0) ->
    %% Find the next don't(). Call match/1 for the segment in between.
    Pattern = <<"don\\'t\\(\\)">>,
    case re:run(L, Pattern, [{capture,first}]) of
        {match, [{Position,_}]} ->
            NewStart = Position + byte_size(<<"don't()">>),
            L1 = binary:part(L, 0, Position),
            L2 = binary:part(L, NewStart, byte_size(L) - NewStart),
            match2(L2, dont, Acc0 + match(L1));
        nomatch ->
            match(L) + Acc0
    end;
match2(L, dont, Acc) ->
    %% Find the next do().
    Pattern = <<"do\\(\\)">>,
    case re:run(L, Pattern, [{capture,first}]) of
        {match, [{Position,_}]} ->
            NewStart = Position + byte_size(<<"do()">>),
            L1 = binary:part(L, NewStart, byte_size(L) - NewStart),
            match2(L1, do, Acc);
        nomatch ->
            Acc
    end.

%% Main function for task 1 and 2. Call task(1) and task(2) respectively.
task(Number) ->
    FileName = "input03.txt",
    case file:read_file(FileName) of
        {ok, Content} ->
            case Number of
                1 -> match(Content);
                2 -> match2(Content, do, 0)
            end;
        {error, Reason} ->
            {error, Reason}
    end.
2 Likes

Great solution! Using re for pattern matching makes the implementation concise, and I like how you’ve structured the logic for match2 to handle the toggling between do and don’t. Spending time on the options and capture patterns definitely pays off - your approach looks solid! Keep it up! :rocket::christmas_tree:

I think there’s some DRY with the parse_arg* here but it was quick and dirty.

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

-mode('compile').

-export([main/1]).

%% API

main([]) ->
    {'ok', Input} = file:read_file("puzzle.txt"),
    %% {'ok', Input} = file:read_file("sample.txt"),

    Muls = parse_memory(Input),
    io:format("sum of muls: ~p~n", [lists:sum(Muls)]),

    %% {'ok', Input2} = file:read_file("sample2.txt"),
    CondMuls = parse_memory_cond(Input),
    io:format("sum of cond muls: ~p~n", [lists:sum(CondMuls)]).

parse_memory(Input) ->
    parse_memory(Input, []).

parse_memory(<<>>, Muls) -> Muls;
parse_memory(<<"mul(", Rest/binary>>, Muls) ->
    {Mul, Rest1} = parse_mul(Rest),
    parse_memory(Rest1, [Mul | Muls]);
parse_memory(<<_:1/binary, Rest/binary>>, Muls) ->
    parse_memory(Rest, Muls).

parse_memory_cond(Input) ->
    parse_memory_cond(Input, []).

parse_memory_cond(<<>>, Muls) -> lists:reverse(Muls);
parse_memory_cond(<<"mul(", Rest/binary>>, Muls) ->
    {Mul, Rest1} = parse_mul(Rest),
    parse_memory_cond(Rest1, [Mul | Muls]);
parse_memory_cond(<<"don't()", Rest/binary>>, Muls) ->
    Rest1 = parse_dont(Rest),
    parse_memory_cond(Rest1, Muls);
parse_memory_cond(<<_:1/binary, Rest/binary>>, Muls) ->
    parse_memory_cond(Rest, Muls).

parse_dont(<<>>) -> <<>>;
parse_dont(<<"do()", Rest/binary>>) -> Rest;
parse_dont(<<_:1/binary, Rest/binary>>) ->
    parse_dont(Rest).

parse_mul(Input) ->
    case parse_arg1(Input) of
        {Arg1, Rest1} ->
            case parse_arg2(Rest1) of
                {Arg2, Rest2} -> {Arg1*Arg2, Rest2};
                Rest2 -> {0, Rest2}
            end;
        Rest1 ->
            {0, Rest1}
    end.

parse_arg1(Memory) ->
    parse_arg1(Memory, <<>>).

parse_arg1(<<",", Rest/binary>>, Arg) ->
    {binary_to_integer(Arg), Rest};
parse_arg1(<<N:8, Rest/binary>>, Arg) when N >= $0 andalso N =< $9 ->
    parse_arg1(Rest, <<Arg/binary, N>>);
parse_arg1(Rest, _Arg) ->
    Rest.

parse_arg2(Memory) ->
    parse_arg2(Memory, <<>>).

parse_arg2(<<")", Rest/binary>>, Arg) ->
    {binary_to_integer(Arg), Rest};
parse_arg2(<<N:8, Rest/binary>>, Arg) when N >= $0 andalso N =< $9 ->
    parse_arg2(Rest, <<Arg/binary, N>>);
parse_arg2(Rest, _Arg) ->
    Rest.

Viacheslav Katsuba via Erlang Forums writes:

2 Likes

Quick and dirty often gets the job done, and your solution looks solid! :muscle: Great work tackling the challenge and getting it to work efficiently. Thanks for sharing - always inspiring to see different approaches! :rocket::christmas_tree:

Part 1. I think this is the first time I’ve used maybe in anger. Also pretty sure my tokenisation is setting me up for a fail on part 2.

p1(InputData) ->
    opcode(lists:concat(InputData), 0).

opcode([], A) -> A;
opcode(S, A) ->
    [_ | R] = string:split(S, "mul("),
    opcode(R, A + mulify(R)).

mulify(S) ->
    maybe
        {N, [$, | R]} ?= string:to_integer(S),
        {M, [$) | _]} ?= string:to_integer(R),
        N * M
    else
        {error, no_integer} -> 0;
        {X, _S} when is_integer(X) -> 0
    end.
```
1 Like

Interesting approach! Using maybe like this is a clever way to handle parsing and computation together. It’s always fun to see unique strategies in action - great work! :rocket::christmas_tree:

1 Like

I wanted to challenge myself by solving this in a multi-threaded way. Not necessary by any stretch for the default input, but it can save time for some of the fan-made expanded inputs out there, like this one: Aoc2024/bigboys/day3.7z at main · Error916/Aoc2024 · GitHub

You could find safe segments to multithread by searching for instances of do() in the input so you know what the starting state is, but I took a different approach.

  1. Each worker starts with p2_enabled as indeterminate.
  2. They read from i = x * worker_no, to i + x - 1.
  3. Any muls read when enabled is indeterminate are accumulated in a special pool, to be checked when all the workers are finished.
  4. Otherwise you work as normal up to the boundary.
  5. Once all the workers finish, deal with the indeterminate results and make sure no commands are crossing the boundaries.
-module(day03).

-export([go/1, worker/1, run/2]).

go(Filename) ->
    {ok, Data} = file:read_file(Filename),
    run(Data).

run(Data) ->
    Threads = erlang:system_info(schedulers),
    run(Data, Threads).

run(Data, Threads) ->
    PartLength = trunc(byte_size(Data) / Threads) + 1,
    PartsWEnd = [ binary_part(Data, PartLength * N, PartLength) || N <- lists:seq(0, Threads-2) ],
    FinalLength = byte_size(Data) - (PartLength * (Threads - 1)),
    Parts = PartsWEnd ++ [binary_part(Data, PartLength * (Threads - 1), FinalLength)],
    Keys = [ rpc:async_call(node(), ?MODULE, worker, [D]) || D <- Parts ],
    Results = [ rpc:yield(Key) || Key <- Keys ],
    accumulate(true, Results, tl(Parts)).

worker(Data) ->
    worker(read_next(Data), tralse, 0, 0, 0, Data).

worker({A, B, Rest}, Enabled, P1, P2, Ind, _) ->
    N = A * B,
    case Enabled of
        true ->
            worker(read_next(Rest), Enabled, P1 + N, P2 + N, Ind, Rest);
        false ->
            worker(read_next(Rest), Enabled, P1 + N, P2, Ind, Rest);
        tralse ->
            worker(read_next(Rest), Enabled, P1 + N, P2, Ind + N, Rest)
    end;
worker({enable, Rest}, _, P1, P2, Ind, _) ->
    worker(read_next(Rest), true, P1, P2, Ind, Rest);
worker({disable, Rest}, _, P1, P2, Ind, _) ->
    worker(read_next(Rest), false, P1, P2, Ind, Rest);
worker(eof, Enabled, P1, P2, Ind, LastRest) ->
    {Enabled, P1, P2, Ind, LastRest}.

read_next(<<"mul(", NumbersAndRest/binary>>) ->
    case read_numbers(NumbersAndRest) of
        {A, B, Rest} ->
            {A, B, Rest};
        _ ->
            read_next(NumbersAndRest)
    end;
read_next(<<"don't()", Rest/binary>>) ->
    {disable, Rest};
read_next(<<"do()", Rest/binary>>) ->
    {enable, Rest};
read_next(<<_, Rest/binary>>) ->
    read_next(Rest);
read_next(<<>>) ->
    eof.

accumulate(Enabled, [{MaybeNextEnabled,P1,P2,Ind,Rest}|Results], Datas) ->
    % Is something on the boundary?
    {PrevRead, BoundRead, NextDatas} = case Datas of
        [] -> {nil, nil, Datas};
        [NextData|R] ->
            Peek = binary_part(NextData, 0, 100),
            {read_next(Peek), read_next(<<Rest/binary, Peek/binary>>), R}
    end,
    NextEnabled = case MaybeNextEnabled of
        tralse -> Enabled;
        _ -> MaybeNextEnabled
    end,
    {ActualP1, ActualP2, ActualInd} = if
        PrevRead == BoundRead -> {P1,P2,Ind};
        PrevRead /= BoundRead -> account_for_bound_read(NextEnabled, BoundRead, {P1,P2,Ind})
    end,
    P2WithInd = if
        Enabled -> ActualP2 + ActualInd;
    not Enabled -> ActualP2
    end,
    {NextP1, NextP2} = accumulate(NextEnabled, Results, NextDatas),
    {ActualP1 + NextP1, P2WithInd + NextP2};
accumulate(_, [], _) ->
    {0, 0}.

account_for_bound_read(Enabled, {A,B,_}, {P1, P2, Ind}) ->
    N = A * B,
    case Enabled of
        true -> {P1 + N, P2 + N, Ind};
        false -> {P1 + N, P2, Ind}
    end;
account_for_bound_read(_, {enable, _}, {P1, P2, Ind}) ->
    {P1, P2 + Ind, 0};
account_for_bound_read(_, {disable, _}, {P1, P2, _}) ->
    {P1, P2, 0}.

read_numbers(Data) ->
    maybe
        {A, D2} ?= read_number(Data, $,),
        {B, D3} ?= read_number(D2, $)),
        {A, B, D3}
    else
        _ -> invalid
    end.

-define(DIG(X), X >= $0 andalso X =< $9).
read_number(<<A, B, C, Term, Rest/binary>>, Term) when ?DIG(A) andalso ?DIG(B) andalso ?DIG(C) ->
    {(A-$0) * 100 + (B-$0) * 10 + (C-$0), Rest};
read_number(<<A, B, Term, Rest/binary>>, Term) when ?DIG(A) andalso ?DIG(B) ->
    {(A-$0) * 10 + (B-$0), Rest};
read_number(<<A, Term, Rest/binary>>, Term) when ?DIG(A) ->
    {A-$0, Rest};
read_number(_, _) ->
    invalid.

This ended up working quite well. On the input I linked above, my runtime went from about a second to only 115ms. More importantly, it was fun to think about this kind of parallellization.

As a bonus, I got to use one of my favorite jokes for the indeterminate state; true | false | tralse :slight_smile:

2 Likes

Good job! Breaking the problem into parallel tasks like this is a fantastic approach, especially with the huge performance boost you achieved. The added touch of using ‘tralse’ for the indeterminate state is brilliant - functional programming humor at its finest! :rocket::christmas_tree: