Unsung Moments

`struct tm` and we humans just can’t agree on a "standard"

Here’s struct tm as defined by ANSI/ISO C.

struct tm
{
  int tm_sec;			/* Seconds.	[0-60] (1 leap second) */
  int tm_min;			/* Minutes.	[0-59] */
  int tm_hour;			/* Hours.	[0-23] */
  int tm_mday;			/* Day.		[1-31] */
  int tm_mon;			/* Month.	[0-11] */
  int tm_year;			/* Year	- 1900.  */
  int tm_wday;			/* Day of week.	[0-6] */
  int tm_yday;			/* Days in year.[0-365]	*/
  int tm_isdst;			/* DST.		[-1/0/1]*/
};

All the fields are 0-based except tm_mday. Why? As it turns out, the fields are expected to be used as both literal values and indices.

For tm_sec, tm_min, and tm_hour, the literal 0 is used on digital clocks, and people are quite used to it, even though there’s no real reason for it not to start at 1.

❯ date +%H:%M:%S
# 00:00:00

But for tm_mday, that’s not the case. We expect the day to start at 1 on a calendar.

❯ cal
#      March 2026
# Su Mo Tu We Th Fr Sa
#  1  2  3  4  5  6  7
#  8  9 10 11 12 13 14
# 15 16 17 18 19 20 21
# 22 23 24 25 26 27 28
# 29 30 31

But what about tm_mon? Shouldn’t it also start at 1? That’s where the “indices” part comes in. Months have names; most of the time, we say “January” not “month one”.

#include <stdio.h>
#include <time.h>
int main() {
    char *months[] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    };
    time_t now = time(NULL);
    struct tm *local = localtime(&now);
    printf("%s\n", months[local->tm_mon]);
}

tm_wday starts at 0 for the same reason. But which day should be 0, and which should be the first day of the week?

In ISO C, the first weekday is Sunday with the value 0. In ISO 8601, Monday is the first day with the value 1, so Sunday is assigned the value 7. Yeah, ISO standards can disagree with each other.

Even though it’s a bit convoluted, I’d say C’s struct tm is quite reasonable. Python apparently disagrees: here’s struct_time.

class struct_time:
    @property
    def tm_year(self) -> int:
        """year, for example, 1993"""

    @property
    def tm_mon(self) -> int:
        """month of year, range [1, 12]"""

    @property
    def tm_mday(self) -> int:
        """day of month, range [1, 31]"""

    @property
    def tm_hour(self) -> int:
        """hours, range [0, 23]"""

    @property
    def tm_min(self) -> int:
        """minutes, range [0, 59]"""

    @property
    def tm_sec(self) -> int:
        """seconds, range [0, 61])"""

    @property
    def tm_wday(self) -> int:
        """day of week, range [0, 6], Monday is 0"""

    @property
    def tm_yday(self) -> int:
        """day of year, range [1, 366]"""

    @property
    def tm_isdst(self) -> int:
        """1 if summer time is in effect, 0 if not, and -1 if unknown"""

CPython, despite being implemented in C, does not align with it.

I mean, this is chaos. Maybe we should just use a string format like RFC3339, but which exact form? Should we use a T or a space to separate the date and time? Should the trailing z be capitalized?

Wait, which encoding should we use for the string in the first place?