Zahlenakrobatische Unterhaltungskünstler wissen zu jedem beliebigen vom verblüfften Publikum gerufenen Datum den Wochentag zu nennen. Mike Schilli macht mit einem spielerischen Trainingsprogramm in Go jeden Hobby-Mathematiker zur Rampensau.
So genannte Mathe-Genies können das: Jemand aus dem Publikum ruft “12. Dezember 2019”, und der Zahlenkünstler verkündet nach wenigen Sekunden: “Donnerstag!” Wie geht das? Verfügt der Zauberer über ein fotografisches Gedächtnis und hat alle Kalender im Kopf? Die Lösung ist überraschend einfach, der Magier muss nur einige einfach zu merkende Regeln durchgehen – schon steht der Wochentag zum Datum fest.
Ein ähnliches Kopfrechenverfahren, aber mit mehr und komplizierteren Rechenschritten, habe ich vor Jahren schon mal vorgestellt [2]. Anschließend merkte ein Leser an, dass die Methode unnötig komplex sei, und verwies auf das Doomsday-Verfahren [3], um das es heute geht.
Jüngster Tag
Nach der Doomsday-Methode fallen folgende Tage eines Jahres immer auf den gleichen Wochentag: 9.5., 5.9., 7.11. und 11.7., das ist einfach zu merken nach der Formel “9-5 at 7/11”, also den Werkstunden eines typischen Arbeitstags für einen Amerikaner (9-5, also 9 bis 17 Uhr) bei “7-Eleven”, einer in Amerika ansässigen Kleinsupermarktkette.
Fast alle restlichen Doomsday-Tage fallen auf Tag-Monat-Dubletten, nämlich 4.4., 6.6., 8.8., 10.10. und 12.12. Nur Januar, Februar und März bilden Ausnahmen, in Nicht-Schaltjahren sind es der 3. Januar, der 7. Februar und der 7. März (Abbildung 1). In Schaltjahren wechselt der Doomsday auf 4. Januar und 8. Februar. Der 7. März bleibt gleich.

