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.
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:
-
In the Unix epoch, the Christian epoch starts in −1970. Converting a year number from that "Old Calendar" is thus of course simply done by subtracting 1970.
-
Since negatively numbered years are not very user-friendly, those years should be referred to as Before Unix, or BU for short. To explicitly refer to a positive year number, we might adapt something like Anno Unix (AU). It's perhaps silly, I don't know—we'll see.
-
A few randomly selected major events in history (to "reorient"): Cæsar was murdered on the ides of March, in 2014 BU. The Middle Ages started around the 14th century BU and ended around the 5th century BU. Sgt. Pepper was released in the year 3 BU, Star Wars screened in AU 7. The Berlin Wall fell in 19, and 9/11 happened in 31. And (hopefully not last or least) we'll run into the Year 68 problem in… well, 68.
-
Many of us might be young enough to witness the second century.
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:
Should'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).