Table of contents
Die Aufgabe
Als Aufgabe (im NoName e.V.-Wiki) war gestellt, dass man ein Programm schreiben soll, welches den Wochentag (Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag) inklusive Zeilenumbruch auf stdout ausgibt. Die Eingabe ist im Format:
\d{1,2}\. (Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember) \d{4}
Also zum Beispiel „1. Januar 2008”.
Das Datum soll nur für den gregorianischen Kalender berechnet werden, der in Deutschland ab 1776 definiert ist.
Beim sogenannten Codegolf geht es nun darum, dass das Programm möglichst kurz wird. Als Sonderdisziplin betrachte ich es, das Programm möglichst kryptisch zu gestalten :-).
Ich werde auf dieser Seite folgende Lösungen präsentieren und erklären:
- zsh, erste Lösung (148 Zeichen)
- zsh, zweite Lösung (132 Zeichen)
- C, Lösung (249 Zeichen)
- C, Obfuscated (216 Zeichen)
- C, Nicht den Regeln entsprechend (155 Zeichen)
- C, Nicht den Regeln entsprechend, obfuscated (128 Zeichen)
- bash, von Jiska (225 Zeichen)
Zählweise
Bei Scripts werden die Shebangs an sich nicht mitgezählt („#!/bin/sh” zum Beispiel), die Parameter allerdings schon, beispielsweise fallen bei folgender Shebang drei Byte an:
#!/usr/bin/perl -w
Analog dazu wird bei kompilierten Sprachen der Compileraufruf nicht mitgezählt („make dateiname” für C-Programme zum Beispiel), zusätzliche Optionen allerdings schon.
zsh, erste Lösung (148 Zeichen)
#!/bin/zsh l=(locale LC_TIME) a=$($l|awk "/$2/&&NR>23&&\$0=(NR-2)%12+1" RS=\;) Y=$[$3-(a>10)] $l|awk -F\; "NR==2&&\$0=\$$[($1+Y+Y/4-Y/100+Y/400+31*a/12)%7+1]"
Das Programm funktioniert so, dass die Monatsnamen aus den locales ausgelesen werden. Dadurch funktioniert das Programm mit beliebigen Character Sets und Monats/Tages-Namen. Das war zwar nicht gefordert, ist aber ein netter Nebeneffekt. Eingabe- und Ausgabeformat richten sich also nach gesetzter LC_TIME- beziehungsweise LC_ALL-Variable.
Zeile 2: l=(locale LC_TIME)
l wird auf den Aufruf von locale gesetzt, den wir zweimal benutzen (Zeile 3 und Zeile 5). Zu beachten ist, dass es nicht funktionieren würde, wenn man einen String verwendet (er würde nach dem Programm „locale LC_TIME” suchen, statt nach locale), sondern dass es ein Array (durch die runden Klammern definiert) sein muss. Auch das Angeben der Strings in Anführungszeichen ist nicht nötig.
Zeile 3: a=$($l|awk "/$2/&&NR>23&&\$0=(NR-2)%12+1" RS=\;)
Die ersten vier Zeilen der Ausgabe von locale LC_TIME
sehen
folgendermaßen aus:
Sun;Mon;Tue;Wed;Thu;Fri;Sat Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday Jan;Feb;Mar;Apr;May;Jun;Jul;Aug;Sep;Oct;Nov;Dec January;February;March;April;May;June;July;August;September;October;November;December
Diese werden nun an awk
, eine uralte UNIX-Scriptsprache übergeben.
awk
besitzt eine Variable, den sogenannten Record Seperator (RS),
welcher, sofern er auf ein Semikolon gesetzt wird (am Ende des Aufrufs),
bewirkt, dass awk nicht nur neue Zeilen sondern auch ein Semikolon als
Trennzeichen interpretiert. Man kann sich die Ausgabe dann folgendermaßen
vorstellen (verkürzt):
Sun Mon Tue ...
awk
kann man nun mit einem Pattern aufrufen. Wenn dieses in der
Eingabe gefunden wird, wird die betreffende Zeile ausgegeben. Die kürzeste
Form, ein Pattern zu formulieren, das nach einem bestimmten Wort (dem
Monatsnamen in unserem Fall) sucht, ist, eine Regular Expression zu verwenden:
/Januar/
Weiterhin gibt es die interne Variable NR, die Anzahl an eingelesenen Records, die automatisch gesetzt wird. Die ersten 24 Records sind für uns nicht interessant, danach folgen jedoch die nicht abgekürzten Monatsnamen (gäbe es nicht den Mai, der abgekürzt ebenso lang ist, hätten wir uns die Abfrage auf NR sparen können).
Die vorhin erwähnte Ausgabe der betreffenden Zeile bei zutreffendem Pattern
funktioniert so, dass die Zeile in $0
gespeichert wird und
awk
prinzipiell print $0
ausführt. Um uns die Ausgabe
zu ersparen, definieren wir also einfach $0
um und lassen
awk
den Rest erledigen.
Diese drei Bedingungen verknüpfen wir nun mit dem boolschen Und-Operator.
In der Zuweisung von $0
wird durch (NR-2)
auch gleich
ein Teil der Zellerschen Kongruenz erledigt, nämlich die Verschiebung in Januar
und Februar, sodass effektiv folgende Indizes zugeordnet werden:
- Januar: 11
- Februar: 12
- März: 1
- April: 2
- Mai: 3
- Juni: 4
- Juli: 5
- August: 6
- September: 7
- Oktober: 8
- November: 9
- Dezember: 10
Die Modulo-Operation brauchen wir, damit nur 1-12 herauskommt, obwohl wir eigentlich 24-36 als NR haben.
Zeile 4: Y=$[$3-(a>10)]
Hier wird die Variable Y (Year) auf den dritten Parameter zugewiesen sowie eine
Korrektur für Januar und Februar vorgenommen. Das geht am schnellsten, indem
man im arithmetischen Kontext (a>10)
prüft, was ansonsten
leider nicht klappt. In den arithmetischen Kontext gelangen wir am schnellsten
durch das eigentlich veraltete $[]
anstelle von
$(())
.
Zeile 5: $l|awk -F\; "NR==2&&\$0=\$$[($1+Y+Y/4-Y/100+Y/400+31*a/12)%7+1]"
Auch hier greifen wir auf awk
und die locales zurück, diesmal
allerdings „andersherum”. Damit wir einfach auf die verschiedenen Zugreifen
können, benutzen wir statt des Record Seperators diesmal den Field Seperator
(FS), den man auch mit der Option -F
angegeben kann, was in diesem
Fall kürzer ist (da man das Semikolon escapen muss). Der Field Seperator
bewirkt, dass $1
mit dem ersten Feld gefüllt ist, $2
mit dem zweiten und so weiter...
Der awk
-Teil ist nun nahezu beendet. Via altbekannter Variable NR
suchen wir uns die zweite Zeile der Ausgabe und setzen $0
(zur
Ausgabe) auf die entsprechende Variable, also $1
oder
$2
und so weiter.
Im arithmetischen Kontext berechnen wir nun das eigentliche Datum via Zellers Kongruenz. Interessant hierbei ist, dass man keine Dollarzeichen für Variablen im arithmetischen Kontext braucht, was 5 Byte spart.
zsh, zweite Lösung (132 Zeichen)
#!/bin/zsh p=riMnlASbvzJu;a=$p[(i)${2[$[19%$#2]]}];Y=$[$3-(a>10)] locale LC_TIME|awk -F\; "NR==2&&\$0=\$$[($1+Y+Y/4-Y/100+Y/400+31*a/12)%7+1]"
Bei dieser Lösung ist die zweite Zeile identisch mit der letzten der ersten Lösung, lediglich das Umwandeln von Monatsname in den entsprechenden Index wurde verkürzt.
Zeile 1: p=riMnlASbvzJu;a=$p[(i)${2[$[19%$#2]]}];Y=$[$3-(a>10)]
Zuerst wird die Variable p zugewiesen, hierbei brauchen wir keine Anführungszeichen, das spart zwei Byte. Zu beachten ist, dass der String im Gegensatz zu der C-Lösung oder zu manchen Perl-Lösungen keine Sonderzeichen enthält.
Beim Umsetzen des Monatsnamen machen wir uns nun mehrere Effekte zu nutze. Das
(i)
ist ein Subscript Flag, welches das Offset des Ergebnisses der
(Regexp)Suche nach dem nachfolgenen Pattern zurückgibt. Bei
p=abc;a=$p[(i)b]
wäre also a = 2. Der andere Effekt ist die Suche
nach einem eindeutigen Buchstaben im Monatsname, dessen Position man möglichst
einfach bestimmen kann. Wenn man den 19. Buchstaben nimmt, also 19 %
$#2
($#2
ist die Länge des zweiten Parameters, also des
Monatsnamens) bekommt man folgende Werte/Buchstaben:
- Januar: 1 (J)
- Februar: 5 (u)
- März: 3 (r)
- April: 4 (i)
- Mai: 1 (M)
- Juni: 3 (n)
- Juli: 3 (l)
- August: 1 (A)
- September: 1 (S)
- Oktober: 5 (b)
- November: 3 (v)
- Dezember: 3 (z)
Y=...
entspricht dann Zeile 4 der ersten Lösung.
C, Lösung (249 Zeichen)
main(int a,char**b){char*H[]={"Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"}, *d=b[2],*s=" $c-VX\\`]fdZ_"; printf("%s\n",H[(31*(a=strchr(s,(*d^d[3]^d[2]&127)+9)-s)/12+atoi(b[1])+(a=atoi(b[3])-(a>10))+a/4-a/100+a/400)%7]);}
Kompilieren und testen (die Datei muss als wochentag.c gespeichert werden):
$ make wochentag $ ./wochentag 1. Januar 2008
(Die Lösung wurde natürlich ohne die Zeilenumbrüche abgegeben, diese dienen nur zur Lesbarkeit.)
C ist natürlich eine Sprache, in der man nicht sonderlich gute Chancen hat, beim Golfen zu gewinnen. Nichtsdestotrotz ist es interessant, wie nahe man an andere Lösungen kommt, weil man doch einige Möglichkeiten ausnutzen kann.
Worum man leider nicht kommt, ist eine Definition der main-Funktion inklusive Parameter (auf die wir ja zugreifen wollen). Ich hätte erwartet, dass man die Funktion nicht unbedingt main nennen muss (was aber leider nicht so ist), da der Compiler ja weiß, dass wir ein Executable bauen und keine Library und es ansonsten keine Funktionen gibt.
Anschließend folgt die Definition eines Arrays mit Strings für jeden Wochentag,
die später ausgegeben werden. Weglassen kann man hierbei die Anzahl der
Einträge, der Compiler kann sie zur Compilezeit ermitteln, das spart ein Byte.
Außerdem definieren wir *d
als Abkürzung für den Zugriff auf
b[2]
(den zweiten Parameter, also den Monatsnamen). Da wir
b[2]
später drei mal benutzen, lohnt sich das.
Nun folgt ein Teil der Magie, nämlich der String s
, der als Lookup
Table verwendet wird. Etwas unschön ist, dass er einen Backslash enthält, der
natürlich escaped werden muss. Das hätte man durch eine noch weitere
Verschiebung bei strchr
zwar ändern können, aber dann hätte man ja
an anderer Stelle wiederum ein Byte mehr. Der besseren Lesbarkeit zuliebe ist
hier die Definition von a
(eigentlich speichert argc die Anzahl
der Argumente, aber diese brauchen wir nicht und können somit die Definition
eines integers sparen):
a = strchr(s,(*d ^ d[3] ^ d[2]&127)+9)-s
Was hier geschieht ist das Erzeugen eines eindeutigen Kennzeichners des
Monatsnamen, indem der erste, dritte und ein Teil des vierten Buchstabens via
XOR vermischt werden und dieses Ergebnis anschließend um 9 Zeichen verschoben
wird (damit s
möglichst schön aussieht). Wir suchen nun mit
strchr
die Position des erzeugten Zeichens und ziehen davon die
Speicheradresse von s ab. Das ist nötig, da es in C leider keine Funktion gibt,
die das Offset eines Zeichens in einem String zurückgibt, sondern nur einen
Zeiger auf die Position.
Anschließend folgt die Ausgabe mit derselben Berechnung wie bei den zsh-Lösungen. Was die C-Lösung so lang macht, sind die expliziten Funktionsaufrufe wie atoi, strchr, printf und die Deklarationen wie char und main.
C, Obfuscated (216 Zeichen)
N(I a,C**b){C*H[]={"Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"},*s=" $c-VX\\`]fdZ_", *d=b[2];X(F,H[(31*(a=Y(s,(*d^d[3]^d[2]&127)+9)-s)/12+E(b[1])+(a=E(b[3])-(a>10))+a/4-a/100+a/400)%7]);}
Dem eben genannten Problem, was den C-Code so lange macht, habe ich mich dann
in der Form angenommen, dass ich main, int, char, strchr, atoi und printf in
den Compileraufruf via define
ausgelagert habe. Dass diese Lösung
dadurch nicht weniger Zeichen hat insgesamt ist mir natürlich klar (da die
Compileroptionen ja gezählt werden), aber es ist dennoch „schön” zu sehen, wie
der Code dann aussieht ;-).
C, Nicht den Regeln entsprechend (155 Zeichen)
main(int a,char**b){char*s=" $c-VX\\`]fdZ_",*d=b[2]; return(31*(a=strchr(s,(*d^d[3]^d[2]&127)+9)-s)/12+atoi(b[1])+(a=atoi(b[3])-(a>10))+a/4-a/100+a/400)%7;}
Bei dieser Lösung war das Ziel, eine Programm zu schreiben, welches zwar nicht den Regeln des Wettbewerbs entspricht, aber dennoch funktionsfähig ist. Dieses Programm gibt den errechneten Tag nicht aus, sondern übergibt ihn via Returncode. Das spart die Definition der Tagesnahmen.
C, Nicht den Regeln entsprechend, obfuscated (128 Zeichen)
N(I a,C**b){C*s=" $c-VX\\`]fdZ_",*d=b[2];R(31*(a=Y(s,(*d^d[3]^d[2]&127)+9)-s)/12+E(b[1])+(a=E(b[3])-(a>10))+a/4-a/100+a/400)%7;}
Wenn man jetzt die eben genannte Variante nochmal obfuscated, sieht das ganze so aus. Vom Quellcode her ist das die kürzeste Variante und sicherlich die unverständlichste, wenn man damit angefangen hätte ;-).
bash, von Jiska (225 Zeichen)
#!/bin/bash t=(J F z A Ma ni li g S O N D) w=(SonnX MonX DiensX Mittwoch DonnersX FreiX SamsX) for((i=0;i<12;i++));do echo $2|grep -q ${t[i]}&&m=$[i+2];done y=$3 echo ${w[$[m<4&&(m+=12,y--),(${1/.}+13*m/5+y+y/4-y/100+y/400+6)%7]]/X/tag}
Was mir an dieser Lösung gut gefällt, ist die Art und Weise, wie die Monate
erkannt werden. Jiska benutzt hierbei Regular Expressions, die jeden
Monatsnamen eindeutig identifizeren und ziemlich kurz sind. Der Trick dabei
ist, dass Regular Expressions natürlich an jeder Stelle zutreffen. Dadurch
fällt jegliche Logik weg um einen eindeutigen Teil zu erzeugen, leider ist der
Code aufgrund der vergleichsweise beschränkten Möglichkeiten der
bash
trotzdem länger.
Ansonsten interpretiert die bash
im Gegensatz zur zsh
den ersten Parameter mit abschließendem Punkt nicht als integer, sodass man den
Punkt vorher via ${1/.}
entfernt. Ebenso wird die Ausgabe der Tage
verkürzt, indem „tag” durch X abgekürzt wird und später wieder ersetzt wird.
Leider gelang es uns nicht, die Regular Expressions auf einen Buchstaben abzukürzen (ohne auf den selben Ansatz wie oben zurückzugreifen), problematisch ist zum Beispiel der Januar, der schwer vom Juni zu unterscheiden ist (weil das i als einzig unterschiedlicher Buchstabe schon im Mai vergeben ist). Auch durch Tricks wie das Entfernen von „uar” und „ber” kam ich nicht weiter. Wer hier eine schöne Lösung findet, möge sie mir bitte zusenden :-).
I run a blog since 2005, spreading knowledge and experience for almost 20 years! :)
If you want to support my work, you can buy me a coffee.
Thank you for your support! ❤️