Abbildung 1: Wer diese einfach zu merkende Tabelle im Kopf hat, kann zu jedem beliebigen Datum den Wochentag ausrechnen.
Nun steht der Doomsday für 2019 nach einem gesonderten Verfahren als Donnerstag fest (2018 war Mittwoch, 2020 wird Samstag sein, siehe Abbildung 2). Fragt also jemand nach dem 4.4.2019, ist die Antwort blitzschnell klar: Donnerstag, denn der 4.4. ist der Doomsday.
Wie verhält es sich mit dem 25.4.2019, auf welchen Wochentag fiel dieses Datum? Ebenfalls Donnerstag natürlich, denn der 25. ist exakt 21 Tage nach dem 4., also auf den Tag genau drei Wochen später. Der 12. November 2019? Wegen der “9-5 at 7/11”-Regel ist der 7.11. ein Donnerstag, also ist der 12. November fünf Tage später und ein Dienstag.
Die Zählerei der Wochentage bewältigt der Zauberer entweder im Kopf, mit den Fingern oder indem er die Wochentage Sonntag bis Samstag von null bis sechs durchnummeriert und dann vom Ergebnis den Rest einer Division durch sieben ausrechnet. Donnerstag ist in diesem Schema die vier, dazu fünf hinzuaddiert ergibt neun, und bei der Division durch sieben bleiben zwei übrig, also Dienstag.
Spielerisch zum Erfolg
Oder der 1. Januar 2020? Nächstes Jahr fällt der Doomsday laut Tabelle in Abbildung 2 auf Samstag, also ist der 4.1. (Vorsicht, Schaltjahr!) ein Samstag und damit der Neujahrstag ein Mittwoch. Langsam lichtet sich also der Schleier und die Wahrheit kommt zutage: Keine Zauberei ist im Spiel, sondern schlichte Merkregeln, die jeder einfach üben kann, bevor es auf die große Bühne geht.
Um diese zu trainieren, wählt das in diesem Snapshot vorgestellte Go-Programm ein zufälliges Datum im aktuellen Jahr aus und lässt dem User die Wahl zwischen sieben Wochentagen. Klickt dieser nach Anwenden der Formel mit der Maus auf den richtigen Tag, bekommt er einen Punkt zur Belohnung, und der Zähler rechts oben in der Anzeige erhöht sich um eins (Abbildung 3). Die Anzeige wechselt zu einem neuen Datum und das Spiel setzt sich fort.
Verrechnet sich der Spieler hingegen und tippt auf den falschen Wochentag, folgt die grausame Strafe auf dem Fuß: Alle bislang erspielten Punkte verfallen, der Zähler springt zurück auf null (Abbildung 4). Anschließend darf der Spieler noch mal nachrechnen und den richtigen Wochentag auswählen, worauf er einen Punkt bekommt und langsam neue Höhen im Punkte-Olymp erklimmt.
Das Spiel läuft in einem Terminal-UI, nachdem der User es von der Kommandozeile aus gestartet hat. So kann auch ein erschöpfter Systemadministrator im Rechenzentrum mal ein kleines Spielpäuschen zur Entspannung einlegen. Go und die schon in einem früheren Snapshot [4] vorgestellte Library »termui« laufen auf allen erdenklichen Plattformen, auf Linux natürlich, aber auch auf anderen Unix-Derivaten und auch auf dem Mac, sogar auf Windows.
Um aus dem Go-Code von Listing 1 ein lauffähiges Binary zu erzeugen, legt der User zunächst ein neues Go-Modul (ab »go-1.12«) an und leitet dann mit »build« den Compilationsprozess ein, der automatisch alle als Abhängigkeiten entdeckten Libraries vom Netz holt und gleich mitkompiliert:
Listing 1
dateday.go
001 package main
002
003 import (
004 "errors"
005 "fmt"
006 ui "github.com/gizak/termui/v3"
007 "github.com/gizak/termui/v3/widgets"
008 "math/rand"
009 "strings"
010 "time"
011 )
012
013 var wdays = []string{"Sunday", "Monday",
014 "Tuesday", "Wednesday", "Thursday",
015 "Friday", "Saturday"}
016
017 func main() {
018 year := time.Now().Year()
019 wins := 0
020
021 if err := ui.Init(); err != nil {
022 panic(err)
023 }
024 defer ui.Close()
025
026 task := randDate(year)
027
028 p := widgets.NewParagraph()
029 p.SetRect(0, 0, 25, 3)
030 displayTask(task, wins, p)
031
032 days := widgets.NewParagraph()
033 days.Text = fmt.Sprintf(
034 "[%s](fg:black)",
035 strings.Join(wdays, "\n"))
036 days.SetRect(0, 3, 25, 12)
037 ui.Render(p, days)
038
039 uiEvents := ui.PollEvents()
040 for {
041 e := <-uiEvents
042 switch e.ID {
043 case "q", "<C-c>":
044 return
045 case "<MouseLeft>":
046 wdayGuess, err := wdayPick(
047 e.Payload.(ui.Mouse).Y)
048 if err != nil { // invalid click?
049 continue
050 }
051 wdayName := wdays[task.Weekday()]
052
053 if wdayGuess == wdayName {
054 days.BorderStyle.Fg =
055 ui.ColorGreen
056 task = randDate(year)
057 wins++
058 } else {
059 days.BorderStyle.Fg = ui.ColorRed
060 wins = 0
061 }
062
063 displayTask(task, wins, p)
064 ui.Render(p, days)
065 go func() {
066 <-time.After(
067 200 * time.Millisecond)
068 days.BorderStyle.Fg =
069 ui.ColorWhite
070 ui.Render(days)
071 }()
072 }
073 }
074 }
075
076 func displayTask(task time.Time,
077 wins int, widget *widgets.Paragraph) {
078
079 widget.Text = fmt.Sprintf(
080 "[%d-%02d-%02d](fg:black)" +
081 "%s[%3d](fg:green)",
082 task.Year(), task.Month(), task.Day(),
083 " ", wins)
084 }
085
086 func wdayPick(y int) (string, error) {
087 if y > 10 || y < 4 {
088 return "", errors.New("Invalid pick")
089 }
090 return wdays[y-4], nil
091 }
092
093 func randDate(year int) time.Time {
094 start := time.Date(year, time.Month(1),
095 1, 0, 0, 0, 0, time.UTC)
096 end := start.AddDate(1, 0, 0)
097
098 s1 := rand.NewSource( // random seed
099 time.Now().UnixNano())
100 r1 := rand.New(s1)
101
102 epoch := start.Unix() + int64(r1.Intn(
103 int(end.Unix()-start.Unix())))
104 return time.Unix(epoch, 0)
105 }
go mod init dateday go build dateday.go
Wie der Installationsprozess in Abbildung 5 zeigt, holt »go build« einen ganzen Schwung an Libraries als Sourcecode aus ihren Github-Repositories und bündelt alles in einem Binary, das mit seinen 2,8 MByte gar nicht mal so dick aufträgt.

