Implementing calendars

Calendars seem simple at first, but no system survives first context with reality. Let’s go through what types of calendars exist and how to support them all in one codebase. Our eventual focus will be on producing a calendar for games — but those more on the Dwarf Fortress end of the spectrum.

The Gregorian calendar

We’re probably all familiar with the Gregorian calendar. It’s relatively simple:

  • A year has twelve months with arbitrary lengths.
  • Normal years have 365 days.
  • Every fourth year, except every hundredth year, but including every four hundredth year, adds an extra day to one of the months.
  • Weeks have seven days. Every day advances the day of the week.

Supporting this isn’t too difficult. A little ugly at times, and not the easiest for efficiently calculating the difference between two dates, but that’s okay. Let’s implement it:

struct Month {
  string name;
  uint days;
  uint leapDays;
}

class Calendar {
  Month[] months;
  string[] weekdays;
  size_t weekdayOfFirstDay;  /// weekday for 1 Jan 1 AD

  bool isLeapYear(int year) {
    return year % 400 == 0 || (year % 100 != 0 && year % 4 == 0);
  }

  static Calendar gregorian() {
    Month[] months = [...];
    return new Calendar(
      months,
      ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
      5);
  }
}

That works, though the ‘leap year’ concept as written isn’t great. We’ll work on it in a little while.

Days of the week

As an exercise, let’s write a method to figure out the day of the week for a given date. We’re concerned with correctness, not speed, for now:

  string dayOfWeek(uint year, uint month, uint dayOfMonth) {
    month--; // one-indexed
    dayOfMonth--;
    auto normalYearLength = months.map!(m => m.days).sum;
    auto leapYearLength = months.map!(m => m.leapDays).sum;
    ulong days = dayOfMonth;
    auto endIsLeap = isLeapYear(year);
    foreach (m; months[0..month])
      days += endIsLeap ? m.leapDays : m.days;
    foreach (y; 1..year)
      days += isLeapYear(y) ? leapYearLength : normalYearLength;
    auto d = cast(size_t)((days + weekdayOfFirstDay) % weekdays.length);
    return weekdays[d];
  }

It’s not fast, but it produces good results.

Days between dates

Let’s also add a method to calculate the difference between two dates, which should be pretty similar. (I added a Date struct so I don’t have to pass a bajillion parameters everywhere.)

long dateDiff(Date d1, Date d2) {
  long days = d2.day;
  days -= d1.day;
  if (d1.year == d2.year) {
    foreach (month; months[d1.month-1..d2.month-1])
      days += isLeapYear(d1.year) ? month.leapDays : month.days;
  } else {
    foreach (month; months[d1.month-1..$])
      days += isLeapYear(d1.year) ? month.leapDays : month.days;
    foreach (month; months[0..d2.month-1])
      days += isLeapYear(d2.year) ? month.leapDays : month.days;
  }
  foreach (year; (d1.year + 1) .. d2.year)
    days += isLeapYear(year) ? leapYearLength : normalYearLength;
  return days;
}

Slightly abbreviated for length — this doesn’t handle the case of d2 < d1. But it seems to work.

Vive la France!

Let’s introduce a wrinkle. (Two, in fact!) Let’s support the French revolutionary calendar.

Epagomenal? That’s not even a real word.

This calendar has twelve months. Each month is three weeks long, and each week is ten days long. This adds up to 360 days in a year, right? Well, no! Because the French republican calendar also has several days that don’t belong to any month. Most years have five; some have six.

These extra days are known as epagomenal days. They don’t appear within a month, don’t advance the day of the week, and in this case, each one has its own name. Since they’re not part of a month, we don’t need to represent them, right?

…well, no. That would work well for calculating the day of the week, but it wouldn’t properly calculate the difference between two dates. Conversely, just including each as a month, or a month of just these days, would give us the right date difference, but the wrong day of the week.

Furthermore, these holidays don’t have days of the week. The most sensible thing to report for the day of the week is the name of the holiday — for instance, while usually the day after décadi is primidi, once per year, it’s La Fête du Vertu.

We solve this by giving some months their own, special day-of-week lists. They don’t advance the day-of-week in the primary system. We modify our day-of-week algorithm as follows:

// Special-case months that have their own special days-of-week
auto currentMonth = months[d.month];
if (currentMonth.overrideDaysOfWeek)
  return currentMonth.overrideDaysOfWeek[d.day % $];
// Skip over those months in the general reckoning
auto normalYearLength = months.filter!(m => !m.overrideDaysOfWeek).map!(m => m.days).sum;
auto leapYearLength = months.filter!(m => !m.overrideDaysOfWeek).map!(m => m.leapDays).sum;

And to define this span of holidays, we use:

Month holidays = {
  name: "Sansculottides",
  days: 5,
  leapDays: 6,
  epagomenal: true,
  overrideDaysOfWeek: ["fête de la vertu", ...]
};

This presents an undesirable notational quirk — we write fête de la vertu, an IV as Date(4, 13, 1). Not the best, but what can ya do?

A note on leap years

Remember the calculation for leap years from the Gregorian calendar? Officially, the French revolutionary calendar determines its leap years experimentally: every year starts on the autumnal equinox. Astronomers make predictions about when that will be, but (for instance) in the year 144, it wasn’t entirely certain whether that would be a leap year or the following year.

The nice part about this is that it will continue to keep the seasons and months lined up as long as the earth’s rotation doesn’t speed up by more than six hours a year or slow by more than eighteen hours per year.

The Romme method, which seems to be the most popular today, uses the Gregorian method for determining whether a given year is a leap year or not.

The other half of the world

Isfahan is half the world, and they use the Islamic calendar. (For some things, at least.)

Specifically, we’ll support the tabular Islamic calendar. Recall how the French calendar determined leap years through observing when the equinox was? Well, the traditional Islamic calendar determines months according to the lunar cycle directly. If you live in an area where the sky clouds over a lot, you might be uncertain what month it is.

Both the traditional and tabular calendars agree on many points: there are twelve months, they alternate between 30 and 29 days, and the twelfth month on leap years is also 30 days. In the tabular calendar, eleven out of every thirty years are leap years — specifically, years 2, 5, 7, 10, 13, 16, 18, 21, 24, 26 and 29. (In the most popular reckoning.)

No matter what, a year is between 354 and 355 days long, so the Islamic calendar produces significant drift from the equinox.

Our current strategy can’t support this. We hard-coded our previous rule, and it’s in conflict with the new one. So we’ll go more general. In class Calendar, we’ll add a couple fields:

/// How many years are in the full leap year cycle.
uint yearsPerCycle;
/// Which years in a cycle are leapyears.
uint[] leapYears;

To determine whether a given year is a leapyear, we use:

bool isLeapYear(uint year) {
  return leapYears.canFind(year % yearsPerCycle);
}

The rest should be straightforward.

Where we stand

At this point, we can represent several calendars precisely. They aren’t necessarily the most popular calendars around, but they represent a good range and should give you a good idea how to implement interesting calendars for your game (or the real world). But there are several important calendars we haven’t touched on. Tune in next time to see what we can do.

Leave a Reply