Doing calendrical calculations in Go

In this post, I present a command line application that displays a given date in different calendars, and the library that powers this application.

Representing dates

Every once in a while, I wonder what a given date would look like if we were using a different calendar, e.g. the French Revolutionary or a Mayan calendar. Ideally, I’d like to have the different date representations be displayed at once with one single command.

Online tools, Emacs, and deployability considerations

There are several websites that allow converting dates into a specific calendar, however, none of them include all of those I’m regularly interested in – not to mention that accessing websites requires a working internet connection. With regard to offline applications, there is – of course – the powerful Emacs,1 which ships with various calendar functions.2 To convert a given Gregorian date to another calendar, start Emacs, open its calendar, select a date, and have Emacs convert it to the desired calendar. Repeat the last step for each calendar you’re interested in.

This process works certainly well if you are using Emacs anyways, and do not want to display a date in several other calendars at once. Provided sufficient Emacs- and Lisp-knowlegde, writing a simple function for displaying a Gregorian date in multiple calendars should no be too complicated a task. One could also probably get Emacs to run in batch mode, pass it the date, and have the custom function convert it to the different calendars. While certainly quite a nerdy approach – do you see the imaginary duct tape already? – I’d prefer a solution that can be more easily built,3 maintained4 and deployed,5 which provides a single binary that does date conversion only.

Language musings

Since there seems to be no such tool – and despite the duct tape approach’s undeniable appeal – the only remedy left is to write my own application. So, looking at the programming languages I have already worked with and used in previous blog posts – namely C, Fortran, Java, Python, and R – which one should I choose?

I’d like my command line tool to be self contained in a single executable binary – this can be achieved with C for instance – and be able to easily compile the binary for different target platforms (e.g. Windows, Linux, MacOS) – this however can’t be done with any of the languages discussed above. Hence, the answer to the question above is: none of them.

In consequence, I need to find a language that does fit my requirements. But fear not, for programming languages are plenty! So plentiful indeed that – in light of developing a date conversion tool – assessing each language (even superficially) may prove rather time consuming and possibly not quite expedient an endeavour.10 Luckily, there is no need for such an assessment: I’ve kept a short list of languages which I’d like to use in a new project. From this list, I chose Go: Go is a compiled language, generating standalone binaries, supports cross-compilation out of the box, and ships with many useful tools for testing, generating documentation, and formatting.11 As an added benefit,12 its syntax is similar to C.

Now that the language question has been answered, we may proceed with the date conversion tool.

The date conversion tool

As stated above, the command line tool – calcal – shall take a Gregorian date, convert it to different calendars, and display the results. When no date is provided, it should use the current date. Furthermore, I should be able to specify the calendars to which the date shall be converted.

calcal consists of two main components, a front end, and a date conversion engine.

The user interacts with the front end from the command line. Inputs that may be provided are a date, and an option that specifies the target calendars. calcal parses and validates these inputs. If the inputs are valid, they are processed by the date conversion engine, the results returned to the front end, which in turn displays them to the user; otherwise, calcal exits with a non-zero exit code.

Date conversion engine

The date conversion engine I’ve been writing about in the previous section provides the calendrical calculation functions required for date conversion. This tautololgical epiphany aside, where do these functions come from?

Edward Reingold and Nachum Dershowitz developed a unified way to convert dates from and to 33 calendars, which they describe in great detail in: Reingold, Edward, and Nachum Dershowitz. 2018. Calendrical Calculations: The Ultimate Edition. 4th edition. Cambridge: Cambridge University Press.

For this project however, I’ll rely on two older papers by Reingold13 and Dershowitz discussing date conversion between eleven calendars, and which are freely available from their web site:14

Based on the Lisp code and valuable context information from the papers, I wrote a calendrical calculation library in Go – libcalendar. To be more easily reused and integrated in other projects, libcalendar also includes several helper functions, a generalized type for calendar dates, utilities for JSON-serialization and deserialization, and a test suite to validate the conversion functions against the Lisp code. For more information about libcalendar, see the source code here and the package documentation at pkg.go.dev.

Command-line interface

The command-line interface is written in Go as well and uses the standard library’s flag package. Since most of the heavy lifting is done by libcalendar, the code of calcal’s command line interface consists of less than 200 lines of code.

Please note that calcal is currently only available as source code (see here). Hence, you will need a working Go toolchain for compilation and installation via go install staudtlex.de/calcal@latest (binary releases may be made available in the future).

Provided the executable is placed somewhere on your $PATH – which it should be by default when installing with go install – you may call calcal15 from the command line. For an overview of calcal’s options, simply run the tool with the -h option (see below):

~$ calcal -h
Usage of calcal:
  -c string
        comma-separated list of calendars. Currently, calcal supports
        all             all calendars listed below (default)
        gregorian       Gregorian calendar
        iso             ISO calendar
        julian          Julian calendar
        islamic         Islamic calendar
        hebrew          Hebrew calendar
        mayanLongCount  Mayan Long Count calendar
        mayanHaab       Mayan Haab calendar
        mayanTzolkin    Mayan Tzolkin calendar
        french          French Revolutionary calendar
        oldHinduSolar   Old Hindu Solar calendar
        oldHinduLunar   Old Hindu Lunar calendar
  -d string
        date (format: yyyy-mm-dd). When omitted, '-d' defaults to the
        current date (2022-08-29)

