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 Unix-phile person in me would welcome a system that is not too complicated to use and implement—I'd say the current Gregorian calendar is not all too bad in that respect. So the following suggestion by a friend of mine (who incidentally happens to be a Finn) got my full approval:

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

T. Tuuva

Seems simple. Relocate year 0, and keep things the same otherwise.

Of course this would blatantly ignore the fact that not all cultures use 365-day or 12-month cycles to mark a "year", or 7-day cycles to mark a weekAlthough from a quick glance I could not find any other week lengths., but those are rooted in very ancient religions or astrology, and are thus not necessarily limited to just one geographic region.

Either way, we need to "rethink" a few things, of course:

To force myself to get into the habit of using that new system, I decided to concretise this idea a bit more and configure my computer to display and use dates in this system; 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 starts counting the years in 70 BU (AD 1900), so strftime() does some transforming on its own already, which does not exactly simplify things.

Moreover, there are several 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).

So…

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—independently of the language they are written in—somehow make use of libc's strftime() at some point either directly or through bindings, 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 us geeks in particular, as long as the computer keeps using 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 break sooner or later anyway.

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 the leap years have been established using the Gregorian calendar; in the Unix calendar, they would be in 2, 6, 10, 14… with the first "non-leap-year" in AU 130 (AD 2100). We have to convert our dates to the Gregorian calendar before determining whether they are leap years or not—and frankly that isn't very convenient. But we'd need more adoption for changing that.

We'd need more people.


erratum

Actually, the Gregorian calendar starts counting years at AD 1. The year preceding AD 1 is 1 BC. Consequently, we have Gaius Julius Caesar who died in 44 BC, which is actually 2013 and not 2014 BU.

The astronomical calendar (and ISO-8601) do have a year 0 that corresponds to 1 BC (so the year numbers for everything before AD 1 are off by 1), even though it's not used very often in historical contexts.

Speaking of standards—RFC 3339 also states that year numbers are expected to be 4-digit numbers. So coreutils padding the year number as seen is actually standard-conform behaviour. The standard also explicitly states that years are expected to lie between AD 0000 and AD 9999—it does not mention negative years or years above 10,000 at all, so at least I can rest assured that the RFC will be adapted someday (and perhaps the Unix epoch can establish itself until then and profit from the era of the date notation instability).

read more

2021

2019

2017

2015