Aus Linux-Magazin 08/2019

Go-Programm trainiert die Verknüpfung von Datum mit Wochentag

© Fernando Gregory Milan, 123RF

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.

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.

Abbildung 2: Die Tabelle offenbart, dass der Doomsday im Jahr 2019 auf einen Donnerstag fällt.

Abbildung 2: Die Tabelle offenbart, dass der Doomsday im Jahr 2019 auf einen Donnerstag fällt.

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.

Abbildung 3: Der Spieler hat den Wochentag richtig ermittelt und bekommt einen Punkt.

Abbildung 3: Der Spieler hat den Wochentag richtig ermittelt und bekommt einen Punkt.

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.

Abbildung 4: Oh nein, falsch getippt! Der Zähler springt zurück auf null.

Abbildung 4: Oh nein, falsch getippt! Der Zähler springt zurück auf null.

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&ouml;tigter Libraries von Github ab und kompiliert ihn gleich mit, um ein Rundum-sorglos-Binary zu erzeugen.

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

  1. Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2019/08/snapshot/

  2. Michael Schilli, “Datumsarithmetik”: Linux-Magazin 12/07, S. 114: https://www.linux-magazin.de/ausgaben/2007/12/datumsarithmetik/

  3. “Doomsday-Methode”: https://de.wikipedia.org/wiki/Doomsday-Methode

  4. Michael Schilli, “Fortschritt auf Raten”: Linux-Magazin 12/18, S. 104, https://www.linux-magazin.de/ausgaben/2018/12/snapshot-9/

  5. Schaltsekunde: https://de.wikipedia.org/wiki/Schaltsekunde

Der Autor

Michael Schilli arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben