11 Erlang

Part I Assignment

I’ve been planning to try out Erlang for some time.

It’s straightforward to install on Ubuntu, just use apt install.

In the script, we need to compile the module aoc11 stored in file aoc11.erl and then run its main function:

$ erl
erl>c(aoc11).
aoc11:main().

Part I Assignment

-module(aoc11).
-export([main/0]).
-import(lists,[reverse/1,sort/1,sublist/3,sublist/2,nth/2,nthtail/2,map/2]).
-import(maps,[get/2]).
-import(string,[slice/3,slice/2]).

% reads lines from Fn and returns a list of strings
readlines(Fn) ->
    % we should check if this fails, but ...
    {ok, Data} = file:read_file(Fn),
    % last expressions is returned
    % here the binary strings are mapped to strings
    map(fun(X) -> binary:bin_to_list(X) end, string:lexemes(Data, "\n")).


% converting a list of string tokens (lexemes) into integers
% this pattern matches the end of the recursion and reverses the result
lexemes_to_integers([], L) -> reverse(L);
lexemes_to_integers([H|T], []) ->
    case H of
        % just ignore the first token
        "Starting" -> lexemes_to_integers(T, []);
        % just ignore the second token
        "items:" -> lexemes_to_integers(T, []);
        _ ->
            % to_integer might return an error, ignore it
            {F, _} = string:to_integer(H),
            % return the integer and concatenate with the recursive result
            [F] ++ map(fun(X) -> {Int, _} = string:to_integer(X), Int end, T)
    end.


