hikaru hat geschrieben: 08.10.2021 22:48:46
Nachdem ich von JTHs Lösungen und dem Traps-Exkurs bereits angeschlagen war, habe ich gerade fünf Minuten wie hypnotisiert auf diesen Beitrag gestarrt (beide Code-Schnipsel).
Ich finde, wir brauchen einen neuen BB-Tag für Gesundheitswarnungen.
Ich nehme das mal zum Anlass, meinen Code schrittweise zu zerlegen und zu erklaeren.
Code: Alles auswählen
</usr/share/dict/words awk 'length==8&&++a[i=substr($0,3,4)]>a[w]{w=i}END{print w}'
Beginnen wir mit der Formatierung:
Code: Alles auswählen
</usr/share/dict/words awk '
length==8 && ++a[i=substr($0,3,4)]>a[w] {
w=i
}
END{
print w
}
'
Wir haben also zwei Bloecke: Der erste hat als Bedingung zwei Vergleiche, der zweite ist ein END-Block. Zuerst der einfache Teil: Der END-Block wird ausgefuehrt, wenn der Input komplett gelesen worden ist, also bevor awk sich beendet. Darin wird die Variable `w' ausgegeben.
Nun zum koplizierteren Teil:
Awk funktioniert so, dass ein Script aus Bedingungs-Aktions-Paaren besteht. Die Aktion steht in geschweiften Klammern (hier `{w=i}'), die Bedingung ist das davor (hier zwei verundete Vergleiche). Die Bedingung sagt, fuer welche Zeilen die Aktion ausgefuehrt wird. Gibt man keine Bedingung an, dann wird die Aktion fuer alle Zeilen ausgefuehrt.
Statt die Bedingung vor den Block zu schreiben, kann man sie auch mit einem if nutzen. Die folgenden vier Formen sind alle aequivalent:
Code: Alles auswählen
{
if (length==8 && ++a[i=substr($0,3,4)]>a[w]) {
w=i
}
}
Code: Alles auswählen
{
if (length==8) {
if (++a[i=substr($0,3,4)]>a[w]) {
w=i
}
}
}
Letztere Form hatte Phineas gewaehlt. Ich habe das dann zur ersten Form verkuerzt, um mir das if zu sparen.
`length' ohne etwas ist identisch zu `length($0)', was eine Funktion ist, die die Laenge der Inputzeile ausgibt. In awk kann man bei Funktionen manchmal die Klammern weglassen, so auch z.B. bei `print', das ohne irgendwas gleich wie `print($0)' ist, aber man kann es sowohl in der Form `print var` als auch `print(var)' nutzen. Das ist ein bisschen eine awk-Eigenheit, die nur darum akzeptabel ist, weil die Sprache so klein ist und die Anzahl der Funktionen so uebersichtlich (also verhaeltnismaessig zu anderen Sprachen).
Damit trifft die erste Bedingung nur auf Zeilen der Laenge 8 zu.
Nun weiter zum eigentlich kryptischen Teil -- die zweite Bedingung:
Hierzu muss man sich ein bisschen mit C-aehnlichen Sprachen auskennen, was Praezedenzen und Seiteneffekte angeht. Wenn man so einen Ausdruck zerlegen will, dann muss man Parser spielen und anhand der Praezedenzen gruppieren. Dann beginnt man bei der niedrigsten Praezedenz zu zerlegen, wie man das auch mit einem mathematischen Ausdruck ohne Klammern machen wuerde, bloss dass es dort nicht so viele Praezedenzstufen gibt.
Hier die Praezedenzen von C (die auch hier gelten) aus der Manpage operator(7):
Code: Alles auswählen
Operator Associativity
() [] -> . left to right
! ~ ++ -- + - (type) * & sizeof right to left
* / % left to right
+ - left to right
<< >> left to right
< <= > >= left to right
== != left to right
& left to right
^ left to right
| left to right
&& left to right
|| left to right
?: right to left
= += -= *= /= %= <<= >>= &= ^= |= right to left
, left to right
Das `&&' habe ich oben stillschweigend schon zerteilt.
Nun geht es weiter mit dem `>':
Das Linke muss also groesser als das Rechte sein. Das Rechte ist ein Arrayzugriff des Arrays `a' an der Stelle `w'. Arrays in awk sind assoziativ (also Hashes). `w' ist, wie man spaeter versteht, ein Mittelteil, also ein String mit vier Buchstaben. Der Wert von `a[w]' ist eine Zahl, wie auch spaeter klar wird.
Nehmen wir uns die linke Seite vor:
Die eckige Klammer ist eine Klammer (hoechste Praezedenz), also muessen wir uns das darn erstmal noch nicht anschauen. Das `++' (Prae-Inkrement) hat eine niedrigere Praezedenz als der Arrayzugriff (`[...]') damit wirkt das `++' auf alles was rechts davon steht. Und da steht ein Arrayzugriff vom Array `a' an der Stelle:
Das ist eine Zuweisung. In C-aehnlichen Sprachen sind Zuweisungen zugleich Ausdruecke, d.h. sie geben den zugewiesenen Wert auch zurueck. Die folgenden zwei Zeilen sind aequivalent:
Ob ich also erst zuweise und dann die Variable nutze, oder ob ich die Zuweisung auch zur Nutzung verwende, ist in C-aehnlichen Sprachen egal.
Was das `substr()' macht sollte recht klar sein: Es extrahiert den Mittelteil, also von der Inputzeile (`$0' ... die aus genau einem 8-buchstabigen Wort besteht), nimmt es den Teil ab Buchstaben 3 und zwar 4 Buchstaben lang.
Dieser Mittelteil wird dann der Variable `i' zugewiesen und dann wird ...
-- jetzt, wo wir am innersten Punkt angekommen sind, bauen wir das ganze wieder schrittweise nach aussen zusammen --
(Anm: Ich habe `a[i_]' mit Unterstrich schreiben muessen, damit der BB-Code darauf nicht anspricht. Den Unterstrich muesst ihr euch wegdenken. Es ging technisch leider nicht anders.)
... der Wert von `a[i_]' um 1 inkrementiert mit dem (`++'). (Falls es einen Arraywert in awk nicht gibt, dann wird er mit dem Wert 0 automatisch angelegt.)
D.h. wir speichern in `a' ab, wie oft wir einen Mittelteil schon gefunden haben. Beim ersten Mal wird `a[i_]' von 0 auf 1 erhoeht, beim zweiten Mal dann von 1 auf 2, usw. Das macht dieser Code:
Der Unterschied zu
wirkt sich nur auf die weiteren Aktionen aus. Wenn das `++' vorne steht, dann wird das Inkrement naemlich sofort ausgefuehrt, bevor wir nun weiter gehen. Wenn es hinten steht, dann wird erst der ganze Ausdruck ausgewertet und *danach* erst wird das Inkrement ausgefuehrt. Wir moechten hier, dass es sofort passiert. Hier nochmal jeweils drei aequivalente Zeilen:
Code: Alles auswählen
# es wird der neue Wert ausgegeben
++a[i]; print a[i]
a[i]++; print a[i]
print ++a[i]
# es wird der alte Wert ausgegeben
print a[i]; ++a[i]
print a[i]; a[i]++
print a[i]++
Also wird bei uns `a[i_]' erhoeht und dann mit `a[w]' verglichen. `i' ist der aktuelle Mittelteil, den wir gerade mit `substr()' ermittelt haben. `w' ist der bislaeng haeufigste Mittelteil, den wir uns merken (dazu kommen wir gleich).
Wir vergleichen nun also, ob der aktuelle Mittelteil haeufiger ist als der bislang haeufigste:
Und wenn dem so ist, dann merken wir uns den aktuellen als den haeufigsten:
So sind wir nun einmal durch.
Phineas hatte etwa mehr hintereinander was ich dann zu mehr verschachteltem Code umgeformt habe.
Code: Alles auswählen
</usr/share/dict/words awk '
length==8 && ++a[i=substr($0,3,4)]>a[w] {
w=i
}
END{
print w
}
'
Nochmal natuerlichlichsprachlich: Bei allen Zeilen mit 8 Zeichen, speichern wir den Mittelteil in `i', inkrementieren wie oft er bisher vorgekommen ist `a[i_]', und wenn dies oefter war als der bisher haeufigste Mittelteil `a[w]', dann merken wir uns den aktuellen (`i') als den haeufigsten Mittelteil (`w'). Am Ende des Scripts geben wir den haeufigsten Mittelteil (`w') aus.
(Es waere fuer das Verstaendnis wahrscheinlich besser gewesen, wenn der aktuelle Mittelteil `m' geheissen haette und nicht `i', weil `i' per Konvention normalerweise nur fuer Zaehlvariablen verwendet wird und meist nur fuer Zahlen. `w' steht hier moeglicherweise fuer ``Winner''. `a' steht fuer ``Array''. -- Aussagekraeftige Bezeichner helfen beim Codeverstaendnis. Bei einbuchstabigen Bezeichnern wird das natuerlich umsoschwieriger.)
Vielleicht hat diese (lange) Erklaerung etwas Licht ins Dunkel gebracht.