Epoch

I always wondered why we would anchor the counting of our years to an event mostly relevant to only one World religion: the "international and secular" year 0 really just denotes the year in which the Christian religion's prophet is believed to have been born. This might explain why some parts of the globe have not adopted that calendar yetOr rather simply that Status Quo has repeatedly shown itself to be the strongest force opposing any kind of improvements.. And who would blame them.

But then the question is, what makes a good secular calendar?

The IT-phile person in me would welcome a system that is not too complicated to use (I'd say the current Gregorian calendar is not all too bad in that respect). The Unix-phile person would welcome a system that is not too complicated to implement on a Unix-based operating system.

The following suggestion by a friend of mine (who incidentally happens to be a Finn) therefore got my full approval:

Year 0 denotes the beginning of the Unix epoch, i.e. AD 1970.

T. Tuuva

Seems simple. We don't really need to change the structure of a year (months, weeks/weekdays); those are unproblematic, as they are rooted in religions that are next to dead these days. We simply relocate year 0, and that's it.

This requires us to "rethink" a few things, of course:

To get a bit more the "feeling" for this new, religion-agnostic calendar, I decided to concretise the idea a bit more; the obvious first place for that was the standard C library. Since we do not touch the internal time system (it already is in the Unix epoch), we only need to adapt the external representation of the date, namely what strftime() returns.

Unfortunately, strftime() acts on a struct tm, which stores the year as the number of years since 70 BU (or AD 1900), so strftime() needs to do some transforming on its own already, which does not exactly simplify things.

Moreover, there are various format specifiers for the year (e.g. %Y and %y for the year itself, %C for the century, and %G and %g for the ISO-week-based yearE.g. if december 31 is a sunday in 48, it is part of week 1 in the next year) + a few "composite" specifiers like %D (%m/%d/%y) and %F (%Y-%m-%d). To make matters more complicated, there are also format modifiers like %E and %O (although I have not yet looked into those).

Lastly, there are quite some tools (like coreutils' date command), which happen to add a few format specifiers, and I'm not quite sure whether those need to be adapted separately.

quick'n'dirty

For a quick test-"baptising", rather than trying to modify the abovementioned format specifiers correctly, I decided to simply add a new specifier, %f (note that this diff applies to glibc 2.25—it may need to be adapted for other releases):

diff --git a/time/strftime_l.c b/time/strftime_l.c
index eb3efb8129..18b339d926 100644
--- a/time/strftime_l.c
+++ b/time/strftime_l.c
@@ -1013,6 +1013,9 @@ __strftime_internal (CHAR_T *s, size_t maxsize, const CHAR_T *format,
      cpy (buf + sizeof (buf) / sizeof (buf[0]) - bufp, bufp);
      break;

+   case L_('f'):
+     DO_NUMBER(1, tp->tm_year - 70);
+
    case L_('F'):
      if (modifier != 0)
        goto bad_format;

Then, to enjoy my transition to the new epoch, I modified my status bar to use %f-%m-%d for the date display:

karuibar time module in epochShould've waited another 6 minutes… just for the sake of it.

Of course, applications do not use %f, so this approach is a dead end.
But it's a start.

slow'n'properly

I initially feared that changing the code for %Y, %y and all the other specifiers would degrade to a gorefest, but the code turned out to be remarkably clean. In comparison, the code I then introduced was a little less clean. But for testing purposes, I considered it "well enough":

--- a/time/strftime_l.c
+++ b/time/strftime_l.c
@@ -880,7 +880,7 @@ __strftime_internal (CHAR_T *s, size_t maxsize, const CHAR_T *format,
            }

          {
-           int year = tp->tm_year + TM_YEAR_BASE;
+           int year = tp->tm_year - 70;
            DO_NUMBER (1, year / 100 - (year % 100 < 0));
          }

@@ -1212,10 +1212,10 @@ __strftime_internal (CHAR_T *s, size_t maxsize, const CHAR_T *format,
            switch (*f)
              {
              case L_('g'):
-               DO_NUMBER (2, (year % 100 + 100) % 100);
+               DO_NUMBER (2, ((year - 1970) % 100 + 100) % 100);

              case L_('G'):
-               DO_NUMBER (1, year);
+               DO_NUMBER (1, year - 1970);

              default:
                DO_NUMBER (2, days / 7 + 1);
@@ -1257,7 +1257,7 @@ __strftime_internal (CHAR_T *s, size_t maxsize, const CHAR_T *format,
          if (modifier == L_('O'))
            goto bad_format;
          else
-           DO_NUMBER (1, tp->tm_year + TM_YEAR_BASE);
+           DO_NUMBER (1, tp->tm_year - 70);

        case L_('y'):
          if (modifier == L_('E'))
@@ -1276,7 +1276,7 @@ __strftime_internal (CHAR_T *s, size_t maxsize, const CHAR_T *format,
 # endif
 #endif
            }
-         DO_NUMBER (2, (tp->tm_year % 100 + 100) % 100);
+         DO_NUMBER (2, ((tp->tm_year - 70) % 100 + 100) % 100);

        case L_('Z'):
          if (change_case)

And indeed, after installing the patched glibc and rebooting the machine, the system started displaying dates with the new epoch format. Here's an example output of systemd:

$ systemctl --user status
● testbox
    State: running
     Jobs: 0 queued
   Failed: 0 units
    Since: Tue 47-03-28 15:47:49 CEST; 1h 4min ago
   CGroup: /user.slice/user-1000.slice/user@1000.service
           └─init.scope
             ├─306 /usr/lib/systemd/systemd --user
             └─315 (sd-pam)

So everything seemed neat and rainbow-y…

… except for coreutils

For some reason, date, ls and all the other programs provided by coreutils were still stuck in the Gregorian calendar. And indeed, after taking a closer look, it turned out that coreutils maintains its own version of strftime() (and quite probably a few other (g)libc components).

Yay! More patching!

--- a/lib/strftime.c
+++ b/lib/strftime.c
@@ -906,9 +906,9 @@
             }

           {
-            int century = tp->tm_year / 100 + TM_YEAR_BASE / 100;
-            century -= tp->tm_year % 100 < 0 && 0 < century;
-            DO_SIGNED_NUMBER (2, tp->tm_year < - TM_YEAR_BASE, century);
+            int century = (tp->tm_year - 70) / 100;
+            century -= (tp->tm_year - 70) % 100 < 0 && 0 < century;
+            DO_SIGNED_NUMBER (2, tp->tm_year - 70 < 0, century);
           }

         case L_('x'):
@@ -1275,18 +1275,17 @@
               {
               case L_('g'):
                 {
-                  int yy = (tp->tm_year % 100 + year_adjust) % 100;
+                  int yy = ((tp->tm_year - 70) % 100 + year_adjust) % 100;
                   DO_NUMBER (2, (0 <= yy
                                  ? yy
-                                 : tp->tm_year < -TM_YEAR_BASE - year_adjust
+                                 : tp->tm_year - 70 < -year_adjust
                                  ? -yy
                                  : yy + 100));
                 }

               case L_('G'):
-                DO_SIGNED_NUMBER (4, tp->tm_year < -TM_YEAR_BASE - year_adjust,
-                                  (tp->tm_year + (unsigned int) TM_YEAR_BASE
-                                   + year_adjust));
+                DO_SIGNED_NUMBER (4, tp->tm_year - 70 < -year_adjust,
+                                  (tp->tm_year - 70 + year_adjust));

               default:
                 DO_NUMBER (2, days / 7 + 1);
@@ -1326,8 +1325,7 @@
           if (modifier == L_('O'))
             goto bad_format;

-          DO_SIGNED_NUMBER (4, tp->tm_year < -TM_YEAR_BASE,
-                            tp->tm_year + (unsigned int) TM_YEAR_BASE);
+          DO_SIGNED_NUMBER (4, tp->tm_year -70 < 0, tp->tm_year - 70);

         case L_('y'):
           if (modifier == L_('E'))
@@ -1346,9 +1344,9 @@
             }

           {
-            int yy = tp->tm_year % 100;
+            int yy = (tp->tm_year - 70) % 100;
             if (yy < 0)
-              yy = tp->tm_year < - TM_YEAR_BASE ? -yy : yy + 100;
+              yy = tp->tm_year - 70 < 0 ? -yy : yy + 100;
             DO_NUMBER (2, yy);
           }

Now that seems almost fine. Notice how for the %Y modifier, coreutils actually pads the year number with zeroes to have it use at least 4 digits, in any case? So we get 0047 instead of just 47, for instance.

However, I sense that fixing this would cause a great disturbance in the Force. As if millions of badly written scripts suddenly errored out in terror and were suddenly terminated.

Alright, we done here?

a rabbit hole

What about the input? For instance, how would coreutils' date interpret dates passed with the --date option?

$ date --date='2017-03-28'
Tue 28 Mar 00:00:00 CEST 0047

Well, that doesn't look terribly right—that'll require patching, too…

At this point, I stopped.

The problem is, patching strftime() is comparatively trivial (as we've seen above) and solves the output problem. Most applications (and other languages) somehow make use of libc's strftime() at some point, so there isn't really much of duplication of code here (except for coreutils, because).

But reading date and time values and converting them to an internal representation is a different thing: there are quite possibly countless different implementations for that, in all sorts of programs and languages, and they most likely all assume that we're in the Gregorian calendar. The fact that struct tm puts its year 0 to 70 BU (AD 1900) doesn't exactly simplify things, although if we think about it, that's a different problemBut still a problem..

That's a rabbit hole that I don't really want to step into.

final thoughts

The idea still seems tempting: switch to a different time epoch and get rid of the religious baggage. But as long as our everyday live keeps using the Gregorian calendar (and for geeks in particular, as long as the computer keeps displaying the Gregorian calendar), it's rather difficult to get into the "Unix mode".

And while my naive approach of simply adding a new format specifier (%f, remember?) to strftime() seems a little lazy, the alternative would be a matter of "all or nothing", with "all" being pretty much impossible for a single person, and "nothing" being downright frustrating. "Some" is a dangerous middle ground where things will simply break sooner or later.

Another aspect we haven't looked into yet is the way we treat leap years. Because the years in the Unix calendar basically have the same length as the ones in the Gregorian calendar, the same 4/100/400 rule can be applied here, too. But as leap years have already been established with the Gregorian calendar, we currently need to convert our dates to latter for then calculating the leap years. And frankly that isn't very convenient. But we'd need more adoption for changing that.

We'd need more people.

read more