15

While answering another question, I told the OP he needs to initialize his struct tm variable correctly but needs to be careful because he couldn't simply use

struct tm mytime;
memset(&mytime, 0, sizeof(mytime));

because not all fields of struct tm are valid from 0. A closer look on struct tm showed me that it is exactly one field of struct tm that doesn't have 0 as valid value, namely tm_mday:

int    tm_sec   seconds [0,61]
int    tm_min   minutes [0,59]
int    tm_hour  hour [0,23]
int    tm_mday  day of month [1,31]
int    tm_mon   month of year [0,11]
int    tm_year  years since 1900
int    tm_wday  day of week [0,6] (Sunday = 0)
int    tm_yday  day of year [0,365]
int    tm_isdst daylight savings flag

Why? What were the thoughts behind the decision that for this very element, 0 shall be no valid value???

Community
  • 1
  • 1
eckes
  • 64,417
  • 29
  • 168
  • 201
  • 2
    `tm_year` starts from 1 too (there's no year 'zero'), it is just reduced by 1900, which may mask the lack of zero. ;) Generally all values have ranges which best fit either the direct display (hours .. seconds) or indexing string arrays (months). – CiaPan Jul 13 '15 at 10:19
  • @CiaPan: `indexing string arrays` sounds very reasonable, however, there's no string-indexing for `tm_yday` where `0` is valid. – eckes Jul 13 '15 at 10:24
  • 2
    @CiaPan A `tm_year` of zero is fine: it means the year `1900`. – TripeHound Jul 13 '15 at 10:24
  • I don't know whether it's part of the rationale, but if `tm_mday` starts from 1, to find next/previous Sunday, you simply have to do `tm_mday - tm_wday` (and ±7), etc., so it allows for relatively "clean" formulas. – Michael Foukarakis Jul 13 '15 at 10:32
  • Also, methods for [Computus](https://en.wikipedia.org/wiki/Computus#Week_table:_Julian_and_Gregorian_calendars), arguably earlier than the first `struct tm` definition, use the same convention. – Michael Foukarakis Jul 13 '15 at 10:49
  • 2
    Just some guess. There is fixed conversion for seconds, minutes, hours, months,years, but not for days. E.g. 1m = 60s; 1h = 60m; 1month = ??? day; 1y = 12m.So, you may use `mod` to s/m/h/y , such `x mod 60s ` , 'x mod 60m', 'x mod 60h',`x mod 12m`, which will probably produce `0`s,`0`m,`0`h result. But we will not apply `mod` to day, such as `x mod ??? day`(base is 30 ?31 ? or 29 ?). – Eric Tsui Jul 13 '15 at 11:01

3 Answers3

5

It makes sense if you assume the following two rules:

  • Store the value starting from 1 if that allows for easiest display without having to add or subtract one in common date formats
  • In all other cases (or when the first rule could go either way depending on the format), store the value starting from 0

Applying the rule:

  • tm_sec, tm_min, tm_hour displayed starting from 0 so store starting from 0. In 12-hour format the first hour is 12, but the rest can be displayed "as-is" by starting from 0.
  • tm_mday displayed starting from 1 so store starting from 1
  • tm_mon displayed starting from 1 in dates like 24/02/1964, but also makes sense to store starting from 0 for ease of indexing strings in an array for dates like 24th February 1964, so could go either way -> start from 0
  • tm_year 20th century years can be displayed as-is in 2 year format e.g. 24/02/64, or else add 1900, no case where starting from 1 makes sense
  • tm_wday Usually displayed by indexing a string array, start from 0
  • tm_yday No clear reason to start from 1 for ease of display, start from 0

So tm_mday is the only case where there is a clear advantage to storing it starting from 1 for ease of display in all common cases.

The reference implementation of asctime from the C-89 standard is consistent with this, the only adjustment to any of the values being to add 1900 to tm_year:

     char *asctime(const struct tm *timeptr)
     {
         static const char wday_name[7][3] = {
                  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
         };
         static const char mon_name[12][3] = {
                  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
         };
         static char result[26];

         sprintf(result, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",
                  wday_name[timeptr->tm_wday],
                  mon_name[timeptr->tm_mon],
                  timeptr->tm_mday, timeptr->tm_hour,
                  timeptr->tm_min, timeptr->tm_sec,
                  1900 + timeptr->tm_year);
         return result;
     }
samgak
  • 23,944
  • 4
  • 60
  • 82
  • 5
    The reasoning for `tm_mon` is not convincing; you could all other things in an array too. And `tm_wday` and `tm_yday` do not follow the "rule 1" either. – P.P Jul 13 '15 at 10:49
  • 1
    @BlueMoon There is no clear reason for why tm_wday and tm_yday would be easier to display if they were stored starting from 1, so rule 1 fits IMO. – samgak Jul 13 '15 at 10:54
  • "tm_mday displayed starting from 1 so store starting from 1" Seems that would apply to tm_yday as well. – Scooter Jul 13 '15 at 11:31
  • 1
    @Scooter, the day of the year isn't used in any common date display formats, so the rationale isn't as compelling as for the day of the month, which is. – samgak Jul 13 '15 at 11:40
  • 1
    Hmmm `static char result[26]; sprintf(result, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",` ripe for exploits with fields with large values. Hacker concern was not so high then versus byte cost. – chux - Reinstate Monica Jul 13 '15 at 14:15
  • 1
    I don't find that very convincing. Most of the time you would show the month as a number, e.g. `2018-03-19` or `19.3.2018` or `3/19/2018`; thus you would have to add 1 everytime. – ckruse Mar 19 '18 at 08:26
0

I guess the babylonians decided to use number 1 for the first day in a month (for quite obvious reasons) because they hadn't invented number 0 yet.

(Same reason for year 1 being the first year AC)

Since then no one changed the numbering of months.

DrKoch
  • 9,556
  • 2
  • 34
  • 43
  • 3
    But then why is `tm_mon` starting at 0 and not at 1 ?? – Jabberwocky Jul 13 '15 at 10:22
  • 1
    @MichaelWalz: See the comment of CiaPan above (http://stackoverflow.com/questions/31380566/why-does-tm-mday-start-from-1-while-all-other-elements-of-struct-tm-start-from-0/31380773#comment50739357_31380566). Indexing string arrays may be an explanation here... – eckes Jul 13 '15 at 10:24
  • Well the first month is called "January", not "0" or "1", so you need a lookup table anyway. And this table is most naturally indexed with an integer starting at 0 in C. – DrKoch Jul 13 '15 at 10:24
  • @eckes But zero _is_ valid for `tm_year`. And if `tm_mday` starts from `1` because it "doesn't need to index a string array", then why doesn't `tm_yday` start from `1`? – TripeHound Jul 13 '15 at 10:27
  • 4
    @DrKoch For fun: Over-simplification: the 10th month _was_ Dec-ember, 9th Nov-ember, 8th Oct-ober and 7th Sept-ember and started the year in March and all that leap-year junk was in the last month February. Then the politicians got involved. Julius Caesar sorted it out and was assassinated the next year. That is what one gets for fixing the calendar. – chux - Reinstate Monica Jul 13 '15 at 14:09
0

Another possible reason is that tm_mday == 0 check can be used to treat such a date as "null date" (or "not set" / "not available"), retaining the ability to implicitly initialize it through zero-initialization semantics, which is not possible if some non-zero (say, INT_MIN) value is being used for this purpose.