Abbildung 5: Ein neu erzeugtes Go-Modul holt beim Buildprozess automatisch den Sourcecode benötigter Libraries von Github ab und kompiliert ihn gleich mit, um ein Rundum-sorglos-Binary zu erzeugen.
Auf- und zuklappen
Listing 1 holt in Zeile 6 den Code der genutzten Terminal-UI-Library unter dem Kürzel »ui« in den Code. Deren Funktion »Init()« versetzt das Terminalfenster in Zeile 21 in den Grafikmodus und verzögert das ordnungsgemäße Zusammenfalten mit »defer« in Zeile 24 bis ans Ende des Hauptprogramms. Das UI in den Abbildungen 3 und 4 besteht aus zwei übereinanderliegenden Widgets vom Typ »Paragraph« aus der Termui-Widgets-Library.
Der obere Absatz zeigt das Datum zum gesuchten Wochentag an sowie am rechten Rand die Anzahl der richtig geratenen Tage in der Variablen »wins«. Der untere Absatz stellt einen statischen String dar, der die Wochentage von Sonntag bis Samstag durch Newline-Zeichen getrennt anzeigt. Die Methode »SetRect()« setzt jeweils die Größe der Widgets in Zeilen und Spalten, in denen jeweils ein Zeichen Platz findet.
Damit das UI-Framework die Fensterchen auch auf der Terminaloberfläche anzeigt, gibt sie der Aufruf von »ui.Render()« in Zeile 37 an die Rendering-Engine weiter. Damit steht die Oberfläche. Zeile 39 öffnet mit dem Aufruf von »ui.PollEvents()« einen Channel, der UI-Ereignisse wie Tastatureingaben, Mausklicks oder Fensterverkleinerungen meldet. Dazu blockt Zeile 41, bis ein Event vorliegt und die nachfolgende Switch-Anweisung prüft, ob der User [Ctrl]+[C] oder [Q]gedrückt hat, also das Programm abbrechen will.
Terminal mit Mauseingabe
Das Event »MouseLeft« kommt hoch, falls der User mit der Maus geklickt hat. Die dem Event beiliegende Payload gibt die Koordinaten an, an denen sich der Mauszeiger zum Zeitpunkt des Klicks befand. Zeile 47 muss dazu das als generischen »interface{}«-Typ empfangene Event noch dynamisch in den Typ »ui.Mouse« umwandeln und greift anschließend mit »Y« auf den (vertikalen) y-Wert der Klickposition zu.
Die Funktion »wdayPick()« ab Zeile 86 ermittelt daraus den geklickten Wochentag, denn das Programm weiß wegen des selbst konstruierten UI-Layouts, dass sich der Sonntag auf der Klickposition mit dem y-Wert 4 befindet, Montag auf der 5, …, Samstag auf der Position 10. Andere Positionen verwirft die Funktion und gibt einen Fehler zurück, damit das Hauptprogramm den Klick ignoriert.
Hat der User richtig gerechnet und den korrekten Wochentag ausgewählt, ist die Bedingung in Zeile 53 wahr und das Attribut »BorderStyle.Fg« setzt den Rahmen des Wochentag-Widget auf Grün. Der anschließende Aufruf von »randDate()« in Zeile 56 holt eine neue Aufgabe und der Erfolgszähler »wins« wird um eins hochgesetzt. Hat der User hingegen falsch geraten, färbt Zeile 59 den Rand des Fensters rot und setzt »wins« zurück auf null. Strafe muss sein.
Damit das UI den Erfolgszähler auch grafisch auffrischt, ändert der Aufruf von »displayTask()« in Zeile 63 den Inhalt des Widget, und der Aufruf »ui.Render()« mit beiden Widgets als Parameter bringt den aktuellen Stand im Terminal zur Anzeige. Damit der rote oder grüne Rand zur Erfolgsrückmeldung an den User aber nur kurz erscheint und dann sofort wieder verschwindet, startet Zeile 65 eine parallel laufende Go-Routine, die erst mit »time.After()« 200 Millisekunden schläft, dann den Rand des Fensters wieder auf das ursprüngliche »White« zurücksetzt (was eigentlich grau aussieht) und mit »ui.Render()« das Ganze auch wirklich anzeigt. So kommt Dynamik ins Spiel, einfach dank der in Go standardmäßig eingebauten Nebenläufigkeit.
Text in Farbe
Auch den Text in den Paragraph-Fenstern färbt »termui« auf Wunsch ein, allerdings nicht mit einem Attribut wie bei den Widget-Rändern, sondern durch spezielle Tags im dargestellten Text. Damit das zu errechnende Datum in Schwarz und die Anzahl der erfolgreich geratenen Aufgaben in Grün erscheinen, bauen die Zeilen 80 bis 81 mit »(fg:black)« und »(fg:green)« entsprechende Farbbefehle in den darzustellenden Text ein.
Knifflige Datumsarithmetik
Einen zufälligen Tag eines Jahres auszuwählen ist schwieriger, als man zunächst annimmt. Klar, die meisten Jahre haben 365 Tage, aber in einem Schaltjahr mit 366 Tagen sollte auch der 29. Februar hin und wieder drankommen. Go bietet mit dem Duration-Typ aus dem Paket »time« ein Verfahren, die Zeitspanne zwischen dem 1. Januar des untersuchten Jahres und dem gleichen Datum des Folgejahres auszurechnen, weigert sich allerdings, dies in Tagen auszudrücken, sondern beschränkt sich auf Stunden.
Der Grund dafür ist die Plagerei, die entsteht, wenn zwischen zwei Datumsangaben eine Schaltsekunde [5] oder die Sommerzeitumstellung liegt. Zählt die dann als Bruchteil eines Tages oder nicht? Go zwingt den Programmierer zur Multiplikation der Stundendifferenz mit 24, um auf die Tageszahl zu kommen – als Hinweis darauf, dass dies unter Umständen nicht ganz stimmt.
Einfacher geht es mit der unter Linux üblichen Unixzeit, die die verstrichenen Sekunden seit dem 1. Januar 1970 angibt, aufgetretene Schaltsekunden aber nicht mit einbezieht, sondern die Zeitpunkte vor und während einer Schaltsekunde mit dem gleichen Zeitstempel quittiert. Die Funktion »randDate()« ab Zeile 93 in Listing 1 bestimmt die Unixzeit des ersten Januars des untersuchten Jahres sowie den Zeitstempel des 1. Januars des Folgejahrs und ermittelt die Differenz in Sekunden.
Danach wählt die Funktion »rand.Intn()« aus dem Paket »math/rand« eine Zufallszahl, die zwischen 0 (einschließlich) und dem Zeitstempel des 1. Januars des Folgejahrs (ausschließlich) liegt, und addiert den Wert zum Startdatum. Heraus kommt ein Sekundenstempel irgendwann im aktuellen Jahr, den die Funktion »time.Unix()« in ein »time.Time«-Objekt aus der Go-Standardbibliothek umrechnet, dessen Monat und Tag mit »Month()« und »Day()« herauspurzeln. Da Unix-Sekunden als »int64« vorliegen und die Zufallsfunktionen aus »math/rand« normale Ints mögen und liefern, muss Zeile 102 zwischen beiden vermitteln und die Typen entsprechend umbiegen.
Der von Go bereitgestellte Zufallsgenerator hat noch die unangenehme Eigenschaft, bei jedem neuen Aufruf des Programms die gleichen Zufallswerte zu liefern, was auf Dauer keinen nachhaltigen Trainingserfolg bringt. Aus diesem Grund zapft Zeile 98 eine neue Entropiequelle an, initialisiert sie mit der aktuellen Uhrzeit in Nanosekunden als »Seed« und ruft damit den neuen Zufallsgenerator in der Variablen »r1« ins Leben. Dessen Methode »Intn()« reagiert deshalb bei jedem neuen Aufruf des Binary »dateday« mit neuen Zufallssequenzen.
Bühnenstress
Fertig ist der Lack, und das Trainingsprogramm kann beginnen! Fortgeschrittene dürfen gerne auch mehrere wechselnde Jahre mit verschiedenen Doomsdays einprogrammieren. Wer den Zeitdruck des Magiers vor ungeduldigen Zuschauern simulieren möchte, kann als zusätzliche Schikane ins Spiel einen Fortschrittsbalken [4] einbauen, der dem Kandidaten nur kurze Zeit lässt, um den Wochentag auszuwählen. Verstreicht die Zeit, sackt der Spielstand auf null Punkte, vielleicht untermalt von einer Gameshow-Tröte als Sounddatei.
Online PLUS
Im Screencast demonstriert Michael Schilli das Programmierbeispiel: http://www.linux-magazin.de/videos/
Infos
-
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2019/08/snapshot/
-
Michael Schilli, “Datumsarithmetik”: Linux-Magazin 12/07, S. 114: https://www.linux-magazin.de/ausgaben/2007/12/datumsarithmetik/
-
“Doomsday-Methode”: https://de.wikipedia.org/wiki/Doomsday-Methode
-
Michael Schilli, “Fortschritt auf Raten”: Linux-Magazin 12/18, S. 104, https://www.linux-magazin.de/ausgaben/2018/12/snapshot-9/
-
Schaltsekunde: https://de.wikipedia.org/wiki/Schaltsekunde









