Time, Clock and Calendar Programming 1.0

A bit late, because I’ve been hammering on some code the last several days. But here it is: Time, Clock, and Calendar Programming In C.

Suggestions for 1.1 revisions and improvements will of course be cheerfully accepted. Comments here or email will be fine.

29 comments

  1. In section: Unix time and UTC/GMT/Zulu, paragraph 7:

    Mixed tense?
    “…when it crosses midnight during a leap-second insertions or deletions…”

  2. Editorial, speculative: (as opposed to what is actually there)

    “On today’s true 64-bit machines with relatively inexpensive floating point the natural float representation of time would look like this:

    seconds: 64 bits
    fractional seconds: 48 bits
    ——–
    total: 112 bits

    offering sub-picosecond resolution with plenty of headroom to avoid serious roundoff issues. So the scripting languages are heading in a direction that the C API could in theory eventually follow”.

    I detailed the problems in a response to an earlier post. On all systems a 64 bit integer is a 64 bit integer (also see the DEC 10 and DEC 20 legacy for non 2**2**N word length).

    1. >I detailed the problems in a response to an earlier post. On all systems a 64 bit integer is a 64 bit integer (also see the DEC 10 and DEC 20 legacy for non 2**2**N word length)

      I am not clear what change you are recommending.

  3. This is an interesting recap, and pretty accurate as far as I can tell, but it’s not what I expected after your announcement. Somehow I thought you were about to release a new set of time functions which would use One True Portable Time Format instead of struct tm, time_t, clock_t, and so forth. Then again, maybe you’ve concluded that such an effort would be wasted; I’d love to hear your views on the idea.

    The variations in historical time zones (such as the temporary adoption of year-round DST, and DST in states that ordinarily didn’t use it, during the Nixon administration) are interesting trivia but probably aren’t relevant at all, unless someone wants to write a function that converts a past time and location to a local time string that would have been correct then.

    I find it weird that tm_sec (in struct tm) allows the value 60 but not 61. There was at least one year with two leap seconds at the end. Then again, there is no reason to recognize leap seconds at all if time_t is going to continue to be defined as though they’ve never happened (and I don’t expect any computer clock to be accurate enough that they’ll ever matter anyway).

    Nits:

    Table 3 re. float precision — this seems to me vaxocentrism. IEEE suggests those floating-point formats, but on every machine I’ve used, the hardware determines the actual formats available, and few machines even pay lip service to the IEEE standard.

    In the section “gettimeofday(2|3)/settimeofday(2)” you refer to POSIX.1-1001/SUSv3, which I assume is a typo (1001 should be 2001).

    Also in that section, the declaration of gettimeofday() uses “restrict” as a modifier I’ve never seen before in C (at least, the latest standard I’ve looked at, around 2001, didn’t have it).

    In the section “tzset(3) and ftime(3)” is the declaration extern char *tzname[2];I would think this should be [3] or [4] unless there is a system of two letter time zone names I’m unaware of. (Yes, I know the array is zero-based, but the declaration is a number of members, not the index of the last member. [4] would allow a ” at the end, but perhaps that is too much to expect.)

    1. >This is an interesting recap, and pretty accurate as far as I can tell, but it’s not what I expected after your announcement. Somehow I thought you were about to release a new set of time functions

      You’re confusing two different projects. The time library design I’ve been thinking about was motivated by the search I did for this document, but is only speculative at this point.

      >few machines even pay lip service to the IEEE standard.

      The FPUs on Intel and ARM are IEEE754-format-conformant in hardware. Also IBM Power and PowerPC, HP PA-RISC, Motorola 68xxx and 88xxx, SGI (MIPS) R-xxxx and Sun SPARC normally implement these formats, though not all the extensions, That seems like more than a “few machines” to me.

  4. My formatting got clobbered, but the last sentence meant that [4] would allow a null character at the end.

  5. > In the section “tzset(3) and ftime(3)” is the declaration extern char *tzname[2];I would think this should be [3] or [4] unless there is a system of two letter time zone names I’m unaware of. (Yes, I know the array is zero-based, but the declaration is a number of members, not the index of the last member. [4] would allow a ” at the end, but perhaps that is too much to expect.)

    It is two strings, not 2-character array. As I read it, it consists of possibly ambiguous name of non-DST timezone, and possibly ambiguous name of DST timezone (e.g. “CET”, “CEST” – though this is as far as I know unambiguous).

  6. > I find it weird that tm_sec (in struct tm) allows the value 60 but not 61. There was at least one year with two leap seconds at the end.

    No there wasn’t.

  7. I respectfully suggest:

    (a) That you make all the examples in Table 4 show the same date+time, so the reader can compare formats without being distracted by value disparities (except maybe for showing the effect of changing from EDT to Zulu) (or perhaps you intended to do that but accidentally confused September, the ninth month, with October (the tenth) when formulating some examples?).

    (b) That you mention HTTP+rfc2616/7231 along with email+rfc822/2822 since HTTP probably looms larger nowadays for most readers than email.

    (c) That you add a row for the Generalized Time syntax (e.g., 20140924203227.000001Z, perhaps best known from use in LDAP+rfc2252) from X.680 (formerly X.208) which is also the basic syntax of ISO 8601 (hyphens, ‘T’, colons, etc. within 8601 strings are optional for human convenience), not because it is vital in Unix but because your readers will often need to choose a string representation for storing or communicating their date-time values and you can alert them to the wise choices without cluttering your document too much. (I realize you already recommended the full 8601 with convenience characters, but those hyphens and colons can be inconvenient in data intended mainly for machine interpretation.)

    1. >(a) That you make all the examples in Table 4 show the same date+time

      Done.

      >(b) That you mention HTTP+rfc2616/7231

      Done.

      >(c) That you add a row for the Generalized Time syntax

      Done, though reluctantly. IMO that is ugly, and because of poor human-readability inferior to RFC3339 format.

  8. > which is also the basic syntax of ISO 8601 (hyphens, ‘T’, colons, etc. within 8601 strings are optional for human convenience)

    That’s not quite right. The T is required by default

    ISO 8601:2004(E): “4.3.2 NOTE: By mutual agreement of the partners in information interchange, the character [T] may be omitted in applications where there is no risk of confusing a date and time of day representation with others defined in this International Standard.”

    In the absence of that mutual agreement, omitting the T is non-ISO-8601-conformant. So when discussing the standard itself, one must not presuppose such agreement exists.

  9. This is *very* useful. I wonder if there is room for some advice about doing all calculations in UTC and only converting to local time at the last possible moment before presentation to the user. I’ve come across some annoying bugs where people have got confused about that.

    1. >I wonder if there is room for some advice about doing all calculations in UTC and only converting to local time at the last possible moment before presentation to the user.

      Hmmm. I’d like to add that but I don’t see anywhere it fits in ther structure of the document as is. Perhaps it needs a “Programming Practices” section?

      It would look kind of weak to have just that one piece of advice there, though. What other practice guidelines could we add?

  10. > doing all calculations in UTC and only converting to local time at the last possible moment before presentation to the user

    This is fine for past events, but for future events defined in terms of a particular TZ a particular occurrence must be converted from that TZ into UTC first. What you can’t do is take something like “BBT at 7 pm CT Mondays”, note that it is currently CDT, add 5 hours to get “0:00Z Tue” only to be wrong in a few weeks when we fall back.

  11. ESR wrote: ” IMO that [Generalized Time string] is ugly, and because of poor human-readability inferior to RFC3339 format.”

    Oh, I agree! I’m just afraid that young coders afraid of rfc3339 will resort to a decimal representation of time_t (I have seen that a zillion times) and set themselves up for 2038 if nothing worse.

  12. The Monster wrote: “What you can’t do is take something like “BBT at 7 pm CT Mondays”, note that it is currently CDT, add 5 hours to get “0:00Z Tue” only to be wrong in a few weeks when we fall back.”

    Good point. If your input is in local time, you can have all sorts of problems. For instance, 1.30am can occur twice in one night (which was a problem I had working on UI for a video recording device, once). I think any code handling local time inputs is going to be very domain specific.

  13. IMHO the my_gmtime() comments are a bit frightening to mention that if any of the time_t, long or int is 32 bits the conversion may fail outside 1901 – 2038. In 64-bit systems int is often 32-bit
    (this modern x86-64 Linux seems to use 32-bit int, 64-bit long & 64-bit time_t by default)
    To me it looks the function does all arithemic using long & time_t types so it looks to me that
    if these types are 64-bit calculations works well beyond the 1901 – 2038 range. The only way
    something there could fail outside this range is trying to set the value tm_sec beyond it’s 32-bit limit.

  14. my_timegm and adjusting to leap year division

    I am getting different values from my_timegm() and timegm() when year is < 1970 — and I think the reason is something like that "flooring" goes to zero always.

    When I changed the code as:
    result += year / 4 – 454;
    result -= year / 100 + 19;
    result += year / 400 – 4;

    then I started to get same results (I tested years 1600 – 2500) — I just wonder why
    1968 / 4 == 492 was not right there above :O (I just hand-iterated the 454 there).

    1. >When I changed the code as:

      Please do me a favor. Actually, two:

      (1) Send me your fixed version. I’ll hold the release of 1.1 until you do.

      (2) Even more important – send me the unit test code! you’re clearly coming up with test pairs for weird cases and finding breakage. I want those badly, because this code would be a core piece of the new time library I’ve been thinking about.

  15. so, those lines “reduce” to result += year / 4 – year / 100 + year / 400 – 477;

    now, this time I got the 477 w/o counting as I just used something I tried yesterday:

    (void)mktime(&tm);
    d1(” %d, %d”, tm.tm_year + 1900, tm.tm_yday);
    return (time_t)(tm.tm_year – 70) * 31536000
    // + ((tm.tm_year-101)/4 – (tm.tm_year-101)/100 + (tm.tm_year-101)/400 + 8)
    + ((tm.tm_year+1899)/4-(tm.tm_year+1899)/100+(tm.tm_year+1899)/400-477)
    * 86400 + tm.tm_yday * 86400
    + tm.tm_hour * 3600 + tm.tm_min * 60 + tm.tm_sec – tm.tm_isdst * 3600;

    Yesterday I come up with similar when I was looking other timegm() replacements,
    but lost the page so I had to experiment myself. In this case if the outcommented line
    is used instead of the next one. time_t values are wrong in some dates before year
    2000. With this in mind It was easy to guess what might be wrong there…

  16. two things more about timegm()

    The timegm() manual page says:
    “These functions are nonstandard GNU extensions that are also present on the BSDs.”
    (and “Avoid their use; see NOTES.” but…)

    And that really is the case. At least all modern NetBSD, OpenBSD, FreeBSD and Darwin
    has timegm() available — so the document could mention this *BSD availability.

    Then, under what license/copyright are the code snippets in that document available. There
    is no mention anywhere about licencing stuff of anything in the doc. I’d like to use my_timegm()
    in my program (to be simplified BSD -licensed) instead of working leap year detection
    to the code snipped i posted earlyer (and drop timegm() usage there)

  17. > I am getting different values from my_timegm() and timegm() when year is < 1970 — and I think the reason is something like that "flooring" goes to zero always.

    #define DIV(a,b) ((a)/(b)-((a)%(b)<0))

    I went through a couple iterations of this before finding a version that, optimized, doesn't use any divide instructions (with gcc's constant division optimization) or branches. Only works with positive divisors.

  18. > Even more important – send me the unit test code!

    I’m not working on your code (I did some preliminary work on my own concept for a better set of library functions), but I made a unit test consisting of checking the lengths (Jan 1 of this year to Jan 1 of next one) of every year from 1 to 10000 vs that given by the system’s dysize function, precisely to catch bugs in that tricky leap year counting code. I can’t remember where I put it (I’m a bit disorganized) though at the moment.

Leave a comment

Your email address will not be published. Required fields are marked *