Dzisiaj nadal obyło się bez większych problemów, choć zacząłem czuć że używanie AWKa zaczyna mi bardziej przeszkadzać niż pomagać.

Zadanie

Dzisiaj musimy zliczyć wystąpienia słowa XMAS w dwuwymiarowej tablicy liter. Słowa mogą występować w liniach pionowych, poziomych i ukośnych, w poniższym przykładzie słwo występuje 3 razy. (kropki są dodane dla ułatwienia ale w ostatecznie w inpucie będą tam inne losowe litery).

XMAS
..AA
.M.M
X..X

Rozwiązanie

Jeśli wypiszemy sobie wszystkie wiersze, kolumny i linie ukośne, mógłbym za pomocą grepa znaleźć wystąpienia słowa XMAS i jego odwrotności SAMX.

Wypisanie lini

Do tego celu użyję języka AWK. O ile wypisanie liń poziomych jest banalnie proste (w każdej lini wypisujemy $0), to będzie to trudniejsze dla kolumn i przekątnych. Będziemy mieć dwuwymiarowe listy trzymające dla każdej lini litery w niej występujące.

W awku możemy wygodnie iterować po znakach w linijce jeśli ustawimy sobie separator fieldów na pustego stringa (""), w ten sposób poszczególne fieldy ($1, $2, ..., $NF), będą kolejnymi znakami, można to ustawić w command linie jako argument -F, ale ja ustawię go w naszym programie w kodzie od BEGIN żeby użytkownik nie musiał o tym pamiętać:

BEGIN {FS = ""}

Nasz dalszy kod będzie wyglądał w ten sposób:

{
    for (i = 1; i <= NF; i++) {
        cols[i][NR] = $i    # dodaj aktualny znak do kolumny
        diags[i - NR][NR] = $i   # dodaj znak do przekątnej (prawo-dół)
        diags2[NF - i - NR + 1][NR] = $i   # dodaj znak do przekątnej (lewo-dół)
    }
    print $0               # wypisz linię poziomą
}
END {
    print_2d_arr(cols);    # wypisz linie pionowe
    print_2d_arr(diags);   # wypisz przekątne (prawo-dół)
    print_2d_arr(diags2);  # wypisz przekątne (lewo-dół)
}

Funkcja print_2d została zdefiniowana wcześniej jako funkcja pomocnicza:

function print_arr(arr) { 
    for (i in arr) { printf arr[i] } 
    printf "\n"
}
function print_2d_arr(arr) { for (i in arr) print_arr(arr[i]) }

Po uruchomieniu kodu otrzmujemy taki output:

$ awk -f part1.awk input.txt > lines
$ cat lines
XMAS
..AA
# ... celowo obcięte
SAMX
A..
M.
X

Zauważmy że nie wszystkie przekątne mają tyle znaków co linie poziome czy pionowe.

Zliczanie wystąpień XMAS

Kolejny raz przyda nam się grep -o "pattern" [plik], szukałem jak złapać wystąpienia słowa XMAS do przodu i do tyłu, ale nie udało mi się to. Niestety poniższy kod nie działa:

grep -o "XMAS\|SAMX" lines

Z tego powodu że nie łapie on obu słów jeśli nachodzą na siebie XMASAMX, wypisze tylko raz.

Żeby to naprawić użyję po prostu komendy dwa razy. Połączę wyniki obu komend narzędziem cat (które służy do konkatenacji dwóch plików), i policzę liczbę lini poleceniem wc -l

cat <(grep -o "XMAS" lines) <(grep -o "SAMX" lines) | wc -l

Wynik jest poprawny!

Cały kod

# part1.awk
function print_arr(arr) { 
    for (i in arr) { printf arr[i] } 
    printf "\n"
}
function print_2d_arr(arr) { for (i in arr) print_arr(arr[i]) }
BEGIN {FS = ""}
{
    for (i = 1; i <= NF; i++) {
        cols[i][NR] = $i
        diags[i - NR][NR] = $i
        diags2[NF - i - NR + 1][NR] = $i
    }
    print $0
}
END {
    print_2d_arr(cols);
    print_2d_arr(diags);
    print_2d_arr(diags2);
}

Uruchamiamy całość komendą:

awk -f part1.awk input.txt > lines; cat <(grep -o "XMAS" lines) <(grep -o "SAMX" lines) | wc -l

Część 2

Żeby zdobyć drugą gwiazdę musimy się jeszcze trochę namęczyć, okazuje się że nie mamy szukać wystąpień słowa XMAS a X-MAS, a zapisane jest ono w taki sposób że dwa słowa MAS się ze sobą przecinają w taki sposób że tworzą dużego X.

M.S
.A.
M.S

Rozwiązanie

Podejdziemy do rozwiązania podobnie, tym razem jednak zamiast wypisywać linie wypiszemy wszystkie możliwe kwadraty 3x3 z pliku (każdy we własnej lini). I użyjemy grepa do wyfiltrowania tylko tych kwadratów które spełniają jedno z czterech możliwych wzorców. każde MAS ma dwie możliwości kierunku co daje w sumie cztery.

Kod wypisujący kwadraty

Nie jestem szczególnie dumny z tego kodu i nie będę tłumaczył co robi każda linijka, powiem tylko że zarówno w tej jak i poprzedniej części bardzo przydatna okazała się asocjacyjna natura macierzy w AWK, nie ma żadnych problemów z przypisaniem liczby do negatywnego indeksu w macierzy, więc nie muszę się tu przejmować edge case’ami. W końcowej pętli po prostu ignoruję kwadraty które wychodzą poza nasz obszar (negatywne indeksy, albo zaczynające się dwa lub jeden od końca macierzy).

# part2.awk
BEGIN {FS = ""}
{
    offset = NF * (NR - 1)
    for (i = 1; i <= NF; i++) {
        si = offset + i - 1
        for (x = 0; x < 3; x++) {
            for (y = 0; y < 3; y++) {
                pos = si - (x + NF * y)
                squares[pos][x + 3 * y] = $i
            }
        }
    }
} END {
    for (i = 0; i < NR - 2; i++) {
        for (j = 0; j < NF - 2; j++) {
            pos = i * NF + j
            for (v in squares[pos]) {
                printf squares[pos][v]
            }
            printf "\n"
        }
    }
}

Output dla wcześniejszego przykładu:

$ awk -f part2.awk input.txt
XMA..A.M.
MAS.AAM.M
..A.M.X..
.AAM.M..X

Jak widać zwracam nasze kwadraty w spłaszczonej formie, gdzie pierwsze trzy znaki odpowiadają za pierwszą linię, drugie trzy znaki za drugą linię i tak dalej.

Liczenie wysąpień X-MAS

Stworzyłem 4 wzorce które obejmują wszystkie możliwości wystąpienia X-MAS, zamiast kropki w regexie może wejść jaki kolwiek inny znak

Końcowy kod:

awk -f part2.awk input.txt | grep -e "M.S.A.M.S" -e "M.M.A.S.S" -e "S.M.A.S.M" -e "S.S.A.M.M" | wc -l

Podsumowanie

Cieszę się że kolejny raz udało się zadanie rozwiązać w AWKu, jednak powoli wchodzimy w terytorium gdzie mam wrażenie że będziemy potrzebować bardziej wydajnych metod, wypisywanie wszystkich kwadratów 3x3 brzmi bardzo redundantnie, no ale póki co wydajnościowo i tak nieźle sobie radzimy. Porównałem później parę rozwiązań z reddita r/adventofcode i nadal biję czasowo większość rozwiązań w pythonie.