Calculating weekdays and dates with Go

Programming Snapshot – Go

Article from Issue 227/2019
Author(s):

Math wizards amaze audiences by naming the day of the week for any date called out by the audience. Mike Schilli writes a training program in Go to turn amateurs into headline performers.

Math geniuses can do this: Someone from the audience calls out "December 12, 2019," and the numbers wizard announces "Thursday!" after just a few seconds. How did he do that? Does the entertainer have a photographic memory, or some kind of calendar function built into his head? The solution is surprisingly simple: He only has to go through a few rules that are easy to remember and, with a little bit of practice, can come up with the day of the week for any given date.

Years ago in this column, I introduced a similar mental arithmetic method for calculating weekdays, but with more elaborate steps [1]. A reader then replied that the method was unnecessarily complex and referred me to the simpler Doomsday rule [2], which I will use here to create a Go training program for weekday prediction.

Last Day

According to the Doomsday rule, the following days of a year always fall on the same weekday: 5/9, 9/5, 11/7, and 7/11 (using the Month/Day format). You can easily memorize this with the formula "9-5 at 7/11" (i.e., the typical nine-to-five workday at 7-Eleven, the US convenience store chain).

Many other Doomsday days fall on day-month duplicates: 4/4, 6/6, 8/8, 10/10, and 12/12. January, February, and March are the only exceptions; in non-leap years, the day Doomsday falls on is January 3, February 7, and March 7 (Figure 1). In leap years, Doomsday changes to January 4 and February 8, while March 7 stays the same.

Figure 1: If you have these easy-to-remember rules in your head, you can calculate the day of the week for any date.

The Doomsday for 2019 is Thursday according to a separate procedure (2018 was a Wednesday; 2020 will be a Saturday; see Figure 2). So, if someone asks you for April 4, 2019, the answer is obvious: Thursday, because April 4 is the Doomsday.

Figure 2: The table reveals that the Doomsday in 2019 is Thursday.

What about April 25, 2019? Which weekday was this date? Again, it's a Thursday, of course, because the 25th is exactly 21 days after the 4th (i.e., exactly three weeks later to the day). How about November 12, 2019? Because of the "9-5 at 7/11" rule, November 7 is a Thursday, so November 12 is five days (or one week minus two days) later, and therefore a Tuesday.

Or, you can either count the weekdays in your head, on your fingers, or by numbering the weekdays Sunday to Saturday from zero to six and then knocking off the remainder after dividing by seven to reach a result. Thursday is day four in this scheme; five added to it gives you nine, and after dividing by seven, you are left with two: So, it's a Tuesday.

Easy Learning Method

What about January 1, 2020? Next year, the Doomsday is a Saturday, according to Figure 2, so January 4 (watch out – it's a leap year!) is a Saturday and New Year's Day thus a Wednesday. Slowly the mists clear, and the truth comes to light: There is no magic involved, just simple mnemonic rules that anyone can easily practice before going on stage.

To train would-be number wizards, the Go program presented here selects a random date in the current year and lets the user choose between seven weekdays. If you click on the right day after applying the formula in your head, you win a point, and the counter in the display's upper-right corner is incremented by one (Figure 3). The display changes to a new date, and the game resumes.

Figure 3: The player has determined the day of the week correctly and earns a point.

If, on the other hand, the player miscalculates and bets on the wrong day of the week, a penalty follows: All the points you scored so far expire, and the counter drops back to zero (Figure 4). Afterwards, you can try again and hopefully choose the right day of the week; again you score a point and can slowly climb to a new high score.

Figure 4: Oh no, wrong guess! The counter is reset to zero.

The game runs in a terminal user interface (UI) after you launch it at the command line. Even exhausted datacenter system administrators therefore can take a little break to relax. Go and the termui library, introduced in a previous column [3], run on all conceivable platforms including Linux, but also on other Unix derivatives and macOS – it even runs on Windows.

To create an executable binary from the Go code for Listing 1 [4], first create a new Go module (using go-1.12 or later) and then start the compilation process with build; this automatically retrieves all libraries identified as dependencies off the web and compiles them, too:

go mod init dateday
go build dateday.go

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.Local)
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 }

As the installation process in Figure 5 shows, go build takes a whole bunch of libraries as source code from their GitHub repositories and bundles them all in one binary, which is not overly large at 2.8MB.

Figure 5: A newly created Go module automatically retrieves the source code for required libraries from GitHub during the build process and compiles it to create a worry-free binary.

Opening and Closing

Line 6 of Listing 1 adds the code for the terminal UI library under ui. Its Init() function switches the terminal window into graphics mode in line 21 and delays a clean teardown until the end of the main program with defer in line 24. The UI in Figures 3 and 4 consists of two stacked Paragraph widgets from the termui widgets library.

The upper widget shows the date to be guessed; on the right-hand side, you can see the number of successful guesses in the wins variable. The lower section shows a static string that displays the days of the week from Sunday through Saturday separated by newline characters. The SetRect() method sets the size of the widgets in rows and columns that each can hold precisely one character.

In order for the UI framework to render the widgets on the terminal interface, it notifies the rendering engine via ui.Render() in line 37. That's all there is to drawing the GUI. Line 39 then opens a channel with a call to ui.PollEvents(); it reports UI events like key presses, mouse clicks, or window resize actions. Line 41 blocks until an event occurs, while the subsequent switch statement checks whether the user has pressed Ctrl+C or Q (i.e., to end the program).

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Command Line: cal and date

    The legacy cal and date tools help users keep track of the time and date. You can even change the system time with a single shell command.

  • SuperKaramba Workshop

    If you can’t find the SuperKaramba theme you’re looking for, you can always build your own.

  • Programming Snapshot – Mileage AI

    On the basis of training data in the form of daily car mileage, Mike Schilli's AI program tries to identify patterns in driving behavior and make forecasts.

  • Perl: Math Tricks

    A trick that anybody can learn lets you determine the day of the week from the date. We’ll apply some Perl technology to discover whether the method is reliable.

  • Command Line: Calendar Tools

    We take a spin through several personal calendar apps that you can manage from the command line.

comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95

News