When called without passing a date via the -d option, calcal defaults to taking the current date and converting it to all calendars supported by libcalendar. As of version 0.5.1 of libcalendar, these are the Gregorian, ISO week, Julian, Islamic, Hebrew, Mayan Long Count, Haab, and Tzolkin, French Revolutionary, Old Hindu solar and lunar calendars.

Now, assuming we wanted to know how the 29th of August 2022 (Gregorian) is represented in all other calendars supported by libcalendar – or only one of them, but being wary of wearing out our beloved keyboard, we’d want to avoid pushing its keys too often – we would simply type the command’s name and pass it the desired date, producing the results below:

~$ calcal -d 2022-08-29
Please note:
- for dates before 1 January 1 (Gregorian), calcal may return incorrect
  ISO and Julian dates
- for dates before the beginning of the Islamic calendar (16 July 622
  Julian), calcal returns an invalid Islamic date
- for dates before the beginning of the French Revolutionary calendar
  calendar (22 September 1792), calcal returns an invalid French date
- Old Hindu (solar and lunar) calendar dates may be off by one day

Gregorian               29 August 2022
ISO                     2022-W35-1
Julian                  16 August 2022
Islamic                 1 Safar 1444
Hebrew                  2 Elul 5782
Mayan Long Count        13.0.9.14.18
Mayan Haab              11 Mol
Mayan Tzolkin           6 Etznab
French Revolutionary    12 Fructidor an 230
Old Hindu Solar         13 Simha 5123
Old Hindu Lunar         2 Bhadrapada 5123

If we were less worried about wearing out our keyboard, and much more – and exclusively – interested in knowing the date representations of the 29th of August 2022 in the ISO, Islamic, Hebrew, and Old Hindu solar calendar, we’d restrict the output calendars with the -c option as follows:

~$ calcal -d 2022-08-29 -c iso,islamic,hebrew,oldHinduSolar
Please note:
- for dates before 1 January 1 (Gregorian), calcal may return incorrect
  ISO and Julian dates
- for dates before the beginning of the Islamic calendar (16 July 622
  Julian), calcal returns an invalid Islamic date
- for dates before the beginning of the French Revolutionary calendar
  calendar (22 September 1792), calcal returns an invalid French date
- Old Hindu (solar and lunar) calendar dates may be off by one day

ISO                     2022-W35-1                      
Islamic                 1 Safar 1444                    
Hebrew                  2 Elul 5782                     
Old Hindu Solar         13 Simha 5123

Conclusion

In this post, I presented a small date conversion tool that converts Gregorian dates to various other calendars. Rather than lengthily describing the tool itself, I elaborated on my motivation to write calcal in the first place, spent some paragraphs on my choice of programming language,16 and discussed very useful resources with regard to calendrical calculations. In a future post, I shall possibly talk about other contexts integrating libcalendar.


  1. Whether or not Emacs is to be considered an editor or an "[…] operating system, lacking only a decent editor" is a discussion I shall not indulge. ↩︎

  2. See the Emacs documentation↩︎

  3. This includes straightforward cross-compilation↩︎

  4. For this project, this means no third party dependencies/libraries. ↩︎

  5. Did I hear “Docker”? To be sure, one could create a container with contains Emacs, the custom Lisp function, as well as the shell script that invokes Emacs in batch mode, and deploy/run the resulting tool via some container engine. However, I highly doubt adding yet another layer of complexity and resource requirements to such a narrowly scoped tool is what should be done. ↩︎

  6. Chances are high that current Linux distributions ship with a Python 3 interpreter, at least on desktop systems. This is not true however for Windows or MacOS, where one would have to first download and install Python. ↩︎

  7. To be sure, there are third party applications like PyInstaller that allow packaging a Python interpreter and program in a single binary. Unfortunately, they often do not support cross-compilation. Furthermore, they are third party dependencies that I’d like to avoid for this project. ↩︎

  8. JDK 14 and newer support packaging a Java application (jar and module files) with a Java runtime environment (JRE) into an installable package (e.g. exe/msi, deb/rpm, dmg) via the jpackage tool. While very good news in general, even for small applications, the JRE takes up 30 MB of space. Furthermore, jpackage does not support cross-compilation. ↩︎

  9. Additionally, Fortran may not quite be the first language that comes into mind for writing a command line tool – even though it can be done↩︎

  10. For a glimpse of some (by whatever metric) popular languages, see e.g. IEEE Spectrum’s Top Programming Languages 2021, PYPL (PopularitY of Programming Language) Index, Stackoverflow’s 2021 Developer Survey and the TIOBE index ↩︎

  11. For more information about Go from its developers, see https://go.dev↩︎

  12. Others may disagree. ↩︎

  13. The GNU Emacs date conversion functions I mentioned earlier were in fact contributed by Edward Reingold. ↩︎

  14. The (public domain) Lisp code presented in the papers is available from their website as well, see here↩︎

  15. I am fully aware of the accumulation of the syllable “cal” and find it quite intriguing. ↩︎

  16. Don’t be fooled, however, for my programming language reasoning may actually very well be a sophisticated post-hoc rationalization – or even an elaborate excuse – to learn some Go. ↩︎