% parse lines into maps
% this pattern matches the end of the recursion, returns reversed lists
parse_lines(_, [], A, B) -> {reverse(A), reverse(B)};
parse_lines(N, [H|T], [M|MT], [X|XT]) ->
    % check the first letter of the line
    H1 = slice(H, 0, 1),
    case H1 of
        % Monkey header
        "M" ->
            if
                % we are at the first line, do nothing
                N == 0 ->
                    parse_lines(N+1, T, [M] ++ MT, [X] ++ XT);
                % we are at a header further in the file =>
                % prepend the initialized monkey map and 0 to the two lists
                true ->
                    parse_lines(N+1, T, [#{n=>length(MT)+1}] ++ [M] ++ MT, [0] ++ [X] ++ XT)
            end;
        " " ->
            % non header line
            H3 = slice(H, 2, 1),
            case H3 of
                % parse the list of items at this line
                "S" ->
                    % tokenize the line
                    Toks = string:lexemes(H, ", "),
                    % convert the tokens into a list of integers
                    Ints = lexemes_to_integers(Toks, []),
                    % update the monkey
                    M2 = M#{s => Ints},
                    % recursively call the parsing on the rest of the lines
                    parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                % parse the operation
                "O" ->
                    % set the monkey's operator
                    M2 = M#{o => slice(H, 23, 1)},
                    H26 = slice(H, 25, 1),
                    % check the second operand
                    case H26 of
                        % the second operand is the original number => store "old"
                        "o" ->
                            M3 = M2#{a => "old"};
                        % _ branch to catch the rest
                        % parse the second operand
                        _ ->
                            {Y, _} = string:to_integer(slice(H, 25)),
                            M3 = M2#{a => Y}
                    end,
                    % update the monkey and continue parsing
                    parse_lines(N+1, T, [M3] ++ MT, [X] ++ XT);
                % parse division argument
                "T" ->
                    {Y, _} = string:to_integer(slice(H, 21)),
                    M2 = M#{d => Y},
                    % update the monkey and continue parsing
                    parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                % parse the decision for the receiving monkey
                " " ->
                    H5 = slice(H, 4, 1),
                    case H5 of
                        % this line contains the decision rule
                        "I" ->
                            H7 = slice(H, 7, 1),
                            case H7 of
                                % in the case the number is divisible, select monkey
                                "t" ->
                                    {Y, _} = string:to_integer(slice(H, 29, 1)),
                                    M2 = M#{t => Y + 1};
                                % in the non-divisible case
                                _ ->
                                    {Y, _} = string:to_integer(slice(H, 30, 1)),
                                    M2 = M#{f => Y + 1}
                            end,
                            % update monkey and continue
                            parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                        % this should not happen!
                        _ -> nil
                    end
            end
    end.


% find out what value is thrown at what monkey
monkey_turn(M) ->
    % current items of monkey M
    Mitems = get(s, M),
    if
        % nothing to throw => end the recursion
        length(Mitems) == 0 -> {0, nil};
        % select what to throw
        true ->
            % first item to throw
            X = nth(1, Mitems),
            % do the operation as prescribed for monkey M
            Opres = operation(X, get(o, M), get(a, M)),
            % divide by 3 without the reminder
            New = Opres div 3,
            % check the divisibility
            R = New rem get(d, M),
            % choose the monkey to which to throw the value
            case R of
                0 -> {get(t, M), New};
                _ -> {get(f, M), New}
            end
    end.


% process items of one monkey
monkey_loop(N, L1, L2) ->
    % the current monkey to process
    M = nth(N, L1),
    % get the thing to throw at <Next> monkey
    {Next, Thing} = monkey_turn(M),
    if
        % nothing to throw, finish processing this monkey
        Next == 0 -> {L1, L2};
        true ->
            % remove the item from the current monkey
            Mless = M#{s => nthtail(1, get(s, M))},
            % get the monkey which will receive the thing
            NextM = nth(Next, L1),
            % append the thing to the list of its things
            NextM2 = NextM#{s => get(s, NextM) ++ [Thing]},
            % immutable lists need to be updated...
            Newl0 = sublist(L1, N-1) ++ [Mless] ++ nthtail(N, L1),
            Newl1 = sublist(Newl0, Next-1) ++ [NextM2] ++ nthtail(Next, Newl0),
            Newl2 = sublist(L2, N-1) ++ [nth(N, L2)+1] ++ nthtail(N, L2),
            % continue processing things of monkey at position N
            monkey_loop(N, Newl1, Newl2)
    end.


% go through the monkeys
% at the end of the list => finish recursion
monkeys_loop(0, L1, L2) -> {L1, L2};
monkeys_loop(I, L1, L2) ->
    % update the lists and process the Nth monkey
    {L1b, L2b} = monkey_loop(length(L1) - I + 1, L1, L2),
    % continue recursively
    monkeys_loop(I-1, L1b, L2b).


% do this 20 times
main_loop(0, M, T) -> {M, T};
main_loop(N, M, T) ->
    {M2, T2} = monkeys_loop(length(M), M, T),
    main_loop(N-1, M2, T2).


% based on the operands and operator, compute the value
operation(A, "*", "old") -> A * A;
operation(A, "+", "old") -> A + A;
operation(A, "*", N) -> A * N;
operation(A, "+", N) -> A + N.


main() ->
    % get the lines from the file
    Lines = readlines("11.input"),
    % parse the lines and create Monkey = list of monkeys (maps) and
    % TX = the counts of their activity
    {Monkeys, TX} = parse_lines(0, Lines, [#{n=>1}], [1]),
    % main loop, update the activity
    {_, Times} = main_loop(20, Monkeys, TX),
    % sort and reverse to find out the two most active monkey at the beginning
    T2 = reverse(sort(Times)),
    % multiply the activities to get the final result
    Result = nth(1, T2) * nth(2, T2),
    % print out the result
    io:format("Result: ~p\n", [Result]).

Part II

Since the main is now parametrized, we need to call:

$ erl
> c(aoc11).
> aoc11:main(10000, "11.input").

The code is almost the same. The principle is to limit the size of the huge numbers, it is done by modulo by the least common multiply. I shamelessly took the numbers from the input file and got the LCM from Python3:

from math import lcm
from functools import reduce
print(reduce(lcm, [<numbers>]))

The code itself is here:

-module(aoc11).
-export([main/2]).
-import(lists,[reverse/1,sort/1,sublist/3,sublist/2,nth/2,nthtail/2,map/2]).
-import(maps,[get/2]).
-import(string,[slice/3,slice/2]).

readlines(Fn) ->
    {ok, Data} = file:read_file(Fn),
    map(fun(X) -> binary:bin_to_list(X) end, string:lexemes(Data, "\n")).


lexemes_to_integers([], L) -> reverse(L);
lexemes_to_integers([H|T], []) ->
    case H of
        "Starting" -> lexemes_to_integers(T, []);
        "items:" -> lexemes_to_integers(T, []);
        _ ->
            {F, _} = string:to_integer(H),
            [F] ++ map(fun(X) -> {Int, _} = string:to_integer(X), Int end, T)
    end.


parse_lines(_, [], A, B) -> {reverse(A), reverse(B)};
parse_lines(N, [H|T], [M|MT], [X|XT]) ->
    H1 = slice(H, 0, 1),
    case H1 of
        "M" ->
            if
                N == 0 ->
                    parse_lines(N+1, T, [M] ++ MT, [X] ++ XT);
                true ->
                    parse_lines(
                        N+1,
                        T,
                        [#{n=>length(MT)+1}] ++ [M] ++ MT,
                        [0] ++ [X] ++ XT
                    )
            end;
        " " ->
            H3 = slice(H, 2, 1),
            case H3 of
                "S" ->
                    Toks = string:lexemes(H, ", "),
                    Ints = lexemes_to_integers(Toks, []),
                    M2 = M#{s => Ints},
                    parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                "O" ->
                    XX = slice(H, 23, 1),
                    if
                        XX == "*" -> M2 = M#{o => 1};
                        true -> M2 = M#{o => 0}
                    end,
                    H26 = slice(H, 25, 1),
                    case H26 of
                        "o" ->
                            M3 = M2#{a => 0};
                        _ ->
                            {Y, _} = string:to_integer(slice(H, 25)),
                            M3 = M2#{a => Y}
                    end,
                    parse_lines(N+1, T, [M3] ++ MT, [X] ++ XT);
                "T" ->
                    {Y, _} = string:to_integer(slice(H, 21)),
                    M2 = M#{d => Y},
                    parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                " " ->
                    H5 = slice(H, 4, 1),
                    case H5 of
                        "I" ->
                            H7 = slice(H, 7, 1),
                            case H7 of
                                "t" ->
                                    {Y, _} = string:to_integer(slice(H, 29, 1)),
                                    M2 = M#{t => Y + 1};
                                _ ->
                                    {Y, _} = string:to_integer(slice(H, 30, 1)),
                                    M2 = M#{f => Y + 1}
                            end,
                            parse_lines(N+1, T, [M2] ++ MT, [X] ++ XT);
                        _ -> nil
                    end
            end
    end.


monkey_turn(M) ->
    Mitems = get(s, M),
    if
        length(Mitems) == 0 -> {0, nil};
        true ->
            New = operation(nth(1, Mitems), get(o, M), get(a, M)),
            R = New rem get(d, M),
            case R of
                % normalize with the least common multiple of all items
                0 -> {get(t, M), New rem 1582340793412478299200};
                _ -> {get(f, M), New rem 1582340793412478299200}
            end
    end.


monkey_loop(N, L1, L2) ->
    M = nth(N, L1),
    {Next, Thing} = monkey_turn(M),
    if
        Next == 0 -> {L1, L2};
        true ->
            Mless = M#{s => nthtail(1, get(s, M))},
            NextM = nth(Next, L1),
            NextM2 = NextM#{s => reverse([Thing] ++ reverse(get(s, NextM)))},
            Newl0 = sublist(L1, N-1) ++ [Mless] ++ nthtail(N, L1),
            Newl1 = sublist(Newl0, Next-1) ++ [NextM2] ++ nthtail(Next, Newl0),
            Newl2 = sublist(L2, N-1) ++ [nth(N, L2)+1] ++ nthtail(N, L2),
            monkey_loop(N, Newl1, Newl2)
    end.


monkeys_loop(0, L1, L2) -> {L1, L2};
monkeys_loop(I, L1, L2) ->
    {L1b, L2b} = monkey_loop(length(L1) - I + 1, L1, L2),
    monkeys_loop(I-1, L1b, L2b).


main_loop(0, M, T) -> {M, T};
main_loop(N, M, T) ->
    {M2, T2} = monkeys_loop(length(M), M, T),
    main_loop(N-1, M2, T2).


% slightly improved operation matching
operation(A, 1, 0) -> A * A;
operation(A, 0, 0) -> A + A;
operation(A, 1, N) -> A * N;
operation(A, 0, N) -> A + N.


% main is now parametrized: number of loops, filename
main(Loops, Fn) ->
    Lines = readlines(Fn),
    {Monkeys, TX} = parse_lines(0, Lines, [#{n=>1}], [1]),
    {_, Times} = main_loop(Loops, Monkeys, TX),
    T2 = reverse(sort(Times)),
    Result = nth(1, T2) * nth(2, T2),
    io:format("Result: ~p\n", [Result]).

Some good resources I used

What I learned

published: 2022-12-11
last modified: 2023-01-21

https://vit.baisa.cz/notes/code/advent-of-code-2022/11/